diff --git a/.circleci/config.yml b/.circleci/config.yml index 22539912268..dcf2ba804c6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ aliases: - &environment docker: # specify the version you desire here - - image: cimg/node:16.20-browsers + - image: cimg/node:20.14.0-browsers resource_class: xlarge # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -18,8 +18,6 @@ aliases: - &restore_dep_cache keys: - v1-dependencies-{{ checksum "package.json" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - &save_dep_cache paths: diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 69e13850258..9b1bb6e39cf 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -ARG VARIANT="12" +ARG VARIANT="20" FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:${VARIANT} RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index f17c7a0063d..00000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,99 +0,0 @@ - -const allowedModules = require('./allowedModules.js'); - -module.exports = { - env: { - browser: true, - commonjs: true - }, - settings: { - 'import/resolver': { - node: { - moduleDirectory: ['node_modules', './'] - } - }, - 'jsdoc': { - mode: 'typescript', - tagNamePreference: { - 'tag constructor': 'constructor', - extends: 'extends', - method: 'method', - return: 'return', - } - } - }, - extends: [ - 'standard', - 'plugin:jsdoc/recommended' - ], - plugins: [ - 'prebid', - 'import', - 'jsdoc' - ], - globals: { - 'BROWSERSTACK_USERNAME': false, - 'BROWSERSTACK_KEY': false, - 'FEATURES': 'readonly', - }, - // use babel as parser for fancy syntax - parser: '@babel/eslint-parser', - parserOptions: { - sourceType: 'module', - ecmaVersion: 2018, - }, - ignorePatterns: ['libraries/creative-renderer*'], - - rules: { - 'comma-dangle': 'off', - semi: 'off', - 'space-before-function-paren': 'off', - 'import/extensions': ['error', 'ignorePackages'], - - // Exceptions below this line are temporary, so that eslint can be added into the CI process. - // Violations of these styles should be fixed, and the exceptions removed over time. - // - // See Issue #1111. - eqeqeq: 'off', - 'no-return-assign': 'off', - 'no-throw-literal': 'off', - 'no-undef': 2, - 'no-useless-escape': 'off', - 'no-console': 'error', - 'jsdoc/check-types': 'off', - 'jsdoc/newline-after-description': 'off', - 'jsdoc/require-jsdoc': 'off', - 'jsdoc/require-param': 'off', - 'jsdoc/require-param-description': 'off', - 'jsdoc/require-param-name': 'off', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-property': 'off', - 'jsdoc/require-property-description': 'off', - 'jsdoc/require-property-name': 'off', - 'jsdoc/require-property-type': 'off', - 'jsdoc/require-returns': 'off', - 'jsdoc/require-returns-check': 'off', - 'jsdoc/require-returns-description': 'off', - 'jsdoc/require-returns-type': 'off', - 'jsdoc/require-yields': 'off', - 'jsdoc/require-yields-check': 'off', - 'jsdoc/tag-lines': 'off' - }, - overrides: Object.keys(allowedModules).map((key) => ({ - files: key + '/**/*.js', - rules: { - 'prebid/validate-imports': ['error', allowedModules[key]], - 'no-restricted-globals': [ - 'error', - { - name: 'require', - message: 'use import instead' - } - ] - } - })).concat([{ - // code in other packages (such as plugins/eslint) is not "seen" by babel and its parser will complain. - files: 'plugins/*/**/*.js', - parser: 'esprima' - }]) -}; diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 09ef5c445f2..367ace94d37 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -16,7 +16,8 @@ For any user facing change, submit a link to a PR on the docs repo at https://gi - [ ] Bugfix - [ ] Feature -- [ ] New bidder adapter +- [ ] New bidder adapter +- [ ] Updated bidder adapter - [ ] Code style update (formatting, local variables) - [ ] Refactoring (no functional changes, no api changes) - [ ] Build related changes @@ -40,7 +41,7 @@ For any user facing change, submit a link to a PR on the docs repo at https://gi } ``` -Be sure to test the integration with your adserver using the [Hello World](/integrationExamples/gpt/hello_world.html) sample page. --> +Be sure to test the integration with your adserver using the [Hello World](https://github.com/prebid/Prebid.js/blob/master/integrationExamples/gpt/hello_world.html) sample page. --> ## Other information diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 2e8465003e4..8d3788e8956 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -2,3 +2,6 @@ paths: - src - modules - libraries +queries: + - name: Prebid queries + uses: ./.github/codeql/queries diff --git a/.github/codeql/queries/deviceMemory.ql b/.github/codeql/queries/deviceMemory.ql new file mode 100644 index 00000000000..6f650abf0e1 --- /dev/null +++ b/.github/codeql/queries/deviceMemory.ql @@ -0,0 +1,14 @@ +/** + * @id prebid/device-memory + * @name Access to navigator.deviceMemory + * @kind problem + * @problem.severity warning + * @description Finds uses of deviceMemory + */ + +import prebid + +from SourceNode nav +where + nav = windowPropertyRead("navigator") +select nav.getAPropertyRead("deviceMemory"), "deviceMemory is an indicator of fingerprinting" diff --git a/.github/codeql/queries/hardwareConcurrency.ql b/.github/codeql/queries/hardwareConcurrency.ql new file mode 100644 index 00000000000..350dbd1ae81 --- /dev/null +++ b/.github/codeql/queries/hardwareConcurrency.ql @@ -0,0 +1,14 @@ +/** + * @id prebid/hardware-concurrency + * @name Access to navigator.hardwareConcurrency + * @kind problem + * @problem.severity warning + * @description Finds uses of hardwareConcurrency + */ + +import prebid + +from SourceNode nav +where + nav = windowPropertyRead("navigator") +select nav.getAPropertyRead("hardwareConcurrency"), "hardwareConcurrency is an indicator of fingerprinting" diff --git a/.github/codeql/queries/prebid.qll b/.github/codeql/queries/prebid.qll new file mode 100644 index 00000000000..02fb5adc93c --- /dev/null +++ b/.github/codeql/queries/prebid.qll @@ -0,0 +1,36 @@ +import javascript +import DataFlow + +SourceNode otherWindow() { + result = globalVarRef("top") or + result = globalVarRef("self") or + result = globalVarRef("parent") or + result = globalVarRef("frames").getAPropertyRead() or + result = DOM::documentRef().getAPropertyRead("defaultView") +} + +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") +} + +SourceNode relatedWindow(SourceNode win) { + result = connectedWindow(win) or + result = relatedWindow+(connectedWindow(win)) +} + +SourceNode anyWindow() { + result = otherWindow() or + result = relatedWindow(otherWindow()) +} + +/* + Matches uses of property `prop` done on any window object. +*/ +SourceNode windowPropertyRead(string prop) { + result = globalVarRef(prop) or + result = anyWindow().getAPropertyRead(prop) +} diff --git a/.github/codeql/queries/qlpack.yml b/.github/codeql/queries/qlpack.yml new file mode 100644 index 00000000000..72e90d3de9b --- /dev/null +++ b/.github/codeql/queries/qlpack.yml @@ -0,0 +1,8 @@ +--- +library: false +warnOnImplicitThis: false +name: queries +version: 0.0.1 +dependencies: + codeql/javascript-all: ^1.1.1 + codeql/javascript-queries: ^1.1.0 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index a3246cffd6d..5876dfa0138 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,6 +1,10 @@ name-template: 'Prebid $RESOLVED_VERSION Release' tag-template: '$RESOLVED_VERSION' +autolabeler: + - label: 'maintenance' + title: + - '/^(?!.*(bug|initial|release|fix)).*$/i' categories: - title: '🚀 New Features' label: 'feature' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bfde353bb7..4e3a8e19f29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - run: npm install && npm install -g gulp - name: Run tests - run: node_modules/.bin/gulp test --file test/spec/modules/ringieraxelspringerBidAdapter_spec.js,test/spec/modules/dasBidAdapter_spec.js + run: node_modules/.bin/gulp test --file test/spec/modules/ringieraxelspringerBidAdapter_spec.js --file test/spec/modules/dasBidAdapter_spec.js - name: build run: node_modules/.bin/gulp build --modules=modules.json @@ -65,4 +65,4 @@ jobs: release="${version}_$(date +"%Y%m%d%H%M%S")" echo "Uploading Prebid.js ver:${version} to OCDN" echo -e "\e[32mRelease: ${release}\e[0m" - aws s3 sync "${version}" s3://${BUCKET}/prebid/prod/${release}/ --region ocdn --endpoint "https://ocdn.eu" \ No newline at end of file + aws s3 sync "${version}" s3://${BUCKET}/prebid/prod/${release}/ --region ocdn --endpoint "https://ocdn.eu" diff --git a/.github/workflows/code-path-changes.yml b/.github/workflows/code-path-changes.yml new file mode 100644 index 00000000000..d98ae8e2d72 --- /dev/null +++ b/.github/workflows/code-path-changes.yml @@ -0,0 +1,37 @@ +name: Notify Code Path Changes + +on: + pull_request_target: + types: [opened, synchronize] + paths: + - '**' + +env: + OAUTH2_CLIENT_ID: ${{ secrets.OAUTH2_CLIENT_ID }} + OAUTH2_CLIENT_SECRET: ${{ secrets.OAUTH2_CLIENT_SECRET }} + OAUTH2_REFRESH_TOKEN: ${{ secrets.OAUTH2_REFRESH_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +permissions: + contents: read + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install dependencies + run: npm install axios nodemailer + + - name: Run Notification Script + run: | + node .github/workflows/scripts/send-notification-on-change.js diff --git a/.github/workflows/jscpd.yml b/.github/workflows/jscpd.yml new file mode 100644 index 00000000000..de5f1408dff --- /dev/null +++ b/.github/workflows/jscpd.yml @@ -0,0 +1,124 @@ +name: Check for Duplicated Code + +on: + pull_request_target: + branches: + - master + +jobs: + check-duplication: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all branches + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + npm install -g jscpd diff-so-fancy + + - name: Create jscpd config file + run: | + echo '{ + "threshold": 20, + "minTokens": 100, + "reporters": [ + "json" + ], + "output": "./", + "pattern": "**/*.js", + "ignore": "**/*spec.js" + }' > .jscpd.json + + - name: Run jscpd on entire codebase + run: jscpd + + - name: Fetch base and target branches + run: | + git fetch origin +refs/heads/${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} + git fetch origin +refs/pull/${{ github.event.pull_request.number }}/merge:refs/remotes/pull/${{ github.event.pull_request.number }}/merge + + - name: Get the diff + run: git diff --name-only origin/${{ github.event.pull_request.base.ref }}...refs/remotes/pull/${{ github.event.pull_request.number }}/merge > changed_files.txt + + - name: List generated files (debug) + run: ls -l + + - name: Upload unfiltered jscpd report + if: always() + uses: actions/upload-artifact@v4 + with: + name: unfiltered-jscpd-report + path: ./jscpd-report.json + + - name: Filter jscpd report for changed files + run: | + if [ ! -f ./jscpd-report.json ]; then + echo "jscpd-report.json not found" + exit 1 + fi + echo "Filtering jscpd report for changed files..." + CHANGED_FILES=$(jq -R -s -c 'split("\n")[:-1]' changed_files.txt) + echo "Changed files: $CHANGED_FILES" + jq --argjson changed_files "$CHANGED_FILES" ' + .duplicates | map(select( + (.firstFile?.name as $fname | $changed_files | any(. == $fname)) or + (.secondFile?.name as $sname | $changed_files | any(. == $sname)) + )) + ' ./jscpd-report.json > filtered-jscpd-report.json + cat filtered-jscpd-report.json + + - name: Check if filtered jscpd report exists + id: check_filtered_report + run: | + if [ $(wc -l < ./filtered-jscpd-report.json) -gt 1 ]; then + echo "filtered_report_exists=true" >> $GITHUB_ENV + else + echo "filtered_report_exists=false" >> $GITHUB_ENV + fi + + - name: Upload filtered jscpd report + if: env.filtered_report_exists == 'true' + uses: actions/upload-artifact@v4 + with: + name: filtered-jscpd-report + path: ./filtered-jscpd-report.json + + - name: Post GitHub comment + if: env.filtered_report_exists == 'true' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const filteredReport = JSON.parse(fs.readFileSync('filtered-jscpd-report.json', 'utf8')); + let comment = "Whoa there, partner! 🌵🤠 We wrangled some duplicated code in your PR:\n\n"; + function link(dup) { + return `https://github.com/${{ github.event.repository.full_name }}/blob/${{ github.event.pull_request.head.sha }}/${dup.name}#L${dup.start + 1}-L${dup.end - 1}` + } + filteredReport.forEach(duplication => { + const firstFile = duplication.firstFile; + const secondFile = duplication.secondFile; + const lines = duplication.lines; + comment += `- [\`${firstFile.name}\`](${link(firstFile)}) has ${lines} duplicated lines with [\`${secondFile.name}\`](${link(secondFile)})\n`; + }); + comment += "\nReducing code duplication by importing common functions from a library not only makes our code cleaner but also easier to maintain. Please move the common code from both files into a library and import it in each. We hate that we have to mention this, however, commits designed to hide from this utility by renaming variables or reordering an object are poor conduct. We will not look upon them kindly! Keep up the great work! 🚀"; + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + + - name: Fail if duplications are found + if: env.filtered_report_exists == 'true' + run: | + echo "Duplications found, failing the check." + exit 1 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 00000000000..03ef6478f1c --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,110 @@ +name: Check for linter warnings / exceptions + +on: + pull_request_target: + branches: + - master + +jobs: + check-linter: + runs-on: ubuntu-latest + + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.base.sha }} + + - name: Fetch base and target branches + run: | + git fetch origin +refs/heads/${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} + git fetch origin +refs/pull/${{ github.event.pull_request.number }}/merge:refs/remotes/pull/${{ github.event.pull_request.number }}/merge + + - name: Install dependencies + run: npm ci + + - name: Get the diff + run: git diff --name-only origin/${{ github.event.pull_request.base.ref }}...refs/remotes/pull/${{ github.event.pull_request.number }}/merge | grep '^\(modules\|src\|libraries\|creative\)/.*\.js$' > __changed_files.txt || true + + - name: Run linter on base branch + run: npx eslint --no-inline-config --format json $(cat __changed_files.txt | xargs stat --printf '%n\n' 2> /dev/null) > __base.json || true + + - name: Check out PR + run: git checkout ${{ github.event.pull_request.head.sha }} + + - name: Install dependencies + run: npm ci + + - name: Run linter on PR + run: npx eslint --no-inline-config --format json $(cat __changed_files.txt | xargs stat --printf '%n\n' 2> /dev/null) > __pr.json || true + + - name: Compare them and post comment if necessary + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const process = require('process'); + + function parse(fn) { + return JSON.parse(fs.readFileSync(fn)).reduce((memo, data) => { + const file = path.relative(process.cwd(), data.filePath); + if (!memo.hasOwnProperty(file)) { memo[file] = { errors: 0, warnings: 0} } + data.messages.forEach(({severity}) => { + memo[file][severity > 1 ? 'errors' : 'warnings']++; + }); + return memo; + }, {}) + } + + function mkDiff(old, new_) { + const files = Object.fromEntries( + Object.entries(new_) + .map(([file, {errors, warnings}]) => { + const {errors: oldErrors, warnings: oldWarnings} = old[file] || {}; + return [file, {errors: Math.max(0, errors - (oldErrors ?? 0)), warnings: Math.max(0, warnings - (oldWarnings ?? 0))}] + }) + .filter(([_, {errors, warnings}]) => errors > 0 || warnings > 0) + ) + return Object.values(files).reduce((memo, {warnings, errors}) => { + memo.errors += errors; + memo.warnings += warnings; + return memo; + }, {errors: 0, warnings: 0, files}) + } + + function mkComment({errors, warnings, files}) { + function pl(noun, number) { + return noun + (number === 1 ? '' : 's') + } + if (errors === 0 && warnings === 0) return; + const summary = []; + if (errors) summary.push(`**${errors}** linter ${pl('error', errors)}`) + if (warnings) summary.push(`**${warnings}** linter ${pl('warning', warnings)}`) + let cm = `Tread carefully! This PR adds ${summary.join(' and ')} (possibly disabled through directives):\n\n`; + Object.entries(files).forEach(([file, {errors, warnings}]) => { + const summary = []; + if (errors) summary.push(`+${errors} ${pl('error', errors)}`); + if (warnings) summary.push(`+${warnings} ${pl('warning', warnings)}`) + cm += ` * \`${file}\` (${summary.join(', ')})\n` + }) + return cm; + } + + const [base, pr] = ['__base.json', '__pr.json'].map(parse); + const comment = mkComment(mkDiff(base, pr)); + + if (comment) { + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } diff --git a/.github/workflows/scripts/codepath-notification b/.github/workflows/scripts/codepath-notification new file mode 100644 index 00000000000..cdbc30b0e97 --- /dev/null +++ b/.github/workflows/scripts/codepath-notification @@ -0,0 +1,18 @@ +# when a changed file paths matches the regex, send an alert email +# structure of the file is: +# +# javascriptRegex : email address +# +# For example, in the Prebid.js repo, the file pattern is generally +# +# /modules/BIDDERCODE +# /spec/modules/BIDDERCODE +# +# The aim is to find a minimal set of regex patterns that matches any file in these paths + +rubicon|magnite : header-bidding@magnite.com +/modules/ix|/spec/modules/ix : pdu-supply-prebid@indexexchange.com +appnexus : prebid@microsoft.com +pubmatic : header-bidding@pubmatic.com +openx : prebid@openx.com +medianet : prebid@media.net diff --git a/.github/workflows/scripts/send-notification-on-change.js b/.github/workflows/scripts/send-notification-on-change.js new file mode 100644 index 00000000000..57079ef37cb --- /dev/null +++ b/.github/workflows/scripts/send-notification-on-change.js @@ -0,0 +1,139 @@ +// send-notification-on-change.js +// +// called by the code-path-changes.yml workflow, this script queries github for +// the changes in the current PR, checks the config file for whether any of those +// file paths are set to alert an email address, and sends email to multiple +// parties if needed + +const fs = require('fs'); +const path = require('path'); +const axios = require('axios'); +const nodemailer = require('nodemailer'); + +async function getAccessToken(clientId, clientSecret, refreshToken) { + try { + const response = await axios.post('https://oauth2.googleapis.com/token', { + client_id: clientId, + client_secret: clientSecret, + refresh_token: refreshToken, + grant_type: 'refresh_token', + }); + return response.data.access_token; + } catch (error) { + console.error('Failed to fetch access token:', error.response?.data || error.message); + process.exit(1); + } +} + +(async () => { + const configFilePath = path.join(__dirname, 'codepath-notification'); + const repo = process.env.GITHUB_REPOSITORY; + const prNumber = process.env.GITHUB_PR_NUMBER; + const token = process.env.GITHUB_TOKEN; + + // Generate OAuth2 access token + const clientId = process.env.OAUTH2_CLIENT_ID; + const clientSecret = process.env.OAUTH2_CLIENT_SECRET; + const refreshToken = process.env.OAUTH2_REFRESH_TOKEN; + + // validate params + if (!repo || !prNumber || !token || !clientId || !clientSecret || !refreshToken) { + console.error('Missing required environment variables.'); + process.exit(1); + } + + // the whole process is in a big try/catch. e.g. if the config file doesn't exist, github is down, etc. + try { + // Read and process the configuration file + const configFileContent = fs.readFileSync(configFilePath, 'utf-8'); + const configRules = configFileContent + .split('\n') + .filter(line => line.trim() !== '' && !line.trim().startsWith('#')) // Ignore empty lines and comments + .map(line => { + const [regex, email] = line.split(':').map(part => part.trim()); + return { regex: new RegExp(regex), email }; + }); + + // Fetch changed files from github + const [owner, repoName] = repo.split('/'); + const apiUrl = `https://api.github.com/repos/${owner}/${repoName}/pulls/${prNumber}/files`; + const response = await axios.get(apiUrl, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + }); + + const changedFiles = response.data.map(file => file.filename); + console.log('Changed files:', changedFiles); + + // match file pathnames that are in the config and group them by email address + const matchesByEmail = {}; + changedFiles.forEach(file => { + configRules.forEach(rule => { + if (rule.regex.test(file)) { + if (!matchesByEmail[rule.email]) { + matchesByEmail[rule.email] = []; + } + matchesByEmail[rule.email].push(file); + } + }); + }); + + // Exit successfully if no matches were found + if (Object.keys(matchesByEmail).length === 0) { + console.log('No matches found. Exiting successfully.'); + process.exit(0); + } + + console.log('Grouped matches by email:', matchesByEmail); + + // get ready to email the changes + const accessToken = await getAccessToken(clientId, clientSecret, refreshToken); + + // Configure Nodemailer with OAuth2 + // service: 'Gmail', + const transporter = nodemailer.createTransport({ + host: "smtp.gmail.com", + port: 465, + secure: true, + auth: { + type: 'OAuth2', + user: 'info@prebid.org', + clientId: clientId, + clientSecret: clientSecret, + refreshToken: refreshToken, + accessToken: accessToken + }, + }); + + // Send one email per recipient + for (const [email, files] of Object.entries(matchesByEmail)) { + const emailBody = ` + ${email}, +

+ Files relevant to your integration have been changed in open source ${repo}. The pull request is #${prNumber}. These are the files you monitor that have been modified: +

+ `; + + try { + await transporter.sendMail({ + from: `"Prebid Info" `, + to: email, + subject: `Files have been changed in open source ${repo}`, + html: emailBody, + }); + + console.log(`Email sent successfully to ${email}`); + console.log(`${emailBody}`); + } catch (error) { + console.error(`Failed to send email to ${email}:`, error.message); + } + } + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +})(); diff --git a/.gitignore b/.gitignore index e5f000dd4d5..080b95e0753 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Built Files node_modules/ build +# dist and npmignore are generated by "gulp build" +/dist/ +.npmignore # Test Files test/app diff --git a/.nvmrc b/.nvmrc index 66df3b7ab2d..f203ab89b79 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.16.1 +20.13.1 diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 9deac9963fb..f6a2c157d2d 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -23,10 +23,11 @@ General gulp commands include separate commands for serving the codebase on a bu - Checkout the branch (these instructions are available on the GitHub PR page as well). - Verify PR is a single change type. Example, refactor OR bugfix. If more than 1 type, ask submitter to break out requests. - Verify code under review has at least 80% unit test coverage. If legacy code doesn't have enough unit test coverage, require that additional unit tests to be included in the PR. -- Verify tests are green in Travis-ci + local build by running `gulp serve` | `gulp test` +- Verify tests are green in circle-ci + local build by running `gulp serve` | `gulp test` - Verify no code quality violations are present from linting (should be reported in terminal) - Make sure the code is not setting cookies or localstorage directly -- it must use the `StorageManager`. - Review for obvious errors or bad coding practice / use best judgement here. +- Don't allow needless code duplication with other js files; require both files import common code. Do not allow commits designed to fool the code duplication checker. - If the change is a new feature / change to core prebid.js - review the change with a Tech Lead on the project and make sure they agree with the nature of change. - If the change results in needing updates to docs (such as public API change, module interface etc), add a label for "needs docs" and inform the submitter they must submit a docs PR to update the appropriate area of Prebid.org **before the PR can merge**. Help them with finding where the docs are located on prebid.org if needed. - If all above is good, add a `LGTM` comment and, if the change is in PBS-core or is an important module like the prebidServerBidAdapter, request 1 additional core member to review. @@ -51,20 +52,21 @@ Follow steps above for general review process. In addition, please verify the fo - If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. - All bidder parameter conventions must be followed: - Video params must be read from AdUnit.mediaTypes.video when available; however bidder config can override the ad unit. - - First party data must be read from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd). + - First party data must be read from the bid request object: bidrequest.ortb2 - Adapters that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloor()` function. - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. - The bidderRequest.refererInfo.referer must be checked in addition to any bidder-specific parameter. - Page position must come from bidrequest.mediaTypes.banner.pos or bidrequest.mediaTypes.video.pos - - Global OpenRTB fields should come from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd): + - Eids object is to be preferred to Userids object in the bid request, as the userid object may be removed in a future version + - Global OpenRTB fields should come from bidrequest.ortb2 - bcat, battr, badv - Impression-specific OpenRTB fields should come from bidrequest.ortb2imp - instl - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/BIDDER.md file): - - If they support the GDPR consentManagement module and TCF1, add `gdpr_supported: true` - - If they support the GDPR consentManagement module and TCF2, add `tcf2_supported: true` + - If they support the TCF consentManagementTcf module and TCF2, add `tcf2_supported: true` - If they support the US Privacy consentManagementUsp module, add `usp_supported: true` - - If they support one or more userId modules, add `userId: (list of supported vendors)` + - If they support the GPP consentManagementGpp module, add `gpp_supported: true` + - If they support one or more userId modules, add `userId: (list of supported vendors) or (all)` - If they support video and/or native mediaTypes add `media_types: video, native`. Note that display is added by default. If you don't support display, add "no-display" as the first entry, e.g. `media_types: no-display, native` - If they support COPPA, add `coppa_supported: true` - If they support SChain, add `schain_supported: true` @@ -100,7 +102,7 @@ Follow steps above for general review process. In addition: - modules/userId/userId.md - tests can go either within the userId_spec.js file or in their own _spec file if they wish - GVLID is recommended in the *IdSystem file if they operate in EU -- make sure example configurations align to the actual code (some modules use the userId storage settings and allow pub configuration, while others handle reading/writing cookies on their own, so should not include the storage params in examples) +- make sure example configurations align to the actual code (some modules use the userId storage settings and allow pub configuration, while others handle reading/writing cookies on their own, so should not include the storage params in examples). This ability to write will be removed in a future version, see https://github.com/prebid/Prebid.js/issues/10710 - the 3 available methods (getId, extendId, decode) should be used as they were intended - decode (required method) should not be making requests to retrieve a new ID, it should just be decoding a response - extendId (optional method) should not be making requests to retrieve a new ID, it should just be adding additional data to the id object @@ -121,6 +123,7 @@ Follow steps above for general review process. In addition: - Confirm that the module - is not loading external code. If it is, escalate to the #prebid-js Slack channel. - is reading `config` from the function signature rather than calling `getConfig`. + - Is practicing reasonable data minimization, eg not sending all eids over the wire without publisher whitelisting - is sending data to the bid request only as either First Party Data or in bidRequest.rtd.RTDPROVIDERCODE. - is making HTTPS requests as early as possible, but not more often than needed. - doesn't force bid adapters to load additional code. diff --git a/README.md b/README.md index 70e058d122b..2644419886c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Build Status](https://circleci.com/gh/prebid/Prebid.js.svg?style=svg)](https://circleci.com/gh/prebid/Prebid.js) -[![Percentage of issues still open](http://isitmaintained.com/badge/open/prebid/Prebid.js.svg)](http://isitmaintained.com/project/prebid/Prebid.js "Percentage of issues still open") +[![Percentage of issues still open](http://isitmaintained.com/badge/open/prebid/Prebid.js.svg)](https://isitmaintained.com/project/prebid/Prebid.js "Percentage of issues still open") [![Coverage Status](https://coveralls.io/repos/github/prebid/Prebid.js/badge.svg)](https://coveralls.io/github/prebid/Prebid.js) # Prebid.js @@ -7,8 +7,8 @@ > A free and open source library for publishers to quickly implement header bidding. This README is for developers who want to contribute to Prebid.js. -Additional documentation can be found at [the Prebid homepage](http://prebid.org). -Working examples can be found in [the developer docs](http://prebid.org/dev-docs/getting-started.html). +Additional documentation can be found at [the Prebid.js documentation homepage](https://docs.prebid.org/prebid/prebidjs.html). +Working examples can be found in [the developer docs](https://prebid.org/dev-docs/getting-started.html). Prebid.js is open source software that is offered for free as a convenience. While it is designed to help companies address legal requirements associated with header bidding, we cannot and do not warrant that your use of Prebid.js will satisfy legal requirements. You are solely responsible for ensuring that your use of Prebid.js complies with all applicable laws. We strongly encourage you to obtain legal advice when using Prebid.js to ensure your implementation complies with all laws where you operate. @@ -26,7 +26,7 @@ Prebid.js is open source software that is offered for free as a convenience. Whi *Note:* Requires Prebid.js v1.38.0+ -Prebid.js depends on Babel and some Babel Plugins in order to run correctly in the browser. Here are some examples for +Prebid.js depends on Babel and some Babel Plugins in order to run correctly in the browser. Here are some examples for configuring webpack to work with Prebid.js. With Babel 7: @@ -37,7 +37,7 @@ module.exports = { mode: 'production', module: { rules: [ - + // this rule can be excluded if you don't require babel-loader for your other application files { test: /\.m?js$/, @@ -46,7 +46,7 @@ module.exports = { loader: 'babel-loader', } }, - + // this separate rule is required to make sure that the Prebid.js files are babel-ified. this rule will // override the regular exclusion from above (for being inside node_modules). { @@ -71,7 +71,7 @@ Or for Babel 6: // you must manually install and specify the presets and plugins yourself options: { plugins: [ - "transform-object-assign", // required (for IE support) and "babel-plugin-transform-object-assign" + "transform-object-assign", // required (for IE support) and "babel-plugin-transform-object-assign" // must be installed as part of your package. require('prebid.js/plugins/pbjsGlobals.js') // required! ], @@ -79,7 +79,7 @@ Or for Babel 6: ["env", { // you can use other presets if you wish. "targets": { // this example is using "babel-presets-env", which must be installed if you "browsers": [ // follow this example. - ... // your browser targets. they should probably match the targets you're using for the rest + ... // your browser targets. they should probably match the targets you're using for the rest // of your application ] } @@ -143,7 +143,7 @@ This will run testing but not linting. A web server will start at `http://localh Development may be a bit slower but if you prefer linting and additional watch files you can also still run just: - $ gulp serve + $ gulp serve ### Build Optimization @@ -162,11 +162,11 @@ Building with just these adapters will result in a smaller bundle which should a - Then run the build: $ gulp build --modules=openxBidAdapter,rubiconBidAdapter,sovrnBidAdapter - + Alternatively, a `.json` file can be specified that contains a list of modules you would like to include. $ gulp build --modules=modules.json - + With `modules.json` containing the following ```json modules.json [ @@ -202,7 +202,7 @@ gulp bundle --tag one --modules=one.json gulp bundle --tag two --modules=two.json ``` -This generates slightly larger files, but has the advantage of being much faster to run (after the initial `gulp build`). It's also the method used by [the Prebid.org download page](https://docs.prebid.org/download.html). +This generates slightly larger files, but has the advantage of being much faster to run (after the initial `gulp build`). It's also the method used by [the Prebid.org download page](https://docs.prebid.org/download.html). @@ -227,7 +227,34 @@ Or, if you are consuming Prebid through npm, with the `disableFeatures` option i } ``` -**Note**: this is still a work in progress - at the moment, `NATIVE` is the only feature that can be disabled this way, resulting in a minimal decrease in size (but you can expect that to improve over time). +Features that can be disabled this way are: + + - `VIDEO` - support for video bids; + - `NATIVE` - support for native bids; + - `UID2_CSTG` - support for UID2 client side token generation (see [Unified ID 2.0](https://docs.prebid.org/dev-docs/modules/userid-submodules/unified2.html)) + - `GREEDY` - disables the use blocking, "greedy" promises within Prebid (see below). + +#### Greedy promises + +By default, Prebid attempts to hold control of the main thread when possible, using a [custom implementation of `Promise`](https://github.com/prebid/Prebid.js/blob/master/libraries/greedy/greedyPromise.js) that does not submit callbacks to the scheduler once the promise is resolved (running them immediately instead). +Disabling this behavior instructs Prebid to use the standard `window.Promise` instead; this has the effect of breaking up task execution, making them slower overall but giving the browser more chances to run other tasks in between, which can improve UX. + +You may also override the `Promise` constructor used by Prebid through `pbjs.Promise`, for example: + +```javascript +var pbjs = pbjs || {}; +pbjs.Promise = myCustomPromiseConstructor; +``` + +## Unminified code + +You can get a version of the code that's unminified for debugging with `build-bundle-dev`: + +```bash +gulp build-bundle-dev --modules=bidderA,module1,... +``` + +The results will be in build/dev/prebid.js. ## Test locally @@ -364,11 +391,11 @@ The results will be in *Note*: Starting in June 2016, all pull requests to Prebid.js need to include tests with greater than 80% code coverage before they can be merged. For more information, see [#421](https://github.com/prebid/Prebid.js/issues/421). -For instructions on writing tests for Prebid.js, see [Testing Prebid.js](http://prebid.org/dev-docs/testing-prebid.html). +For instructions on writing tests for Prebid.js, see [Testing Prebid.js](https://prebid.org/dev-docs/testing-prebid.html). ### Supported Browsers -Prebid.js is supported on IE11 and modern browsers until 5.x. 6.x+ transpiles to target >0.25%; not Opera Mini; not IE11. +Prebid.js is supported on IE11 and modern browsers until 5.x. 6.x+ transpiles to target >0.25%; not Opera Mini; not IE11. ### Governance Review our governance model [here](https://github.com/prebid/Prebid.js/tree/master/governance.md). diff --git a/allowedModules.js b/allowedModules.js deleted file mode 100644 index 75ad4141a6c..00000000000 --- a/allowedModules.js +++ /dev/null @@ -1,18 +0,0 @@ - -module.exports = { - 'modules': [ - 'criteo-direct-rsa-validate', - 'crypto-js', - 'live-connect' // Maintained by LiveIntent : https://github.com/liveintent-berlin/live-connect/ - ], - 'src': [ - 'fun-hooks/no-eval', - 'just-clone', - 'dlv', - 'dset' - ], - 'libraries': [ - ], - 'creative': [ - ] -}; diff --git a/browsers.json b/browsers.json index bd6bd5772d6..0649a13e873 100644 --- a/browsers.json +++ b/browsers.json @@ -1,39 +1,39 @@ { - "bs_edge_latest_windows_10": { + "bs_edge_latest_windows_11": { "base": "BrowserStack", - "os_version": "10", + "os_version": "11", "browser": "edge", "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_chrome_latest_windows_10": { + "bs_chrome_latest_windows_11": { "base": "BrowserStack", - "os_version": "10", + "os_version": "11", "browser": "chrome", "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_chrome_87_windows_10": { + "bs_chrome_107_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "chrome", - "browser_version": "87.0", + "browser_version": "107.0", "device": null, "os": "Windows" }, - "bs_firefox_latest_windows_10": { + "bs_firefox_latest_windows_11": { "base": "BrowserStack", - "os_version": "10", + "os_version": "11", "browser": "firefox", "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_safari_latest_mac_bigsur": { + "bs_safari_latest_mac": { "base": "BrowserStack", - "os_version": "Big Sur", + "os_version": "Sonoma", "browser": "safari", "browser_version": "latest", "device": null, @@ -41,11 +41,11 @@ }, "bs_safari_15_catalina": { "base": "BrowserStack", - "os_version": "Catalina", + "os_version": "Monterey", "browser": "safari", - "browser_version": "13.1", + "browser_version": "15.6", "device": null, "os": "OS X" } - + } diff --git a/creative/constants.js b/creative/constants.js index d02c4c9d5e4..5748324f02c 100644 --- a/creative/constants.js +++ b/creative/constants.js @@ -1,6 +1,9 @@ + // eslint-disable-next-line prebid/validate-imports -import { AD_RENDER_FAILED_REASON, EVENTS, MESSAGES } from '../src/constants.js'; +import {AD_RENDER_FAILED_REASON, EVENTS, MESSAGES} from '../src/constants.js'; +// eslint-disable-next-line prebid/validate-imports +export {PB_LOCATOR} from '../src/constants.js'; export const MESSAGE_REQUEST = MESSAGES.REQUEST; export const MESSAGE_RESPONSE = MESSAGES.RESPONSE; export const MESSAGE_EVENT = MESSAGES.EVENT; diff --git a/creative/crossDomain.js b/creative/crossDomain.js index a851885bfc0..d3524f61d4b 100644 --- a/creative/crossDomain.js +++ b/creative/crossDomain.js @@ -1,9 +1,11 @@ import { ERROR_EXCEPTION, - EVENT_AD_RENDER_FAILED, EVENT_AD_RENDER_SUCCEEDED, + EVENT_AD_RENDER_FAILED, + EVENT_AD_RENDER_SUCCEEDED, MESSAGE_EVENT, MESSAGE_REQUEST, - MESSAGE_RESPONSE + MESSAGE_RESPONSE, + PB_LOCATOR } from './constants.js'; const mkFrame = (() => { @@ -24,14 +26,27 @@ const mkFrame = (() => { }; })(); +function isPrebidWindow(win) { + return !!win.frames[PB_LOCATOR]; +} + export function renderer(win) { + let target = win.parent; + try { + while (target !== win.top && !isPrebidWindow(target)) { + target = target.parent; + } + if (!isPrebidWindow(target)) target = win.parent; + } catch (e) { + } + return function ({adId, pubUrl, clickUrl}) { const pubDomain = new URL(pubUrl, window.location).origin; function sendMessage(type, payload, responseListener) { const channel = new MessageChannel(); channel.port1.onmessage = guard(responseListener); - win.parent.postMessage(JSON.stringify(Object.assign({message: type, adId}, payload)), pubDomain, [channel.port2]); + target.postMessage(JSON.stringify(Object.assign({message: type, adId}, payload)), pubDomain, [channel.port2]); } function onError(e) { @@ -77,7 +92,7 @@ export function renderer(win) { W.Promise.resolve(W.render(data, {sendMessage, mkFrame}, win)).then( () => sendMessage(MESSAGE_EVENT, {event: EVENT_AD_RENDER_SUCCEEDED}), onError - ) + ); }); win.document.body.appendChild(renderer); } diff --git a/creative/renderers/display/renderer.js b/creative/renderers/display/renderer.js index e031679b116..5a493bf1249 100644 --- a/creative/renderers/display/renderer.js +++ b/creative/renderers/display/renderer.js @@ -1,20 +1,30 @@ import {ERROR_NO_AD} from './constants.js'; -export function render({ad, adUrl, width, height}, {mkFrame}, win) { +export function render({ad, adUrl, width, height, instl}, {mkFrame}, win) { if (!ad && !adUrl) { throw { reason: ERROR_NO_AD, message: 'Missing ad markup or URL' }; } else { + if (height == null) { + const body = win.document?.body; + [body, body?.parentElement].filter(elm => elm?.style != null).forEach(elm => elm.style.height = '100%'); + } const doc = win.document; - const attrs = {width, height}; + const attrs = {width: width ?? '100%', height: height ?? '100%'}; if (adUrl && !ad) { attrs.src = adUrl; } else { attrs.srcdoc = ad; } doc.body.appendChild(mkFrame(doc, attrs)); + if (instl && win.frameElement) { + // interstitials are rendered in a nested iframe that needs to be sized + const style = win.frameElement.style; + style.width = width ? `${width}px` : '100vw'; + style.height = height ? `${height}px` : '100vh'; + } } } diff --git a/creative/renderers/native/renderer.js b/creative/renderers/native/renderer.js index 5cc8f100108..f7c124b41eb 100644 --- a/creative/renderers/native/renderer.js +++ b/creative/renderers/native/renderer.js @@ -45,6 +45,16 @@ function loadScript(url, doc) { }); } +function getRenderFrames(node) { + return Array.from(node.querySelectorAll('iframe[srcdoc*="render"]')) +} + +function getInnerHTML(node) { + const clone = node.cloneNode(true); + getRenderFrames(clone).forEach(node => node.parentNode.removeChild(node)); + return clone.innerHTML; +} + export function getAdMarkup(adId, nativeData, replacer, win, load = loadScript) { const {rendererUrl, assets, ortb, adTemplate} = nativeData; const doc = win.document; @@ -58,21 +68,32 @@ export function getAdMarkup(adId, nativeData, replacer, win, load = loadScript) return win.renderAd(payload); }); } else { - return Promise.resolve(replacer(adTemplate ?? doc.body.innerHTML)); + return Promise.resolve(replacer(adTemplate ?? getInnerHTML(doc.body))); } } export function render({adId, native}, {sendMessage}, win, getMarkup = getAdMarkup) { const {head, body} = win.document; - const resize = () => sendMessage(MESSAGE_NATIVE, { - action: ACTION_RESIZE, - height: body.offsetHeight, - width: body.offsetWidth - }); + const resize = () => { + // force redraw - for some reason this is needed to get the right dimensions + body.style.display = 'none'; + body.style.display = 'block'; + sendMessage(MESSAGE_NATIVE, { + action: ACTION_RESIZE, + height: body.offsetHeight, + width: body.offsetWidth + }); + } + function replaceMarkup(target, markup) { + // do not remove the rendering logic if it's embedded in this window; things will break otherwise + const renderFrames = getRenderFrames(target); + Array.from(target.childNodes).filter(node => !renderFrames.includes(node)).forEach(node => target.removeChild(node)); + target.insertAdjacentHTML('afterbegin', markup); + } const replacer = getReplacer(adId, native); - head && (head.innerHTML = replacer(head.innerHTML)); + replaceMarkup(head, replacer(getInnerHTML(head))); return getMarkup(adId, native, replacer, win).then(markup => { - body.innerHTML = markup; + replaceMarkup(body, markup); if (typeof win.postRenderAd === 'function') { win.postRenderAd({adId, ...native}); } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000000..e683c044e71 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,248 @@ +const jsdoc = require('eslint-plugin-jsdoc') +const lintImports = require('eslint-plugin-import') +const neostandard = require('neostandard') +const babelParser = require('@babel/eslint-parser'); +const globals = require('globals'); +const prebid = require('./plugins/eslint/index.js'); +const {includeIgnoreFile} = require('@eslint/compat'); +const path = require('path'); +const _ = require('lodash'); + +function sourcePattern(name) { + return [`${name}/**/*.js`, `${name}/**/*.mjs`] +} + +const sources = ['src', 'modules', 'libraries', 'creative'].flatMap(sourcePattern) +const autogen = 'libraries/creative-renderer-*/**/*' + +const allowedImports = { + modules: [ + 'crypto-js', + 'live-connect' // Maintained by LiveIntent : https://github.com/liveintent-berlin/live-connect/ + ], + src: [ + 'fun-hooks/no-eval', + 'klona', + 'dlv', + 'dset' + ], + libraries: [], + creative: [], +} + +function noGlobals(names) { + return { + globals: Object.entries(names).map(([name, message]) => ({ + name, + message + })), + props: Object.entries(names).map(([name, message]) => ({ + object: 'window', + property: name, + message + })) + } +} + +function commonConfig(overrides) { + return _.merge({ + plugins: { + jsdoc, + import: lintImports, + prebid + }, + settings: { + jsdoc: { + tagNamePreference: { + return: 'return' + } + } + }, + languageOptions: { + parser: babelParser, + sourceType: 'module', + ecmaVersion: 2018, + globals: { + BROWSERSTACK_USERNAME: false, + BROWSERSTACK_KEY: false, + FEATURES: 'readonly', + ...globals.browser + }, + }, + rules: { + 'comma-dangle': 'off', + semi: 'off', + 'space-before-function-paren': 'off', + 'import/extensions': ['error', 'ignorePackages'], + + // Exceptions below this line are temporary (TM), so that eslint can be added into the CI process. + // Violations of these styles should be fixed, and the exceptions removed over time. + // + // See Issue #1111. + // also see: reality. These are here to stay. + + eqeqeq: 'off', + 'no-return-assign': 'off', + 'no-throw-literal': 'off', + 'no-undef': 2, + 'no-useless-escape': 'off', + 'no-console': 'error', + 'jsdoc/check-types': 'off', + 'jsdoc/no-defaults': 'off', + 'jsdoc/newline-after-description': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-param-name': 'off', + 'jsdoc/require-param-type': 'off', + 'jsdoc/require-property': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-property-name': 'off', + 'jsdoc/require-property-type': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/require-returns-check': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-returns-type': 'off', + 'jsdoc/require-yields': 'off', + 'jsdoc/require-yields-check': 'off', + 'jsdoc/tag-lines': 'off', + 'no-var': 'off', + 'no-empty': 'off', + 'no-void': 'off', + 'array-callback-return': 'off', + 'import-x/no-named-default': 'off', + 'prefer-const': 'off', + 'no-prototype-builtins': 'off', + 'object-shorthand': 'off', + 'prefer-regex-literals': 'off', + 'no-case-declarations': 'off', + 'no-useless-catch': 'off', + '@stylistic/quotes': 'off', + '@stylistic/quote-props': 'off', + '@stylistic/array-bracket-spacing': 'off', + '@stylistic/object-curly-spacing': 'off', + '@stylistic/semi': 'off', + '@stylistic/space-before-function-paren': 'off', + '@stylistic/multiline-ternary': 'off', + '@stylistic/computed-property-spacing': 'off', + '@stylistic/lines-between-class-members': 'off', + '@stylistic/indent': 'off', + '@stylistic/comma-dangle': 'off', + '@stylistic/object-curly-newline': 'off', + '@stylistic/object-property-newline': 'off', + '@stylistic/no-multiple-empty-lines': 'off', + + } + }, overrides); +} + +module.exports = [ + includeIgnoreFile(path.resolve(__dirname, '.gitignore')), + { + ignores: [ + autogen, + 'integrationExamples/**/*', + // do not lint build-related stuff + '*.js', + ...sourcePattern('plugins'), + ...sourcePattern('.github'), + ], + }, + jsdoc.configs['flat/recommended'], + ...neostandard({ + files: sources, + }), + commonConfig({ + files: sources, + }), + ...Object.entries(allowedImports).map(([path, allowed]) => { + const {globals, props} = noGlobals({ + require: 'use import instead', + ...Object.fromEntries(['localStorage', 'sessionStorage'].map(k => [k, 'use storageManager instead'])), + XMLHttpRequest: 'use ajax.js instead' + }) + return commonConfig({ + files: sourcePattern(path), + plugins: { + prebid, + }, + rules: { + 'prebid/validate-imports': ['error', allowed], + 'no-restricted-globals': [ + 'error', + ...globals + ], + 'no-restricted-properties': [ + 'error', + ...props, + { + property: 'cookie', + object: 'document', + message: 'use storageManager instead' + }, + { + property: 'sendBeacon', + object: 'navigator', + message: 'use ajax.js instead' + }, + ...['outerText', 'innerText'].map(property => ({ + property, + message: 'use .textContent instead' + })), + { + property: 'getBoundingClientRect', + message: 'use libraries/boundingClientRect instead' + }, + ...['scrollTop', 'scrollLeft', 'innerHeight', 'innerWidth', 'visualViewport'].map((property) => ({ + object: 'window', + property, + message: 'use utils/getWinDimensions instead' + })) + ] + } + }) + }), + { + files: ['**/*BidAdapter.js'], + rules: { + 'no-restricted-imports': [ + 'error', { + patterns: [ + '**/src/events.js', + '**/src/adloader.js' + ] + } + ] + } + }, + commonConfig({ + files: sourcePattern('test'), + languageOptions: { + globals: { + ...globals.mocha, + ...globals.chai, + 'sinon': false + } + }, + rules: { + // tests were not subject to many rules and they are now a nightmare + 'no-template-curly-in-string': 'off', + 'no-unused-expressions': 'off', + 'one-var': 'off', + 'no-undef': 'off', + 'no-unused-vars': 'off', + 'import/extensions': 'off', + 'camelcase': 'off', + 'no-array-constructor': 'off', + 'import-x/no-duplicates': 'off', + 'import-x/no-absolute-path': 'off', + 'no-loss-of-precision': 'off', + 'no-redeclare': 'off', + 'no-global-assign': 'off', + 'default-case-last': 'off', + '@stylistic/no-mixed-spaces-and-tabs': 'off', + '@stylistic/no-tabs': 'off', + '@stylistic/no-trailing-spaces': 'off' + } + }) +] diff --git a/features.json b/features.json index 4d8377cda7d..ee89ea6abaf 100644 --- a/features.json +++ b/features.json @@ -1,5 +1,6 @@ [ "NATIVE", "VIDEO", - "UID2_CSTG" + "UID2_CSTG", + "GREEDY" ] diff --git a/gulpfile.js b/gulpfile.js index 86c1b7fe509..608c9f67819 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,7 +16,6 @@ var helpers = require('./gulpHelpers.js'); var concat = require('gulp-concat'); var replace = require('gulp-replace'); var shell = require('gulp-shell'); -var eslint = require('gulp-eslint'); var gulpif = require('gulp-if'); var sourcemaps = require('gulp-sourcemaps'); var through = require('through2'); @@ -28,6 +27,7 @@ const {minify} = require('terser'); const Vinyl = require('vinyl'); const wrap = require('gulp-wrap'); const rename = require('gulp-rename'); +const run = require('gulp-run-command').default; var prebid = require('./package.json'); var port = 9999; @@ -48,7 +48,7 @@ function bundleToStdout() { bundleToStdout.displayName = 'bundle-to-stdout'; function clean() { - return gulp.src(['build'], { + return gulp.src(['build', 'dist'], { read: false, allowEmpty: true }) @@ -79,23 +79,18 @@ function lint(done) { if (argv.nolint) { return done(); } - const isFixed = function (file) { - return file.eslint != null && file.eslint.fixed; + const args = ['eslint']; + if (!argv.nolintfix) { + args.push('--fix'); } - return gulp.src([ - 'src/**/*.js', - 'modules/**/*.js', - 'libraries/**/*.js', - 'creative/**/*.js', - 'test/**/*.js', - 'plugins/**/*.js', - '!plugins/**/node_modules/**', - './*.js' - ], { base: './' }) - .pipe(eslint({ fix: !argv.nolintfix, quiet: !(typeof argv.lintWarnings === 'boolean' ? argv.lintWarnings : true) })) - .pipe(eslint.format('stylish')) - .pipe(eslint.failAfterError()) - .pipe(gulpif(isFixed, gulp.dest('./'))); + if (!(typeof argv.lintWarnings === 'boolean' ? argv.lintWarnings : true)) { + args.push('--quiet') + } + return run(args.join(' '))().then(() => { + done(); + }, (err) => { + done(err); + }); }; // View the code coverage report in the browser. @@ -147,6 +142,17 @@ function makeVerbose(config = webpackConfig) { }); } +function prebidSource(webpackCfg) { + var externalModules = helpers.getArgModules(); + + const analyticsSources = helpers.getAnalyticsSources(); + const moduleSources = helpers.getModulePaths(externalModules); + + return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) + .pipe(helpers.nameModules(externalModules)) + .pipe(webpackStream(webpackCfg, webpack)); +} + function makeDevpackPkg(config = webpackConfig) { return function() { var cloned = _.cloneDeep(config); @@ -163,14 +169,7 @@ function makeDevpackPkg(config = webpackConfig) { .filter((use) => use.loader === 'babel-loader') .forEach((use) => use.options = Object.assign({}, use.options, babelConfig)); - var externalModules = helpers.getArgModules(); - - const analyticsSources = helpers.getAnalyticsSources(); - const moduleSources = helpers.getModulePaths(externalModules); - - return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) - .pipe(helpers.nameModules(externalModules)) - .pipe(webpackStream(cloned, webpack)) + return prebidSource(cloned) .pipe(gulp.dest('build/dev')) .pipe(connect.reload()); } @@ -183,14 +182,7 @@ function makeWebpackPkg(config = webpackConfig) { } return function buildBundle() { - var externalModules = helpers.getArgModules(); - - const analyticsSources = helpers.getAnalyticsSources(); - const moduleSources = helpers.getModulePaths(externalModules); - - return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) - .pipe(helpers.nameModules(externalModules)) - .pipe(webpackStream(cloned, webpack)) + return prebidSource(cloned) .pipe(gulp.dest('build/dist')); } } @@ -337,6 +329,16 @@ function bundle(dev, moduleArr) { .pipe(gulpif(sm, sourcemaps.write('.'))); } +function setupDist() { + return gulp.src(['build/dist/**/*']) + .pipe(rename(function (path) { + if (path.dirname === '.' && path.basename === 'prebid') { + path.dirname = 'not-for-prod'; + } + })) + .pipe(gulp.dest('dist')) +} + // Run the unit tests. // // By default, this runs in headless chrome. @@ -413,7 +415,9 @@ function runKarma(options, done) { // the karma server appears to leak memory; starting it multiple times in a row will run out of heap // here we run it in a separate process to bypass the problem options = Object.assign({browsers: helpers.parseBrowserArgs(argv)}, options) - const child = fork('./karmaRunner.js'); + const child = fork('./karmaRunner.js', null, { + env: Object.assign({}, options.env, process.env) + }); child.on('exit', (exitCode) => { if (exitCode) { done(new Error('Karma tests failed with exit code ' + exitCode)); @@ -426,7 +430,15 @@ function runKarma(options, done) { // If --file "" is given, the task will only run tests in the specified file. function testCoverage(done) { - runKarma({coverage: true, browserstack: false, watch: false, file: argv.file}, done); + runKarma({ + coverage: true, + browserstack: false, + watch: false, + file: argv.file, + env: { + NODE_OPTIONS: '--max-old-space-size=8096' + } + }, done); } function coveralls() { // 2nd arg is a dependency: 'test' must be finished @@ -521,7 +533,7 @@ gulp.task('build-bundle-dev', gulp.series('build-creative-dev', makeDevpackPkg(s gulp.task('build-bundle-prod', gulp.series('build-creative-prod', makeWebpackPkg(standaloneDebuggingConfig), makeWebpackPkg(), gulpBundle.bind(null, false))); // build-bundle-verbose - prod bundle except names and comments are preserved. Use this to see the effects // of dead code elimination. -gulp.task('build-bundle-verbose', gulp.series('build-creative-dev', makeWebpackPkg(makeVerbose(standaloneDebuggingConfig)), makeWebpackPkg(makeVerbose()), gulpBundle.bind(null, true))); +gulp.task('build-bundle-verbose', gulp.series('build-creative-dev', makeWebpackPkg(makeVerbose(standaloneDebuggingConfig)), makeWebpackPkg(makeVerbose()), gulpBundle.bind(null, false))); // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('test-only', test); @@ -533,7 +545,9 @@ gulp.task(viewCoverage); gulp.task('coveralls', gulp.series('test-coverage', coveralls)); -gulp.task('build', gulp.series(clean, 'build-bundle-prod', updateCreativeExample)); +// npm will by default use .gitignore, so create an .npmignore that is a copy of it except it includes "dist" +gulp.task('setup-npmignore', run("sed 's/^\\/\\?dist\\/\\?$//g;w .npmignore' .gitignore", {quiet: true})); +gulp.task('build', gulp.series(clean, 'build-bundle-prod', updateCreativeExample, setupDist, 'setup-npmignore')); gulp.task('build-postbid', gulp.series(escapePostbidConfig, buildPostbid)); gulp.task('serve', gulp.series(clean, lint, gulp.parallel('build-bundle-dev', watch, test))); diff --git a/integrationExamples/gpt/51DegreesRtdProvider_example.html b/integrationExamples/gpt/51DegreesRtdProvider_example.html new file mode 100644 index 00000000000..7864f2e05f5 --- /dev/null +++ b/integrationExamples/gpt/51DegreesRtdProvider_example.html @@ -0,0 +1,195 @@ + + + + + + + + + + + + 51Degrees RTD submodule example - Prebid.js + + +

51Degrees RTD submodule - example of usage

+ +

div-banner-native-1

+
+

No response

+ +
+ +

div-banner-native-2

+
+

No response

+ +
+ +
+

Testing/Debugging Guidance

+
    +
  1. Make sure you have debug: true under pbjs.setConfig in this example code (be sure to remove it for production!) +
  2. Make sure you have replaced <YOUR RESOURCE KEY> in this example code with the one you have obtained + from the 51Degrees Configurator Tool
  3. +
  4. Open DevTools Console in your browser and refresh the page
  5. +
  6. Observe the enriched ortb device data shown below and also in the console as part of the [51Degrees RTD Submodule]: reqBidsConfigObj: message (under reqBidsConfigObj.global.device)
  7. +
+
+ + + diff --git a/integrationExamples/gpt/adnuntius_multiformat_example.html b/integrationExamples/gpt/adnuntius_multiformat_example.html new file mode 100644 index 00000000000..87b30d5887a --- /dev/null +++ b/integrationExamples/gpt/adnuntius_multiformat_example.html @@ -0,0 +1,132 @@ + + + + + + + +

Adnuntius NATIVE

+
Ad Slot 1
+ + +
+ +
+ + + diff --git a/integrationExamples/gpt/anonymised_segments_example.html b/integrationExamples/gpt/anonymised_segments_example.html index 16f3f879636..baa47ab84f4 100644 --- a/integrationExamples/gpt/anonymised_segments_example.html +++ b/integrationExamples/gpt/anonymised_segments_example.html @@ -56,7 +56,10 @@ params: { cohortStorageKey: "cohort_ids", bidders: ['smartadserver', 'appnexus'], - segtax: 1000 + segtax: 1000, + tagConfig: { + clientId: 'testId' + } } } ] diff --git a/integrationExamples/gpt/contxtfulRtdProvider_example.html b/integrationExamples/gpt/contxtfulRtdProvider_example.html index 29284de81a2..e47bd4142f9 100644 --- a/integrationExamples/gpt/contxtfulRtdProvider_example.html +++ b/integrationExamples/gpt/contxtfulRtdProvider_example.html @@ -1,91 +1,212 @@ + - - - - + +Contxtful Rtd Provider Example + - +googletag.cmd.push(function () { + googletag + .defineSlot('/19968336/header-bid-tag-1', + div2Sizes, 'div-2') + .addService(googletag.pubads()); + googletag.pubads().enableSingleRequest(); + googletag.enableServices(); +}); + + -

Contxtful RTD Provider

-
- - +

Contxtful Rtd Provider Example

+ +

+ +
Div-1
+
+ +
+ +
+ +
Div-2
+
+ +
+ + diff --git a/integrationExamples/gpt/creative_rendering.html b/integrationExamples/gpt/creative_rendering.html deleted file mode 100644 index 04d4736c631..00000000000 --- a/integrationExamples/gpt/creative_rendering.html +++ /dev/null @@ -1,15 +0,0 @@ - - diff --git a/integrationExamples/gpt/cstg_example.html b/integrationExamples/gpt/cstg_example.html index 8ca049a0ed0..2ca8b43b9de 100644 --- a/integrationExamples/gpt/cstg_example.html +++ b/integrationExamples/gpt/cstg_example.html @@ -29,8 +29,8 @@ function updateUID2GuiElements() { console.log('Updating displayed values.'); const uid2 = pbjs.getUserIds().uid2; - document.querySelector('#uid2TargetedAdvertisingReady').innerText = uid2 ? 'yes' : 'no'; - document.querySelector('#uid2AdvertisingToken').innerText = uid2 ? String(uid2.id) : ''; + document.querySelector('#uid2TargetedAdvertisingReady').innerText = uid2 && !uid2.optout ? 'yes' : 'no'; + document.querySelector('#uid2AdvertisingToken').innerText = uid2 && !uid2.optout ? String(uid2.id) : uid2 && uid2.optout ? 'Optout' : ''; if (!uid2) { document.querySelector('#uid2LoginForm').style['display'] = 'block'; document.querySelector('#uid2ClearStorageForm').style['display'] = 'none';; diff --git a/integrationExamples/gpt/hello_world.html b/integrationExamples/gpt/hello_world.html index 03a2356f0ef..bcf0c2fa1f2 100644 --- a/integrationExamples/gpt/hello_world.html +++ b/integrationExamples/gpt/hello_world.html @@ -92,4 +92,4 @@
Div-1
- \ No newline at end of file + diff --git a/integrationExamples/gpt/id_lift_measurement.html b/integrationExamples/gpt/id_lift_measurement.html new file mode 100644 index 00000000000..fef5fd224ac --- /dev/null +++ b/integrationExamples/gpt/id_lift_measurement.html @@ -0,0 +1,173 @@ + + + + + Measure Lift of Multiple ID Modules + + + + + + + + + + +

Measure Lift of Multiple ID Modules

+ +

Generated IDs:

+

+
+    

Generated EIDs:

+

+
+    
+    

Instructions

+
    +
  1. Ensure that the `abg` key is definied in GAM targeting with all possible keys. Each value will be a combination of the following six possible key-value pairs: +
      +
    • id1:t0
    • +
    • id1:t1
    • +
    • id2:t0
    • +
    • id2:t1
    • +
    • id3:t0
    • +
    • id3:t1
    • +
    +
  2. +
  3. In Google Ad Manager (GAM), create a report with the following setup: +
      +
    • Dimensions: Ad Unit, Key-Value Targeting (`abg`).
    • +
    • Metrics: Impressions, Revenue.
    • +
    • Filters: Include the `abg` key in the report.
    • +
    +
  4. +
  5. Analyze the report for each ID module: +
      +
    • Filter by combinations of `t1` (treatment) and `t0` (control) for each ID module (e.g., `id1:t1`, `id1:t0`).
    • +
    • Compare performance metrics (eg Impressions, Revenue) for the `t1` vs. `t0` values.
    • +
    • Calculate lift for each module using the formula: +
      Lift (%) = ((Treatment Metric / Treatment Rate - Control Metric / Control Rate) / (Control Metric / Control Rate)) * 100
      + Replace "Metric" with the relevant performance metric. +
    • +
    +
  6. +
+ + + diff --git a/integrationExamples/gpt/liveIntentRtdProviderExample.html b/integrationExamples/gpt/liveIntentRtdProviderExample.html new file mode 100644 index 00000000000..489df86f7a7 --- /dev/null +++ b/integrationExamples/gpt/liveIntentRtdProviderExample.html @@ -0,0 +1,164 @@ + + + + + + + + +

Basic Prebid.js Example

+
Div-1
+
+ +
+
+
Div-2
+
+ +
+ + diff --git a/integrationExamples/gpt/localCacheGam.html b/integrationExamples/gpt/localCacheGam.html new file mode 100644 index 00000000000..ce0299e7036 --- /dev/null +++ b/integrationExamples/gpt/localCacheGam.html @@ -0,0 +1,134 @@ + + + + + + + JW Player with Local Cache + + + + + + +

JW Player with Local cache

+ +
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/gpt/nexverse.html b/integrationExamples/gpt/nexverse.html new file mode 100644 index 00000000000..498cf2bd2f3 --- /dev/null +++ b/integrationExamples/gpt/nexverse.html @@ -0,0 +1,126 @@ + + + + + + NexVerse Prebid.Js Demo + + + + + + + + + + +

Nexverse Prebid.js Test

+
Div-1
+
+ +
+ +
Div-2
+
+ +
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/optableRtdProvider_example.html b/integrationExamples/gpt/optableRtdProvider_example.html new file mode 100644 index 00000000000..f539fe90554 --- /dev/null +++ b/integrationExamples/gpt/optableRtdProvider_example.html @@ -0,0 +1,274 @@ + + + + Optable RTD submodule example - Prebid.js + + + + + + + + + + + + +
+
+ optable +
+
+
+

Optable RTD module example

+
+ +

web-sdk-demo-gam360/header-ad

+
+

No response

+ +
+ +

web-sdk-demo-gam360/box-ad

+
+

No response

+ +
+ +

web-sdk-demo-gam360/footer-ad

+
+

No response

+ +
+ + +
+ + diff --git a/integrationExamples/gpt/fledge_example.html b/integrationExamples/gpt/paapi_example.html similarity index 97% rename from integrationExamples/gpt/fledge_example.html rename to integrationExamples/gpt/paapi_example.html index 5a6ab7a5fef..860d7c22edf 100644 --- a/integrationExamples/gpt/fledge_example.html +++ b/integrationExamples/gpt/paapi_example.html @@ -3,7 +3,7 @@ diff --git a/integrationExamples/gpt/prebidServer_fledge_example.html b/integrationExamples/gpt/prebidServer_paapi_example.html similarity index 91% rename from integrationExamples/gpt/prebidServer_fledge_example.html rename to integrationExamples/gpt/prebidServer_paapi_example.html index eb2fc438997..d138d2b7753 100644 --- a/integrationExamples/gpt/prebidServer_fledge_example.html +++ b/integrationExamples/gpt/prebidServer_paapi_example.html @@ -3,7 +3,7 @@ @@ -44,8 +44,8 @@ pbjs.que.push(function() { pbjs.setConfig({ - fledgeForGpt: { - enabled: true + paapi: { + enabled: true, }, s2sConfig: [{ accountId : '1', @@ -57,13 +57,6 @@ }] }); - pbjs.setBidderConfig({ - bidders: ['openx'], - config: { - fledgeEnabled: true - } - }); - pbjs.addAdUnits(adUnits); pbjs.requestBids({ diff --git a/integrationExamples/gpt/precisoExample.html b/integrationExamples/gpt/precisoExample.html new file mode 100644 index 00000000000..04f44b28345 --- /dev/null +++ b/integrationExamples/gpt/precisoExample.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + + +

Basic Prebid.js Example with Preciso Bidder

+

Adslot-1

+
+ +
+ +
+

Adslot-2

+ +
+ + + + diff --git a/integrationExamples/gpt/precisonativeExample.html b/integrationExamples/gpt/precisonativeExample.html new file mode 100644 index 00000000000..4e7009e2c58 --- /dev/null +++ b/integrationExamples/gpt/precisonativeExample.html @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + +

Ad Serverless Test Page

+

+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's + standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make + a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing + Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions + of Lorem Ipsum +

+
+

+ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin + literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney + College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and + going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum + comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by + Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. + The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32. +

+
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/raynRtdProvider_example.html b/integrationExamples/gpt/raynRtdProvider_example.html index 2d43c37513a..7965daa6e85 100644 --- a/integrationExamples/gpt/raynRtdProvider_example.html +++ b/integrationExamples/gpt/raynRtdProvider_example.html @@ -6,6 +6,7 @@ "3": ["264", "267", "261"], "4": ["438"] }, + "103015": ['agdv23', 'avscg3'], "903555595": { "7": { "2": ["51", "246"] diff --git a/integrationExamples/gpt/rewardedInterestIdSystem_example.html b/integrationExamples/gpt/rewardedInterestIdSystem_example.html new file mode 100644 index 00000000000..c9730f354b3 --- /dev/null +++ b/integrationExamples/gpt/rewardedInterestIdSystem_example.html @@ -0,0 +1,114 @@ + + + + + Rewarded Interest ID Example + + + + + + + + + + + + + +

Rewarded Interest ID Example

+ +

Generated IDs:

+

+
+

Generated EIDs

+

+
+
+
diff --git a/integrationExamples/gpt/idward_segments_example.html b/integrationExamples/gpt/symitridap_segments_example.html
similarity index 82%
rename from integrationExamples/gpt/idward_segments_example.html
rename to integrationExamples/gpt/symitridap_segments_example.html
index 9bc06124c77..1f5a654cfdb 100644
--- a/integrationExamples/gpt/idward_segments_example.html
+++ b/integrationExamples/gpt/symitridap_segments_example.html
@@ -1,7 +1,7 @@
 
 
     
-    
+    
     
 
-
First Party Data (ortb2) Sent to Bidding Adapter
+
Segments Sent to Bidding Adapter
diff --git a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html deleted file mode 100644 index 9a4991d2711..00000000000 --- a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - -

Standalone PAAPI Prebid.js Example

-

Start local server with:

-gulp serve-fast --https -

Chrome flags:

---enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true - --privacy-sandbox-enrollment-overrides=https://localhost:9999 -

Join interest group at https://privacysandbox.openx.net/fledge/advertiser -

-
Div-1
-
- -
- - - diff --git a/integrationExamples/gpt/weboramaRtdProvider_example.html b/integrationExamples/gpt/weboramaRtdProvider_example.html index 7e75721103f..82b97696371 100644 --- a/integrationExamples/gpt/weboramaRtdProvider_example.html +++ b/integrationExamples/gpt/weboramaRtdProvider_example.html @@ -1,187 +1,201 @@ - - - - weborama rtd submodule example - - - - + + + + weborama rtd submodule example + + +
-

- test webo rtd submodule with prebid.js -

+

test webo rtd submodule with prebid.js

Basic Prebid.js Example

Div-1
-
- +
+
- - - - \ No newline at end of file + + diff --git a/integrationExamples/gpt/wurflRtdProvider_example.html b/integrationExamples/gpt/wurflRtdProvider_example.html new file mode 100644 index 00000000000..f2bfe7f76b7 --- /dev/null +++ b/integrationExamples/gpt/wurflRtdProvider_example.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index 0d0f63cbf1b..d7b97b93baa 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -2,12 +2,12 @@ // creative will be rendered, e.g. GAM delivering a SafeFrame // this code is autogenerated, also available in 'build/creative/creative.js' - + diff --git a/integrationExamples/realTimeData/jwplayerRtdProvider_example.html b/integrationExamples/realTimeData/jwplayerRtdProvider_example.html index f3f0c64fb1a..c6170b565b5 100644 --- a/integrationExamples/realTimeData/jwplayerRtdProvider_example.html +++ b/integrationExamples/realTimeData/jwplayerRtdProvider_example.html @@ -65,7 +65,11 @@ waitForIt: true, params: { // Note: the following media Ids are placeholders and should be replaced with your Ids. - mediaIDs: ['abc', 'def', 'ghi', 'jkl'] + mediaIDs: ['abc', 'def', 'ghi', 'jkl'], + overrideContentId: 'always', + overrideContentUrl: 'always', + overrideContentTitle: 'always', + overrideContentDescription: 'always' } }] } diff --git a/integrationExamples/testBidder/testBidderBannerExample.html b/integrationExamples/testBidder/testBidderBannerExample.html new file mode 100644 index 00000000000..665625b5044 --- /dev/null +++ b/integrationExamples/testBidder/testBidderBannerExample.html @@ -0,0 +1,79 @@ + + + Prebid Test Bidder Example + + + + +

Prebid Test Bidder Example

+
Banner ad
+ + + \ No newline at end of file diff --git a/integrationExamples/testBidder/testBidderNativeExample.html b/integrationExamples/testBidder/testBidderNativeExample.html new file mode 100644 index 00000000000..d9b51674e2e --- /dev/null +++ b/integrationExamples/testBidder/testBidderNativeExample.html @@ -0,0 +1,151 @@ + + + Prebid Test Bidder Example + + + + + +

Prebid Test Bidder Example

+
Native ad
+
+ + \ No newline at end of file diff --git a/integrationExamples/testBidder/testBidderVideoExample.html b/integrationExamples/testBidder/testBidderVideoExample.html new file mode 100644 index 00000000000..72412306d82 --- /dev/null +++ b/integrationExamples/testBidder/testBidderVideoExample.html @@ -0,0 +1,75 @@ + + + Prebid Test Bidder Example + + + + +

Prebid Test Bidder Example

+
Video ad
+
+ + diff --git a/integrationExamples/top-level-paapi/gam-contextual.html b/integrationExamples/top-level-paapi/gam-contextual.html new file mode 100644 index 00000000000..47bf9607680 --- /dev/null +++ b/integrationExamples/top-level-paapi/gam-contextual.html @@ -0,0 +1,135 @@ + + + + + + + + + + + +

GAM contextual + Publisher as top level PAAPI seller example

+ +

+ This example starts PAAPI auctions at the same time as GAM targeting. The flow is + similar to a typical GAM auction, but if Prebid wins, and got a + PAAPI bid, it is rendered instead of the contextual bid. +

+
+ +
+
Div-1
+
+ +
+
+

Instructions

+

Start local server with:

+gulp serve-fast --https +

Chrome flags:

+--enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true + --privacy-sandbox-enrollment-overrides=https://localhost:9999 +

Join interest group at https://www.optable.co/ +

+
+ + diff --git a/integrationExamples/top-level-paapi/no_adserver.html b/integrationExamples/top-level-paapi/no_adserver.html new file mode 100644 index 00000000000..dd363e53485 --- /dev/null +++ b/integrationExamples/top-level-paapi/no_adserver.html @@ -0,0 +1,114 @@ + + + + + + + + + +

No ad server, publisher as top level PAAPI seller example

+ +

+ +

+
+ +
+
Div-1
+
+ +
+
+

Instructions

+

Start local server with:

+ gulp serve-fast --https +

Chrome flags:

+ --enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true + --privacy-sandbox-enrollment-overrides=https://localhost:9999 +

Join interest group at https://www.optable.co/ +

+
+ + diff --git a/integrationExamples/gpt/top-level-paapi/decisionLogic.js b/integrationExamples/top-level-paapi/shared/decisionLogic.js similarity index 100% rename from integrationExamples/gpt/top-level-paapi/decisionLogic.js rename to integrationExamples/top-level-paapi/shared/decisionLogic.js diff --git a/integrationExamples/top-level-paapi/shared/example-setup.js b/integrationExamples/top-level-paapi/shared/example-setup.js new file mode 100644 index 00000000000..1c52abf02c9 --- /dev/null +++ b/integrationExamples/top-level-paapi/shared/example-setup.js @@ -0,0 +1,95 @@ +// intercept navigator.runAdAuction and print parameters to console +(() => { + var originalRunAdAuction = navigator.runAdAuction; + navigator.runAdAuction = function (...args) { + console.log('%c runAdAuction', 'background: cyan; border: 2px; border-radius: 3px', ...args); + return originalRunAdAuction.apply(navigator, args); + }; +})(); +init(); +setupContextualResponse(); + +function addExampleControls(requestBids) { + const ctl = document.createElement('div'); + ctl.innerHTML = ` + + Simulate contextual bid: + + CPM + + + `; + ctl.style = 'margin-top: 30px'; + document.body.appendChild(ctl); + ctl.querySelector('.bid').addEventListener('click', function (ev) { + const cpm = ctl.querySelector('.cpm').value; + if (cpm) { + setupContextualResponse(parseInt(cpm, 10)); + } + requestBids(); + }); +} + +function init() { + window.pbjs = window.pbjs || {que: []}; + window.pbjs.que.push(() => { + pbjs.aliasBidder('optable', 'contextual'); + [ + 'auctionInit', + 'auctionTimeout', + 'auctionEnd', + 'bidAdjustment', + 'bidTimeout', + 'bidRequested', + 'bidResponse', + 'bidRejected', + 'noBid', + 'seatNonBid', + 'bidWon', + 'bidderDone', + 'bidderError', + 'setTargeting', + 'beforeRequestBids', + 'beforeBidderHttp', + 'requestBids', + 'addAdUnits', + 'adRenderFailed', + 'adRenderSucceeded', + 'tcf2Enforcement', + 'auctionDebug', + 'bidViewable', + 'staleRender', + 'billableEvent', + 'bidAccepted', + 'paapiRunAuction', + 'paapiBid', + 'paapiNoBid', + 'paapiError', + ].forEach(evt => { + pbjs.onEvent(evt, (arg) => { + console.log('Event:', evt, arg); + }) + }); + }); +} + +function setupContextualResponse(cpm = 1) { + pbjs.que.push(() => { + pbjs.setConfig({ + debugging: { + enabled: true, + intercept: [ + { + when: { + bidder: 'contextual' + }, + then: { + cpm, + currency: 'USD' + } + } + ] + } + }); + }); +} diff --git a/integrationExamples/videoModule/adPlayerPro/bidRequestScheduling.html b/integrationExamples/videoModule/adPlayerPro/bidRequestScheduling.html new file mode 100644 index 00000000000..9c799d25058 --- /dev/null +++ b/integrationExamples/videoModule/adPlayerPro/bidRequestScheduling.html @@ -0,0 +1,143 @@ + + + + + + + AdPlayer.Pro bid request scheduling + + + + + + + + +

AdPlayer.Pro Event Listeners

+ +
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/videoModule/adPlayerPro/eventListeners.html b/integrationExamples/videoModule/adPlayerPro/eventListeners.html new file mode 100644 index 00000000000..8265d3c26a6 --- /dev/null +++ b/integrationExamples/videoModule/adPlayerPro/eventListeners.html @@ -0,0 +1,154 @@ + + + + + + + AdPlayer.Pro Event Listeners + + + + + + + + +

AdPlayer.Pro Event Listeners

+ +
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/videoModule/adPlayerPro/localVideoCache.html b/integrationExamples/videoModule/adPlayerPro/localVideoCache.html new file mode 100644 index 00000000000..24d6287f844 --- /dev/null +++ b/integrationExamples/videoModule/adPlayerPro/localVideoCache.html @@ -0,0 +1,167 @@ + + + + + + + + AdPlayer.Pro with Local Cache & GAM + + + + + + +

AdPlayer.Pro with Local Cache & GAM

+ +
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html b/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html index d0b261043e4..2246f4e76e3 100644 --- a/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html +++ b/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html @@ -48,7 +48,9 @@ params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', - advertising: { client: 'vast' } + advertising: { client: 'vast' }, + width: 640, + height: 480 } } }, diff --git a/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html b/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html index c40af32cac2..fac6b51b44e 100644 --- a/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html +++ b/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html @@ -24,6 +24,8 @@ params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', + width: 640, + height: 480, advertising: { client: "googima", schedule: { diff --git a/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html b/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html index 75a72ba3501..094af9c32a2 100644 --- a/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html +++ b/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html @@ -47,6 +47,8 @@ params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', + width: 640, + height: 480, advertising: { client: 'vast' } } } @@ -141,4 +143,4 @@
Div-1: Player placeholder div
- + \ No newline at end of file diff --git a/integrationExamples/videoModule/jwplayer/eventListeners.html b/integrationExamples/videoModule/jwplayer/eventListeners.html index 6f04f37264b..e6f49af3e24 100644 --- a/integrationExamples/videoModule/jwplayer/eventListeners.html +++ b/integrationExamples/videoModule/jwplayer/eventListeners.html @@ -49,6 +49,8 @@ vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', mediaid: 'd9J2zcaA', + width: 640, + height: 480, advertising: { client: 'vast' } } } diff --git a/integrationExamples/videoModule/jwplayer/eventsUI.html b/integrationExamples/videoModule/jwplayer/eventsUI.html index cfd1efe7624..f86721a8b7e 100644 --- a/integrationExamples/videoModule/jwplayer/eventsUI.html +++ b/integrationExamples/videoModule/jwplayer/eventsUI.html @@ -53,6 +53,8 @@ file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', title: "Subaru Outback on Street and Dirt", }], + width: 640, + height: 480, advertising: { client: 'vast' } } } diff --git a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html index 1f4331785ea..94555dce8a2 100644 --- a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html +++ b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html @@ -47,6 +47,8 @@ params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', + width: 640, + height: 480, advertising: { client: 'googima' } } } @@ -63,7 +65,6 @@ // output: 'vast' // }, baseAdTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=' - //'https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/137679306/HB_Dev_Center_Example&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&correlator=', }, },] }, diff --git a/integrationExamples/videoModule/jwplayer/localVideoCache.html b/integrationExamples/videoModule/jwplayer/localVideoCache.html new file mode 100644 index 00000000000..248a25c41cd --- /dev/null +++ b/integrationExamples/videoModule/jwplayer/localVideoCache.html @@ -0,0 +1,135 @@ + + + + + + + JW Player with Local Cache & GAM + + + + + + +

JW Player with Local Cache & GAM

+ +
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/videoModule/jwplayer/mediaMetadata.html b/integrationExamples/videoModule/jwplayer/mediaMetadata.html index 63e62aa4b82..11762733f18 100644 --- a/integrationExamples/videoModule/jwplayer/mediaMetadata.html +++ b/integrationExamples/videoModule/jwplayer/mediaMetadata.html @@ -51,7 +51,9 @@ file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', title: 'Subaru Outback On Street And Dirt', description: 'Smoking Tire takes the all-new Subaru Outback to the highest point we can find in hopes our customer-appreciation Balloon Launch will get some free T-shirts into the hands of our viewers.', - advertising: { client: 'googima' } + advertising: { client: 'googima' }, + width: 640, + height: 480 } } } diff --git a/integrationExamples/videoModule/jwplayer/playlist.html b/integrationExamples/videoModule/jwplayer/playlist.html index 9e89f606f23..447817ffd80 100644 --- a/integrationExamples/videoModule/jwplayer/playlist.html +++ b/integrationExamples/videoModule/jwplayer/playlist.html @@ -63,7 +63,9 @@ title : "Sintel", description: "Sintel is an independently produced short film, initiated by the Blender Foundation as a means to further improve and validate the free/open source 3D creation suite Blender. With initial funding provided by 1000s of donations via the internet community, it has again proven to be a viable development model for both open 3D technology as for independent animation film.\nThis 15 minute film has been realized in the studio of the Amsterdam Blender Institute, by an international team of artists and developers. In addition to that, several crucial technical and creative targets have been realized online, by developers and artists and teams all over the world.\nwww.sintel.org", }], - advertising: { client: 'vast' } + advertising: { client: 'vast' }, + width: 640, + height: 480 } } } diff --git a/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html b/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html index 74217ecee2c..23ae1345d4c 100644 --- a/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html +++ b/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html @@ -168,4 +168,4 @@
Div-1: Player placeholder div
- + \ No newline at end of file diff --git a/integrationExamples/videoModule/videojs/localVideoCache.html b/integrationExamples/videoModule/videojs/localVideoCache.html new file mode 100644 index 00000000000..973a7826def --- /dev/null +++ b/integrationExamples/videoModule/videojs/localVideoCache.html @@ -0,0 +1,147 @@ + + + + + + + + + + --> + + + + VideoJS with Local Cache & GAM Ad Server Mediation + + + + + + + +

VideoJS with GAM Ad Server Mediation

+
Div-1: Player placeholder div
+ + + + diff --git a/karma.conf.maker.js b/karma.conf.maker.js index 9b10cd69c42..fbef10ff567 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -109,15 +109,10 @@ module.exports = function(codeCoverage, browserstack, watchMode, file, disableFe var webpackConfig = newWebpackConfig(codeCoverage, disableFeatures); var plugins = newPluginsArray(browserstack); if (file) { - file = file.split(',') + file = Array.isArray(file) ? ['test/pipeline_setup.js', ...file] : [file] } - var files = file ? ['test/test_deps.js', file, 'test/helpers/hookSetup.js'].flatMap(f => f) : ['test/test_index.js']; - // This file opens the /debug.html tab automatically. - // It has no real value unless you're running --watch, and intend to do some debugging in the browser. - if (watchMode) { - files.push('test/helpers/karma-init.js'); - } + var files = file ? ['test/test_deps.js', ...file, 'test/helpers/hookSetup.js'].flatMap(f => f) : ['test/test_index.js']; var config = { // base path that will be used to resolve all patterns (eg. files, exclude) @@ -130,15 +125,15 @@ module.exports = function(codeCoverage, browserstack, watchMode, file, disableFe }, // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['es5-shim', 'mocha', 'chai', 'sinon'], + frameworks: ['es5-shim', 'mocha', 'chai', 'sinon', 'webpack'], - files: files, + // test files should not be watched or they'll run twice after an update + // (they are still, in fact, watched through autoWatch: true) + files: files.map(fn => ({pattern: fn, watched: false, served: true, included: true})), // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - 'test/test_index.js': ['webpack', 'sourcemap'] - }, + preprocessors: Object.fromEntries(files.map(f => [f, ['webpack', 'sourcemap']])), // web server port port: 9876, @@ -151,7 +146,7 @@ module.exports = function(codeCoverage, browserstack, watchMode, file, disableFe logLevel: karmaConstants.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, + autoWatch: watchMode, reporters: ['mocha'], @@ -178,15 +173,6 @@ module.exports = function(codeCoverage, browserstack, watchMode, file, disableFe plugins: plugins }; - // To ensure that, we are able to run single spec file - // here we are adding preprocessors, when file is passed - if (file) { - config.files.forEach((file) => { - config.preprocessors[file] = ['webpack', 'sourcemap']; - }); - delete config.preprocessors['test/test_index.js']; - } - setReporters(config, codeCoverage, browserstack); setBrowsers(config, browserstack); return config; diff --git a/karmaRunner.js b/karmaRunner.js index 96259069966..7239d2a2556 100644 --- a/karmaRunner.js +++ b/karmaRunner.js @@ -1,23 +1,97 @@ const karma = require('karma'); const process = require('process'); const karmaConfMaker = require('./karma.conf.maker.js'); +const glob = require('glob'); +/** + * Environment variables: + * + * TEST_CHUNKS: number of chunks to split tests into, or MAX to run each test suite in isolation + * TEST_CHUNK: run only this chunk (e.g. TEST_CHUNKS=4 TEST_CHUNK=2 gulp test) will run only the second quarter + * TEST_ALL: set to continue running remaining chunks after a previous chunk failed + * TEST_PAT: test file pattern (default is *_spec.js) + */ -process.on('message', function(options) { - try { - let cfg = karmaConfMaker(options.coverage, options.browserstack, options.watch, options.file, options.disableFeatures); +process.on('message', function (options) { + function info(msg) { + // eslint-disable-next-line no-console + console.log('\x1b[46m\x1b[30m%s\x1b[0m', msg); + } + + function error(msg) { + // eslint-disable-next-line no-console + console.log('\x1b[41m\x1b[37m%s\x1b[0m', msg); + } + + function chunkDesc(chunk) { + return chunk.length > 1 ? `From ${chunk[0]} to ${chunk[chunk.length - 1]}` : chunk[0]; + } + + const failures = []; + + function quit(fail) { + // eslint-disable-next-line no-console + console.log(''); + failures.forEach(([chunkNo, chunkTot, chunk]) => { + error(`Chunk ${chunkNo + 1} of ${chunkTot} failed: ${chunkDesc(chunk)}`); + fail = true; + }); + process.exit(fail ? 1 : 0); + } + + process.on('SIGINT', () => quit()); + + function runKarma(file) { + let cfg = karmaConfMaker(options.coverage, options.browserstack, options.watch, file, options.disableFeatures); if (options.browsers && options.browsers.length) { cfg.browsers = options.browsers; } if (options.oneBrowser) { - cfg.browsers = [cfg.browsers.find((b) => b.toLowerCase().includes(options.oneBrowser.toLowerCase())) || cfg.browsers[0]] + cfg.browsers = [cfg.browsers.find((b) => b.toLowerCase().includes(options.oneBrowser.toLowerCase())) || cfg.browsers[0]]; } cfg = karma.config.parseConfig(null, cfg); - new karma.Server(cfg, (exitCode) => { - process.exit(exitCode); - }).start(); + return new Promise((resolve, reject) => { + new karma.Server(cfg, (exitCode) => { + exitCode ? reject(exitCode) : resolve(exitCode); + }).start(); + }); + } + + try { + let chunks = []; + if (options.file) { + chunks.push([options.file]); + } else { + const chunkNum = process.env['TEST_CHUNKS'] ?? 1; + const pat = process.env['TEST_PAT'] ?? '*_spec.js' + const tests = glob.sync('test/**/' + pat).sort(); + const chunkLen = chunkNum === 'MAX' ? 0 : Math.floor(tests.length / Number(chunkNum)); + chunks.push([]); + tests.forEach((fn) => { + chunks[chunks.length - 1].push(fn); + if (chunks[chunks.length - 1].length > chunkLen) chunks.push([]); + }); + chunks = chunks.filter(chunk => chunk.length > 0); + if (chunks.length > 1) { + info(`Splitting tests into ${chunkNum} chunks, ${chunkLen + 1} suites each`); + } + } + let pm = Promise.resolve(); + chunks.forEach((chunk, i) => { + if (process.env['TEST_CHUNK'] && Number(process.env['TEST_CHUNK']) !== i + 1) return; + pm = pm.then(() => { + info(`Starting chunk ${i + 1} of ${chunks.length}: ${chunkDesc(chunk)}`); + return runKarma(chunk); + }).catch(() => { + failures.push([i, chunks.length, chunk]); + if (!process.env['TEST_ALL']) quit(); + }).finally(() => { + info(`Chunk ${i + 1} of ${chunks.length}: done`); + }); + }); + pm.then(() => quit()); } catch (e) { // eslint-disable-next-line - console.error(e); - process.exit(1); + error(e); + quit(true); } }); diff --git a/libraries/adagioUtils/adagioUtils.js b/libraries/adagioUtils/adagioUtils.js new file mode 100644 index 00000000000..c2614c45d0c --- /dev/null +++ b/libraries/adagioUtils/adagioUtils.js @@ -0,0 +1,35 @@ +import { + canAccessWindowTop, + generateUUID, + getWindowSelf, + getWindowTop, + isSafeFrameWindow +} from '../../src/utils.js'; + +/** + * Returns the best Window object to use with ADAGIO. + * @returns {Window} window.top or window.self object + */ +export function getBestWindowForAdagio() { + return (canAccessWindowTop()) ? getWindowTop() : getWindowSelf(); +} + +/** + * Returns the window.ADAGIO global object used to store Adagio data. + * This object is created in window.top if possible, otherwise in window.self. + */ +export const _ADAGIO = (function() { + const w = getBestWindowForAdagio(); + + w.ADAGIO = w.ADAGIO || {}; + w.ADAGIO.pageviewId = w.ADAGIO.pageviewId || generateUUID(); + w.ADAGIO.adUnits = w.ADAGIO.adUnits || {}; + w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits || []; + w.ADAGIO.queue = w.ADAGIO.queue || []; + w.ADAGIO.versions = w.ADAGIO.versions || {}; + w.ADAGIO.versions.pbjs = '$prebid.version$'; + w.ADAGIO.windows = w.ADAGIO.windows || []; + w.ADAGIO.isSafeFrameWindow = isSafeFrameWindow(); + + return w.ADAGIO; +})(); diff --git a/libraries/adkernelUtils/adkernelUtils.js b/libraries/adkernelUtils/adkernelUtils.js new file mode 100644 index 00000000000..0b2d48f3824 --- /dev/null +++ b/libraries/adkernelUtils/adkernelUtils.js @@ -0,0 +1,11 @@ +export function getBidFloor(bid, mediaType, sizes) { + var floor; + var size = sizes.length === 1 ? sizes[0] : '*'; + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({currency: 'USD', mediaType, size}); + if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + } + return floor; +} diff --git a/libraries/adtelligentUtils/adtelligentUtils.js b/libraries/adtelligentUtils/adtelligentUtils.js new file mode 100644 index 00000000000..c2543fa4cae --- /dev/null +++ b/libraries/adtelligentUtils/adtelligentUtils.js @@ -0,0 +1,79 @@ +import {deepAccess, isArray} from '../../src/utils.js'; +import { config } from '../../src/config.js'; +import {BANNER, VIDEO} from '../../src/mediaTypes.js'; + +export const supportedMediaTypes = [VIDEO, BANNER] + +export function isBidRequestValid (bid) { + return !!deepAccess(bid, 'params.aid'); +} + +export function getUserSyncsFn (syncOptions, serverResponses, syncsCache = {}) { + const syncs = []; + function addSyncs(bid) { + const uris = bid.cookieURLs; + const types = bid.cookieURLSTypes || []; + + if (Array.isArray(uris)) { + uris.forEach((uri, i) => { + const type = types[i] || 'image'; + + if ((!syncOptions.pixelEnabled && type === 'image') || + (!syncOptions.iframeEnabled && type === 'iframe') || + syncsCache[uri]) { + return; + } + + syncsCache[uri] = true; + syncs.push({ + type: type, + url: uri + }) + }) + } + } + + if (syncOptions.pixelEnabled || syncOptions.iframeEnabled) { + isArray(serverResponses) && serverResponses.forEach((response) => { + if (response.body) { + if (isArray(response.body)) { + response.body.forEach(b => { + addSyncs(b); + }) + } else { + addSyncs(response.body) + } + } + }) + } + return syncs; +} + +export function createTag(bidRequests, adapterRequest) { + const tag = { + // TODO: is 'page' the right value here? + Domain: deepAccess(adapterRequest, 'refererInfo.page'), + }; + + if (config.getConfig('coppa') === true) { + tag.Coppa = 1; + } + if (deepAccess(adapterRequest, 'gdprConsent.gdprApplies')) { + tag.GDPR = 1; + tag.GDPRConsent = deepAccess(adapterRequest, 'gdprConsent.consentString'); + } + if (deepAccess(adapterRequest, 'uspConsent')) { + tag.USP = deepAccess(adapterRequest, 'uspConsent'); + } + if (deepAccess(bidRequests[0], 'schain')) { + tag.Schain = deepAccess(bidRequests[0], 'schain'); + } + if (deepAccess(bidRequests[0], 'userId')) { + tag.UserIds = deepAccess(bidRequests[0], 'userId'); + } + if (deepAccess(bidRequests[0], 'userIdAsEids')) { + tag.UserEids = deepAccess(bidRequests[0], 'userIdAsEids'); + } + + return tag; +} diff --git a/libraries/advangUtils/index.js b/libraries/advangUtils/index.js new file mode 100644 index 00000000000..f815f389ed6 --- /dev/null +++ b/libraries/advangUtils/index.js @@ -0,0 +1,228 @@ +import { deepAccess, generateUUID, isFn, parseSizesInput, parseUrl } from '../../src/utils.js'; +import { config } from '../../src/config.js'; +import { find, includes } from '../../src/polyfill.js'; + +export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; + +export function isBannerBid(bid) { + return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +} + +export function isVideoBid(bid) { + return deepAccess(bid, 'mediaTypes.video'); +} + +export function getBannerBidFloor(bid) { + let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }) : {}; + return floorInfo?.floor || getBannerBidParam(bid, 'bidfloor'); +} + +export function getVideoBidFloor(bid) { + let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'video', size: '*' }) : {}; + return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); +} + +export function isVideoBidValid(bid) { + return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); +} + +export function isBannerBidValid(bid) { + return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); +} + +export function getVideoBidParam(bid, key) { + return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); +} + +export function getBannerBidParam(bid, key) { + return deepAccess(bid, 'params.banner.' + key) || deepAccess(bid, 'params.' + key); +} + +export function isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); +} + +export function isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} + +export function getDoNotTrack() { + return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; +} + +export function findAndFillParam(o, key, value) { + try { + if (typeof value === 'function') { + o[key] = value(); + } else { + o[key] = value; + } + } catch (ex) {} +} + +export function getOsVersion() { + let clientStrings = [ + { s: 'Android', r: /Android/ }, + { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, + { s: 'Mac OS X', r: /Mac OS X/ }, + { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, + { s: 'Linux', r: /(Linux|X11)/ }, + { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, + { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, + { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, + { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, + { s: 'Windows Vista', r: /Windows NT 6.0/ }, + { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, + { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, + { s: 'UNIX', r: /UNIX/ }, + { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } + ]; + let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); + return cs ? cs.s : 'unknown'; +} + +export function getFirstSize(sizes) { + return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; +} + +export function parseSizes(sizes) { + return parseSizesInput(sizes).map(size => { + let [ width, height ] = size.split('x'); + return { + w: parseInt(width, 10) || undefined, + h: parseInt(height, 10) || undefined + }; + }); +} + +export function getVideoSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); +} + +export function getBannerSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); +} + +export function getTopWindowReferrer(bidderRequest) { + return bidderRequest?.refererInfo?.ref || ''; +} + +export function getTopWindowLocation(bidderRequest) { + return parseUrl(bidderRequest?.refererInfo?.page, {decodeSearchAsString: true}); +} + +export function getVideoTargetingParams(bid, VIDEO_TARGETING) { + const result = {}; + const excludeProps = ['playerSize', 'context', 'w', 'h']; + Object.keys(Object(bid.mediaTypes.video)) + .filter(key => !includes(excludeProps, key)) + .forEach(key => { + result[ key ] = bid.mediaTypes.video[ key ]; + }); + Object.keys(Object(bid.params.video)) + .filter(key => includes(VIDEO_TARGETING, key)) + .forEach(key => { + result[ key ] = bid.params.video[ key ]; + }); + return result; +} + +export function createRequestData(bid, bidderRequest, isVideo, getBidParam, getSizes, getBidFloor, BIDDER_CODE, ADAPTER_VERSION) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(bidderRequest); + let paramSize = getBidParam(bid, 'size'); + let sizes = []; + let coppa = config.getConfig('coppa'); + + if (typeof paramSize !== 'undefined' && paramSize != '') { + sizes = parseSizes(paramSize); + } else { + sizes = getSizes(bid); + } + + const firstSize = getFirstSize(sizes); + let floor = getBidFloor(bid) || (isVideo ? 0.5 : 0.1); + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1, + 'os': getOsVersion() + }, + 'at': 2, + 'site': {}, + 'tmax': Math.min(3000, bidderRequest.timeout), + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': {} + }, + 'user': { + 'ext': {} + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getBidParam(bid, 'placement'); + let impType = isVideo ? { + 'video': Object.assign({ + 'id': generateUUID(), + 'pos': 0, + 'w': firstSize.w, + 'h': firstSize.h, + 'mimes': DEFAULT_MIMES + }, getVideoTargetingParams(bid)) + } : { + 'banner': { + 'id': generateUUID(), + 'pos': 0, + 'w': firstSize.w, + 'h': firstSize.h + } + }; + + for (let j = 0; j < sizes.length; j++) { + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + ...impType + }); + } + + if (coppa) { + o.regs.ext = {'coppa': 1}; + } + + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} diff --git a/libraries/analyticsAdapter/AnalyticsAdapter.js b/libraries/analyticsAdapter/AnalyticsAdapter.js index 395a21e5571..d6455750ea3 100644 --- a/libraries/analyticsAdapter/AnalyticsAdapter.js +++ b/libraries/analyticsAdapter/AnalyticsAdapter.js @@ -2,12 +2,20 @@ import { EVENTS } from '../../src/constants.js'; import {ajax} from '../../src/ajax.js'; import {logError, logMessage} from '../../src/utils.js'; import * as events from '../../src/events.js'; +import {config} from '../../src/config.js'; export const _internal = { ajax }; const ENDPOINT = 'endpoint'; const BUNDLE = 'bundle'; +const LABELS_KEY = 'analyticsLabels'; + +let labels = {}; + +config.getConfig(LABELS_KEY, (cfg) => { + labels = cfg[LABELS_KEY] +}); export const DEFAULT_INCLUDE_EVENTS = Object.values(EVENTS) .filter(ev => ev !== EVENTS.AUCTION_DEBUG); @@ -90,12 +98,18 @@ export default function AnalyticsAdapter({ url, analyticsType, global, handler } } function _callEndpoint({ eventType, args, callback }) { - _internal.ajax(url, callback, JSON.stringify({ eventType, args })); + _internal.ajax(url, callback, JSON.stringify({ eventType, args, labels })); } function _enqueue({eventType, args}) { queue.push(() => { - this.track({eventType, args}); + if (Object.keys(labels || []).length > 0) { + args = { + [LABELS_KEY]: labels, + ...args, + } + } + this.track({eventType, labels, args}); }); emptyQueue(); } diff --git a/libraries/appnexusUtils/anKeywords.js b/libraries/appnexusUtils/anKeywords.js index a6fa8d7a21e..4a0da18024e 100644 --- a/libraries/appnexusUtils/anKeywords.js +++ b/libraries/appnexusUtils/anKeywords.js @@ -136,7 +136,7 @@ export function getANMapFromOrtbSegments(ortb2) { let ortbSegsArrObj = deepAccess(ortb2, path) || []; ortbSegsArrObj.forEach(segObj => { // only read segment data from known sources - const segtax = ORTB_SEGTAX_KEY_MAP[deepAccess(segObj, 'ext.segtax')]; + const segtax = ORTB_SEGTAX_KEY_MAP[segObj?.ext?.segtax]; if (segtax) { segObj.segment.forEach(seg => { // if source was in multiple locations of ortb or had multiple segments in same area, stack them together into an array diff --git a/libraries/appnexusUtils/anUtils.js b/libraries/appnexusUtils/anUtils.js index 9b55cd5c2a4..1d04711bd0f 100644 --- a/libraries/appnexusUtils/anUtils.js +++ b/libraries/appnexusUtils/anUtils.js @@ -10,6 +10,23 @@ export function convertCamelToUnderscore(value) { }).replace(/^_/, ''); } +export const appnexusAliases = [ + { code: 'appnexusAst', gvlid: 32 }, + { code: 'emxdigital', gvlid: 183 }, + { code: 'emetriq', gvlid: 213 }, + { code: 'pagescience', gvlid: 32 }, + { code: 'gourmetads', gvlid: 32 }, + { code: 'matomy', gvlid: 32 }, + { code: 'featureforward', gvlid: 32 }, + { code: 'oftmedia', gvlid: 32 }, + { code: 'adasta', gvlid: 32 }, + { code: 'beintoo', gvlid: 618 }, + { code: 'projectagora', gvlid: 1032 }, + { code: 'stailamedia', gvlid: 32 }, + { code: 'uol', gvlid: 32 }, + { code: 'adzymic', gvlid: 723 }, +]; + /** * Creates an array of n length and fills each item with the given value */ diff --git a/libraries/audUtils/bidderUtils.js b/libraries/audUtils/bidderUtils.js new file mode 100644 index 00000000000..12b4ed04374 --- /dev/null +++ b/libraries/audUtils/bidderUtils.js @@ -0,0 +1,244 @@ +import { + deepAccess, + deepSetValue, + generateUUID, + logError +} from '../../src/utils.js'; + +// Declare native assets +const NATIVE_ASSETS = [ + { id: 1, required: 1, title: { len: 100 } }, // Title + { id: 2, required: 1, img: { type: 3, w: 300, h: 250 } }, // Main image + { id: 3, required: 0, data: { type: 1, len: 140 } }, // Body + { id: 4, required: 1, data: { type: 2 } }, // Sponsored by + { id: 5, required: 1, icon: { w: 50, h: 50 } }, // Icon + { id: 6, required: 1, data: { type: 12, len: 15 } } // Call to action +]; +// Function to get Request +export const getBannerRequest = (bidRequests, bidderRequest, ENDPOINT) => { + let request = []; + // Loop for each bid request + bidRequests.forEach(bidReq => { + let guid = generateUUID(); + const req = { + id: guid, + imp: [getImpDetails(bidReq)], + placementId: bidReq.params.placement_id, + site: getSiteDetails(bidderRequest), + user: getUserDetails(bidReq) + }; + // Fetch GPP Consent from bidderRequest + if (bidderRequest && bidderRequest.gppConsent && bidderRequest.gppConsent.gppString) { + deepSetValue(req, 'regs.gpp', bidderRequest.gppConsent.gppString); + deepSetValue(req, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); + } else if (bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.regs && bidderRequest.ortb2.regs.gpp) { + deepSetValue(req, 'regs.gpp', bidderRequest.ortb2.regs.gpp); + deepSetValue(req, 'regs.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); + } + // Fetch coppa compliance from bidderRequest + if (bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.regs && bidderRequest.ortb2.regs.coppa) { + deepSetValue(req, 'regs.coppa', 1); + } + // Fetch uspConsent from bidderRequest + if (bidderRequest?.uspConsent) { + deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + req.MediaType = getMediaType(bidReq); + // Adding eids if passed + if (bidReq.userIdAsEids) { + req.user.ext.eids = bidReq.userIdAsEids; + } + request.push(req); + }); + // Return the array of request + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(request), + options: { + contentType: 'application/json', + } + }; +} +// Function to get Response +export const getBannerResponse = (bidResponse, mediaType) => { + return formatResponse(bidResponse, mediaType); +} +// Function to get NATIVE Response +export const getNativeResponse = (bidResponse, bidRequest, mediaType) => { + const assets = JSON.parse(JSON.parse(bidRequest.data)[0].imp[0].native.request).assets; + return formatResponse(bidResponse, mediaType, assets); +} +// Function to format response +const formatResponse = (bidResponse, mediaType, assets) => { + let responseArray = []; + if (bidResponse) { + try { + let bidResp = deepAccess(bidResponse, 'body.seatbid', []); + if (bidResp && bidResp[0] && bidResp[0].bid) { + bidResp[0].bid.forEach(bidReq => { + let response = {}; + response.requestId = bidReq.impid; + response.cpm = bidReq.price; + response.width = bidReq.w; + response.height = bidReq.h; + response.ad = bidReq.adm; + response.meta = { + advertiserDomains: bidReq.adomain, + primaryCatId: bidReq.cat || [], + attr: bidReq.attr || [] + }; + response.creativeId = bidReq.crid; + response.netRevenue = false; + response.currency = 'USD'; + response.ttl = 300; + response.dealId = bidReq.dealId; + response.mediaType = mediaType; + if (mediaType == 'native') { + let nativeResp = JSON.parse(bidReq.adm).native; + let nativeData = { + clickUrl: nativeResp.link.url, + impressionTrackers: nativeResp.imptrackers + }; + nativeResp.assets.forEach(asst => { + let data = getNativeAssestData(asst, assets); + nativeData[data.key] = data.value; + }); + response.native = nativeData; + } + responseArray.push(response); + }); + } + } catch (e) { + logError(e); + } + } + return responseArray; +} +// Function to get imp based on Media Type +const getImpDetails = (bidReq) => { + let imp = {}; + if (bidReq) { + imp.id = bidReq.bidId; + imp.bidfloor = getFloorPrice(bidReq); + if (bidReq.mediaTypes.native) { + let assets = { assets: NATIVE_ASSETS }; + imp.native = { request: JSON.stringify(assets) }; + } else if (bidReq.mediaTypes.banner) { + imp.banner = getBannerDetails(bidReq); + } + } + return imp; +} +// Function to get banner object +const getBannerDetails = (bidReq) => { + let response = {}; + if (bidReq.mediaTypes.banner) { + // Fetch width and height from MediaTypes object, if not provided in bidReq params + if (bidReq.mediaTypes.banner.sizes && !bidReq.params.height && !bidReq.params.width) { + let sizes = bidReq.mediaTypes.banner.sizes; + if (sizes.length > 0) { + response.h = sizes[0][1]; + response.w = sizes[0][0]; + } + } else { + response.h = bidReq.params.height; + response.w = bidReq.params.width; + } + } + return response; +} +// Function to get floor price +const getFloorPrice = (bidReq) => { + let bidfloor = deepAccess(bidReq, 'params.bid_floor', 0); + return bidfloor; +} +// Function to get site object +const getSiteDetails = (bidderRequest) => { + let page = ''; + let name = ''; + if (bidderRequest && bidderRequest.refererInfo) { + page = bidderRequest.refererInfo.page; + name = bidderRequest.refererInfo.domain; + } + return {page: page, name: name}; +} +// Function to build the user object +const getUserDetails = (bidReq) => { + let user = {}; + if (bidReq && bidReq.ortb2 && bidReq.ortb2.user) { + user.id = bidReq.ortb2.user.id ? bidReq.ortb2.user.id : ''; + user.buyeruid = bidReq.ortb2.user.buyeruid ? bidReq.ortb2.user.buyeruid : ''; + user.keywords = bidReq.ortb2.user.keywords ? bidReq.ortb2.user.keywords : ''; + user.customdata = bidReq.ortb2.user.customdata ? bidReq.ortb2.user.customdata : ''; + user.ext = bidReq.ortb2.user.ext ? bidReq.ortb2.user.ext : ''; + } else { + user.id = ''; + user.buyeruid = ''; + user.keywords = ''; + user.customdata = ''; + user.ext = {}; + } + return user; +} +// Function to get asset data for response +const getNativeAssestData = (params, assets) => { + let response = {}; + if (params.title) { + response.key = 'title'; + response.value = params.title.text; + } + if (params.data) { + response.key = getAssetData(params.id, assets); + response.value = params.data.value; + } + if (params.img) { + response.key = getAssetImageDataType(params.id, assets); + response.value = { + url: params.img.url, + height: params.img.h, + width: params.img.w + } + } + return response; +} +// Function to get asset data types based on id +const getAssetData = (paramId, asset) => { + let resp = ''; + for (let i = 0; i < asset.length; i++) { + if (asset[i].id == paramId) { + switch (asset[i].data.type) { + case 1 : resp = 'sponsored'; + break; + case 2 : resp = 'desc'; + break; + case 12 : resp = 'cta'; + break; + } + } + } + return resp; +} +// Function to get image type based on the id +const getAssetImageDataType = (paramId, asset) => { + let resp = ''; + for (let i = 0; i < asset.length; i++) { + if (asset[i].id == paramId) { + switch (asset[i].img.type) { + case 1 : resp = 'icon'; + break; + case 3 : resp = 'image'; + break; + } + } + } + return resp; +} +// Function to get Media Type +const getMediaType = (bidReq) => { + if (bidReq.mediaTypes.native) { + return 'native'; + } else if (bidReq.mediaTypes.banner) { + return 'banner'; + } +} diff --git a/libraries/autoplayDetection/autoplay.js b/libraries/autoplayDetection/autoplay.js index b598e46cbd1..4b70145539a 100644 --- a/libraries/autoplayDetection/autoplay.js +++ b/libraries/autoplayDetection/autoplay.js @@ -21,6 +21,12 @@ const autoplayVideoUrl = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAADxtZGF0AAAAMGWIhAAV//73ye/Apuvb3rW/k89I/Cy3PsIqP39atohOSV14BYa1heKCYgALQC5K4QAAAwZtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAD6AABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACMHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAD6AAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAoAAAAFoAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAA+gAAAAAAAEAAAAAAahtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAAEAAAABAAFXEAAAAAAAtaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAFTbWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABE3N0YmwAAACvc3RzZAAAAAAAAAABAAAAn2F2YzEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAoABaAEgAAABIAAAAAAAAAAEVTGF2YzYwLjMxLjEwMiBsaWJ4MjY0AAAAAAAAAAAAAAAY//8AAAA1YXZjQwFkAAr/4QAYZ2QACqzZQo35IQAAAwABAAADAAIPEiWWAQAGaOvjyyLA/fj4AAAAABRidHJ0AAAAAAAAAaAAAAGgAAAAGHN0dHMAAAAAAAAAAQAAAAEAAEAAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAABRzdHN6AAAAAAAAADQAAAABAAAAFHN0Y28AAAAAAAAAAQAAADAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjYwLjE2LjEwMA=='; function startDetection() { + const version = navigator.userAgent.match(/iPhone OS (\d+)_(\d+)/) + if (version !== null && parseInt(version[1]) < 17 && !navigator.userAgent.includes('Safari')) { + // skip autodetection on iOS 16 WebView + return + } + // we create an HTMLVideoElement muted and not displayed in which we try to play a one frame video const videoElement = document.createElement('video'); videoElement.src = autoplayVideoUrl; @@ -31,10 +37,15 @@ function startDetection() { .play() .then(() => { autoplayEnabled = true; - videoElement.pause(); + // if the video is played on a WebView with playsinline = false, this stops the video, to prevent it from being displayed fullscreen + videoElement.src = ''; }) - .catch(() => { - autoplayEnabled = false; + .catch((error) => { + if (error instanceof DOMException && error.name === 'NotSupportedError') { + // ignore this error caused by a Content Security Policy that disables data: scheme for media URLs + } else { + autoplayEnabled = false; + } }); } diff --git a/libraries/biddoInvamiaUtils/index.js b/libraries/biddoInvamiaUtils/index.js new file mode 100644 index 00000000000..0d7c4b1b683 --- /dev/null +++ b/libraries/biddoInvamiaUtils/index.js @@ -0,0 +1,70 @@ +/** + * Helper function to build request payload for banner ads. + * @param {Object} bidRequest - The bid request object. + * @param {string} endpointUrl - The endpoint URL specific to the bidder. + * @returns {Array} An array of server requests. + */ +export function buildBannerRequests(bidRequest, endpointUrl) { + const serverRequests = []; + const sizes = bidRequest.mediaTypes.banner.sizes; + + sizes.forEach(([width, height]) => { + bidRequest.params.requestedSizes = [width, height]; + + const payload = { + ctype: 'div', + pzoneid: bidRequest.params.zoneId, + width, + height, + }; + + const payloadString = Object.keys(payload) + .map((key) => `${key}=${encodeURIComponent(payload[key])}`) + .join('&'); + + serverRequests.push({ + method: 'GET', + url: endpointUrl, + data: payloadString, + bidderRequest: bidRequest, + }); + }); + + return serverRequests; +} + +/** + * Helper function to interpret server response for banner ads. + * @param {Object} serverResponse - The server response object. + * @param {Object} bidderRequest - The matched bid request for this response. + * @returns {Array} An array of bid responses. + */ +export function interpretBannerResponse(serverResponse, bidderRequest) { + const response = serverResponse.body; + const bidResponses = []; + + if (response && response.template && response.template.html) { + const { bidId } = bidderRequest; + const [width, height] = bidderRequest.params.requestedSizes; + + const bidResponse = { + requestId: bidId, + cpm: response.hb.cpm, + creativeId: response.banner.hash, + currency: 'USD', + netRevenue: response.hb.netRevenue, + ttl: 600, + ad: response.template.html, + mediaType: 'banner', + meta: { + advertiserDomains: response.hb.adomains || [], + }, + width, + height, + }; + + bidResponses.push(bidResponse); + } + + return bidResponses; +} diff --git a/libraries/boundingClientRect/boundingClientRect.js b/libraries/boundingClientRect/boundingClientRect.js new file mode 100644 index 00000000000..0abb5af157b --- /dev/null +++ b/libraries/boundingClientRect/boundingClientRect.js @@ -0,0 +1,25 @@ +import { startAuction } from '../../src/prebid.js'; + +const cache = new Map(); + +startAuction.before((next, auctionConfig) => { + clearCache(); + next(auctionConfig); +}); + +export function clearCache() { + cache.clear(); +} + +export function getBoundingClientRect(element) { + let clientRect; + if (cache.has(element)) { + clientRect = cache.get(element); + } else { + // eslint-disable-next-line no-restricted-properties + clientRect = element.getBoundingClientRect(); + cache.set(element, clientRect); + } + + return clientRect; +} diff --git a/libraries/braveUtils/buildAndInterpret.js b/libraries/braveUtils/buildAndInterpret.js new file mode 100644 index 00000000000..8dda7f6060c --- /dev/null +++ b/libraries/braveUtils/buildAndInterpret.js @@ -0,0 +1,75 @@ +import { isEmpty } from '../../src/utils.js'; +import {config} from '../../src/config.js'; +import { createNativeRequest, createBannerRequest, createVideoRequest, getFloor, prepareSite, prepareConsents, prepareEids } from './index.js'; +import { convertOrtbRequestToProprietaryNative } from '../../src/native.js'; + +export const buildRequests = (validBidRequests, bidderRequest, endpointURL, defaultCur) => { + if (!validBidRequests.length || !bidderRequest) return []; + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + const endpoint = endpointURL.replace('hash', validBidRequests[0].params.placementId); + const imp = validBidRequests.map((br) => { + const impObject = { id: br.bidId, secure: 1, bidfloor: getFloor(br, Object.keys(br.mediaTypes)[0]), defaultCur }; + if (br.mediaTypes.banner) impObject.banner = createBannerRequest(br); + else if (br.mediaTypes.video) impObject.video = createVideoRequest(br); + else if (br.mediaTypes.native) impObject.native = { id: br.transactionId, ver: '1.2', request: createNativeRequest(br) }; + return impObject; + }); + + const data = { + id: bidderRequest.bidderRequestId, + cur: [defaultCur], + device: bidderRequest.ortb2?.device || { w: screen.width, h: screen.height, language: navigator.language?.split('-')[0], ua: navigator.userAgent }, + site: prepareSite(validBidRequests[0], bidderRequest), + tmax: bidderRequest.timeout, + regs: { ext: {}, coppa: config.getConfig('coppa') == true ? 1 : 0 }, + user: { ext: {} }, + imp + }; + + prepareConsents(data, bidderRequest); + prepareEids(data, validBidRequests[0]); + + if (validBidRequests[0].schain) data.source = { ext: { schain: validBidRequests[0].schain } }; + + return { method: 'POST', url: endpoint, data }; +}; + +export const interpretResponse = (serverResponse, defaultCur, parseNative) => { + if (!serverResponse || isEmpty(serverResponse.body)) return []; + + let bids = []; + serverResponse.body.seatbid.forEach(response => { + response.bid.forEach(bid => { + const mediaType = bid.ext?.mediaType || 'banner'; + + const bidObj = { + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + ttl: 1200, + currency: defaultCur, + netRevenue: true, + creativeId: bid.crid, + dealId: bid.dealid || null, + mediaType + }; + + switch (mediaType) { + case 'video': + bidObj.vastXml = bid.adm; + break; + case 'native': + bidObj.native = parseNative(bid.adm); + break; + default: + bidObj.ad = bid.adm; + } + + bids.push(bidObj); + }); + }); + + return bids; +}; diff --git a/libraries/braveUtils/index.js b/libraries/braveUtils/index.js new file mode 100644 index 00000000000..26a045e9815 --- /dev/null +++ b/libraries/braveUtils/index.js @@ -0,0 +1,186 @@ +import { NATIVE_ASSETS, NATIVE_ASSETS_IDS } from './nativeAssets.js'; +import { isPlainObject, isArray, isArrayOfNums, parseUrl, isFn } from '../../src/utils.js'; + +/** + * Builds a native request object based on the bid request + * @param {object} br - The bid request + * @returns {object} The native request object + */ +export function createNativeRequest(br) { + let impObject = { + ver: '1.2', + assets: [] + }; + + Object.keys(br.mediaTypes.native).forEach((key) => { + const props = NATIVE_ASSETS[key]; + if (props) { + const asset = { + required: br.mediaTypes.native[key].required ? 1 : 0, + id: props.id, + [props.name]: {} + }; + + if (props.type) asset[props.name]['type'] = props.type; + if (br.mediaTypes.native[key].len) asset[props.name]['len'] = br.mediaTypes.native[key].len; + if (br.mediaTypes.native[key].sizes && br.mediaTypes.native[key].sizes[0]) { + asset[props.name]['w'] = br.mediaTypes.native[key].sizes[0]; + asset[props.name]['h'] = br.mediaTypes.native[key].sizes[1]; + } + + impObject.assets.push(asset); + } + }); + + return impObject; +} + +/** + * Builds a banner request object based on the bid request + * @param {object} br - The bid request + * @returns {object} The banner request object + */ +export function createBannerRequest(br) { + let [w, h] = [300, 250]; + let format = []; + + if (isArrayOfNums(br.mediaTypes.banner.sizes)) { + [w, h] = br.mediaTypes.banner.sizes; + format.push({ w, h }); + } else if (isArray(br.mediaTypes.banner.sizes)) { + [w, h] = br.mediaTypes.banner.sizes[0]; + if (br.mediaTypes.banner.sizes.length > 1) { format = br.mediaTypes.banner.sizes.map((size) => ({ w: size[0], h: size[1] })); } + } + + return { + w, + h, + format, + id: br.transactionId + } +} + +/** + * Builds a video request object based on the bid request + * @param {object} br - The bid request + * @returns {object} The video request object + */ +export function createVideoRequest(br) { + let videoObj = {...br.mediaTypes.video, id: br.transactionId}; + + if (videoObj.playerSize) { + const size = Array.isArray(videoObj.playerSize[0]) ? videoObj.playerSize[0] : videoObj.playerSize; + videoObj.w = size[0]; + videoObj.h = size[1]; + } else { + videoObj.w = 640; + videoObj.h = 480; + } + + return videoObj; +} +/** + * Parses the native ad response + * @param {object} adm - The native ad response + * @returns {object} Parsed native ad object + */ +export function parseNative(adm) { + let bid = { + clickUrl: adm.native.link?.url, + impressionTrackers: adm.native.imptrackers || [], + clickTrackers: adm.native.link?.clicktrackers || [], + jstracker: adm.native.jstracker || [] + }; + adm.native.assets.forEach((asset) => { + const kind = NATIVE_ASSETS_IDS[asset.id]; + const content = kind && asset[NATIVE_ASSETS[kind].name]; + if (content) { + bid[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + } + }); + + return bid; +} + +/** + * Prepare Bid Floor for request + * @param {object} br - The bid request + * @param {string} mediaType - tyoe of media in request + * @param {string} defaultCur - currency which support bidder + * @returns {number} Parsed float bid floor price + */ +export function getFloor(br, mediaType, defaultCur) { + let floor = 0.05; + + if (!isFn(br.getFloor)) { + return floor; + } + + let floorObj = br.getFloor({ + currency: defaultCur, + mediaType, + size: '*' + }); + + if (isPlainObject(floorObj) && !isNaN(parseFloat(floorObj.floor))) { + floor = parseFloat(floorObj.floor) || floor; + } + + return floor; +} + +/** + * Builds site object + * @param {object} br - The bid request, request - bidderRequest data + * @param {object} request - bidderRequest data + * @returns {object} The site request object + */ +export function prepareSite(br, request) { + let siteObj = {}; + + siteObj.publisher = { + id: br.params.placementId.toString() + }; + + siteObj.domain = parseUrl(request.refererInfo.page || request.refererInfo.topmostLocation).hostname; + siteObj.page = request.refererInfo.page || request.refererInfo.topmostLocation; + + if (request.refererInfo.ref) { + siteObj.site.ref = request.refererInfo.ref; + } + + return siteObj; +} + +/** + * Adds privacy data to request object + * @param {object} data - The request object to bidder + * @param {object} request - bidderRequest data + * @returns {boolean} Response with true once finish + */ +export function prepareConsents(data, request) { + if (request.gdprConsent !== undefined) { + data.regs.ext.gdpr = request.gdprConsent.gdprApplies ? 1 : 0; + data.user.ext.consent = request.gdprConsent.consentString ? request.gdprConsent.consentString : ''; + } + + if (request.uspConsent !== undefined) { + data.regs.ext.us_privacy = request.uspConsent; + } + + return true; +} + +/** + * Adds Eids object to request object + * @param {object} data - The request object to bidder + * @param {object} br - The bid request + * @returns {boolean} Response with true once finish + */ +export function prepareEids(data, br) { + if (br.userIdAsEids !== undefined) { + data.user.ext.eids = br.userIdAsEids; + } + + return true; +} diff --git a/libraries/braveUtils/nativeAssets.js b/libraries/braveUtils/nativeAssets.js new file mode 100644 index 00000000000..07de1264d0c --- /dev/null +++ b/libraries/braveUtils/nativeAssets.js @@ -0,0 +1,23 @@ +/** + * IDs and asset types for native ad assets. + */ +export const NATIVE_ASSETS_IDS = { + 1: 'title', + 2: 'icon', + 3: 'image', + 4: 'body', + 5: 'sponsoredBy', + 6: 'cta' +}; + +/** + * Native assets definition for mapping purposes. + */ +export const NATIVE_ASSETS = { + title: { id: 1, name: 'title' }, + icon: { id: 2, type: 1, name: 'img' }, + image: { id: 3, type: 3, name: 'img' }, + body: { id: 4, type: 2, name: 'data' }, + sponsoredBy: { id: 5, type: 1, name: 'data' }, + cta: { id: 6, type: 12, name: 'data' } +}; diff --git a/libraries/browsiUtils/browsiUtils.js b/libraries/browsiUtils/browsiUtils.js new file mode 100644 index 00000000000..af9011faf1b --- /dev/null +++ b/libraries/browsiUtils/browsiUtils.js @@ -0,0 +1,263 @@ +import { isGptPubadsDefined, logError } from '../../src/utils.js'; +import { setKeyValue as setGptKeyValue } from '../../libraries/gptUtils/gptUtils.js'; +import { find } from '../../src/polyfill.js'; + +/** @type {string} */ +const VIEWABILITY_KEYNAME = 'browsiViewability'; +/** @type {string} */ +const SCROLL_KEYNAME = 'browsiScroll'; +/** @type {string} */ +const REVENUE_KEYNAME = 'browsiRevenue'; + +export function isObjectDefined(obj) { + return !!(obj && typeof obj === 'object' && Object.keys(obj).length); +} + +export function generateRandomString() { + const getRandomLetter = () => String.fromCharCode(65 + Math.floor(Math.random() * 26)); // A-Z + return `_${getRandomLetter()}${getRandomLetter()}b${getRandomLetter()}${getRandomLetter()}`; +} + +export function getUUID() { + if (window.crypto && window.crypto.randomUUID) { + return window.crypto.randomUUID() || undefined; + } + return undefined; +} + +function getDaysDifference(firstDate, secondDate) { + const diffInMilliseconds = Math.abs(firstDate - secondDate); + const millisecondsPerDay = 24 * 60 * 60 * 1000; + return diffInMilliseconds / millisecondsPerDay; +} + +function isEngagingUser() { + const pageYOffset = window.scrollY || (document.compatMode === 'CSS1Compat' ? document.documentElement?.scrollTop : document.body?.scrollTop); + return pageYOffset > 0; +} + +function getRevenueTargetingValue(p) { + if (!p) { + return undefined; + } else if (p <= 0) { + return 'no fill'; + } else if (p <= 0.3) { + return 'low'; + } else if (p <= 0.7) { + return 'medium'; + } + return 'high'; +} + +function getTargetingValue(p) { + return (!p || p < 0) ? undefined : (Math.floor(p * 10) / 10).toFixed(2); +} + +export function getTargetingKeys(viewabilityKeyName) { + return { + viewabilityKey: (viewabilityKeyName || VIEWABILITY_KEYNAME).toString(), + scrollKey: SCROLL_KEYNAME, + revenueKey: REVENUE_KEYNAME, + } +} + +export function getTargetingValues(v) { + return { + viewabilityValue: getTargetingValue(v['viewability']), + scrollValue: getTargetingValue(v['scrollDepth']), + revenueValue: getRevenueTargetingValue(v['revenue']) + } +} + +export const setKeyValue = (key, random) => setGptKeyValue(key, random.toString()); + +/** + * get all slots on page + * @return {Object[]} slot GoogleTag slots + */ +export function getAllSlots() { + return isGptPubadsDefined() && window.googletag.pubads().getSlots(); +} + +/** + * get GPT slot by placement id + * @param {string} code placement id + * @return {?Object} + */ +export function getSlotByCode(code) { + const slots = getAllSlots(); + if (!slots || !slots.length) { + return null; + } + return find(slots, s => s.getSlotElementId() === code || s.getAdUnitPath() === code) || null; +} + +function getLocalStorageData(storage) { + let brtd = null; + let bus = null; + try { + brtd = storage.getDataFromLocalStorage('__brtd'); + } catch (e) { + logError('unable to parse __brtd'); + } + try { + bus = storage.getDataFromLocalStorage('__bus'); + } catch (e) { + logError('unable to parse __bus'); + } + return { brtd, bus }; +} + +function convertBusData(bus) { + try { + return JSON.parse(bus); + } catch (e) { + return undefined; + } +} + +export function getHbm(bus, timestamp) { + try { + if (!isObjectDefined(bus)) { + return undefined; + } + const uahb = isObjectDefined(bus.uahb) ? bus.uahb : undefined; + const rahb = getRahb(bus.rahb, timestamp); + const lahb = getLahb(bus.lahb, timestamp); + return { + uahb: uahb?.avg && Number(uahb.avg?.toFixed(3)), + rahb: rahb?.avg && Number(rahb.avg?.toFixed(3)), + lahb: lahb?.avg && Number(lahb.avg?.toFixed(3)), + lbsa: lahb?.age && Number(lahb?.age?.toFixed(3)) + } + } catch (e) { + return undefined; + } +} + +export function getLahb(lahb, timestamp) { + try { + if (!isObjectDefined(lahb)) { + return undefined; + } + return { + avg: lahb.avg, + age: getDaysDifference(timestamp, lahb.time) + } + } catch (e) { + return undefined; + } +} + +export function getRahb(rahb, timestamp) { + try { + const rahbByTs = getRahbByTs(rahb, timestamp); + if (!isObjectDefined(rahbByTs)) { + return undefined; + } + + const rs = Object.keys(rahbByTs).reduce((sum, curTimestamp) => { + sum.sum += rahbByTs[curTimestamp].sum; + sum.smp += rahbByTs[curTimestamp].smp; + return sum; + }, { sum: 0, smp: 0 }); + + return { + avg: rs.sum / rs.smp + } + } catch (e) { + return undefined; + } +} + +export function getRahbByTs(rahb, timestamp) { + try { + if (!isObjectDefined(rahb)) { + return undefined + }; + const weekAgoTimestamp = timestamp - (7 * 24 * 60 * 60 * 1000); + Object.keys(rahb).forEach((ts) => { + if (parseInt(ts) < weekAgoTimestamp) { + delete rahb[ts]; + } + }); + return rahb; + } catch (e) { + return undefined; + } +} + +export function getPredictorData(storage, _moduleParams, timestamp, pvid) { + const win = window.top; + const doc = win.document; + const { brtd, bus } = getLocalStorageData(storage); + const convertedBus = convertBusData(bus); + const { uahb, rahb, lahb, lbsa } = getHbm(convertedBus, timestamp) || {}; + return { + ...{ + sk: _moduleParams.siteKey, + pk: _moduleParams.pubKey, + sw: (win.screen && win.screen.width) || -1, + sh: (win.screen && win.screen.height) || -1, + url: `${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`, + eu: isEngagingUser(), + t: timestamp, + pvid + }, + ...(brtd ? { us: brtd } : { us: '{}' }), + ...(document.referrer ? { r: document.referrer } : {}), + ...(document.title ? { at: document.title } : {}), + ...(uahb ? { uahb } : {}), + ...(rahb ? { rahb } : {}), + ...(lahb ? { lahb } : {}), + ...(lbsa ? { lbsa } : {}) + }; +} + +/** + * serialize object and return query params string + * @param {Object} data + * @return {string} + */ +export function toUrlParams(data) { + return Object.keys(data) + .map(key => key + '=' + encodeURIComponent(data[key])) + .join('&'); +} + +/** + * generate id according to macro script + * @param {Object} macro replacement macro + * @param {Object} slot google slot + * @return {?Object} + */ +export function getMacroId(macro, slot) { + if (macro) { + try { + const macroResult = evaluate(macro, slot.getSlotElementId(), slot.getAdUnitPath(), (match, p1) => { + return (p1 && slot.getTargeting(p1).join('_')) || 'NA'; + }); + return macroResult; + } catch (e) { + logError(`failed to evaluate: ${macro}`); + } + } + return slot.getSlotElementId(); +} + +function evaluate(macro, divId, adUnit, replacer) { + let macroResult = macro.p + .replace(/['"]+/g, '') + .replace(//g, divId); + + if (adUnit) { + macroResult = macroResult.replace(//g, adUnit); + } + if (replacer) { + macroResult = macroResult.replace(//g, replacer); + } + if (macro.s) { + macroResult = macroResult.substring(macro.s.s, macro.s.e); + } + return macroResult; +} diff --git a/libraries/cmp/cmpClient.js b/libraries/cmp/cmpClient.js index 1d0b327cee4..9e7e225ddb4 100644 --- a/libraries/cmp/cmpClient.js +++ b/libraries/cmp/cmpClient.js @@ -1,4 +1,4 @@ -import {GreedyPromise} from '../../src/utils/promise.js'; +import {PbPromise} from '../../src/utils/promise.js'; /** * @typedef {function} CMPClient @@ -130,7 +130,7 @@ export function cmpClient( if (isDirect) { client = function invokeCMPDirect(params = {}) { - return new GreedyPromise((resolve, reject) => { + return new PbPromise((resolve, reject) => { const ret = cmpFrame[apiName](...resolveParams({ ...params, callback: (params.callback || mode === MODE_CALLBACK) ? wrapCallback(params.callback, resolve, reject) : undefined, @@ -144,7 +144,7 @@ export function cmpClient( win.addEventListener('message', handleMessage, false); client = function invokeCMPFrame(params, once = false) { - return new GreedyPromise((resolve, reject) => { + return new PbPromise((resolve, reject) => { // call CMP via postMessage const callId = Math.random().toString(); const msg = { diff --git a/libraries/connectionInfo/connectionUtils.js b/libraries/connectionInfo/connectionUtils.js new file mode 100644 index 00000000000..29fed27b91d --- /dev/null +++ b/libraries/connectionInfo/connectionUtils.js @@ -0,0 +1,33 @@ +/** + * Returns the type of connection. + * + * @returns {number} - Type of connection. + */ +export function getConnectionType() { + const connection = navigator.connection || navigator.webkitConnection; + if (!connection) { + return 0; + } + switch (connection.type) { + case 'ethernet': + return 1; + case 'wifi': + return 2; + case 'wimax': + return 6; + default: + switch (connection.effectiveType) { + case 'slow-2g': + case '2g': + return 4; + case '3g': + return 5; + case '4g': + return 6; + case '5g': + return 7; + default: + return connection.type == 'cellular' ? 3 : 0; + } + } +} diff --git a/libraries/consentManagement/cmUtils.js b/libraries/consentManagement/cmUtils.js new file mode 100644 index 00000000000..b2afc8c7322 --- /dev/null +++ b/libraries/consentManagement/cmUtils.js @@ -0,0 +1,217 @@ +import {timedAuctionHook} from '../../src/utils/perfMetrics.js'; +import {isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../../src/utils.js'; +import {ConsentHandler} from '../../src/consentHandler.js'; +import {getGlobal} from '../../src/prebidGlobal.js'; +import {PbPromise} from '../../src/utils/promise.js'; +import {buildActivityParams} from '../../src/activities/params.js'; + +export function consentManagementHook(name, loadConsentData) { + const SEEN = new WeakSet(); + return timedAuctionHook(name, function requestBidsHook(fn, reqBidsConfigObj) { + return loadConsentData().then(({consentData, error}) => { + if (error && (!consentData || !SEEN.has(error))) { + SEEN.add(error); + logWarn(error.message, ...(error.args || [])); + } + fn.call(this, reqBidsConfigObj); + }).catch((error) => { + logError(`${error?.message} Canceling auction as per consentManagement config.`, ...(error?.args || [])); + fn.stopTiming(); + if (typeof reqBidsConfigObj.bidsBackHandler === 'function') { + reqBidsConfigObj.bidsBackHandler(); + } else { + logError('Error executing bidsBackHandler'); + } + }); + }); +} + +/** + * + * @typedef {Function} CmpLookupFn CMP lookup function. Should set up communication and keep consent data updated + * through consent data handlers' `setConsentData`. + * @param {SetProvisionalConsent} setProvisionalConsent optionally, the function can call this with provisional consent + * data, which will be used if the lookup times out before "proper" consent data can be retrieved. + * @returns {Promise<{void}>} a promise that resolves when the auction should be continued, or rejects if it should be canceled. + * + * @typedef {Function} SetProvisionalConsent + * @param {*} provisionalConsent + * @returns {void} + */ + +/** + * Look up consent data from CMP or config. + * + * @param {Object} options + * @param {String} options.name e.g. 'GPP'. Used only for log messages. + * @param {ConsentHandler} options.consentDataHandler consent data handler object (from src/consentHandler) + * @param {CmpLookupFn} options.setupCmp + * @param {Number?} options.cmpTimeout timeout (in ms) after which the auction should continue without consent data. + * @param {Number?} options.actionTimeout timeout (in ms) from when provisional consent is available to when the auction should continue with it + * @param {() => {}} options.getNullConsent consent data to use on timeout + * @returns {Promise<{error: Error, consentData: {}}>} + */ +export function lookupConsentData( + { + name, + consentDataHandler, + setupCmp, + cmpTimeout, + actionTimeout, + getNullConsent + } +) { + consentDataHandler.enable(); + let timeoutHandle; + + return new Promise((resolve, reject) => { + let provisionalConsent; + let cmpLoaded = false; + + function setProvisionalConsent(consentData) { + provisionalConsent = consentData; + if (!cmpLoaded) { + cmpLoaded = true; + actionTimeout != null && resetTimeout(actionTimeout); + } + } + + function resetTimeout(timeout) { + if (timeoutHandle != null) clearTimeout(timeoutHandle); + if (timeout != null) { + timeoutHandle = setTimeout(() => { + const consentData = consentDataHandler.getConsentData() ?? (cmpLoaded ? provisionalConsent : getNullConsent()); + const message = `timeout waiting for ${cmpLoaded ? 'user action on CMP' : 'CMP to load'}`; + consentDataHandler.setConsentData(consentData); + resolve({consentData, error: new Error(`${name} ${message}`)}); + }, timeout); + } else { + timeoutHandle = null; + } + } + setupCmp(setProvisionalConsent) + .then(() => resolve({consentData: consentDataHandler.getConsentData()}), reject); + cmpTimeout != null && resetTimeout(cmpTimeout); + }).finally(() => { + timeoutHandle && clearTimeout(timeoutHandle); + }).catch((e) => { + consentDataHandler.setConsentData(null); + throw e; + }); +} + +export function configParser( + { + namespace, + displayName, + consentDataHandler, + parseConsentData, + getNullConsent, + cmpHandlers, + DEFAULT_CMP = 'iab', + DEFAULT_CONSENT_TIMEOUT = 10000 + } = {} +) { + function msg(message) { + return `consentManagement.${namespace} ${message}`; + } + let requestBidsHook, cdLoader, staticConsentData; + + function attachActivityParams(next, params) { + return next(Object.assign({[`${namespace}Consent`]: consentDataHandler.getConsentData()}, params)); + } + + function loadConsentData() { + return cdLoader().then(({error}) => ({error, consentData: consentDataHandler.getConsentData()})) + } + + function activate() { + if (requestBidsHook == null) { + requestBidsHook = consentManagementHook(namespace, () => cdLoader()); + getGlobal().requestBids.before(requestBidsHook, 50); + buildActivityParams.before(attachActivityParams); + logInfo(`${displayName} consentManagement module has been activated...`) + } + } + + function reset() { + if (requestBidsHook != null) { + getGlobal().requestBids.getHooks({hook: requestBidsHook}).remove(); + buildActivityParams.getHooks({hook: attachActivityParams}).remove(); + requestBidsHook = null; + } + } + + + return function getConsentConfig(config) { + config = config?.[namespace]; + if (!config || typeof config !== 'object') { + logWarn(msg(`config not defined, exiting consent manager module`)); + reset(); + return {}; + } + let cmpHandler; + if (isStr(config.cmpApi)) { + cmpHandler = config.cmpApi; + } else { + cmpHandler = DEFAULT_CMP; + logInfo(msg(`config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`)); + } + let cmpTimeout; + if (isNumber(config.timeout)) { + cmpTimeout = config.timeout; + } else { + cmpTimeout = DEFAULT_CONSENT_TIMEOUT; + logInfo(msg(`config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`)); + } + const actionTimeout = isNumber(config.actionTimeout) ? config.actionTimeout : null; + let setupCmp; + if (cmpHandler === 'static') { + if (isPlainObject(config.consentData)) { + staticConsentData = config.consentData; + cmpTimeout = null; + setupCmp = () => new PbPromise(resolve => resolve(consentDataHandler.setConsentData(parseConsentData(staticConsentData)))) + } else { + logError(msg(`config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`)); + } + } else if (!cmpHandlers.hasOwnProperty(cmpHandler)) { + consentDataHandler.setConsentData(null); + logWarn(`${displayName} CMP framework (${cmpHandler}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + setupCmp = () => PbPromise.resolve(); + } else { + setupCmp = cmpHandlers[cmpHandler]; + } + + const lookup = () => lookupConsentData({ + name: displayName, + consentDataHandler, + setupCmp, + cmpTimeout, + actionTimeout, + getNullConsent, + }); + + cdLoader = (() => { + let cd; + return function () { + if (cd == null) { + cd = lookup().catch(err => { + cd = null; + throw err; + }) + } + return cd; + } + })(); + + activate(); + return { + cmpHandler, + cmpTimeout, + actionTimeout, + staticConsentData, + loadConsentData, + requestBidsHook + } + } +} diff --git a/libraries/cookieSync/cookieSync.js b/libraries/cookieSync/cookieSync.js new file mode 100644 index 00000000000..286c1297530 --- /dev/null +++ b/libraries/cookieSync/cookieSync.js @@ -0,0 +1,42 @@ +import { getStorageManager } from '../../src/storageManager.js'; +const COOKIE_KEY_MGUID = '__mguid_'; + +export function cookieSync(syncOptions, gdprConsent, uspConsent, bidderCode, cookieOrigin, ckIframeUrl, cookieTime) { + const storage = getStorageManager({bidderCode: bidderCode}); + const origin = encodeURIComponent(location.origin || `https://${location.host}`); + let syncParamUrl = `dm=${origin}`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + window.addEventListener('message', function handler(event) { + if (!event.data || event.origin != cookieOrigin) { + return; + } + + this.removeEventListener('message', handler); + + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, cookieTime); + } + }, true); + return [ + { + type: 'iframe', + url: `${ckIframeUrl}?${syncParamUrl}` + } + ]; + } +} diff --git a/libraries/creative-renderer-display/renderer.js b/libraries/creative-renderer-display/renderer.js index 72f3658fe79..9d9fbc7dfda 100644 --- a/libraries/creative-renderer-display/renderer.js +++ b/libraries/creative-renderer-display/renderer.js @@ -1,2 +1,2 @@ // this file is autogenerated, see creative/README.md -export const RENDERER = "!function(){\"use strict\";window.render=function({ad:d,adUrl:i,width:n,height:e},{mkFrame:o},r){if(!d&&!i)throw{reason:\"noAd\",message:\"Missing ad markup or URL\"};{const t=r.document,s={width:n,height:e};i&&!d?s.src=i:s.srcdoc=d,t.body.appendChild(o(t,s))}}}();" \ No newline at end of file +export const RENDERER = "(()=>{\"use strict\";window.render=function({ad:e,adUrl:t,width:n,height:i,instl:d},{mkFrame:r},s){if(!e&&!t)throw{reason:\"noAd\",message:\"Missing ad markup or URL\"};{if(null==i){const e=s.document?.body;[e,e?.parentElement].filter((e=>null!=e?.style)).forEach((e=>e.style.height=\"100%\"))}const h=s.document,o={width:n??\"100%\",height:i??\"100%\"};if(t&&!e?o.src=t:o.srcdoc=e,h.body.appendChild(r(h,o)),d&&s.frameElement){const e=s.frameElement.style;e.width=n?`${n}px`:\"100vw\",e.height=i?`${i}px`:\"100vh\"}}}})();" \ No newline at end of file diff --git a/libraries/creative-renderer-native/renderer.js b/libraries/creative-renderer-native/renderer.js index 57d86fc8ce3..5651cc3f0ca 100644 --- a/libraries/creative-renderer-native/renderer.js +++ b/libraries/creative-renderer-native/renderer.js @@ -1,2 +1,2 @@ // this file is autogenerated, see creative/README.md -export const RENDERER = "!function(){\"use strict\";const e=\"Prebid Native\",t={title:\"text\",data:\"value\",img:\"url\",video:\"vasttag\"};function n(e,t){return new Promise(((n,r)=>{const i=t.createElement(\"script\");i.onload=n,i.onerror=r,i.src=e,t.body.appendChild(i)}))}function r(e,t,r,i,o=n){const{rendererUrl:s,assets:a,ortb:d,adTemplate:c}=t,l=i.document;return s?o(s,l).then((()=>{if(\"function\"!=typeof i.renderAd)throw new Error(`Renderer from '${s}' does not define renderAd()`);const e=a||[];return e.ortb=d,i.renderAd(e)})):Promise.resolve(r(c??l.body.innerHTML))}window.render=function({adId:n,native:i},{sendMessage:o},s,a=r){const{head:d,body:c}=s.document,l=()=>o(e,{action:\"resizeNativeHeight\",height:c.offsetHeight,width:c.offsetWidth}),u=function(e,{assets:n=[],ortb:r,nativeKeys:i={}}){const o=Object.fromEntries(n.map((({key:e,value:t})=>[e,t])));let s=Object.fromEntries(Object.entries(i).flatMap((([t,n])=>{const r=o.hasOwnProperty(t)?o[t]:void 0;return[[`##${n}##`,r],[`${n}:${e}`,r]]})));return r&&Object.assign(s,{\"##hb_native_linkurl##\":r.link?.url,\"##hb_native_privacy##\":r.privacy},Object.fromEntries((r.assets||[]).flatMap((e=>{const n=Object.keys(t).find((t=>e[t]));return[n&&[`##hb_native_asset_id_${e.id}##`,e[n][t[n]]],e.link?.url&&[`##hb_native_asset_link_id_${e.id}##`,e.link.url]].filter((e=>e))})))),s=Object.entries(s).concat([[/##hb_native_asset_(link_)?id_\\d+##/g]]),function(e){return s.reduce(((e,[t,n])=>e.replaceAll(t,n||\"\")),e)}}(n,i);return d&&(d.innerHTML=u(d.innerHTML)),a(n,i,u,s).then((t=>{c.innerHTML=t,\"function\"==typeof s.postRenderAd&&s.postRenderAd({adId:n,...i}),s.document.querySelectorAll(\".pb-click\").forEach((t=>{const n=t.getAttribute(\"hb_native_asset_id\");t.addEventListener(\"click\",(()=>o(e,{action:\"click\",assetId:n})))})),o(e,{action:\"fireNativeImpressionTrackers\"}),\"complete\"===s.document.readyState?l():s.onload=l}))}}();" \ No newline at end of file +export const RENDERER = "(()=>{\"use strict\";const e=\"Prebid Native\",t={title:\"text\",data:\"value\",img:\"url\",video:\"vasttag\"};function n(e,t){return new Promise(((n,r)=>{const i=t.createElement(\"script\");i.onload=n,i.onerror=r,i.src=e,t.body.appendChild(i)}))}function r(e){return Array.from(e.querySelectorAll('iframe[srcdoc*=\"render\"]'))}function i(e){const t=e.cloneNode(!0);return r(t).forEach((e=>e.parentNode.removeChild(e))),t.innerHTML}function o(e,t,r,o,s=n){const{rendererUrl:c,assets:d,ortb:a,adTemplate:l}=t,u=o.document;return c?s(c,u).then((()=>{if(\"function\"!=typeof o.renderAd)throw new Error(`Renderer from '${c}' does not define renderAd()`);const e=d||[];return e.ortb=a,o.renderAd(e)})):Promise.resolve(r(l??i(u.body)))}window.render=function({adId:n,native:s},{sendMessage:c},d,a=o){const{head:l,body:u}=d.document,f=()=>{u.style.display=\"none\",u.style.display=\"block\",c(e,{action:\"resizeNativeHeight\",height:u.offsetHeight,width:u.offsetWidth})};function b(e,t){const n=r(e);Array.from(e.childNodes).filter((e=>!n.includes(e))).forEach((t=>e.removeChild(t))),e.insertAdjacentHTML(\"afterbegin\",t)}const h=function(e,{assets:n=[],ortb:r,nativeKeys:i={}}){const o=Object.fromEntries(n.map((({key:e,value:t})=>[e,t])));let s=Object.fromEntries(Object.entries(i).flatMap((([t,n])=>{const r=o.hasOwnProperty(t)?o[t]:void 0;return[[`##${n}##`,r],[`${n}:${e}`,r]]})));return r&&Object.assign(s,{\"##hb_native_linkurl##\":r.link?.url,\"##hb_native_privacy##\":r.privacy},Object.fromEntries((r.assets||[]).flatMap((e=>{const n=Object.keys(t).find((t=>e[t]));return[n&&[`##hb_native_asset_id_${e.id}##`,e[n][t[n]]],e.link?.url&&[`##hb_native_asset_link_id_${e.id}##`,e.link.url]].filter((e=>e))})))),s=Object.entries(s).concat([[/##hb_native_asset_(link_)?id_\\d+##/g]]),function(e){return s.reduce(((e,[t,n])=>e.replaceAll(t,n||\"\")),e)}}(n,s);return b(l,h(i(l))),a(n,s,h,d).then((t=>{b(u,t),\"function\"==typeof d.postRenderAd&&d.postRenderAd({adId:n,...s}),d.document.querySelectorAll(\".pb-click\").forEach((t=>{const n=t.getAttribute(\"hb_native_asset_id\");t.addEventListener(\"click\",(()=>c(e,{action:\"click\",assetId:n})))})),c(e,{action:\"fireNativeImpressionTrackers\"}),\"complete\"===d.document.readyState?f():d.onload=f}))}})();" \ No newline at end of file diff --git a/libraries/currencyUtils/floor.js b/libraries/currencyUtils/floor.js new file mode 100644 index 00000000000..ee41bad01da --- /dev/null +++ b/libraries/currencyUtils/floor.js @@ -0,0 +1,23 @@ +import * as utils from '../../src/utils.js'; + +/** + * get BidFloor + * @param {*} bid + * @returns + */ +export function getBidFloor(bid) { + if (!utils.isFn(bid.getFloor)) { + return utils.deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor?.floor; + } catch (_) { + return 0; + } +} diff --git a/libraries/deepintentUtils/index.js b/libraries/deepintentUtils/index.js new file mode 100644 index 00000000000..5abe2d1d061 --- /dev/null +++ b/libraries/deepintentUtils/index.js @@ -0,0 +1,42 @@ +import { isInteger } from '../../src/utils.js'; + +export const COMMON_ORTB_VIDEO_PARAMS = { + 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), + 'minduration': (value) => isInteger(value), + 'maxduration': (value) => isInteger(value), + 'protocols': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 10), + 'w': (value) => isInteger(value), + 'h': (value) => isInteger(value), + 'startdelay': (value) => isInteger(value), + 'linearity': (value) => [1, 2].indexOf(value) !== -1, + 'skip': (value) => [0, 1].indexOf(value) !== -1, + 'skipmin': (value) => isInteger(value), + 'skipafter': (value) => isInteger(value), + 'sequence': (value) => isInteger(value), + 'battr': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 17), + 'maxextended': (value) => isInteger(value), + 'minbitrate': (value) => isInteger(value), + 'maxbitrate': (value) => isInteger(value), + 'boxingallowed': (value) => [0, 1].indexOf(value) !== -1, + 'playbackmethod': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6), + 'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1, + 'api': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6) +}; + +export function formatResponse(bid) { + return { + requestId: bid && bid.impid ? bid.impid : undefined, + cpm: bid && bid.price ? bid.price : 0.0, + width: bid && bid.w ? bid.w : 0, + height: bid && bid.h ? bid.h : 0, + ad: bid && bid.adm ? bid.adm : '', + meta: { + advertiserDomains: bid && bid.adomain ? bid.adomain : [] + }, + creativeId: bid && bid.crid ? bid.crid : undefined, + netRevenue: false, + currency: bid && bid.cur ? bid.cur : 'USD', + ttl: 300, + dealId: bid && bid.dealId ? bid.dealId : undefined + } +} diff --git a/libraries/dfpUtils/dfpUtils.js b/libraries/dfpUtils/dfpUtils.js new file mode 100644 index 00000000000..4b957eb4999 --- /dev/null +++ b/libraries/dfpUtils/dfpUtils.js @@ -0,0 +1,26 @@ +import {gdprDataHandler} from '../../src/consentHandler.js'; + +/** Safe defaults which work on pretty much all video calls. */ +export const DEFAULT_DFP_PARAMS = { + env: 'vp', + gdfp_req: 1, + output: 'vast', + unviewed_position_start: 1, +} + +export const DFP_ENDPOINT = { + protocol: 'https', + host: 'securepubads.g.doubleclick.net', + pathname: '/gampad/ads' +} + +export function gdprParams() { + const gdprConsent = gdprDataHandler.getConsentData(); + const params = {}; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { params.gdpr = Number(gdprConsent.gdprApplies); } + if (gdprConsent.consentString) { params.gdpr_consent = gdprConsent.consentString; } + if (gdprConsent.addtlConsent) { params.addtl_consent = gdprConsent.addtlConsent; } + } + return params; +} diff --git a/libraries/dspxUtils/bidderUtils.js b/libraries/dspxUtils/bidderUtils.js new file mode 100644 index 00000000000..612a20f6865 --- /dev/null +++ b/libraries/dspxUtils/bidderUtils.js @@ -0,0 +1,390 @@ +import { BANNER, VIDEO } from '../../src/mediaTypes.js'; +import {deepAccess, isArray, isEmptyStr, isFn, logError} from '../../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ +/** + * Adds userIds to payload + * + * @param bidRequest + * @param payload + */ +export function fillUsersIds(bidRequest, payload) { + if (bidRequest.hasOwnProperty('userId')) { + let didMapping = { + did_netid: 'userId.netId', + did_id5: 'userId.id5id.uid', + did_id5_linktype: 'userId.id5id.ext.linkType', + did_uid2: 'userId.uid2', + did_sharedid: 'userId.sharedid', + did_pubcid: 'userId.pubcid', + did_uqid: 'userId.utiq', + did_cruid: 'userId.criteoid', + did_euid: 'userId.euid', + // did_tdid: 'unifiedId', + did_tdid: 'userId.tdid', + did_ppuid: function() { + let path = 'userId.pubProvidedId'; + let value = deepAccess(bidRequest, path); + if (isArray(value)) { + for (const rec of value) { + if (rec.uids && rec.uids.length > 0) { + for (let i = 0; i < rec.uids.length; i++) { + if ('id' in rec.uids[i] && deepAccess(rec.uids[i], 'ext.stype') === 'ppuid') { + return (rec.uids[i].atype ?? '') + ':' + rec.source + ':' + rec.uids[i].id; + } + } + } + } + } + return undefined; + }, + did_cpubcid: 'crumbs.pubcid' + }; + for (let paramName in didMapping) { + let path = didMapping[paramName]; + + // handle function + if (typeof path == 'function') { + let value = path(paramName); + if (value) { + payload[paramName] = value; + } + continue; + } + // direct access + let value = deepAccess(bidRequest, path); + if (typeof value == 'string' || typeof value == 'number') { + payload[paramName] = value; + } else if (typeof value == 'object') { + // trying to find string ID value + if (typeof deepAccess(bidRequest, path + '.id') == 'string') { + payload[paramName] = deepAccess(bidRequest, path + '.id'); + } else { + if (Object.keys(value).length > 0) { + logError(`WARNING: fillUserIds had to use first key in user object to get value for bid.userId key: ${path}.`); + payload[paramName] = value[Object.keys(value)[0]]; + } + } + } + } + } +} + +export function appendToUrl(url, what) { + if (!what) { + return url; + } + return url + (url.indexOf('?') !== -1 ? '&' : '?') + what; +} + +export function objectToQueryString(obj, prefix) { + let str = []; + let p; + for (p in obj) { + if (obj.hasOwnProperty(p)) { + let k = prefix ? prefix + '[' + p + ']' : p; + let v = obj[p]; + str.push((v !== null && typeof v === 'object') + ? objectToQueryString(v, k) + : encodeURIComponent(k) + '=' + encodeURIComponent(v)); + } + } + return str.filter(n => n).join('&'); +} + +/** + * Check if it's a banner bid request + * + * @param {BidRequest} bid - Bid request generated from ad slots + * @returns {boolean} True if it's a banner bid + */ +export function isBannerRequest(bid) { + return bid.mediaType === 'banner' || !!deepAccess(bid, 'mediaTypes.banner') || !isVideoRequest(bid); +} + +/** + * Check if it's a video bid request + * + * @param {BidRequest} bid - Bid request generated from ad slots + * @returns {boolean} True if it's a video bid + */ +export function isVideoRequest(bid) { + return bid.mediaType === 'video' || !!deepAccess(bid, 'mediaTypes.video'); +} + +/** + * Get video sizes + * + * @param {BidRequest} bid - Bid request generated from ad slots + * @returns {object} + */ +export function getVideoSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); +} + +/** + * Get video context + * + * @param {BidRequest} bid - Bid request generated from ad slots + * @returns {object} + */ +export function getVideoContext(bid) { + return deepAccess(bid, 'mediaTypes.video.context') || 'unknown'; +} + +/** + * Get banner sizes + * + * @param {BidRequest} bid - Bid request generated from ad slots + * @returns {object} True if it's a video bid + */ +export function getBannerSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); +} + +/** + * Parse size + * @param size + * @returns {object} sizeObj + */ +export function parseSize(size) { + let sizeObj = {} + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + return sizeObj; +} + +/** + * Parse sizes + * @param sizes + * @returns {{width: number , height: number }[]} + */ +export function parseSizes(sizes) { + if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]]) + return sizes.map(size => parseSize(size)); + } + return [parseSize(sizes)]; // or a single one ? (ie. [728,90]) +} + +/** + * Get MediaInfo object for server request + * + * @param mediaTypesInfo + * @returns {*} + */ +export function convertMediaInfoForRequest(mediaTypesInfo) { + let requestData = {}; + Object.keys(mediaTypesInfo).forEach(mediaType => { + requestData[mediaType] = mediaTypesInfo[mediaType].map(size => { + return size.width + 'x' + size.height; + }).join(','); + }); + return requestData; +} + +/** + * Get media types info + * + * @param bid + */ +export function getMediaTypesInfo(bid) { + let mediaTypesInfo = {}; + + if (bid.mediaTypes) { + Object.keys(bid.mediaTypes).forEach(mediaType => { + if (mediaType === BANNER) { + mediaTypesInfo[mediaType] = getBannerSizes(bid); + } + if (mediaType === VIDEO) { + mediaTypesInfo[mediaType] = getVideoSizes(bid); + } + }); + } else { + mediaTypesInfo[BANNER] = getBannerSizes(bid); + } + return mediaTypesInfo; +} + +/** + * Get Bid Floor + * @param bid + * @returns {number|*} + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'EUR', + mediaType: '*', + size: '*', + }); + return bidFloor?.floor; + } catch (_) { + return 0 + } +} + +/** + * Convert site.content to string + * @param content + */ +export function siteContentToString(content) { + if (!content) { + return ''; + } + let stringKeys = ['id', 'title', 'series', 'season', 'artist', 'genre', 'isrc', 'url', 'keywords']; + let intKeys = ['episode', 'context', 'livestream']; + let arrKeys = ['cat']; + let retArr = []; + arrKeys.forEach(k => { + let val = deepAccess(content, k); + if (val && Array.isArray(val)) { + retArr.push(k + ':' + val.join('|')); + } + }); + intKeys.forEach(k => { + let val = deepAccess(content, k); + if (val && typeof val === 'number') { + retArr.push(k + ':' + val); + } + }); + stringKeys.forEach(k => { + let val = deepAccess(content, k); + if (val && typeof val === 'string') { + retArr.push(k + ':' + encodeURIComponent(val)); + } + }); + return retArr.join(','); +} + +/** + * Assigns multiple values to the specified keys on an object if the values are not undefined. + * @param {Object} target - The object to which the values will be assigned. + * @param {Object} values - An object containing key-value pairs to be assigned. + */ +export function assignDefinedValues(target, values) { + for (const key in values) { + if (values[key] !== undefined) { + target[key] = values[key]; + } + } +} + +/** + * Extracts user segments/topics from the bid request object + * @param {Object} bid - The bid request object + * @returns {{segclass: *, segtax: *, segments: *}|undefined} - User segments/topics or undefined if not found + */ +export function extractUserSegments(bid) { + const userData = deepAccess(bid, 'ortb2.user.data') || []; + for (const dataObj of userData) { + if (dataObj.segment && isArray(dataObj.segment) && dataObj.segment.length > 0) { + const segments = dataObj.segment + .filter(seg => seg.id && !isEmptyStr(seg.id) && isFinite(seg.id)) + .map(seg => Number(seg.id)); + if (segments.length > 0) { + return { + segtax: deepAccess(dataObj, 'ext.segtax'), + segclass: deepAccess(dataObj, 'ext.segclass'), + segments: segments.join(',') + }; + } + } + } + return undefined; +} + +export function handleSyncUrls(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (!serverResponses || serverResponses.length === 0) { + return []; + } + + const syncs = []; + let gdprParams = ''; + if (gdprConsent) { + if ('gdprApplies' in gdprConsent && typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; + } + } + + if (serverResponses.length > 0 && serverResponses[0].body.userSync) { + if (syncOptions.iframeEnabled) { + serverResponses[0].body.userSync.iframeUrl.forEach((url) => syncs.push({ + type: 'iframe', + url: appendToUrl(url, gdprParams) + })); + } + if (syncOptions.pixelEnabled) { + serverResponses[0].body.userSync.imageUrl.forEach((url) => syncs.push({ + type: 'image', + url: appendToUrl(url, gdprParams) + })); + } + } + return syncs; +} + +export function interpretResponse(serverResponse, bidRequest, rendererFunc) { + const bidResponses = []; + const response = serverResponse.body; + const crid = response.crid || 0; + const cpm = response.cpm / 1000000 || 0; + if (cpm !== 0 && crid !== 0) { + const dealId = response.dealid || ''; + const currency = response.currency || 'EUR'; + const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue; + const bidResponse = { + requestId: response.bid_id, + cpm: cpm, + width: response.width, + height: response.height, + creativeId: crid, + dealId: dealId, + currency: currency, + netRevenue: netRevenue, + type: response.type, + ttl: 60, + meta: { + advertiserDomains: response.adomain || [] + } + }; + + if (response.vastUrl) { + bidResponse.vastUrl = response.vastUrl; + bidResponse.mediaType = 'video'; + } + if (response.vastXml) { + bidResponse.vastXml = response.vastXml; + bidResponse.mediaType = 'video'; + } + if (response.renderer) { + bidResponse.renderer = rendererFunc(bidRequest, response); + } + + if (response.videoCacheKey) { + bidResponse.videoCacheKey = response.videoCacheKey; + } + + if (response.adTag) { + bidResponse.ad = response.adTag; + } + + if (response.bid_appendix) { + Object.keys(response.bid_appendix).forEach(fieldName => { + bidResponse[fieldName] = response.bid_appendix[fieldName]; + }); + } + + bidResponses.push(bidResponse); + } + return bidResponses; +} diff --git a/libraries/equativUtils/equativUtils.js b/libraries/equativUtils/equativUtils.js new file mode 100644 index 00000000000..bdcbdad2f33 --- /dev/null +++ b/libraries/equativUtils/equativUtils.js @@ -0,0 +1,30 @@ +import { VIDEO } from '../../src/mediaTypes.js'; +import { deepAccess, isFn } from '../../src/utils.js'; + +const DEFAULT_FLOOR = 0.0; + +/** + * Get floors from Prebid Price Floors module + * + * @param {object} bid Bid request object + * @param {string} currency Ad server currency + * @param {string} mediaType Bid media type + * @return {number} Floor price + */ +export function getBidFloor (bid, currency, mediaType) { + const floors = []; + + if (isFn(bid.getFloor)) { + (deepAccess(bid, `mediaTypes.${mediaType}.${mediaType === VIDEO ? 'playerSize' : 'sizes'}`) || []).forEach(size => { + const floor = bid.getFloor({ + currency: currency || 'USD', + mediaType, + size + }).floor; + + floors.push(!isNaN(floor) ? floor : DEFAULT_FLOOR); + }); + } + + return floors.length ? Math.min(...floors) : DEFAULT_FLOOR; +} diff --git a/libraries/fpdUtils/deviceInfo.js b/libraries/fpdUtils/deviceInfo.js new file mode 100644 index 00000000000..fd2c4bf47d6 --- /dev/null +++ b/libraries/fpdUtils/deviceInfo.js @@ -0,0 +1,58 @@ +import * as utils from '../../src/utils.js'; + +/** + * get device + * @return {boolean} + */ +export function getDevice() { + let check = false; + (function (a) { + let reg1 = new RegExp( + [ + '(android|bbd+|meego)', + '.+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', + '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', + '|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap', + '|windows ce|xda|xiino|android|ipad|playbook|silk', + ].join(''), + 'i' + ); + let reg2 = new RegExp( + [ + '1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)', + '|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )', + '|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55/|capi|ccwa|cdm-|cell', + '|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)', + '|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene', + '|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c', + '|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|/)|ibro|idea|ig01|ikom', + '|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |/)|klon|kpt |kwc-|kyo(c|k)', + '|le(no|xi)|lg( g|/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50/|ma(te|ui|xo)|mc(01|21|ca)', + '|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]', + '|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)', + '|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio', + '|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55/|sa(ge|ma|mm|ms', + '|ny|va)|sc(01|h-|oo|p-)|sdk/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al', + '|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)', + '|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|', + 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)', + '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-', + '|your|zeto|zte-', + ].join(''), + 'i' + ); + if (reg1.test(a) || reg2.test(a.substr(0, 4))) { + check = true; + } + })(navigator.userAgent || navigator.vendor || window.opera); + return check; +} + +/** + * get screen size + * + * @returns {Array} eg: "['widthxheight']" + */ +export function getScreenSize() { + return utils.parseSizesInput([window.screen.width, window.screen.height]); +} diff --git a/libraries/fpdUtils/pageInfo.js b/libraries/fpdUtils/pageInfo.js new file mode 100644 index 00000000000..8e02134e070 --- /dev/null +++ b/libraries/fpdUtils/pageInfo.js @@ -0,0 +1,71 @@ +/** + * get page title + * @returns {string} + */ +export function getPageTitle(win = window) { + try { + const ogTitle = win.top.document.querySelector('meta[property="og:title"]'); + return win.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]'); + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +/** + * get page description + * @returns {string} + */ +export function getPageDescription(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="description"]') || + win.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + +/** + * get page keywords + * @returns {string} + */ +export function getPageKeywords(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="keywords"]'); + } catch (e) { + element = document.querySelector('meta[name="keywords"]'); + } + + return (element && element.content) || ''; +} + +/** + * get connection downlink + * @returns {number} + */ +export function getConnectionDownLink(win = window) { + const nav = win.navigator || {}; + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined; +} + +/** + * @param bidRequest + * @param bidderRequest + * @returns {string} + */ +export function getReferrer(bidRequest = {}, bidderRequest = {}) { + let pageUrl; + if (bidRequest.params && bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else { + pageUrl = bidderRequest?.refererInfo?.page; + } + return pageUrl; +} diff --git a/libraries/gptUtils/gptUtils.js b/libraries/gptUtils/gptUtils.js index 950f28c618f..923d207c0d9 100644 --- a/libraries/gptUtils/gptUtils.js +++ b/libraries/gptUtils/gptUtils.js @@ -1,5 +1,6 @@ +import { CLIENT_SECTIONS } from '../../src/fpd/oneClient.js'; import {find} from '../../src/polyfill.js'; -import {compareCodeAndSlot, isGptPubadsDefined} from '../../src/utils.js'; +import {compareCodeAndSlot, deepAccess, isGptPubadsDefined, uniques} from '../../src/utils.js'; /** * Returns filter function to match adUnitCode in slot @@ -10,6 +11,18 @@ export function isSlotMatchingAdUnitCode(adUnitCode) { return (slot) => compareCodeAndSlot(slot, adUnitCode); } +/** + * @summary Export a k-v pair to GAM + */ +export function setKeyValue(key, value) { + if (!key || typeof key !== 'string') return false; + window.googletag = window.googletag || {cmd: []}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(() => { + window.googletag.pubads().setTargeting(key, value); + }); +} + /** * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page */ @@ -35,3 +48,88 @@ export function getGptSlotInfoForAdUnitCode(adUnitCode) { } return {}; } + +export const taxonomies = ['IAB_AUDIENCE_1_1', 'IAB_CONTENT_2_2']; + +export function getSignals(fpd) { + const signals = Object.entries({ + [taxonomies[0]]: getSegments(fpd, ['user.data'], 4), + [taxonomies[1]]: getSegments(fpd, CLIENT_SECTIONS.map(section => `${section}.content.data`), 6) + }).map(([taxonomy, values]) => values.length ? {taxonomy, values} : null) + .filter(ob => ob); + + return signals; +} + +export function getSegments(fpd, sections, segtax) { + return sections + .flatMap(section => deepAccess(fpd, section) || []) + .filter(datum => datum.ext?.segtax === segtax) + .flatMap(datum => datum.segment?.map(seg => seg.id)) + .filter(ob => ob) + .filter(uniques) +} + +/** + * Add an event listener on the given GAM event. + * If GPT Pubads isn't defined, window.googletag is set to a new object. + * @param {String} event + * @param {Function} callback + */ +export function subscribeToGamEvent(event, callback) { + const register = () => window.googletag.pubads().addEventListener(event, callback); + if (isGptPubadsDefined()) { + register(); + return; + } + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(register); +} + +/** + * @typedef {Object} Slot + * @property {function(String): (String|null)} get + * @property {function(): String} getAdUnitPath + * @property {function(): String[]} getAttributeKeys + * @property {function(): String[]} getCategoryExclusions + * @property {function(String): String} getSlotElementId + * @property {function(): String[]} getTargeting + * @property {function(): String[]} getTargetingKeys + * @see {@link https://developers.google.com/publisher-tag/reference#googletag.Slot GPT official docs} + */ + +/** + * @typedef {Object} SlotRenderEndedEvent + * @property {(String|null)} advertiserId + * @property {(String|null)} campaignId + * @property {(String[]|null)} companyIds + * @property {(Number|null)} creativeId + * @property {(Number|null)} creativeTemplateId + * @property {(Boolean)} isBackfill + * @property {(Boolean)} isEmpty + * @property {(Number[]|null)} labelIds + * @property {(Number|null)} lineItemId + * @property {(String)} serviceName + * @property {(string|Number[]|null)} size + * @property {(Slot)} slot + * @property {(Boolean)} slotContentChanged + * @property {(Number|null)} sourceAgnosticCreativeId + * @property {(Number|null)} sourceAgnosticLineItemId + * @property {(Number[]|null)} yieldGroupIds + * @see {@link https://developers.google.com/publisher-tag/reference#googletag.events.SlotRenderEndedEvent GPT official docs} + */ + +/** + * @callback SlotRenderEndedEventCallback + * @param {SlotRenderEndedEvent} event + * @returns {void} + */ + +/** + * Add an event listener on the GAM event 'slotRenderEnded'. + * @param {SlotRenderEndedEventCallback} callback + */ +export function subscribeToGamSlotRenderEndedEvent(callback) { + subscribeToGamEvent('slotRenderEnded', callback) +} diff --git a/libraries/greedy/greedyPromise.js b/libraries/greedy/greedyPromise.js new file mode 100644 index 00000000000..46acc29c805 --- /dev/null +++ b/libraries/greedy/greedyPromise.js @@ -0,0 +1,116 @@ +const SUCCESS = 0; +const FAIL = 1; + +/** + * A version of Promise that runs callbacks synchronously when it can (i.e. after it's been fulfilled or rejected). + */ +export class GreedyPromise { + #result; + #callbacks; + + constructor(resolver) { + if (typeof resolver !== 'function') { + throw new Error('resolver not a function'); + } + const result = []; + const callbacks = []; + let [resolve, reject] = [SUCCESS, FAIL].map((type) => { + return function (value) { + if (type === SUCCESS && typeof value?.then === 'function') { + value.then(resolve, reject); + } else if (!result.length) { + result.push(type, value); + while (callbacks.length) callbacks.shift()(); + } + } + }); + try { + resolver(resolve, reject); + } catch (e) { + reject(e); + } + this.#result = result; + this.#callbacks = callbacks; + } + + then(onSuccess, onError) { + const result = this.#result; + return new this.constructor((resolve, reject) => { + const continuation = () => { + let value = result[1]; + let [handler, resolveFn] = result[0] === SUCCESS ? [onSuccess, resolve] : [onError, reject]; + if (typeof handler === 'function') { + try { + value = handler(value); + } catch (e) { + reject(e); + return; + } + resolveFn = resolve; + } + resolveFn(value); + } + result.length ? continuation() : this.#callbacks.push(continuation); + }); + } + + catch(onError) { + return this.then(null, onError); + } + + finally(onFinally) { + let val; + return this.then( + (v) => { val = v; return onFinally(); }, + (e) => { val = this.constructor.reject(e); return onFinally() } + ).then(() => val); + } + + static #collect(promises, collector, done) { + let cnt = promises.length; + function clt() { + collector.apply(this, arguments); + if (--cnt <= 0 && done) done(); + } + promises.length === 0 && done ? done() : promises.forEach((p, i) => this.resolve(p).then( + (val) => clt(true, val, i), + (err) => clt(false, err, i) + )); + } + + static race(promises) { + return new this((resolve, reject) => { + this.#collect(promises, (success, result) => success ? resolve(result) : reject(result)); + }) + } + + static all(promises) { + return new this((resolve, reject) => { + let res = []; + this.#collect(promises, (success, val, i) => success ? res[i] = val : reject(val), () => resolve(res)); + }) + } + + static allSettled(promises) { + return new this((resolve) => { + let res = []; + this.#collect(promises, (success, val, i) => res[i] = success ? {status: 'fulfilled', value: val} : {status: 'rejected', reason: val}, () => resolve(res)) + }) + } + + static resolve(value) { + return new this(resolve => resolve(value)) + } + + static reject(error) { + return new this((resolve, reject) => reject(error)) + } +} + +export function greedySetTimeout(fn, delayMs = 0) { + if (delayMs > 0) { + return setTimeout(fn, delayMs) + } else { + fn() + } +} diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js new file mode 100644 index 00000000000..6dc16969d6c --- /dev/null +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -0,0 +1,11 @@ +export const FIRST_PARTY_KEY = '_iiq_fdata'; +export const SUPPORTED_TYPES = ['html5', 'cookie'] + +export const WITH_IIQ = 'A'; +export const WITHOUT_IIQ = 'B'; +export const NOT_YET_DEFINED = 'U'; +export const BLACK_LIST = 'L'; +export const CLIENT_HINTS_KEY = '_iiq_ch'; +export const EMPTY = 'EMPTY'; +export const GVLID = '1323'; +export const VERSION = 0.27; diff --git a/libraries/intentIqUtils/detectBrowserUtils.js b/libraries/intentIqUtils/detectBrowserUtils.js new file mode 100644 index 00000000000..37a935bda28 --- /dev/null +++ b/libraries/intentIqUtils/detectBrowserUtils.js @@ -0,0 +1,82 @@ +import { logError } from '../../src/utils.js'; + +/** + * Detects the browser using either userAgent or userAgentData + * @return {string} The name of the detected browser or 'unknown' if unable to detect + */ +export function detectBrowser() { + try { + if (navigator.userAgent) { + return detectBrowserFromUserAgent(navigator.userAgent); + } else if (navigator.userAgentData) { + return detectBrowserFromUserAgentData(navigator.userAgentData); + } + } catch (error) { + logError('Error detecting browser:', error); + } + return 'unknown'; +} + +/** + * Detects the browser from the user agent string + * @param {string} userAgent - The user agent string from the browser + * @return {string} The name of the detected browser or 'unknown' if unable to detect + */ +export function detectBrowserFromUserAgent(userAgent) { + const browserRegexPatterns = { + opera: /Opera|OPR/, + edge: /Edg/, + chrome: /Chrome|CriOS/, + safari: /Safari/, + firefox: /Firefox/, + ie: /MSIE|Trident/, + }; + + // Check for Edge first + if (browserRegexPatterns.edge.test(userAgent)) { + return 'edge'; + } + + // Check for Opera next + if (browserRegexPatterns.opera.test(userAgent)) { + return 'opera'; + } + + // Check for Chrome first to avoid confusion with Safari + if (browserRegexPatterns.chrome.test(userAgent)) { + return 'chrome'; + } + + // Now we can safely check for Safari + if (browserRegexPatterns.safari.test(userAgent) && !browserRegexPatterns.chrome.test(userAgent)) { + return 'safari'; + } + + // Check other browsers + for (const browser in browserRegexPatterns) { + if (browserRegexPatterns[browser].test(userAgent)) { + return browser; + } + } + + return 'unknown'; +} + +/** + * Detects the browser from the NavigatorUAData object + * @param {Object} userAgentData - The user agent data object from the browser + * @return {string} The name of the detected browser or 'unknown' if unable to detect + */ +export function detectBrowserFromUserAgentData(userAgentData) { + const brandNames = userAgentData.brands.map(brand => brand.brand); + + if (brandNames.includes('Microsoft Edge')) { + return 'edge'; + } else if (brandNames.includes('Opera')) { + return 'opera'; + } else if (brandNames.some(brand => brand === 'Chromium' || brand === 'Google Chrome')) { + return 'chrome'; + } + + return 'unknown'; +} diff --git a/libraries/intentIqUtils/getCmpData.js b/libraries/intentIqUtils/getCmpData.js new file mode 100644 index 00000000000..b23f0fbaffe --- /dev/null +++ b/libraries/intentIqUtils/getCmpData.js @@ -0,0 +1,19 @@ +import { allConsent } from '../../src/consentHandler.js'; + +/** + * Retrieves consent data from the Consent Management Platform (CMP). + * @return {Object} An object containing the following fields: + * - `gdprString` (string): GDPR consent string if available. + * - `uspString` (string): USP consent string if available. + * - `gppString` (string): GPP consent string if available. + */ +export function getCmpData() { + const consentData = allConsent.getConsentData(); + + return { + gdprApplies: consentData?.gdpr?.gdprApplies || false, + gdprString: typeof consentData?.gdpr?.consentString === 'string' ? consentData.gdpr.consentString : null, + uspString: typeof consentData?.usp === 'string' ? consentData.usp : null, + gppString: typeof consentData?.gpp?.gppString === 'string' ? consentData.gpp.gppString : null, + }; +} diff --git a/libraries/intentIqUtils/getRefferer.js b/libraries/intentIqUtils/getRefferer.js new file mode 100644 index 00000000000..39fde70ac24 --- /dev/null +++ b/libraries/intentIqUtils/getRefferer.js @@ -0,0 +1,64 @@ +import { getWindowTop, logError, getWindowLocation, getWindowSelf } from '../../src/utils.js'; + +/** + * Determines if the script is running inside an iframe and retrieves the URL. + * @return {string} The encoded vrref value representing the relevant URL. + */ +export function getReferrer() { + try { + if (getWindowSelf() === getWindowTop()) { + return encodeURIComponent(getWindowLocation().href); + } else { + return encodeURIComponent(getWindowTop().location.href); + } + } catch (error) { + logError(`Error accessing location: ${error}`); + return ''; + } +} + +/** + * Appends `vrref` and `fui` parameters to the provided URL. + * If the referrer URL is available, it appends `vrref` with the relevant referrer value based on the domain. + * Otherwise, it appends `fui=1`. If a domain name is provided, it may also append `vrref` with the domain. + * @param {string} url - The URL to append parameters to. + * @param {string} domainName - The domain name used to determine the relevant referrer. + * @return {string} The modified URL with appended `vrref` or `fui` parameters. + */ +export function appendVrrefAndFui(url, domainName) { + const fullUrl = getReferrer(); + if (fullUrl) { + return (url += '&vrref=' + getRelevantRefferer(domainName, fullUrl)); + } + url += '&fui=1'; // Full Url Issue + url += '&vrref=' + encodeURIComponent(domainName || ''); + return url; +} + +/** + * Get the relevant referrer based on full URL and domain + * @param {string} domainName The domain name to compare + * @param {string} fullUrl The full URL to analyze + * @return {string} The relevant referrer + */ +export function getRelevantRefferer(domainName, fullUrl) { + if (domainName && isDomainIncluded(fullUrl, domainName)) { + return fullUrl; + } + return domainName ? encodeURIComponent(domainName) : fullUrl; +} + +/** + * Checks if the provided domain name is included in the full URL. + * @param {string} fullUrl - The full URL to check. + * @param {string} domainName - The domain name to search for within the URL. + * @return {boolean} `True` if the domain name is found in the URL, `false` otherwise. + */ +export function isDomainIncluded(fullUrl, domainName) { + try { + return fullUrl.includes(domainName); + } catch (error) { + logError(`Invalid URL provided: ${error}`); + return false; + } +} diff --git a/libraries/intentIqUtils/storageUtils.js b/libraries/intentIqUtils/storageUtils.js new file mode 100644 index 00000000000..8e7bf1d12a5 --- /dev/null +++ b/libraries/intentIqUtils/storageUtils.js @@ -0,0 +1,89 @@ +import {logError, logInfo} from '../../src/utils.js'; +import {SUPPORTED_TYPES, FIRST_PARTY_KEY} from '../../libraries/intentIqConstants/intentIqConstants.js'; +import {getStorageManager} from '../../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; + +const MODULE_NAME = 'intentIqId'; +const PCID_EXPIRY = 365; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); + +/** + * Read data from local storage or cookie based on allowed storage types. + * @param {string} key - The key to read data from. + * @param {Array} allowedStorage - Array of allowed storage types ('html5' or 'cookie'). + * @return {string|null} - The retrieved data or null if an error occurs. + */ +export function readData(key, allowedStorage) { + try { + if (storage.hasLocalStorage() && allowedStorage.includes('html5')) { + return storage.getDataFromLocalStorage(key); + } + if (storage.cookiesAreEnabled() && allowedStorage.includes('cookie')) { + return storage.getCookie(key); + } + } catch (error) { + logError(`${MODULE_NAME}: Error reading data`, error); + } + return null; +} + +/** + * Store Intent IQ data in cookie, local storage or both of them + * expiration date: 365 days + * @param {string} key - The key under which the data will be stored. + * @param {string} value - The value to be stored (e.g., IntentIQ ID). + * @param {Array} allowedStorage - An array of allowed storage types: 'html5' for Local Storage and/or 'cookie' for Cookies. + * @param {Object} firstPartyData - Contains user consent data; if isOptedOut is true, data will not be stored (except for FIRST_PARTY_KEY). + */ +export function storeData(key, value, allowedStorage, firstPartyData) { + try { + if (firstPartyData?.isOptedOut && key !== FIRST_PARTY_KEY) { + return; + } + logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value); + if (value) { + if (storage.hasLocalStorage() && allowedStorage.includes('html5')) { + storage.setDataInLocalStorage(key, value); + } + if (storage.cookiesAreEnabled() && allowedStorage.includes('cookie')) { + const expiresStr = (new Date(Date.now() + (PCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); + storage.setCookie(key, value, expiresStr, 'LAX'); + } + } + } catch (error) { + logError(error); + } +} + +/** + * Remove Intent IQ data from cookie or local storage + * @param key + */ + +export function removeDataByKey(key, allowedStorage) { + try { + if (storage.hasLocalStorage() && allowedStorage.includes('html5')) { + storage.removeDataFromLocalStorage(key); + } + if (storage.cookiesAreEnabled() && allowedStorage.includes('cookie')) { + const expiredDate = new Date(0).toUTCString(); + storage.setCookie(key, '', expiredDate, 'LAX'); + } + } catch (error) { + logError(error); + } +} + +/** + * Determines the allowed storage types based on provided parameters. + * If no valid storage types are provided, it defaults to 'html5'. + * + * @param {Array} params - An array containing storage type preferences, e.g., ['html5', 'cookie']. + * @return {Array} - Returns an array with allowed storage types. Defaults to ['html5'] if no valid options are provided. + */ +export function defineStorageType(params) { + if (!params || !Array.isArray(params)) return ['html5']; // use locale storage be default + const filteredArr = params.filter(item => SUPPORTED_TYPES.includes(item)); + return filteredArr.length ? filteredArr : ['html5']; +} diff --git a/libraries/interpretResponseUtils/index.js b/libraries/interpretResponseUtils/index.js new file mode 100644 index 00000000000..6d081e4c272 --- /dev/null +++ b/libraries/interpretResponseUtils/index.js @@ -0,0 +1,22 @@ +import {logError} from '../../src/utils.js'; + +export function interpretResponseUtil(serverResponse, {bidderRequest}, eachBidCallback) { + const bids = []; + if (!serverResponse.body || serverResponse.body.error) { + let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`; + if (serverResponse.body && serverResponse.body.error) { errorMessage += `: ${serverResponse.body.error}`; } + logError(errorMessage); + return bids; + } + (serverResponse.body.tags || []).forEach(serverBid => { + try { + const bid = eachBidCallback(serverBid); + if (bid) { + bids.push(bid); + } + } catch (e) { + // Do nothing + } + }); + return bids; +} diff --git a/libraries/liveIntentId/externalIdSystem.js b/libraries/liveIntentId/externalIdSystem.js new file mode 100644 index 00000000000..794b4c64c5f --- /dev/null +++ b/libraries/liveIntentId/externalIdSystem.js @@ -0,0 +1,176 @@ +import { logError } from '../../src/utils.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../src/adapterManager.js'; +import { submodule } from '../../src/hook.js'; +import { DEFAULT_AJAX_TIMEOUT, MODULE_NAME, parseRequestedAttributes, composeResult, eids, GVLID, PRIMARY_IDS, makeSourceEventToSend, setUpTreatment } from './shared.js' + +// Reference to the client for the liQHub. +let cachedClientRef + +/** + * This function is used in tests. + */ +export function resetSubmodule() { + cachedClientRef = undefined +} + +window.liQHub = window.liQHub ?? [] + +function initializeClient(configParams) { + // Only initialize once. + if (cachedClientRef != null) return cachedClientRef + + const clientRef = {} + + const clientDetails = { name: 'prebid', version: '$prebid.version$' } + + const collectConfig = configParams.liCollectConfig ?? {}; + + let integration + if (collectConfig.appId != null) { + integration = { type: 'application', appId: collectConfig.appId, publisherId: configParams.publisherId } + } else if (configParams.distributorId != null && configParams.publisherId == null) { + integration = { type: 'distributor', distributorId: configParams.distributorId } + } else { + integration = { type: 'custom', publisherId: configParams.publisherId, distributorId: configParams.distributorId } + } + + const partnerCookies = new Set(configParams.identifiersToResolve ?? []); + + const collectSettings = { timeout: collectConfig.ajaxTimeout ?? DEFAULT_AJAX_TIMEOUT } + + let identityPartner + if (collectConfig.appId == null && configParams.distributorId != null) { + identityPartner = configParams.distributorId + } else if (configParams.partner != null) { + identityPartner = configParams.partner + } else { + identityPartner = 'prebid' + } + + const resolveSettings = { + identityPartner, + timeout: configParams.ajaxTimeout ?? DEFAULT_AJAX_TIMEOUT + } + + let idCookieSettings + if (configParams.fpid != null) { + const fpidConfig = configParams.fpid + let source + if (fpidConfig.strategy === 'html5') { + source = 'local_storage' + } else { + source = fpidConfig.strategy + } + idCookieSettings = { idCookieSettings: { type: 'provided', source, key: fpidConfig.name } }; + } else { + idCookieSettings = {} + } + + function loadConsent() { + const consent = {} + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString != null) { + consent.usPrivacy = { consentString: usPrivacyString } + } + const gdprConsent = gdprDataHandler.getConsentData() + if (gdprConsent != null) { + consent.gdpr = gdprConsent + } + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent != null) { + consent.gpp = { consentString: gppConsent.gppString, applicableSections: gppConsent.applicableSections } + } + + return consent + } + const consent = loadConsent() + + window.liQHub.push({ + type: 'register_client', + clientRef, + clientDetails, + integration, + consent, + partnerCookies, + collectSettings, + ...idCookieSettings, + resolveSettings + }) + + let sourceEvent = makeSourceEventToSend(configParams) + if (sourceEvent != null) { + window.liQHub.push({ type: 'collect', clientRef, sourceEvent }) + } + + cachedClientRef = clientRef + return clientRef +} + +/** + * Create requestedAttributes array to pass to LiveConnect. + * @function + * @param {Object} overrides - object with boolean values that will override defaults { 'foo': true, 'bar': false } + * @returns {Array} + */ + +function resolve(configParams, clientRef, callback) { + function onFailure(error) { + logError(`${MODULE_NAME}: ID fetch encountered an error: `, error); + callback(); + } + + const onSuccess = [{ type: 'callback', callback }] + + window.liQHub.push({ + type: 'resolve', + clientRef, + requestedAttributes: parseRequestedAttributes(configParams.requestedAttributesOverrides), + onFailure, + onSuccess + }) +} + +/** + * @typedef {import('../../modules/userId/index.js').Submodule} Submodule + */ + +/** @type {Submodule} */ +export const liveIntentExternalIdSubmodule = { + /** + * Used to link submodule with config. + * @type {string} + */ + name: MODULE_NAME, + gvlid: GVLID, + + /** + * Decode the stored id value for passing to bid requests. + * @function + */ + decode(value, config) { + const configParams = config?.params ?? {}; + setUpTreatment(configParams); + + // Ensure client is initialized and we fired at least one collect request. + initializeClient(configParams) + + return composeResult(value, configParams) + }, + + /** + * Performs action to obtain id and return a value in the callback's response argument. + * @function + */ + getId(config) { + const configParams = config?.params ?? {}; + setUpTreatment(configParams); + + const clientRef = initializeClient(configParams) + + return { callback: function(cb) { resolve(configParams, clientRef, cb); } }; + }, + primaryIds: PRIMARY_IDS, + eids +}; + +submodule('userId', liveIntentExternalIdSubmodule); diff --git a/libraries/liveIntentId/idSystem.js b/libraries/liveIntentId/idSystem.js new file mode 100644 index 00000000000..49db1e36efa --- /dev/null +++ b/libraries/liveIntentId/idSystem.js @@ -0,0 +1,237 @@ +/** + * This module adds LiveIntentId to the User ID module. + * The {@link module:modules/userId} module is required. + * @module modules/idSystem + * @requires module:modules/userId + */ +import { triggerPixel, logError } from '../../src/utils.js'; +import { ajaxBuilder } from '../../src/ajax.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../src/adapterManager.js'; +import { submodule } from '../../src/hook.js'; +import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports +import { getStorageManager } from '../../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../../src/activities/modules.js'; +import { DEFAULT_AJAX_TIMEOUT, MODULE_NAME, composeResult, eids, GVLID, DEFAULT_DELAY, PRIMARY_IDS, parseRequestedAttributes, makeSourceEventToSend, setUpTreatment } from './shared.js' + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const EVENTS_TOPIC = 'pre_lips'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); +const calls = { + ajaxGet: (url, onSuccess, onError, timeout, headers) => { + ajaxBuilder(timeout)( + url, + { + success: onSuccess, + error: onError + }, + undefined, + { + method: 'GET', + withCredentials: true, + customHeaders: headers + } + ) + }, + pixelGet: (url, onload) => triggerPixel(url, onload) +} + +let eventFired = false; +let liveConnect = null; + +/** + * This function is used in tests. + */ +export function reset() { + if (window && window.liQ_instances) { + window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)); + window.liQ_instances = []; + } + liveIntentIdSubmodule.setModuleMode(null); + eventFired = false; + liveConnect = null; +} + +/** + * This function is used in tests. + */ +export function setEventFiredFlag() { + eventFired = true; +} + +function parseLiveIntentCollectorConfig(collectConfig) { + const config = {}; + collectConfig = collectConfig || {}; + collectConfig.appId && (config.appId = collectConfig.appId); + collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); + collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); + collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); + config.ajaxTimeout = collectConfig.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; + return config; +} + +/** + * Create requestedAttributes array to pass to LiveConnect. + * @function + * @param {Object} overrides - object with boolean values that will override defaults { 'foo': true, 'bar': false } + * @returns {Array} + */ + +function initializeLiveConnect(configParams) { + if (liveConnect) { + return liveConnect; + } + + configParams = configParams || {}; + const fpidConfig = configParams.fpid || {}; + + const publisherId = configParams.publisherId || 'any'; + const identityResolutionConfig = { + publisherId: publisherId, + requestedAttributes: parseRequestedAttributes(configParams.requestedAttributesOverrides), + extraAttributes: { + ipv4: configParams.ipv4, + ipv6: configParams.ipv6 + } + }; + if (configParams.url) { + identityResolutionConfig.url = configParams.url; + }; + + identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; + + const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); + + if (!liveConnectConfig.appId && configParams.distributorId) { + liveConnectConfig.distributorId = configParams.distributorId; + identityResolutionConfig.source = configParams.distributorId; + } else { + identityResolutionConfig.source = configParams.partner || 'prebid'; + } + + liveConnectConfig.wrapperName = 'prebid'; + liveConnectConfig.trackerVersion = '$prebid.version$'; + liveConnectConfig.identityResolutionConfig = identityResolutionConfig; + liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; + liveConnectConfig.fireEventDelay = configParams.fireEventDelay; + + liveConnectConfig.idCookie = {}; + liveConnectConfig.idCookie.name = fpidConfig.name; + liveConnectConfig.idCookie.strategy = fpidConfig.strategy == 'html5' ? 'localStorage' : fpidConfig.strategy; + + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString) { + liveConnectConfig.usPrivacyString = usPrivacyString; + } + const gdprConsent = gdprDataHandler.getConsentData(); + if (gdprConsent) { + liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; + liveConnectConfig.gdprConsent = gdprConsent.consentString; + } + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + liveConnectConfig.gppString = gppConsent.gppString; + liveConnectConfig.gppApplicableSections = gppConsent.applicableSections; + } + // The second param is the storage object, LS & Cookie manipulation uses PBJS. + // The third param is the ajax and pixel object, the AJAX and pixel use PBJS. + liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); + + const sourceEvent = makeSourceEventToSend(configParams) + if (sourceEvent != null) { + liveConnect.push(sourceEvent); + } + return liveConnect; +} + +function tryFireEvent() { + if (!eventFired && liveConnect) { + const eventDelay = liveConnect.config.fireEventDelay || DEFAULT_DELAY; + setTimeout(() => { + const instances = window.liQ_instances; + instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)); + if (!eventFired && liveConnect) { + liveConnect.fire(); + } + }, eventDelay); + } +} + +/** @type {Submodule} */ +export const liveIntentIdSubmodule = { + moduleMode: '$$LIVE_INTENT_MODULE_MODE$$', + /** + * Used to link submodule with config. + * @type {string} + */ + name: MODULE_NAME, + gvlid: GVLID, + setModuleMode(mode) { + this.moduleMode = mode; + }, + getInitializer() { + return (liveConnectConfig, storage, calls) => LiveConnect(liveConnectConfig, storage, calls, this.moduleMode); + }, + + /** + * Decode the stored id value for passing to bid requests. + * Note that lipb object is a wrapper for everything, and + * internally it could contain more data other than `lipbid` + * (e.g. `segments`) depending on the `partner` and `publisherId` + * params. + * @function + * @param {{unifiedId:string}} value + * @param {SubmoduleConfig|undefined} config + * @returns {{lipb:Object}} + */ + decode(value, config) { + const configParams = (config && config.params) || {}; + setUpTreatment(configParams); + + if (!liveConnect) { + initializeLiveConnect(configParams); + } + tryFireEvent(); + + return composeResult(value, configParams); + }, + + /** + * Performs action to obtain id and return a value in the callback's response argument. + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config) { + const configParams = (config && config.params) || {}; + setUpTreatment(configParams); + + const liveConnect = initializeLiveConnect(configParams); + if (!liveConnect) { + return; + } + tryFireEvent(); + const result = function(callback) { + liveConnect.resolve( + response => { + callback(response); + }, + error => { + logError(`${MODULE_NAME}: ID fetch encountered an error: `, error); + callback(); + } + ) + } + + return { callback: result }; + }, + primaryIds: PRIMARY_IDS, + eids +}; + +submodule('userId', liveIntentIdSubmodule); diff --git a/libraries/liveIntentId/shared.js b/libraries/liveIntentId/shared.js new file mode 100644 index 00000000000..84067a18d2a --- /dev/null +++ b/libraries/liveIntentId/shared.js @@ -0,0 +1,329 @@ +import {UID1_EIDS} from '../uid1Eids/uid1Eids.js'; +import {UID2_EIDS} from '../uid2Eids/uid2Eids.js'; +import { getRefererInfo } from '../../src/refererDetection.js'; +import { coppaDataHandler } from '../../src/adapterManager.js'; +import { isNumber } from '../../src/utils.js' + +export const PRIMARY_IDS = ['libp']; +export const GVLID = 148; +export const DEFAULT_AJAX_TIMEOUT = 5000; +export const DEFAULT_DELAY = 500; +export const MODULE_NAME = 'liveIntentId'; +export const LI_PROVIDER_DOMAIN = 'liveintent.com'; +export const DEFAULT_REQUESTED_ATTRIBUTES = { 'nonId': true }; +export const DEFAULT_TREATMENT_RATE = 0.95; + +export function parseRequestedAttributes(overrides) { + function renameAttribute(attribute) { + if (attribute === 'fpid') { + return 'idCookie'; + } else { + return attribute; + }; + } + function createParameterArray(config) { + return Object.entries(config).flatMap(([k, v]) => (typeof v === 'boolean' && v) ? [renameAttribute(k)] : []); + } + if (typeof overrides === 'object') { + return createParameterArray({...DEFAULT_REQUESTED_ATTRIBUTES, ...overrides}); + } else { + return createParameterArray(DEFAULT_REQUESTED_ATTRIBUTES); + } +} + +export function makeSourceEventToSend(configParams) { + const sourceEvent = {} + let nonEmpty = false + if (typeof configParams.emailHash === 'string') { + nonEmpty = true + sourceEvent.emailHash = configParams.emailHash + } + if (typeof configParams.ipv4 === 'string') { + nonEmpty = true + sourceEvent.ipv4 = configParams.ipv4 + } + if (typeof configParams.ipv6 === 'string') { + nonEmpty = true + sourceEvent.ipv6 = configParams.ipv6 + } + if (typeof configParams.userAgent === 'string') { + nonEmpty = true + sourceEvent.userAgent = configParams.userAgent + } + + if (nonEmpty) { + return sourceEvent + } +} + +export function composeResult(value, config) { + if (config.activatePartialTreatment) { + if (window.liModuleEnabled) { + return composeIdObject(value); + } else { + return {}; + } + } else { + return composeIdObject(value); + } +} + +function composeIdObject(value) { + const result = {}; + + // old versions stored lipbid in unifiedId. Ensure that we can still read the data. + const lipbid = value.nonId || value.unifiedId + result.lipb = lipbid ? { ...value, lipbid } : value + delete result.lipb?.unifiedId + + // Lift usage of uid2 by exposing uid2 if we were asked to resolve it. + // As adapters are applied in lexicographical order, we will always + // be overwritten by the 'proper' uid2 module if it is present. + if (value.uid2) { + result.uid2 = { 'id': value.uid2, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.bidswitch) { + result.bidswitch = { 'id': value.bidswitch, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.triplelift) { + result.triplelift = { 'id': value.triplelift, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.zetassp) { + result.zetassp = { 'id': value.zetassp, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.medianet) { + result.medianet = { 'id': value.medianet, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.magnite) { + result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.index) { + result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.openx) { + result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.pubmatic) { + result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.sovrn) { + result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.idCookie) { + if (!coppaDataHandler.getCoppa()) { + result.lipb = { ...result.lipb, fpid: value.idCookie }; + result.fpid = { 'id': value.idCookie }; + } + delete result.lipb.idCookie; + } + + if (value.thetradedesk) { + result.lipb = {...result.lipb, tdid: value.thetradedesk} + result.tdid = { 'id': value.thetradedesk, ext: { rtiPartner: 'TDID', provider: getRefererInfo().domain || LI_PROVIDER_DOMAIN } } + delete result.lipb.thetradedesk + } + + if (value.sharethrough) { + result.sharethrough = { 'id': value.sharethrough, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.sonobi) { + result.sonobi = { 'id': value.sonobi, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.vidazoo) { + result.vidazoo = { 'id': value.vidazoo, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + return result +} + +export function setUpTreatment(config) { + // If the treatment decision has not been made yet + // and Prebid is configured to make this decision. + if (window.liModuleEnabled === undefined && config.activatePartialTreatment) { + const treatmentRate = isNumber(window.liTreatmentRate) ? window.liTreatmentRate : DEFAULT_TREATMENT_RATE; + window.liModuleEnabled = Math.random() < treatmentRate; + window.liTreatmentRate = treatmentRate; + }; +} + +export const eids = { + ...UID1_EIDS, + ...UID2_EIDS, + 'lipb': { + getValue: function(data) { + return data.lipbid; + }, + source: 'liveintent.com', + atype: 3, + getEidExt: function(data) { + if (Array.isArray(data.segments) && data.segments.length) { + return { + segments: data.segments + }; + } + } + }, + 'bidswitch': { + source: 'bidswitch.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'medianet': { + source: 'media.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'magnite': { + source: 'rubiconproject.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'index': { + source: 'liveintent.indexexchange.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'openx': { + source: 'openx.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'pubmatic': { + source: 'pubmatic.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'sovrn': { + source: 'liveintent.sovrn.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'fpid': { + source: 'fpid.liveintent.com', + atype: 1, + getValue: function(data) { + return data.id; + } + }, + 'sharethrough': { + source: 'sharethrough.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'sonobi': { + source: 'liveintent.sonobi.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'triplelift': { + source: 'liveintent.triplelift.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'zetassp': { + source: 'zeta-ssp.liveintent.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'vidazoo': { + source: 'liveintent.vidazoo.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } +} diff --git a/libraries/mediaImpactUtils/index.js b/libraries/mediaImpactUtils/index.js new file mode 100644 index 00000000000..a56761d8ed4 --- /dev/null +++ b/libraries/mediaImpactUtils/index.js @@ -0,0 +1,61 @@ +import { buildUrl } from '../../src/utils.js'; +import { ajax } from '../../src/ajax.js'; + +/** + * Builds the bid requests and beacon parameters. + * @param {Array} validBidRequests - The array of valid bid requests. + * @param {string} referer - The referer URL. + * @returns {Object} - An object containing bidRequests and beaconParams. + */ +export function buildBidRequestsAndParams(validBidRequests, referer) { + const bidRequests = []; + const beaconParams = { tag: [], partner: [], sizes: [], referer: encodeURIComponent(referer) }; + + validBidRequests.forEach(function (validBidRequest) { + const sizes = validBidRequest.params.sizes || validBidRequest.sizes; + + const bidRequestObject = { + adUnitCode: validBidRequest.adUnitCode, + sizes: sizes, + bidId: validBidRequest.bidId, + referer: referer, + }; + + if (parseInt(validBidRequest.params.unitId)) { + bidRequestObject.unitId = parseInt(validBidRequest.params.unitId); + beaconParams.tag.push(validBidRequest.params.unitId); + } + + if (parseInt(validBidRequest.params.partnerId)) { + bidRequestObject.unitId = 0; + bidRequestObject.partnerId = parseInt(validBidRequest.params.partnerId); + beaconParams.partner.push(validBidRequest.params.partnerId); + } + + bidRequests.push(bidRequestObject); + beaconParams.sizes.push(joinSizesToString(sizes)); + }); + + // Finalize beaconParams + if (beaconParams.partner.length > 0) { + beaconParams.partner = beaconParams.partner.join(','); + } else { + delete beaconParams.partner; + } + beaconParams.tag = beaconParams.tag.join(','); + beaconParams.sizes = beaconParams.sizes.join(','); + + return { bidRequests, beaconParams }; +} + +export function joinSizesToString(sizes) { + return sizes.map(size => size.join('x')).join('|'); +} + +export function postRequest(endpoint, data) { + ajax(endpoint, null, data, { method: 'POST' }); +} + +export function buildEndpointUrl(protocol, hostname, pathname, searchParams) { + return buildUrl({ protocol, hostname, pathname, search: searchParams }); +} diff --git a/libraries/medianetUtils/constants.js b/libraries/medianetUtils/constants.js new file mode 100644 index 00000000000..b36d1aeeafb --- /dev/null +++ b/libraries/medianetUtils/constants.js @@ -0,0 +1,78 @@ +export const mnetGlobals = { + auctions: {}, // Stores details of ongoing or completed auctions + infoByAdIdMap: {}, // Maps ad IDs to their respective information + bdpMap: {}, + configuration: {}, + logsQueue: [], // Queue for storing logs + errorQueue: [], // Queue for storing errors, + eventQueue: null, + refererInfo: null, +}; + +export const LOGGING_DELAY = 2000; + +export const LOG_TYPE_ID = 'kfk'; +export const LOG_EVT_ID = 'projectevents'; +export const EVENT_PIXEL_URL = 'https://qsearch-a.akamaihd.net/log'; +export const POST_ENDPOINT = 'https://navvy.media.net/log'; +export const GET_ENDPOINT = 'https://pb-logs.media.net/log'; +export const ANALYTICS_VERSION = '2.0.0'; +export const PREBID_VERSION = '$prebid.version$'; +export const MEDIANET = 'medianet'; +export const GLOBAL_VENDOR_ID = 142; + +// Bid Status +export const BID_SUCCESS = 1; +export const BID_NOBID = 2; +export const BID_TIMEOUT = 3; +export const SUCCESS_AFTER_AUCTION = 5; +export const NOBID_AFTER_AUCTION = 6; +export const TIMEOUT_AFTER_AUCTION = 7; +export const BID_FLOOR_REJECTED = 12; + +export const DBF_PRIORITY = { + [BID_SUCCESS]: 4, + [BID_NOBID]: 3, + [SUCCESS_AFTER_AUCTION]: 2, + [BID_TIMEOUT]: 1, + [NOBID_AFTER_AUCTION]: 1, + [TIMEOUT_AFTER_AUCTION]: 0, + [BID_FLOOR_REJECTED]: 0 +}; + +// Properties +export const SEND_ALL_BID_PROP = 'enableSendAllBids'; +export const AUCTION_OPTIONS = 'auctionOptions'; + +// Errors +export const ERROR_CONFIG_JSON_PARSE = 'analytics_config_parse_fail'; +export const ERROR_CONFIG_FETCH = 'analytics_config_ajax_fail'; +export const PBS_ERROR_STATUS_START = 2000; +export const WINNING_BID_ABSENT_ERROR = 'winning_bid_absent'; +export const WINNING_AUCTION_MISSING_ERROR = 'winning_auction_missing'; +export const ERROR_IWB_BID_MISSING = 'iwb_bid_missing'; +// Config +export const CONFIG_PENDING = 0; +export const CONFIG_PASS = 1; +export const CONFIG_ERROR = 3; +export const DEFAULT_LOGGING_PERCENT = 50; +export const CONFIG_URL = 'https://prebid.media.net/rtb/prebid/analytics/config'; +// Dummy Bidder +export const DUMMY_BIDDER = '-2'; + +// Video Constants +export const VIDEO_UUID_PENDING = 9999; + +export const VIDEO_CONTEXT = { + INSTREAM: 'instream', + OUTSTREAM: 'outstream' +} + +export const VIDEO_PLACEMENT = { + [VIDEO_CONTEXT.INSTREAM]: 1, + [VIDEO_CONTEXT.OUTSTREAM]: 6 +} + +// Log Types +export const LOG_APPR = 'APPR'; +export const LOG_RA = 'RA'; diff --git a/libraries/medianetUtils/logKeys.js b/libraries/medianetUtils/logKeys.js new file mode 100644 index 00000000000..27a2dff52e2 --- /dev/null +++ b/libraries/medianetUtils/logKeys.js @@ -0,0 +1,185 @@ +import { + calculateRoundTripTime, + getBidResponseSize, + getRequestedSizes, + getTopWindowReferrer, + getWindowSize, + pick, +} from './utils.js'; +import { config } from '../../src/config.js'; +import { AUCTION_COMPLETED, AUCTION_IN_PROGRESS } from '../../src/auction.js'; +import { safeJSONEncode, deepAccess } from '../../src/utils.js'; +import { getProcessedParams, mergeFieldsToLog } from './logger.js'; +import { + ANALYTICS_VERSION, + AUCTION_OPTIONS, LOG_APPR, LOG_RA, + MEDIANET, + PREBID_VERSION, + TIMEOUT_AFTER_AUCTION, + VIDEO_PLACEMENT, +} from './constants.js'; + +export const KeysMap = { + Pick: { + Auction: [ + 'adSlots', () => ({}), + 'bidsRequested', () => [], + 'bidsReceived', () => [], + 'responseBids', () => [], + 'bidsTimeout', () => [], + 'noBids', () => [], + 'psiBids', () => [], + 'bidderRequests as pendingRequests', (bidderRequests) => bidderRequests.length, + 'hasEnded', () => false, + 'auctionId', + 'auctionStatus', + 'timestamp', + 'timeout', + 'bidderRequests.0.ortb2.sup_log', + 'bidderRequests.0.bids.0.floorData', + 'bidderRequests.0.refererInfo', + 'bidderRequests.0 as consentInfo', (consentInfo) => pick(consentInfo, ['gdprConsent', 'uspConsent']), + ], + AdSlot: [ + 'code', + 'ext as adext', + 'logged', () => ({[LOG_APPR]: false, [LOG_RA]: false}), + 'supcrid', (_, __, adUnit) => adUnit.emsCode || adUnit.code, + 'ortb2Imp', + ], + BidRequest: [ + // from bidRequest + 'bidder', + 'src', + 'params', + 'bidId', + 'bidId as originalRequestId', + 'adUnitCode', + 'mediaTypes', (mediaTypes) => Object.keys(mediaTypes), + 'iwb', () => 0, + 'winner', () => 0, + 'status', () => TIMEOUT_AFTER_AUCTION, + 'responseReceived', () => false, + 'sizes', (_, __, bidRequest) => getRequestedSizes(bidRequest), + 'ext', () => ({}), + ], + BidResponse: [ + 'originalCurrency', + 'originalRequestId', + 'requestId', + // multi-bid + 'originalBidder', + // from bidderRequest + 'bidderCode', + 'currency', + 'adId', + 'snm as status', + 'mediaType', + 'cpm', + 'timeToRespond', + 'dealId', + 'meta', + 'originalCpm', + 'bidderCode', + 'creativeId', + 'latestTargetedAuctionId', + 'floorData', + 'width', + 'height', + 'size', (size, logObj) => size || getBidResponseSize(logObj.width, logObj.height), + 'ext', + ] + }, + Log: { + Bid: [ + 'meta.advertiserDomains as advurl', (advertiserDomains = []) => advertiserDomains.join(','), + 'currMul as omul', + 'originalCurrency as icurr', + 'inCurrMul as imul', + 'mediaTypes as req_mtype', (mediaTypes) => mediaTypes.join('|'), + 'mediaType as res_mtype', + 'mediaType as mtype', (mediaType, __, {mediaTypes}) => mediaType || mediaTypes.join('|'), + 'ext.seat as ortbseat', + 'ext.int_dsp_id as mx_int_dsp_id', + 'ext.int_agency_id as mx_int_agency_id', + 'ext.pvid as mpvid', + 'ext.crid', (crid, _, bidObj) => crid || deepAccess(bidObj.params, 'crid'), + 'ext', (ext, _, bidObj) => safeJSONEncode(bidObj.bidder === MEDIANET ? ext : {}), + 'requestId as reqid', (requestId, _, bidObj) => requestId || bidObj.bidId, + 'originalRequestId as ogReqId', + 'adId as adid', + 'originalBidder as og_pvnm', + 'bidderCode as pvnm', (bidderCode, _, {bidder}) => bidderCode || bidder, + 'src', + 'originalCpm as ogbdp', + 'bdp', (bdp, _, bidObj) => bdp || bidObj.cpm, + 'cpm as cbdp', + 'dfpbd', + 'dealId as dId', + 'winner', + 'currency as curr', + 'timeToRespond as rests', + 'status', + 'iwb', + 'floorData.floorValue as bidflr', + 'floorData.floorRule as flrrule', + 'floorRuleValue as flrRulePrice', + 'serverLatencyMillis as rtime', + 'creativeId as pcrid', + 'dbf', + 'latestTargetedAuctionId as lacid', + 'utime', + 'metrics as ltime', (metrics, logObj) => logObj.rests || calculateRoundTripTime(metrics), + 'bidder as issec', (bidder) => config.getConfig(AUCTION_OPTIONS)?.secondaryBidders?.includes?.(bidder) ? 1 : 0, + 'sizes as szs', (sizes) => sizes.join('|'), + 'size', (size, _, bidObj) => (bidObj.res_sizes || [size]).join('|'), + 'params', (params, _, bidObj) => getProcessedParams(params, bidObj.status), + ], + AdSlot: [ + 'supcrid', + 'code as og_supcrid', + 'context as vplcmtt', (context) => VIDEO_PLACEMENT[context] || 0, + 'ortb2Imp.instl as oop', + 'targeting as targ', (targeting) => safeJSONEncode(targeting), + 'adext', (adext) => encodeURIComponent(safeJSONEncode(adext)), + ], + Auction: [ + 'auctionId as acid', + 'sup_log', + 'consentInfo.gdprConsent.consentString as gdprConsent', + 'consentInfo.uspConsent as ccpa', + 'consentInfo.gdprConsent.gdprApplies as gdpr', (gdprApplies) => (gdprApplies ? '1' : '0'), + 'coppa', () => (config.getConfig('coppa') === true ? 1 : 0), + 'hasEnded as aucstatus', (hasEnded) => (hasEnded ? AUCTION_COMPLETED : AUCTION_IN_PROGRESS), + 'availableUids as uid_mod_avb', (availableUids) => safeJSONEncode(availableUids), + 'uidValues as id_details', (uidValues) => safeJSONEncode(uidValues), + 'refererInfo.topmostLocation as requrl', + 'refererInfo.domain as dn', + 'refererInfo.ref', getTopWindowReferrer, + 'screen', getWindowSize, + 'timeout as tmax', + 'sts', (_, __, auctionObj) => auctionObj.auctionStartTime - auctionObj.timestamp, + 'ets', (_, __, auctionObj) => auctionObj.auctionEndTime - auctionObj.timestamp || -1, + 'floorData.modelVersion as flrver', + 'floorData as flrdata', (floorData) => mergeFieldsToLog(pick(floorData, [ + 'location as ln', + 'skipped as skp', + 'skipRate as sr', + 'fetchStatus as fs', + 'enforcements.enforceJS as enfj', + 'enforcements.floorDeals as enfd' + ])) + ], + Globals: [ + 'cid', + 'ajaxState as ajx', + 'pubLper as plper', + 'loggingPercent as lper', (loggingPercent) => Math.round(100 / loggingPercent), + 'enableDbf', () => 1, + 'flt', () => 1, + 'pbv', () => PREBID_VERSION, + 'pbav', () => ANALYTICS_VERSION, + 'coppa', () => (config.getConfig('coppa') === true ? 1 : 0) + ] + } +}; diff --git a/libraries/medianetUtils/logger.js b/libraries/medianetUtils/logger.js new file mode 100644 index 00000000000..e394fb50c26 --- /dev/null +++ b/libraries/medianetUtils/logger.js @@ -0,0 +1,111 @@ +import { flattenObj, formatQS as mnFormatQS, pick } from './utils.js'; +import { formatQS, triggerPixel, isPlainObject } from '../../src/utils.js'; +import { + ANALYTICS_VERSION, BID_SUCCESS, + EVENT_PIXEL_URL, LOG_APPR, + LOG_EVT_ID, + LOG_TYPE_ID, + mnetGlobals, POST_ENDPOINT, + PREBID_VERSION +} from './constants.js'; +import { ajax, sendBeacon } from '../../src/ajax.js'; +import { getRefererInfo } from '../../src/refererDetection.js'; +import { getGlobal } from '../../src/prebidGlobal.js'; + +export function shouldLogAPPR(auctionData, adUnitId) { + const adSlot = auctionData.adSlots[adUnitId]; + return ( + ( + mnetGlobals.configuration.shouldLogAPPR + ) && + !adSlot.logged[LOG_APPR] + ); +} + +// common error logger for medianet analytics and bid adapter +export function errorLogger(event, data = undefined, analytics = true) { + const { name, cid, value, relatedData, logData, project } = isPlainObject(event) ? {...event, logData: data} : { name: event, relatedData: data }; + const refererInfo = mnetGlobals.refererInfo || getRefererInfo(); + const errorData = Object.assign({}, + { + logid: LOG_TYPE_ID, + evtid: LOG_EVT_ID, + project: project || (analytics ? 'prebidanalytics' : 'prebid'), + dn: refererInfo.domain || '', + requrl: refererInfo.topmostLocation || '', + pbav: getGlobal().medianetGlobals.analyticsEnabled ? ANALYTICS_VERSION : '', + pbver: PREBID_VERSION, + // To handle media.net alias bidderAdapter (params.cid) code errors + cid: cid || mnetGlobals.configuration.cid || '', + event: name || '', + value: value || '', + rd: relatedData || '', + }, + logData); + const loggingHost = analytics ? EVENT_PIXEL_URL : POST_ENDPOINT; + const payload = analytics ? mnFormatQS(errorData) : formatQS(errorData); + + function send() { + if (!analytics) { + fireAjaxLog(loggingHost, payload, pick(errorData, ['cid', 'project', 'event as value'])); + return; + } + const pixelUrl = getUrl(); + mnetGlobals.errorQueue.push(pixelUrl); + triggerPixel(pixelUrl); + } + + function getUrl() { + return loggingHost + '?' + payload; + } + + return { + send, + getUrl + }; +} + +export function getLoggingPayload(queryParams) { + return `logid=kfk&evtid=prebid_analytics_events_client&${queryParams}`; +} + +export function firePostLog(url, payload) { + try { + mnetGlobals.logsQueue.push(url + '?' + payload); + const isSent = sendBeacon(url, payload); + if (!isSent) { + fireAjaxLog(url, payload); + errorLogger('sb_log_failed').send(); + } + } catch (e) { + fireAjaxLog(url, payload); + errorLogger('sb_not_supported').send(); + } +} + +export function fireAjaxLog(url, payload, errorData = {}) { + ajax(url, + { + success: () => undefined, + error: (_, {reason}) => errorLogger(Object.assign(errorData, {name: 'ajax_log_failed', relatedData: reason})).send() + }, + payload, + { + method: 'POST', + } + ); +} + +export function mergeFieldsToLog(objParams) { + const logParams = Object.keys(objParams).map((param) => { + const value = objParams[param]; + return `${param}=${value === undefined ? '' : value}`; + }); + return logParams.join('||'); +} + +export function getProcessedParams(params, status) { + if (params === undefined || status !== BID_SUCCESS) return ''; + const clonedFlattenParams = flattenObj(params, '', {}); + return JSON.stringify(clonedFlattenParams); +} diff --git a/libraries/medianetUtils/utils.js b/libraries/medianetUtils/utils.js new file mode 100644 index 00000000000..a3d67ded450 --- /dev/null +++ b/libraries/medianetUtils/utils.js @@ -0,0 +1,139 @@ +import { _map, deepAccess, isFn, isPlainObject, uniques } from '../../src/utils.js'; +import {mnetGlobals} from './constants.js'; +import {getViewportSize} from '../viewport/viewport.js'; + +export function findBidObj(list = [], key, value) { + return list.find((bid) => { + return bid[key] === value; + }); +} + +export function filterBidsListByFilters(list = [], filters) { + return list.filter((bid) => { + return Object.entries(filters).every(([key, value]) => bid[key] === value); + }); +} + +export function flattenObj(obj, parent, res = {}) { + for (let key in obj) { + if (Array.isArray(obj[key])) { + continue; + } + const propName = parent ? parent + '.' + key : key; + if (typeof obj[key] == 'object') { + flattenObj(obj[key], propName, res); + } else { + res[propName] = String(obj[key]); + } + } + return res; +} + +export function formatQS(data) { + return _map(data, (value, key) => { + if (value === undefined) { + return key + '='; + } + if (isPlainObject(value)) { + value = JSON.stringify(value); + } + return key + '=' + encodeURIComponent(value); + }).join('&'); +} + +export function getWindowSize() { + const { width, height } = getViewportSize(); + let w = width || -1; + let h = height || -1; + return `${w}x${h}`; +} + +export function getRequestedSizes({ mediaTypes, sizes }) { + const banner = deepAccess(mediaTypes, 'banner.sizes') || sizes || []; + const native = deepAccess(mediaTypes, 'native') ? [[1, 1]] : []; + const playerSize = deepAccess(mediaTypes, 'video.playerSize') || []; + let video = []; + if (playerSize.length === 2) { + video = [playerSize]; + } + return [...banner, ...native, ...video].filter(uniques).map((size) => size.join('x')); +} + +export function getBidResponseSize(width, height) { + if (isNaN(width) || isNaN(height)) { + return ''; + } + return width + 'x' + height; +} + +export function calculateRoundTripTime(metrics) { + if (!metrics || !isFn(metrics.getMetrics)) { + return -1; + } + const prebidMetrics = metrics.getMetrics(); + const ltime = + prebidMetrics['adapter.client.total'] || + prebidMetrics['adapter.s2s.total']?.[0] || + prebidMetrics['adapter.s2s.total'] || + -1; + return parseFloat(ltime.toFixed(2)); +} + +export function pick(context, properties, omitKeys = false) { + if (typeof context !== 'object' || context === null) return {}; + const acc = {}; + properties.forEach((prop, index) => { + if (typeof prop === 'function') { + return; + } + + let value, alias; + let [key, aliasPart] = prop.split(/\sas\s/i); + key = key.trim(); + alias = aliasPart?.trim() || key.split('.').pop(); + + value = deepAccess(context, key); + + if (typeof properties[index + 1] === 'function') { + value = properties[index + 1](value, acc, context); + } + + if (value !== undefined || !omitKeys) { + acc[alias] = value; + } + }); + + return acc; +} + +export const onHidden = (cb, once = true) => { + const onHiddenOrPageHide = (event) => { + if (document.visibilityState === 'hidden') { + cb(event); + if (once) { + window.removeEventListener('visibilitychange', onHiddenOrPageHide, true); + window.removeEventListener('pagehide', onHiddenOrPageHide, true); + } + } + }; + window.addEventListener('visibilitychange', onHiddenOrPageHide, true); + // Some browsers have buggy implementations of visibilitychange, + // so we use pagehide in addition, just to be safe. + window.addEventListener('pagehide', onHiddenOrPageHide, true); + + // if the document is already hidden + onHiddenOrPageHide({}); +}; + +export function getTopWindowReferrer(ref) { + try { + if (ref) return ref; + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } +} + +export function isSampledForLogging() { + return Math.random() * 100 < parseFloat(mnetGlobals.configuration.loggingPercent); +} diff --git a/libraries/mgidUtils/mgidUtils.js b/libraries/mgidUtils/mgidUtils.js new file mode 100644 index 00000000000..9ac84e231b7 --- /dev/null +++ b/libraries/mgidUtils/mgidUtils.js @@ -0,0 +1,73 @@ +import { + isPlainObject, + isArray, + isStr, + isNumber, +} from '../../src/utils.js'; +import { config } from '../../src/config.js'; +import { USERSYNC_DEFAULT_CONFIG } from '../../src/userSync.js'; + +const PIXEL_SYNC_URL = 'https://cm.mgid.com/i.gif'; +const IFRAME_SYNC_URL = 'https://cm.mgid.com/i.html'; + +export function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + const spb = isPlainObject(config.getConfig('userSync')) && + isNumber(config.getConfig('userSync').syncsPerBidder) + ? config.getConfig('userSync').syncsPerBidder : USERSYNC_DEFAULT_CONFIG.syncsPerBidder; + + if (spb > 0 && isPlainObject(syncOptions) && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + let pixels = []; + if (serverResponses && + isArray(serverResponses) && + serverResponses.length > 0 && + isPlainObject(serverResponses[0].body) && + isPlainObject(serverResponses[0].body.ext) && + isArray(serverResponses[0].body.ext.cm) && + serverResponses[0].body.ext.cm.length > 0) { + pixels = serverResponses[0].body.ext.cm; + } + + const syncs = []; + const query = []; + query.push('cbuster={cbuster}'); + query.push('gdpr_consent=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); + if (isPlainObject(gdprConsent) && typeof gdprConsent?.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { + query.push('gdpr=1'); + } else { + query.push('gdpr=0'); + } + if (isPlainObject(uspConsent) && uspConsent?.consentString) { + query.push(`us_privacy=${encodeURIComponent(uspConsent?.consentString)}`); + } + if (isPlainObject(gppConsent) && gppConsent?.gppString) { + query.push(`gppString=${encodeURIComponent(gppConsent?.gppString)}`); + } + if (config.getConfig('coppa')) { + query.push('coppa=1') + } + const q = query.join('&') + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: IFRAME_SYNC_URL + '?' + q.replace('{cbuster}', Math.round(new Date().getTime())) + }); + } else if (syncOptions.pixelEnabled) { + if (pixels.length === 0) { + for (let i = 0; i < spb; i++) { + syncs.push({ + type: 'image', + url: PIXEL_SYNC_URL + '?' + q.replace('{cbuster}', Math.round(new Date().getTime())) // randomly selects partner if sync required + }); + } + } else { + for (let i = 0; i < spb && i < pixels.length; i++) { + syncs.push({ + type: 'image', + url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + q.replace('{cbuster}', Math.round(new Date().getTime())) + }); + } + } + } + return syncs; + } +} diff --git a/libraries/mspa/activityControls.js b/libraries/mspa/activityControls.js index eaf515e2385..c93748f73c7 100644 --- a/libraries/mspa/activityControls.js +++ b/libraries/mspa/activityControls.js @@ -24,6 +24,8 @@ export function isBasicConsentDenied(cd) { cd.PersonalDataConsents === 2 || // minors 13+ who have not given consent cd.KnownChildSensitiveDataConsents[0] === 1 || + // minors 16+ who have not given consent (added in usnat version 2) + cd.KnownChildSensitiveDataConsents[2] === 1 || // minors under 13 cannot consent isApplicable(cd.KnownChildSensitiveDataConsents[1]) || // covered cannot be zero @@ -53,14 +55,31 @@ export function isConsentDenied(cd) { } export const isTransmitUfpdConsentDenied = (() => { - // deny anything that smells like: genetic, biometric, state/national ID, financial, union membership, - // or personal communication data - const cannotBeInScope = [6, 7, 9, 10, 12].map(el => --el); - // require consent for everything else (except geo, which is treated separately) - const allExceptGeo = Array.from(Array(12).keys()).filter((el) => el !== SENSITIVE_DATA_GEO) - const mustHaveConsent = allExceptGeo.filter(el => !cannotBeInScope.includes(el)); + const sensitiveFlags = (() => { + // deny anything that smells like: genetic, biometric, state/national ID, financial, union membership, + // personal communication data, status as victim of crime (version 2), status as transgender/nonbinary (version 2) + const cannotBeInScope = [6, 7, 9, 10, 12, 14, 16].map(el => --el); + // require consent for everything else (except geo, which is treated separately) + const allExceptGeo = Array.from(Array(16).keys()).filter((el) => el !== SENSITIVE_DATA_GEO) + const mustHaveConsent = allExceptGeo.filter(el => !cannotBeInScope.includes(el)); + + return Object.fromEntries( + Object.entries({ + 1: 12, + 2: 16 + }).map(([version, cardinality]) => { + const isInVersion = (el) => el < cardinality + return [version, { + cannotBeInScope: cannotBeInScope.filter(isInVersion), + allExceptGeo: allExceptGeo.filter(isInVersion), + mustHaveConsent: mustHaveConsent.filter(isInVersion) + }] + }) + ) + })() return function (cd) { + const {cannotBeInScope, mustHaveConsent, allExceptGeo} = sensitiveFlags[cd.Version]; return isConsentDenied(cd) || // no notice about sensitive data was given sensitiveNoticeIs(cd, 2) || @@ -97,6 +116,9 @@ export function mspaRule(sids, getConsent, denies, applicableSids = () => gppDat if (consent == null) { return {allow: false, reason: 'consent data not available'}; } + if (![1, 2].includes(consent.Version)) { + return {allow: false, reason: `unsupported consent specification version "${consent.Version}"`} + } if (denies(consent)) { return {allow: false}; } @@ -105,7 +127,7 @@ export function mspaRule(sids, getConsent, denies, applicableSids = () => gppDat } function flatSection(subsections) { - if (subsections == null) return subsections; + if (!Array.isArray(subsections)) return subsections; return subsections.reduceRight((subsection, consent) => { return Object.assign(consent, subsection); }, {}); diff --git a/libraries/navigatorData/navigatorData.js b/libraries/navigatorData/navigatorData.js new file mode 100644 index 00000000000..f1a34fc51eb --- /dev/null +++ b/libraries/navigatorData/navigatorData.js @@ -0,0 +1,29 @@ +export function getHLen(win = window) { + let hLen; + try { + hLen = win.top.history.length; + } catch (error) { + hLen = undefined; + } + return hLen; +} + +export function getHC(win = window) { + let hc; + try { + hc = win.top.navigator.hardwareConcurrency; + } catch (error) { + hc = undefined; + } + return hc; +} + +export function getDM(win = window) { + let dm; + try { + dm = win.top.navigator.deviceMemory; + } catch (error) { + dm = undefined; + } + return dm; +} diff --git a/libraries/nexverseUtils/index.js b/libraries/nexverseUtils/index.js new file mode 100644 index 00000000000..c9e286c221d --- /dev/null +++ b/libraries/nexverseUtils/index.js @@ -0,0 +1,130 @@ +import { logError, logInfo, logWarn, generateUUID } from '../../src/utils.js'; + +const LOG_WARN_PREFIX = '[Nexverse warn]: '; +const LOG_ERROR_PREFIX = '[Nexverse error]: '; +const LOG_INFO_PREFIX = '[Nexverse info]: '; +const NEXVERSE_USER_COOKIE_KEY = 'user_nexverse'; + +/** + * Determines the device model (if possible). + * @returns {string} The device model or a fallback message if not identifiable. + */ +export function getDeviceModel() { + const ua = navigator.userAgent; + if (/iPhone/i.test(ua)) { + return 'iPhone'; + } else if (/iPad/i.test(ua)) { + return 'iPad'; + } else if (/Android/i.test(ua)) { + const match = ua.match(/Android.*;\s([a-zA-Z0-9\s]+)\sBuild/); + return match ? match[1].trim() : 'Unknown Android Device'; + } else if (/Windows Phone/i.test(ua)) { + return 'Windows Phone'; + } else if (/Macintosh/i.test(ua)) { + return 'Mac'; + } else if (/Linux/i.test(ua)) { + return 'Linux'; + } else if (/Windows/i.test(ua)) { + return 'Windows PC'; + } + return ''; +} + +/** + * Prepapre the endpoint URL based on passed bid request. + * @param {string} bidderEndPoint - Bidder End Point. + * @param {object} bid - Bid details. + * @returns {string} The Endpoint URL with required parameters. + */ +export function buildEndpointUrl(bidderEndPoint, bid) { + const { uid, pubId, pubEpid } = bid.params; + const isDebug = bid.isDebug; + let endPoint = `${bidderEndPoint}?uid=${encodeURIComponent(uid)}&pub_id=${encodeURIComponent(pubId)}&pub_epid=${encodeURIComponent(pubEpid)}`; + if (isDebug) { + endPoint = `${endPoint}&test=1`; + } + return endPoint; +} +/** + * Validates the bid request to ensure all required parameters are present. + * @param {Object} bid - The bid request object. + * @returns {boolean} True if the bid request is valid, false otherwise. + */ +export function isBidRequestValid(bid) { + const isValid = !!( + bid.params && + bid.params.uid && bid.params.uid.trim() && + bid.params.pubId && bid.params.pubId.trim() && + bid.params.pubEpid && bid.params.pubEpid.trim() + ); + if (!isValid) { + logError(`${LOG_ERROR_PREFIX} Missing required bid parameters.`); + } + + return isValid; +} + +/** + * Parses the native response from the server into Prebid's native format. + * + * @param {string} adm - The adm field from the bid response (JSON string). + * @returns {Object} The parsed native response object. + */ +export function parseNativeResponse(adm) { + try { + const admObj = JSON.parse(adm); + return admObj.native; + } catch (e) { + printLog('error', `Error parsing native response: `, e) + logError(`${LOG_ERROR_PREFIX} Error parsing native response: `, e); + return {}; + } +} + +/** + * Parses the native response from the server into Prebid's native format. + * @param {type} type - Type of log. default is info + * @param {args} args - Log data. + */ +export function printLog(type, ...args) { + // Determine the prefix based on the log type + const prefixes = { + error: LOG_ERROR_PREFIX, + warning: LOG_WARN_PREFIX, // Assuming warning uses the same prefix as error + info: LOG_INFO_PREFIX + }; + + // Construct the log message by joining all arguments into a single string + const logMessage = args + .map(arg => (arg instanceof Error ? `${arg.name}: ${arg.message}` : arg)) + .join(' '); // Join all arguments into a single string with a space separator + // Add prefix and punctuation (for info type) + const formattedMessage = `${prefixes[type] || LOG_INFO_PREFIX} ${logMessage}${type === 'info' ? '.' : ''}`; + // Map the log type to its corresponding log function + const logFunctions = { + error: logError, + warning: logWarn, + info: logInfo + }; + + // Call the appropriate log function (defaulting to logInfo) + (logFunctions[type] || logInfo)(formattedMessage); +} +/** + * Get or Create Uid for First Party Cookie + */ +export const getUid = (storage) => { + let nexverseUid = storage.getCookie(NEXVERSE_USER_COOKIE_KEY); + if (!nexverseUid) { + nexverseUid = generateUUID(); + } + try { + const expirationInMs = 60 * 60 * 24 * 1000; // 1 day in milliseconds + const expirationTime = new Date(Date.now() + expirationInMs); // Set expiration time + // Set the cookie with the expiration date + storage.setCookie(NEXVERSE_USER_COOKIE_KEY, nexverseUid, expirationTime.toUTCString()); + } catch (e) { + printLog('error', `Failed to set UID cookie: ${e.message}`); + } + return nexverseUid; +}; diff --git a/libraries/ortb2.5Translator/translator.js b/libraries/ortb2.5Translator/translator.js index 1afad516ef0..6dd6d247d1c 100644 --- a/libraries/ortb2.5Translator/translator.js +++ b/libraries/ortb2.5Translator/translator.js @@ -1,10 +1,12 @@ import {deepAccess, deepSetValue, logError} from '../../src/utils.js'; export const EXT_PROMOTIONS = [ + 'device.sua', 'source.schain', 'regs.gdpr', 'regs.us_privacy', 'regs.gpp', + 'regs.gpp_sid', 'user.consent', 'user.eids' ]; diff --git a/libraries/ortb2Utils/currency.js b/libraries/ortb2Utils/currency.js new file mode 100644 index 00000000000..5c2c6b7956d --- /dev/null +++ b/libraries/ortb2Utils/currency.js @@ -0,0 +1,3 @@ +export function getCurrencyFromBidderRequest(bidderRequest) { + return bidderRequest?.ortb2?.ext?.prebid?.adServerCurrency; +} diff --git a/libraries/ortbConverter/README.md b/libraries/ortbConverter/README.md index 751971eebdc..92843c0241e 100644 --- a/libraries/ortbConverter/README.md +++ b/libraries/ortbConverter/README.md @@ -1,7 +1,7 @@ # Prebid.js - ORTB conversion library -This library provides methods to convert Prebid.js bid request objects to ORTB requests, -and ORTB responses to Prebid.js bid response objects. +This library provides methods to convert Prebid.js bid request objects to ORTB requests, +and ORTB responses to Prebid.js bid response objects. ## Usage @@ -37,13 +37,25 @@ registerBidder({ }) ``` -Without any customization, the library will generate complete ORTB requests, but ignores your [bid params](#params). +Without any customization, the library will generate complete ORTB requests, but ignores your [bid params](#params). If your endpoint sets `response.seatbid[].bid[].mtype` (part of the ORTB 2.6 spec), it will also parse the response into complete bidResponse objects. See [setting response mediaTypes](#response-mediaTypes) if that is not the case. ### Module-specific conversions Prebid.js features that require a module also require it for their corresponding ORTB conversion logic. For example, `imp.bidfloor` is only populated if the `priceFloors` module is active; `request.cur` needs the `currency` module, and so on. Notably, this means that to get those fields populated from your unit tests, you must import those modules first; see [this suite](https://github.com/prebid/Prebid.js/blob/master/test/spec/modules/openxOrtbBidAdapter_spec.js) for an example. +#### priceFloors extensions + +In addition to `imp.bidfloor` and `imp.bidfloorcur`, the `priceFloors` module also populates media type and `format` objects, if their floors differ: + +| Path | `getFloor` invocation | +|-----------------------------------------------------|----------------------------------------------------------------| +| `imp.bidfloor` & `.bidfloorcur` | `.getFloor()` | +| `imp.banner.ext.bidfloor` & `.bidfloorcur` | `.getFloor({mediaType: 'banner', size: '*'})` | +| `imp.banner.format[].ext.bidfloor` & `.bidfloorcur` | `.getFloor({mediaType: 'banner', size: [format.w, format.h]})` | +| `imp.native.ext.bidfloor` & `.bidfloorcur` | `.getFloor({mediaType: 'native', size: '*'})` | +| `imp.video.ext.bidfloor` & `.bidfloorcur` | `.getFloor({mediaType: 'video', size: '*'})` | + ## Customization ### Modifying return values directly @@ -57,35 +69,35 @@ deepSetValue(data.imp[0], 'ext.myCustomParam', bidRequests[0].params.myCustomPar However, there are two restrictions (to avoid them, use the [other customization options](#fine-customization)): - - you may not change the `imp[].id` returned by `toORTB`; they ared used internally to match responses to their requests. - ```javascript - const data = converter.toORTB({bidRequests, bidderRequest}); - data.imp[0].id = 'custom-imp-id' // do not do this - it will cause an error later in `fromORTB` - ``` - See also [overriding `imp.id`](#imp-id). - - the `request` argument passed to `fromORTB` must be the same object returned by `toORTB`. +- you may not change the `imp[].id` returned by `toORTB`; they ared used internally to match responses to their requests. ```javascript - let data = converter.toORTB({bidRequests, bidderRequest}); - - data = mergeDeep( // the original object is lost - {ext: {myCustomParam: bidRequests[0].params.myCustomParam}}, // `fromORTB` will later throw an error - data - ); - - // do this instead: - mergeDeep( - data, - {ext: {myCustomParam: bidRequests[0].params.myCustomParam}}, - data - ) + const data = converter.toORTB({bidRequests, bidderRequest}); + data.imp[0].id = 'custom-imp-id' // do not do this - it will cause an error later in `fromORTB` ``` + See also [overriding `imp.id`](#imp-id). +- the `request` argument passed to `fromORTB` must be the same object returned by `toORTB`. + ```javascript + let data = converter.toORTB({bidRequests, bidderRequest}); + + data = mergeDeep( // the original object is lost + {ext: {myCustomParam: bidRequests[0].params.myCustomParam}}, // `fromORTB` will later throw an error + data + ); + + // do this instead: + mergeDeep( + data, + {ext: {myCustomParam: bidRequests[0].params.myCustomParam}}, + data + ) + ``` ### Fine grained customization - imp, request, bidResponse, response When invoked, `toORTB({bidRequests, bidderRequest})` first loops through each request in `bidRequests`, converting them into ORTB `imp` objects. It then packages them into a single ORTB request, adding other parameters that are not imp-specific (such as for example `request.tmax`). -Likewise, `fromORTB({request, response})` first loops through each `response.seatbid[].bid[]`, converting them into Prebid bidResponses; it then packages them into +Likewise, `fromORTB({request, response})` first loops through each `response.seatbid[].bid[]`, converting them into Prebid bidResponses; it then packages them into a single return value. You can customize each of these steps using the `ortbConverter` arguments `imp`, `request`, `bidResponse` and `response`: @@ -98,8 +110,8 @@ The arguments are: - `buildImp`: a function taking `(bidRequest, context)` and returning an ORTB `imp` object; - `bidRequest`: the bid request object to convert; - `context`: a [context object](#context) that contains at least: - - `bidderRequest`: the `bidderRequest` argument passed to `toORTB`. - + - `bidderRequest`: the `bidderRequest` argument passed to `toORTB`. + #### Example: attaching custom bid params ```javascript @@ -194,7 +206,7 @@ const converter = ortbConverter({ }) ``` -If you know that a particular ORTB request/response pair deals with exclusively one mediaType, you may also pass it directly in the [context parameter](#context). +If you know that a particular ORTB request/response pair deals with exclusively one mediaType, you may also pass it directly in the [context parameter](#context). Note that - compared to the above - this has additional effects, because `context.mediaType` is also considered during `imp` generation - see [special context properties](#special-context). ```javascript @@ -223,7 +235,7 @@ const converter = ortbConverter({ ### Customizing the response: `response(buildResponse, bidResponses, ortbResponse, context)` -Invoked once, after all `seatbid[].bid[]` objects have been converted to corresponding bid responses. The value returned +Invoked once, after all `seatbid[].bid[]` objects have been converted to corresponding bid responses. The value returned by this function is also the value returned by `fromORTB`. The arguments are: @@ -249,7 +261,7 @@ const converter = ortbConverter({ ### Even finer grained customization - processor overrides Each of the four conversion steps described above - imp, request, bidResponse and response - is further broken down into -smaller units of work (called _processors_). For example, when the currency module is included, it adds a _request processor_ +smaller units of work (called _processors_). For example, when the currency module is included, it adds a _request processor_ that sets `request.cur`; the priceFloors module adds an _imp processor_ that sets `imp.bidfloor` and `imp.bidfloorcur`, and so on. Each processor can be overridden or disabled through the `overrides` argument: @@ -310,21 +322,21 @@ const converter = ortbConverter({ Processor overrides are similar to the override options described above, except that they take the object to process as argument: - `imp` processor overrides take `(orig, imp, bidRequest, context)`, where: - - `orig` is the processor function being overridden, which itself takes `(imp, bidRequest, context)`; - - `imp` is the (partial) imp object to modify; - - `bidRequest` and `context` are the same arguments passed to [imp](#imp). + - `orig` is the processor function being overridden, which itself takes `(imp, bidRequest, context)`; + - `imp` is the (partial) imp object to modify; + - `bidRequest` and `context` are the same arguments passed to [imp](#imp). - `request` processor overrides take `(orig, ortbRequest, bidderRequest, context)`, where: - - `orig` is the processor function being overridden, and takes `(ortbRequest, bidderRequest, context)`; - - `ortbRequest` is the partial request to modify; - - `bidderRequest` and `context` are the same arguments passed to [request](#reuqest). -- `bidResponse` processor overrides take `(orig, bidResponse, bid, context)`, where: - - `orig` is the processor function being overridden, and takes `(bidResponse, bid, context)`; - - `bidResponse` is the partial bid response to modify; - - `bid` and `context` are the same arguments passed to [bidResponse](#bidResponse) + - `orig` is the processor function being overridden, and takes `(ortbRequest, bidderRequest, context)`; + - `ortbRequest` is the partial request to modify; + - `bidderRequest` and `context` are the same arguments passed to [request](#reuqest). +- `bidResponse` processor overrides take `(orig, bidResponse, bid, context)`, where: + - `orig` is the processor function being overridden, and takes `(bidResponse, bid, context)`; + - `bidResponse` is the partial bid response to modify; + - `bid` and `context` are the same arguments passed to [bidResponse](#bidResponse) - `response` processor overrides take `(orig, response, ortbResponse, context)`, where: - - `orig` is the processor function being overriden, and takes `(response, ortbResponse, context)`; - - `response` is the partial response to modify; - - `ortbRespones` and `context` are the same arguments passed to [response](#response). + - `orig` is the processor function being overriden, and takes `(response, ortbResponse, context)`; + - `response` is the partial response to modify; + - `ortbRespones` and `context` are the same arguments passed to [response](#response). ### The `context` argument @@ -354,19 +366,19 @@ const converter = ortbConverter({ For ease of use, the conversion logic gives special meaning to some context properties: - - `currency`: a currency string (e.g. `'EUR'`). If specified, overrides the currency to use for computing price floors and `request.cur`. If omitted, both default to `getConfig('currency.adServerCurrency')`. - - `mediaType`: a bid mediaType (`'banner'`, `'video'`, or `'native'`). If specified: +- `currency`: a currency string (e.g. `'EUR'`). If specified, overrides the currency to use for computing price floors and `request.cur`. If omitted, both default to `getConfig('currency.adServerCurrency')`. +- `mediaType`: a bid mediaType (`'banner'`, `'video'`, or `'native'`). If specified: - disables `imp` generation for other media types (i.e., if `context.mediaType === 'banner'`, only `imp.banner` will be populated; `imp.video` and `imp.native` will not, even if the bid request specifies them); - is passed as the `mediaType` option to `bidRequest.getFloor` when computing price floors; - sets `bidResponse.mediaType`. - - `nativeRequest`: a plain object that serves as the base value for `imp.native.request` (and is relevant only for native bid requests). - If not specified, the only property that is guaranteed to be populated is `assets`, since Prebid does not require anything else to define a native adUnit. You can use `context.nativeRequest` to provide other properties; for example, you may want to signal support for native impression trackers by setting it to `{eventtrackers: [{event: 1, methods: [1, 2]}]}` (see also the [ORTB Native spec](https://www.iab.com/wp-content/uploads/2018/03/OpenRTB-Native-Ads-Specification-Final-1.2.pdf)). - - `netRevenue`: the value to set as `bidResponse.netRevenue`. This is a required property of bid responses that does not have a clear ORTB counterpart. - - `ttl`: the default value to use for `bidResponse.ttl` (if the ORTB response does not provide one in `seatbid[].bid[].exp`). - +- `nativeRequest`: a plain object that serves as the base value for `imp.native.request` (and is relevant only for native bid requests). + If not specified, the only property that is guaranteed to be populated is `assets`, since Prebid does not require anything else to define a native adUnit. You can use `context.nativeRequest` to provide other properties; for example, you may want to signal support for native impression trackers by setting it to `{eventtrackers: [{event: 1, methods: [1, 2]}]}` (see also the [ORTB Native spec](https://www.iab.com/wp-content/uploads/2018/03/OpenRTB-Native-Ads-Specification-Final-1.2.pdf)). +- `netRevenue`: the value to set as `bidResponse.netRevenue`. This is a required property of bid responses that does not have a clear ORTB counterpart. +- `ttl`: the default value to use for `bidResponse.ttl` (if the ORTB response does not provide one in `seatbid[].bid[].exp`). + ## Prebid Server extensions -If your endpoint is a Prebid Server instance, you may take advantage of the `pbsExtension` companion library, which adds a number of processors that can populate and parse PBS-specific extensions (typically prefixed `ext.prebid`); these include bidder params (with `transformBidParams`), bidder aliases, targeting keys, and others. +If your endpoint is a Prebid Server instance, you may take advantage of the `pbsExtension` companion library, which adds a number of processors that can populate and parse PBS-specific extensions (typically prefixed `ext.prebid`); these include bidder params (with `transformBidParams`), bidder aliases, targeting keys, and others. ```javascript import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js' diff --git a/libraries/ortbConverter/converter.js b/libraries/ortbConverter/converter.js index c367aec268a..9cef8898c39 100644 --- a/libraries/ortbConverter/converter.js +++ b/libraries/ortbConverter/converter.js @@ -117,7 +117,7 @@ export function ortbConverter({ throw new Error('ortbRequest passed to `fromORTB` must be the same object returned by `toORTB`') } function augmentContext(ctx, extraParams = {}) { - return Object.assign(ctx, {ortbRequest: request}, extraParams, ctx); + return Object.assign(ctx, {ortbRequest: request}, extraParams); } const impsById = Object.fromEntries((request.imp || []).map(imp => [imp.id, imp])); const bidResponses = (response.seatbid || []).flatMap(seatbid => diff --git a/libraries/ortbConverter/lib/composer.js b/libraries/ortbConverter/lib/composer.js index 0ceff7f9edb..477d4e10890 100644 --- a/libraries/ortbConverter/lib/composer.js +++ b/libraries/ortbConverter/lib/composer.js @@ -11,13 +11,13 @@ const SORTED = new WeakMap(); /** * - * @param {Object[string, Component]} components to compose - * @param {Object[string, function|boolean]} overrides a map from component name, to a function that should override that component. + * @param {Object.} components - An object where keys are component names and values are components to compose. + * @param {Object.} overrides - A map from component names to functions that should override those components. * Override functions are replacements, except that they get the original function they are overriding as their first argument. If the override * is `false`, the component is disabled. * - * @return a function that will run all components in order of priority, with functions from `overrides` taking - * precedence over components that match names + * @return {function} - A function that will run all components in order of priority, with functions from `overrides` taking + * precedence over components that match names. */ export function compose(components, overrides = {}) { if (!SORTED.has(components)) { diff --git a/libraries/ortbConverter/lib/sizes.js b/libraries/ortbConverter/lib/sizes.js deleted file mode 100644 index 16b75048203..00000000000 --- a/libraries/ortbConverter/lib/sizes.js +++ /dev/null @@ -1,14 +0,0 @@ -import {parseSizesInput} from '../../../src/utils.js'; - -export function sizesToFormat(sizes) { - sizes = parseSizesInput(sizes); - - // get sizes in form [{ w: , h: }, ...] - return sizes.map(size => { - const [width, height] = size.split('x'); - return { - w: parseInt(width, 10), - h: parseInt(height, 10) - }; - }); -} diff --git a/libraries/ortbConverter/processors/banner.js b/libraries/ortbConverter/processors/banner.js index 2d0136c84b2..aed8ffde9e5 100644 --- a/libraries/ortbConverter/processors/banner.js +++ b/libraries/ortbConverter/processors/banner.js @@ -1,6 +1,12 @@ -import {createTrackPixelHtml, deepAccess, encodeMacroURI, inIframe, mergeDeep} from '../../../src/utils.js'; +import { + createTrackPixelHtml, + inIframe, + mergeDeep, + sizesToSizeTuples, + sizeTupleToRtbSize, + encodeMacroURI +} from '../../../src/utils.js'; import {BANNER} from '../../../src/mediaTypes.js'; -import {sizesToFormat} from '../lib/sizes.js'; /** * fill in a request `imp` with banner parameters from `bidRequest`. @@ -8,13 +14,13 @@ import {sizesToFormat} from '../lib/sizes.js'; export function fillBannerImp(imp, bidRequest, context) { if (context.mediaType && context.mediaType !== BANNER) return; - const bannerParams = deepAccess(bidRequest, 'mediaTypes.banner'); + const bannerParams = bidRequest?.mediaTypes?.banner; if (bannerParams) { const banner = { topframe: inIframe() === true ? 0 : 1 }; - if (bannerParams.sizes) { - banner.format = sizesToFormat(bannerParams.sizes); + if (bannerParams.sizes && bidRequest.ortb2Imp?.banner?.format == null) { + banner.format = sizesToSizeTuples(bannerParams.sizes).map(sizeTupleToRtbSize); } if (bannerParams.hasOwnProperty('pos')) { banner.pos = bannerParams.pos; diff --git a/libraries/ortbConverter/processors/default.js b/libraries/ortbConverter/processors/default.js index d92a51daba2..d4d3b7647e8 100644 --- a/libraries/ortbConverter/processors/default.js +++ b/libraries/ortbConverter/processors/default.js @@ -61,6 +61,12 @@ export const DEFAULT_PROCESSORS = { delete imp.ext?.data?.pbadslot; } } + }, + secure: { + // should set imp.secure to 1 unless publisher has set it + fn(imp, bidRequest) { + imp.secure = imp.secure ?? 1; + } } }, [BID_RESPONSE]: { @@ -83,6 +89,8 @@ export const DEFAULT_PROCESSORS = { currency: context.ortbResponse.cur || context.currency, width: bid.w, height: bid.h, + wratio: bid.wratio, + hratio: bid.hratio, dealId: bid.dealid, creative_id: bid.crid, creativeId: bid.crid, @@ -100,6 +108,16 @@ export const DEFAULT_PROCESSORS = { if (bid.ext?.dsa) { bidResponse.meta.dsa = bid.ext.dsa; } + if (bid.cat) { + bidResponse.meta.primaryCatId = bid.cat[0]; + bidResponse.meta.secondaryCatIds = bid.cat.slice(1); + } + if (bid.attr) { + bidResponse.meta.attr = bid.attr; + } + if (bid.ext?.eventtrackers) { + bidResponse.eventtrackers = (bidResponse.eventtrackers ?? []).concat(bid.ext.eventtrackers); + } } } } diff --git a/libraries/ortbConverter/processors/video.js b/libraries/ortbConverter/processors/video.js index c38231d9002..3bb4e69e24d 100644 --- a/libraries/ortbConverter/processors/video.js +++ b/libraries/ortbConverter/processors/video.js @@ -1,63 +1,33 @@ -import {deepAccess, isEmpty, logWarn, mergeDeep} from '../../../src/utils.js'; +import {isEmpty, logWarn, mergeDeep, sizesToSizeTuples, sizeTupleToRtbSize} from '../../../src/utils.js'; import {VIDEO} from '../../../src/mediaTypes.js'; -import {sizesToFormat} from '../lib/sizes.js'; -// parameters that share the same name & semantics between pbjs adUnits and imp.video -const ORTB_VIDEO_PARAMS = new Set([ - 'pos', - 'placement', - 'plcmt', - 'api', - 'mimes', - 'protocols', - 'playbackmethod', - 'minduration', - 'maxduration', - 'w', - 'h', - 'startdelay', - 'placement', - 'linearity', - 'skip', - 'skipmin', - 'skipafter', - 'minbitrate', - 'maxbitrate', - 'delivery', - 'playbackend' -]); - -const PLACEMENT = { - 'instream': 1, -} +import {ORTB_VIDEO_PARAMS} from '../../../src/video.js'; export function fillVideoImp(imp, bidRequest, context) { if (context.mediaType && context.mediaType !== VIDEO) return; - const videoParams = deepAccess(bidRequest, 'mediaTypes.video'); + const videoParams = bidRequest?.mediaTypes?.video; if (!isEmpty(videoParams)) { const video = Object.fromEntries( + // Parameters that share the same name & semantics between pbjs adUnits and imp.video Object.entries(videoParams) .filter(([name]) => ORTB_VIDEO_PARAMS.has(name)) ); if (videoParams.playerSize) { - const format = sizesToFormat(videoParams.playerSize); + const format = sizesToSizeTuples(videoParams.playerSize).map(sizeTupleToRtbSize); if (format.length > 1) { logWarn('video request specifies more than one playerSize; all but the first will be ignored') } Object.assign(video, format[0]); } - const placement = PLACEMENT[videoParams.context]; - if (placement != null) { - video.placement = placement; - } + imp.video = mergeDeep(video, imp.video); } } export function fillVideoResponse(bidResponse, seatbid, context) { if (bidResponse.mediaType === VIDEO) { - if (deepAccess(context.imp, 'video.w') && deepAccess(context.imp, 'video.h')) { + if (context?.imp?.video?.w && context?.imp?.video?.h) { [bidResponse.playerWidth, bidResponse.playerHeight] = [context.imp.video.w, context.imp.video.h]; } diff --git a/libraries/paapiTools/buyerOrigins.js b/libraries/paapiTools/buyerOrigins.js new file mode 100644 index 00000000000..ace9b7da073 --- /dev/null +++ b/libraries/paapiTools/buyerOrigins.js @@ -0,0 +1,35 @@ +/* + This list is several known buyer origins for PAAPI auctions. + Bidders should add anyone they like to it. + It is not intended to be comphensive nor maintained by the Core team. + Rather, Bid adapters should simply append additional constants whenever + the need arises in their adapter. + + The goal is to reduce expression of common constants over many + bid adapters attempting to define interestGroupBuyers + in advance of network traffic. + + Bidders should consider updating their interstGroupBuyer list + with server communication for auctions initiated after the first bid response. + + Known buyers without current importers are commented out. If you need one, uncomment it. +*/ + +export const BO_CSR_ONET = 'https://csr.onet.pl'; +// export const BO_DOUBLECLICK_GOOGLEADS = 'https://googleads.g.doubleclick.net'; +// export const BO_DOUBLECLICK_TD = 'https://td.doubleclick.net'; +// export const BO_RTBHOUSE = 'https://f.creativecdn.com'; +// export const BO_CRITEO_US = 'https://fledge.us.criteo.com'; +// export const BO_CRITEO_EU = 'https://fledge.eu.criteo.com'; +// export const BO_CRITEO_AS = 'https://fledge.as.criteo.com'; +// export const BO_CRITEO_GRID_MERCURY = 'https://grid-mercury.criteo.com'; +// export const BO_CRITEO_BIDSWITCH_TRADR = 'https://tradr.bsw-sb.criteo.com'; +// export const BO_CRITEO_BIDSWITCH_SANDBOX = 'https://dsp-paapi-sandbox.bsw-ig.criteo.com'; +// export const BO_APPSPOT = 'https://fledge-buyer-testing-1.uc.r.appspot.com'; +// export const BO_OPTABLE = 'https://ads.optable.co'; +// export const BO_ADROLL = 'https://x.adroll.com'; +// export const BO_ADFORM = 'https://a2.adform.net'; +// export const BO_RETARGETLY = 'https://cookieless-campaign.prd-00.retargetly.com'; +// export const BO_AUDIGENT = 'https://proton.ad.gt'; +// export const BO_YAHOO = 'https://pa.ybp.yahoo.com'; +// export const BO_DOTOMI = 'https://usadmm.dotomi.com'; diff --git a/libraries/pageInfosUtils/pageInfosUtils.js b/libraries/pageInfosUtils/pageInfosUtils.js new file mode 100644 index 00000000000..5e215ad3f3d --- /dev/null +++ b/libraries/pageInfosUtils/pageInfosUtils.js @@ -0,0 +1,65 @@ +/** + * Retrieves the referrer information from the bidder request. + * + * @param {Object} bidderRequest - The bidder request object. + * @param {Object} [bidderRequest.refererInfo] - The referer information object. + * @param {string} [bidderRequest.refererInfo.page] - The page URL of the referer. + * @returns {string} The referrer URL if available, otherwise an empty string. + */ +export function getReferrerInfo(bidderRequest) { + let ref = ''; + if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { + ref = bidderRequest.refererInfo.page; + } + return ref; +} + +/** + * Retrieves the title of the current web page. + * + * This function attempts to get the title from the top-level window's document. + * If an error occurs (e.g., due to cross-origin restrictions), it falls back to the current document. + * It first tries to get the title from the `og:title` meta tag, and if that is not available, it uses the document's title. + * + * @returns {string} The title of the current web page, or an empty string if no title is found. + */ +export function getPageTitle() { + try { + const ogTitle = window.top.document.querySelector('meta[property="og:title"]'); + return window.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]'); + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +/** + * Retrieves the content of the page description meta tag. + * + * This function attempts to get the description from the top-level window's document. + * If it fails (e.g., due to cross-origin restrictions), it falls back to the current document. + * It looks for meta tags with either the name "description" or the property "og:description". + * + * @returns {string} The content of the description meta tag, or an empty string if not found. + */ +export function getPageDescription() { + try { + const element = window.top.document.querySelector('meta[name="description"]') || + window.top.document.querySelector('meta[property="og:description"]'); + return (element && element.content) || ''; + } catch (e) { + const element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]'); + return (element && element.content) || ''; + } +} + +/** + * Retrieves the downlink speed of the user's network connection. + * + * @param {object} nav - The navigator object, typically `window.navigator`. + * @returns {string} The downlink speed as a string if available, otherwise an empty string. + */ +export function getConnectionDownLink(nav) { + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; +} diff --git a/libraries/pbsExtensions/processors/aliases.js b/libraries/pbsExtensions/processors/aliases.js index 3dcd2c4fd9b..42dea969e6b 100644 --- a/libraries/pbsExtensions/processors/aliases.js +++ b/libraries/pbsExtensions/processors/aliases.js @@ -1,4 +1,5 @@ import adapterManager from '../../../src/adapterManager.js'; +import {config} from '../../../src/config.js'; import {deepSetValue} from '../../../src/utils.js'; export function setRequestExtPrebidAliases(ortbRequest, bidderRequest, context, {am = adapterManager} = {}) { @@ -7,11 +8,22 @@ export function setRequestExtPrebidAliases(ortbRequest, bidderRequest, context, // adding alias only if alias source bidder exists and alias isn't configured to be standalone // pbs adapter if (!bidder || !bidder.getSpec().skipPbsAliasing) { + // set alias deepSetValue( ortbRequest, `ext.prebid.aliases.${bidderRequest.bidderCode}`, am.aliasRegistry[bidderRequest.bidderCode] ); + + // set alias gvlids if present also + const gvlId = config.getConfig(`gvlMapping.${bidderRequest.bidderCode}`) || bidder?.getSpec?.().gvlid; + if (gvlId) { + deepSetValue( + ortbRequest, + `ext.prebid.aliasgvlids.${bidderRequest.bidderCode}`, + gvlId + ); + } } } } diff --git a/libraries/pbsExtensions/processors/eventTrackers.js b/libraries/pbsExtensions/processors/eventTrackers.js new file mode 100644 index 00000000000..287084a3e21 --- /dev/null +++ b/libraries/pbsExtensions/processors/eventTrackers.js @@ -0,0 +1,18 @@ +import {EVENT_TYPE_IMPRESSION, EVENT_TYPE_WIN, TRACKER_METHOD_IMG} from '../../../src/eventTrackers.js'; + +export function addEventTrackers(bidResponse, bid) { + bidResponse.eventtrackers = bidResponse.eventtrackers || []; + [ + [bid.burl, EVENT_TYPE_IMPRESSION], // core used to fire burl directly, but only for bids coming from PBS + [bid?.ext?.prebid?.events?.win, EVENT_TYPE_WIN] + ].filter(([winUrl, type]) => winUrl && bidResponse.eventtrackers.find( + ({method, event, url}) => event === type && method === TRACKER_METHOD_IMG && url === winUrl + ) == null) + .forEach(([url, event]) => { + bidResponse.eventtrackers.push({ + method: TRACKER_METHOD_IMG, + event, + url + }) + }) +} diff --git a/libraries/pbsExtensions/processors/params.js b/libraries/pbsExtensions/processors/params.js index 010ffa5b372..dbfbb928953 100644 --- a/libraries/pbsExtensions/processors/params.js +++ b/libraries/pbsExtensions/processors/params.js @@ -1,17 +1,7 @@ -import {auctionManager} from '../../../src/auctionManager.js'; -import adapterManager from '../../../src/adapterManager.js'; import {deepSetValue} from '../../../src/utils.js'; -export function setImpBidParams( - imp, bidRequest, context, - {adUnit, bidderRequests, index = auctionManager.index, bidderRegistry = adapterManager.bidderRegistry} = {}) { +export function setImpBidParams(imp, bidRequest) { let params = bidRequest.params; - const adapter = bidderRegistry[bidRequest.bidder]; - if (adapter && adapter.getSpec().transformBidParams) { - adUnit = adUnit || index.getAdUnit(bidRequest); - bidderRequests = bidderRequests || [context.bidderRequest]; - params = adapter.getSpec().transformBidParams(params, true, adUnit, bidderRequests); - } if (params) { deepSetValue( imp, diff --git a/libraries/pbsExtensions/processors/pbs.js b/libraries/pbsExtensions/processors/pbs.js index 0ed2d12fad8..8e08c3f3027 100644 --- a/libraries/pbsExtensions/processors/pbs.js +++ b/libraries/pbsExtensions/processors/pbs.js @@ -6,6 +6,7 @@ import {setImpBidParams} from './params.js'; import {setImpAdUnitCode} from './adUnitCode.js'; import {setRequestExtPrebid, setRequestExtPrebidChannel} from './requestExtPrebid.js'; import {setBidResponseVideoCache} from './video.js'; +import {addEventTrackers} from './eventTrackers.js'; export const PBS_PROCESSORS = { [REQUEST]: { @@ -74,14 +75,9 @@ export const PBS_PROCESSORS = { bidResponse.meta = mergeDeep({}, deepAccess(bid, 'ext.prebid.meta'), bidResponse.meta); } }, - pbsWurl: { - // sets bidResponse.pbsWurl from ext.prebid.events.win - fn(bidResponse, bid) { - const wurl = deepAccess(bid, 'ext.prebid.events.win'); - if (isStr(wurl)) { - bidResponse.pbsWurl = wurl; - } - } + pbsWinTrackers: { + // converts "legacy" burl and ext.prebid.events.win into eventtrackers + fn: addEventTrackers }, }, [RESPONSE]: { diff --git a/libraries/percentInView/percentInView.js b/libraries/percentInView/percentInView.js index 13381c5c541..31e81e27745 100644 --- a/libraries/percentInView/percentInView.js +++ b/libraries/percentInView/percentInView.js @@ -1,6 +1,8 @@ +import { getWinDimensions } from '../../src/utils.js'; +import { getBoundingClientRect } from '../boundingClientRect/boundingClientRect.js'; -function getBoundingBox(element, {w, h} = {}) { - let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); +export function getBoundingBox(element, {w, h} = {}) { + let {width, height, left, top, right, bottom, x, y} = getBoundingClientRect(element); if ((width === 0 || height === 0) && w && h) { width = w; @@ -9,7 +11,7 @@ function getBoundingBox(element, {w, h} = {}) { bottom = top + h; } - return {width, height, left, top, right, bottom}; + return {width, height, left, top, right, bottom, x, y}; } function getIntersectionOfRects(rects) { @@ -39,12 +41,17 @@ function getIntersectionOfRects(rects) { return bbox; } -export const percentInView = (element, topWin, {w, h} = {}) => { +export const percentInView = (element, {w, h} = {}) => { const elementBoundingBox = getBoundingBox(element, {w, h}); + const { innerHeight, innerWidth } = getWinDimensions(); + // Obtain the intersection of the element and the viewport const elementInViewBoundingBox = getIntersectionOfRects([{ - left: 0, top: 0, right: topWin.innerWidth, bottom: topWin.innerHeight + left: 0, + top: 0, + right: innerWidth, + bottom: innerHeight }, elementBoundingBox]); let elementInViewArea, elementTotalArea; diff --git a/libraries/precisoUtils/bidNativeUtils.js b/libraries/precisoUtils/bidNativeUtils.js new file mode 100644 index 00000000000..29b39f6d77d --- /dev/null +++ b/libraries/precisoUtils/bidNativeUtils.js @@ -0,0 +1,104 @@ +import { deepAccess, logInfo } from '../../src/utils.js'; +import { NATIVE } from '../../src/mediaTypes.js'; +import { macroReplace } from './bidUtils.js'; + +const TTL = 55; +// Codes defined by OpenRTB Native Ads 1.1 specification +export const OPENRTB = { + NATIVE: { + IMAGE_TYPE: { + ICON: 1, + MAIN: 3, + }, + ASSET_ID: { + TITLE: 1, + IMAGE: 2, + ICON: 3, + BODY: 4, + SPONSORED: 5, + CTA: 6 + }, + DATA_ASSET_TYPE: { + SPONSORED: 1, + DESC: 2, + CTA_TEXT: 12, + }, + } +}; + +/** + * @param {object} serverBid Bid by OpenRTB 2.5 §4.2.3 + * @returns {object} Prebid native bidObject + */ +export function interpretNativeBid(serverBid) { + return { + requestId: serverBid.impid, + mediaType: NATIVE, + cpm: serverBid.price, + creativeId: serverBid.adid || serverBid.crid, + width: 1, + height: 1, + ttl: TTL, + meta: { + advertiserDomains: serverBid.adomain + }, + netRevenue: true, + currency: 'USD', + // native: interpretNativeAd(serverBid.adm) + native: interpretNativeAd(macroReplace(serverBid.adm, serverBid.price)) + } +} + +/** + * @param {string} adm JSON-encoded Request by OpenRTB Native Ads 1.1 §4.1 + * @returns {object} Prebid bidObject.native + */ + +export function interpretNativeAd(adm) { + try { + const native = JSON.parse(adm).native; + if (native) { + const result = { + clickUrl: encodeURI(native.link.url), + impressionTrackers: native.imptrackers || native.eventtrackers[0].url, + }; + if (native.link.clicktrackers) { + result.clickTrackers = native.link.clicktrackers[0]; + } + + native.assets.forEach(asset => { + switch (asset.id) { + case OPENRTB.NATIVE.ASSET_ID.TITLE: + result.title = deepAccess(asset, 'title.text'); + break; + case OPENRTB.NATIVE.ASSET_ID.IMAGE: + result.image = { + url: encodeURI(asset.img.url), + width: deepAccess(asset, 'img.w'), + height: deepAccess(asset, 'img.h') + }; + break; + case OPENRTB.NATIVE.ASSET_ID.ICON: + result.icon = { + url: encodeURI(asset.img.url), + width: deepAccess(asset, 'img.w'), + height: deepAccess(asset, 'img.h') + }; + break; + case OPENRTB.NATIVE.ASSET_ID.BODY: + result.body = deepAccess(asset, 'data.value'); + break; + case OPENRTB.NATIVE.ASSET_ID.SPONSORED: + result.sponsoredBy = deepAccess(asset, 'data.value'); + break; + case OPENRTB.NATIVE.ASSET_ID.CTA: + result.cta = deepAccess(asset, 'data.value'); + break; + } + }); + return result; + } + } catch (error) { + logInfo('Error in bidUtils interpretNativeAd' + error); + } +} diff --git a/libraries/precisoUtils/bidUtils.js b/libraries/precisoUtils/bidUtils.js new file mode 100644 index 00000000000..8359963cd03 --- /dev/null +++ b/libraries/precisoUtils/bidUtils.js @@ -0,0 +1,249 @@ +import { convertOrtbRequestToProprietaryNative } from '../../src/native.js'; +import { replaceAuctionPrice, deepAccess } from '../../src/utils.js'; +import { ajax } from '../../src/ajax.js'; +// import { NATIVE } from '../../src/mediaTypes.js'; +import { consentCheck, getBidFloor } from './bidUtilsCommon.js'; +import { interpretNativeBid } from './bidNativeUtils.js'; + +export const buildRequests = (endpoint) => (validBidRequests = [], bidderRequest) => { + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + var city = Intl.DateTimeFormat().resolvedOptions().timeZone; + let req = { + id: validBidRequests[0].auctionId, + imp: validBidRequests.map(slot => mapImpression(slot, bidderRequest)), + user: { + id: validBidRequests[0].userId.pubcid || '', + buyeruid: validBidRequests[0].buyerUid || '', + geo: { + country: validBidRequests[0].params.region || city, + region: validBidRequests[0].params.region || city, + }, + + }, + device: validBidRequests[0].ortb2.device, + site: validBidRequests[0].ortb2.site, + source: validBidRequests[0].ortb2.source, + bcat: validBidRequests[0].ortb2.bcat || validBidRequests[0].params.bcat, + badv: validBidRequests[0].ortb2.badv || validBidRequests[0].params.badv, + wlang: validBidRequests[0].ortb2.wlang || validBidRequests[0].params.wlang, + }; + if (req.device && req.device != 'undefined') { + req.device.geo = { + country: req.user.geo.country, + region: req.user.geo.region, + + }; + }; + req.site.publisher = { + publisherId: validBidRequests[0].params.publisherId + }; + + consentCheck(bidderRequest, req); + return { + method: 'POST', + url: endpoint, + data: req, + + }; +} + +export function interpretResponse(serverResponse) { + const bidsValue = [] + const bidResponse = serverResponse.body + bidResponse.seatbid.forEach(seat => { + seat.bid.forEach(bid => { + bidsValue.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + ad: macroReplace(bid.adm, bid.price), + currency: 'USD', + netRevenue: true, + ttl: 300, + meta: { + advertiserDomains: bid.adomain || '', + }, + }) + }) + }) + return bidsValue +} + +export function onBidWon(bid) { + if (bid.nurl) { + const resolvedNurl = replaceAuctionPrice(bid.nurl, bid.price); + ajax(resolvedNurl); + } +} + +export function macroReplace(adm, cpm) { + let replacedadm = replaceAuctionPrice(adm, cpm); + return replacedadm; +} + +function mapImpression(slot, bidderRequest) { + const imp = { + id: slot.bidId, + bidFloor: getBidFloor(slot), + }; + + if (slot.mediaType === 'native' || deepAccess(slot, 'mediaTypes.native')) { + imp.native = mapNative(slot) + } else { + imp.banner = mapBanner(slot) + } + return imp +} + +function mapNative(slot) { + if (slot.mediaType === 'native' || deepAccess(slot, 'mediaTypes.native')) { + let request = { + assets: slot.nativeOrtbRequest.assets || slot.nativeParams.ortb.assets, + ver: '1.2' + }; + return { + request: JSON.stringify(request) + } + } +} + +function mapBanner(slot) { + if (slot.mediaTypes.banner) { + let format = (slot.mediaTypes.banner.sizes || slot.sizes).map(size => { + return { w: size[0], h: size[1] } + }); + + return { + format + } + } +} + +export function buildBidResponse(serverResponse) { + const responseBody = serverResponse.body; + const bids = []; + responseBody.seatbid.forEach(seat => { + seat.bid.forEach(serverBid => { + if (!serverBid.price) { + return; + } + if (serverBid.adm.indexOf('{') === 0) { + let interpretedBid = interpretNativeBid(serverBid); + bids.push(interpretedBid + ); + } else { + bids.push({ + requestId: serverBid.impid, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.crid, + ad: macroReplace(serverBid.adm, serverBid.price), + currency: 'USD', + netRevenue: true, + ttl: 300, + meta: { + advertiserDomains: serverBid.adomain || '', + }, + }); + } + }) + }); + return bids; +} + +// export function interpretNativeAd(adm) { +// try { +// // logInfo('adm::' + adm); +// const native = JSON.parse(adm).native; +// if (native) { +// const result = { +// clickUrl: encodeURI(native.link.url), +// impressionTrackers: native.eventtrackers[0].url, +// }; +// if (native.link.clicktrackers[0]) { +// result.clickTrackers = native.link.clicktrackers[0]; +// } + +// native.assets.forEach(asset => { +// switch (asset.id) { +// case OPENRTB.NATIVE.ASSET_ID.TITLE: +// result.title = deepAccess(asset, 'title.text'); +// break; +// case OPENRTB.NATIVE.ASSET_ID.IMAGE: +// result.image = { +// url: encodeURI(asset.img.url), +// width: deepAccess(asset, 'img.w'), +// height: deepAccess(asset, 'img.h') +// }; +// break; +// case OPENRTB.NATIVE.ASSET_ID.ICON: +// result.icon = { +// url: encodeURI(asset.img.url), +// width: deepAccess(asset, 'img.w'), +// height: deepAccess(asset, 'img.h') +// }; +// break; +// case OPENRTB.NATIVE.ASSET_ID.DATA: +// result.body = deepAccess(asset, 'data.value'); +// break; +// case OPENRTB.NATIVE.ASSET_ID.SPONSORED: +// result.sponsoredBy = deepAccess(asset, 'data.value'); +// break; +// case OPENRTB.NATIVE.ASSET_ID.CTA: +// result.cta = deepAccess(asset, 'data.value'); +// break; +// } +// }); +// return result; +// } +// } catch (error) { +// logInfo('Error in bidUtils interpretNativeAd' + error); +// } +// } + +// export const OPENRTB = { +// NATIVE: { +// IMAGE_TYPE: { +// ICON: 1, +// MAIN: 3, +// }, +// ASSET_ID: { +// TITLE: 1, +// IMAGE: 2, +// ICON: 3, +// BODY: 4, +// SPONSORED: 5, +// CTA: 6 +// }, +// DATA_ASSET_TYPE: { +// SPONSORED: 1, +// DESC: 2, +// CTA_TEXT: 12, +// }, +// } +// }; + +// /** +// * @param {object} serverBid Bid by OpenRTB 2.5 §4.2.3 +// * @returns {object} Prebid native bidObject +// */ +// export function interpretNativeBid(serverBid) { +// return { +// requestId: serverBid.impid, +// mediaType: NATIVE, +// cpm: serverBid.price, +// creativeId: serverBid.adid || serverBid.crid, +// width: 1, +// height: 1, +// ttl: 56, +// meta: { +// advertiserDomains: serverBid.adomain +// }, +// netRevenue: true, +// currency: 'USD', +// native: interpretNativeAd(macroReplace(serverBid.adm, serverBid.price)) +// } +// } diff --git a/libraries/precisoUtils/bidUtilsCommon.js b/libraries/precisoUtils/bidUtilsCommon.js new file mode 100644 index 00000000000..a8ea97efcaf --- /dev/null +++ b/libraries/precisoUtils/bidUtilsCommon.js @@ -0,0 +1,162 @@ +import { config } from '../../src/config.js'; +import { + isFn, + isStr, + deepAccess, + getWindowTop, + triggerPixel +} from '../../src/utils.js'; +import { BANNER, VIDEO, NATIVE } from '../../src/mediaTypes.js'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency || !bid.meta) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastXml || bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers); + default: + return false; + } +} + +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor?.floor; + } catch (_) { + return 0 + } +} + +export function isBidRequestValid(bid) { + return Boolean(bid.bidId && bid.params && bid.params.placementId); +} + +export const buildBidRequests = (adurl) => (validBidRequests = [], bidderRequest) => { + const winTop = getWindowTop(); + const location = winTop.location; + const placements = []; + + const request = { + deviceWidth: winTop.screen.width, + deviceHeight: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + host: location.host, + page: location.pathname, + placements: placements + }; + consentCheck(bidderRequest, request); + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + bidfloor: getBidFloor(bid) + }; + + if (typeof bid.userId !== 'undefined') { + placement.userId = bid.userId; + } + + const mediaType = bid.mediaTypes; + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.sizes = mediaType[BANNER].sizes; + placement.adFormat = BANNER; + } + + placements.push(placement); + } + + return { + method: 'POST', + url: adurl, + data: request + }; +} + +export function interpretResponse(serverResponse) { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; +} + +export function consentCheck(bidderRequest, req) { + if (bidderRequest) { + if (bidderRequest.uspConsent) { + req.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + req.gdpr = bidderRequest.gdprConsent + } + if (bidderRequest.gppConsent) { + req.gpp = bidderRequest.gppConsent; + } + } +} + +export const buildUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent, syncEndpoint) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + const isCk2trk = syncEndpoint.includes('ck.2trk.info'); + + let syncUrl = isCk2trk ? syncEndpoint : `${syncEndpoint}/${syncType}?pbjs=1`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } else { + syncUrl += isCk2trk ? `&gdpr=0&gdpr_consent=` : ''; + } + + if (isCk2trk) { + syncUrl += uspConsent ? `&us_privacy=${uspConsent}` : `&us_privacy=`; + syncUrl += (syncOptions.iframeEnabled) ? `&t=4` : `&t=2` + } else { + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + } + + return [{ + type: syncType, + url: syncUrl + }]; +} + +export function bidWinReport (bid) { + const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; + if (isStr(bid.nurl) && bid.nurl !== '') { + bid.nurl = bid.nurl.replace(/\${AUCTION_PRICE}/, cpm); + triggerPixel(bid.nurl); + } +} diff --git a/libraries/processResponse/index.js b/libraries/processResponse/index.js new file mode 100644 index 00000000000..5ca2e9329f3 --- /dev/null +++ b/libraries/processResponse/index.js @@ -0,0 +1,12 @@ +import { logError } from '../../src/utils.js'; + +export function getBidFromResponse(respItem, LOG_ERROR_MESS) { + if (!respItem) { + logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + logError(LOG_ERROR_MESS.noBid); + } + return respItem && respItem.bid && respItem.bid[0]; +} diff --git a/libraries/riseUtils/constants.js b/libraries/riseUtils/constants.js new file mode 100644 index 00000000000..4acb9920291 --- /dev/null +++ b/libraries/riseUtils/constants.js @@ -0,0 +1,20 @@ +import {BANNER, NATIVE, VIDEO} from '../../src/mediaTypes.js'; + +const OW_GVLID = 280 +export const SUPPORTED_AD_TYPES = [BANNER, VIDEO, NATIVE]; +export const ADAPTER_VERSION = '7.0.0'; +export const DEFAULT_TTL = 360; +export const DEFAULT_CURRENCY = 'USD'; +export const BASE_URL = 'https://hb.yellowblue.io/'; +export const BIDDER_CODE = 'rise'; +export const DEFAULT_GVLID = 1043; + +export const ALIASES = [ + { code: 'risexchange', gvlid: DEFAULT_GVLID }, + { code: 'openwebxchange', gvlid: OW_GVLID } +] + +export const MODES = { + PRODUCTION: 'hb-multi', + TEST: 'hb-multi-test' +}; diff --git a/libraries/riseUtils/index.js b/libraries/riseUtils/index.js new file mode 100644 index 00000000000..3046e6dcf4a --- /dev/null +++ b/libraries/riseUtils/index.js @@ -0,0 +1,441 @@ +import { + contains, + deepAccess, + getBidIdParameter, + isArray, + isEmpty, + isFn, + isInteger, + isPlainObject, + logInfo, + triggerPixel +} from '../../src/utils.js'; +import {BANNER, NATIVE, VIDEO} from '../../src/mediaTypes.js'; +import {config} from '../../src/config.js'; +import {ADAPTER_VERSION, DEFAULT_CURRENCY, DEFAULT_TTL, SUPPORTED_AD_TYPES} from './constants.js'; + +export const makeBaseSpec = (baseUrl, modes) => { + return { + version: ADAPTER_VERSION, + supportedMediaTypes: SUPPORTED_AD_TYPES, + buildRequests: function (validBidRequests, bidderRequest) { + const combinedRequestsObject = {}; + + // use data from the first bid, to create the general params for all bids + const generalObject = validBidRequests[0]; + const testMode = generalObject.params.testMode; + const rtbDomain = generalObject.params.rtbDomain || baseUrl; + + combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); + combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); + + return { + method: 'POST', + url: getEndpoint(testMode, rtbDomain, modes), + data: combinedRequestsObject + } + }, + interpretResponse: function ({ body }) { + const bidResponses = []; + + if (body.bids) { + body.bids.forEach(adUnit => { + const bidResponse = buildBidResponse(adUnit); + bidResponses.push(bidResponse); + }); + } + + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && deepAccess(response, 'body.params.userSyncURL')) { + syncs.push({ + type: 'iframe', + url: deepAccess(response, 'body.params.userSyncURL') + }); + } + if (syncOptions.pixelEnabled && isArray(deepAccess(response, 'body.params.userSyncPixels'))) { + const pixels = response.body.params.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }); + syncs.push(...pixels); + } + } + return syncs; + }, + onBidWon: function (bid) { + if (bid == null) { + return; + } + + logInfo('onBidWon:', bid); + if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { + triggerPixel(bid.nurl); + } + } + } +} + +export function getBidRequestMediaTypes(bidRequest) { + const mediaTypes = deepAccess(bidRequest, 'mediaTypes'); + if (isPlainObject(mediaTypes)) { + return Object.keys(mediaTypes); + } + return []; +} + +export function getPos(bidRequest) { + const mediaTypes = getBidRequestMediaTypes(bidRequest); + const firstMediaType = mediaTypes[0]; + if (mediaTypes.length === 1) { + return deepAccess(bidRequest, `mediaTypes.${firstMediaType}.pos`); + } +} + +export function getName(bidRequest) { + const mediaTypes = getBidRequestMediaTypes(bidRequest); + const firstMediaType = mediaTypes[0]; + if (mediaTypes.length === 1) { + return deepAccess(bidRequest, `mediaTypes.${firstMediaType}.name`); + } +} + +export function getFloor(bid) { + if (!isFn(bid.getFloor)) { + return 0; + } + + const mediaTypes = getBidRequestMediaTypes(bid) + const firstMediaType = mediaTypes[0]; + + let floorResult = bid.getFloor({ + currency: 'USD', + mediaType: mediaTypes.length === 1 ? firstMediaType : '*', + size: '*' + }); + return isPlainObject(floorResult) && floorResult.currency === 'USD' && floorResult.floor ? floorResult.floor : 0; +} + +export function getSizesArray(bid) { + let sizesArray = []; + + const mediaTypes = getBidRequestMediaTypes(bid); + const firstMediaType = mediaTypes[0]; + + if (mediaTypes.length === 1 && deepAccess(bid, `mediaTypes.${firstMediaType}.sizes`)) { + sizesArray = bid.mediaTypes[firstMediaType].sizes; + } else if (isArray(bid.sizes) && bid.sizes.length > 0) { + sizesArray = bid.sizes; + } + + return sizesArray; +} + +export function getSupplyChain(schainObject) { + if (isEmpty(schainObject)) { + return ''; + } + let scStr = `${schainObject.ver},${schainObject.complete}`; + schainObject.nodes.forEach((node) => { + scStr += '!'; + scStr += `${getEncodedValIfNotEmpty(node.asi)},`; + scStr += `${getEncodedValIfNotEmpty(node.sid)},`; + scStr += `${getEncodedValIfNotEmpty(node.hp)},`; + scStr += `${getEncodedValIfNotEmpty(node.rid)},`; + scStr += `${getEncodedValIfNotEmpty(node.name)},`; + scStr += `${getEncodedValIfNotEmpty(node.domain)}`; + }); + return scStr; +} + +export function getEncodedValIfNotEmpty(val) { + return (val !== '' && val !== undefined) ? encodeURIComponent(val) : ''; +} + +export function getAllowedSyncMethod(filterSettings, bidderCode) { + const iframeConfigsToCheck = ['all', 'iframe']; + const pixelConfigToCheck = 'image'; + if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { + return 'iframe'; + } + if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { + return 'pixel'; + } +} + +export function isSyncMethodAllowed(syncRule, bidderCode) { + if (!syncRule) { + return false; + } + const isInclude = syncRule.filter === 'include'; + const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && contains(bidders, bidderCode); +} + +export function getEndpoint(testMode, baseUrl, modes) { + const protocol = baseUrl.startsWith('http') ? '' : 'https://'; + const url = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + return testMode + ? `${protocol}${url}${modes.TEST}` + : `${protocol}${url}${modes.PRODUCTION}`; +} + +export function getDeviceType(ua) { + if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(ua.toLowerCase())) { + return '5'; + } + if (/iphone|ipod|android|blackberry|opera|mini|windows\\sce|palm|smartphone|iemobile/i.test(ua.toLowerCase())) { + return '4'; + } + if (/smart[-_\\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\\bdtv\\b|sonydtv|inettvbrowser|\\btv\\b/i.test(ua.toLowerCase())) { + return '3'; + } + return '1'; +} + +export function generateBidsParams(validBidRequests, bidderRequest) { + const bidsArray = []; + + if (validBidRequests.length) { + validBidRequests.forEach(bid => { + bidsArray.push(generateBidParameters(bid, bidderRequest)); + }); + } + + return bidsArray; +} + +export function generateBidParameters(bid, bidderRequest) { + const { params } = bid; + const mediaTypes = getBidRequestMediaTypes(bid); + + if (isNaN(params.floorPrice)) { + params.floorPrice = 0; + } + + const bidObject = { + mediaType: mediaTypes.join(','), + adUnitCode: getBidIdParameter('adUnitCode', bid), + sizes: getSizesArray(bid), + floorPrice: Math.max(getFloor(bid), params.floorPrice), + bidId: getBidIdParameter('bidId', bid), + loop: bid.bidderRequestsCount || 0, + bidderRequestId: getBidIdParameter('bidderRequestId', bid), + transactionId: bid.ortb2Imp?.ext?.tid || '', + coppa: 0, + }; + + const pos = getPos(bid); + if (isInteger(pos)) { + bidObject.pos = pos; + } + + const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); + if (gpid) { + bidObject.gpid = gpid; + } + + const placementId = params.placementId || getName(bid); + if (placementId) { + bidObject.placementId = placementId; + } + + const sua = deepAccess(bid, `ortb2.device.sua`); + if (sua) { + bidObject.sua = sua; + } + + const coppa = deepAccess(bid, `ortb2.regs.coppa`); + if (coppa) { + bidObject.coppa = 1; + } + + if (mediaTypes.includes(VIDEO)) { + const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); + let playbackMethodValue; + + if (isArray(playbackMethod) && isInteger(playbackMethod[0])) { + playbackMethodValue = playbackMethod[0]; + } else if (isInteger(playbackMethod)) { + playbackMethodValue = playbackMethod; + } + + if (playbackMethodValue) { + bidObject.playbackMethod = playbackMethodValue; + } + + const placement = deepAccess(bid, `mediaTypes.video.placement`); + if (placement) { + bidObject.placement = placement; + } + + const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); + if (minDuration) { + bidObject.minDuration = minDuration; + } + + const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); + if (maxDuration) { + bidObject.maxDuration = maxDuration; + } + + const skip = deepAccess(bid, `mediaTypes.video.skip`); + if (skip) { + bidObject.skip = skip; + } + + const linearity = deepAccess(bid, `mediaTypes.video.linearity`); + if (linearity) { + bidObject.linearity = linearity; + } + + const protocols = deepAccess(bid, `mediaTypes.video.protocols`); + if (protocols) { + bidObject.protocols = protocols; + } + + const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); + if (plcmt) { + bidObject.plcmt = plcmt; + } + + const mimes = deepAccess(bid, `mediaTypes.video.mimes`); + if (mimes) { + bidObject.mimes = mimes; + } + + const api = deepAccess(bid, `mediaTypes.video.api`); + if (api) { + bidObject.api = api; + } + } + + if (mediaTypes.includes(NATIVE)) { + const nativeOrtbRequest = deepAccess(bid, `nativeOrtbRequest`); + if (nativeOrtbRequest) { + bidObject.nativeOrtbRequest = nativeOrtbRequest; + } + } + + return bidObject; +} + +export function buildBidResponse(adUnit) { + const bidResponse = { + requestId: adUnit.requestId, + cpm: adUnit.cpm, + currency: adUnit.currency || DEFAULT_CURRENCY, + width: adUnit.width, + height: adUnit.height, + ttl: adUnit.ttl || DEFAULT_TTL, + creativeId: adUnit.creativeId, + netRevenue: adUnit.netRevenue || true, + nurl: adUnit.nurl, + mediaType: adUnit.mediaType, + meta: { + mediaType: adUnit.mediaType + } + }; + + if (adUnit.mediaType === VIDEO) { + bidResponse.vastXml = adUnit.vastXml; + } else if (adUnit.mediaType === BANNER) { + bidResponse.ad = adUnit.ad; + } else if (adUnit.mediaType === NATIVE) { + bidResponse.native = {ortb: adUnit.native}; + } + + if (adUnit.adomain && adUnit.adomain.length) { + bidResponse.meta.advertiserDomains = adUnit.adomain; + } + + return bidResponse; +} + +export function generateGeneralParams(generalObject, bidderRequest, adapterVersion) { + const domain = window.location.hostname; + const { syncEnabled, filterSettings } = config.getConfig('userSync') || {}; + const { bidderCode } = bidderRequest; + const generalBidParams = generalObject.params; + const timeout = bidderRequest.timeout; + const adapVer = adapterVersion || '6.0.0'; + + const generalParams = { + wrapper_type: 'prebidjs', + wrapper_vendor: '$$PREBID_GLOBAL$$', + wrapper_version: '$prebid.version$', + adapter_version: adapVer, + auction_start: bidderRequest.auctionStart, + publisher_id: generalBidParams.org, + publisher_name: domain, + site_domain: domain, + dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0, + device_type: getDeviceType(navigator.userAgent), + ua: navigator.userAgent, + is_wrapper: !!generalBidParams.isWrapper, + session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), + tmax: timeout + }; + + const userIdsParam = getBidIdParameter('userId', generalObject); + if (userIdsParam) { + generalParams.userIds = JSON.stringify(userIdsParam); + } + + const ortb2Metadata = bidderRequest.ortb2 || {}; + if (ortb2Metadata.site) { + generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); + } + if (ortb2Metadata.user) { + generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); + } + + if (ortb2Metadata.device) { + generalParams.device = ortb2Metadata.device; + } + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + generalParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + generalParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (bidderRequest.gppConsent) { + generalParams.gpp = bidderRequest.gppConsent.gppString; + generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + generalParams.gpp = bidderRequest.ortb2.regs.gpp; + generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + + if (generalBidParams.ifa) { + generalParams.ifa = generalBidParams.ifa; + } + + if (generalObject.schain) { + generalParams.schain = getSupplyChain(generalObject.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); + generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); + generalParams.site_domain = deepAccess(bidderRequest, 'refererInfo.domain') || deepAccess(window, 'location.hostname'); + } + + return generalParams; +} diff --git a/libraries/schainSerializer/schainSerializer.js b/libraries/schainSerializer/schainSerializer.js new file mode 100644 index 00000000000..7d9a3c4ddc6 --- /dev/null +++ b/libraries/schainSerializer/schainSerializer.js @@ -0,0 +1,24 @@ +/** + * Serialize the SupplyChain for Non-OpenRTB Requests + * https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/supplychainobject.md + * + * @param {Object} schain The supply chain object. + * @param {string} schain.ver The version of the supply chain. + * @param {number} schain.complete Indicates if the chain is complete (1) or not (0). + * @param {Array} schain.nodes An array of nodes in the supply chain. + * @param {Array} nodesProperties The list of node properties to include in the serialized string. + * Can include: 'asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext'. + * @returns {string|null} The serialized supply chain string or null if the nodes are not present. + */ +export function serializeSupplyChain(schain, nodesProperties) { + if (!schain?.nodes) return null; + + const header = `${schain.ver},${schain.complete}!`; + const nodes = schain.nodes.map( + node => nodesProperties.map( + prop => node[prop] ? encodeURIComponent(node[prop]).replace(/!/g, '%21') : '' + ).join(',') + ).join('!'); + + return header + nodes; +} diff --git a/libraries/sizeUtils/sizeUtils.js b/libraries/sizeUtils/sizeUtils.js index 41cdd71df89..c0fe8510d7e 100644 --- a/libraries/sizeUtils/sizeUtils.js +++ b/libraries/sizeUtils/sizeUtils.js @@ -27,3 +27,28 @@ export function getAdUnitSizes(adUnit) { } return sizes; } + +/** + * Normalize adUnit.mediaTypes.banner.sizes to Array.> + * + * @param {Array. | Array.>} bidSizes - value of adUnit.mediaTypes.banner.sizes. + * @returns {Array.>} - Normalized value. + */ + +export function normalizeBannerSizes(bidSizes) { + let sizes = []; + if (Array.isArray(bidSizes) && bidSizes.length === 2 && !Array.isArray(bidSizes[0])) { + sizes.push({ + width: parseInt(bidSizes[0], 10), + height: parseInt(bidSizes[1], 10), + }); + } else if (Array.isArray(bidSizes) && Array.isArray(bidSizes[0])) { + bidSizes.forEach((size) => { + sizes.push({ + width: parseInt(size[0], 10), + height: parseInt(size[1], 10), + }); + }); + } + return sizes; +} diff --git a/libraries/sizeUtils/tranformSize.js b/libraries/sizeUtils/tranformSize.js new file mode 100644 index 00000000000..1746ac74fb3 --- /dev/null +++ b/libraries/sizeUtils/tranformSize.js @@ -0,0 +1,43 @@ +import * as utils from '../../src/utils.js'; + +/** + * get sizes for rtb + * @param {Array|Object} requestSizes + * @return {Object} + */ +export function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if ( + utils.isArray(requestSizes) && + requestSizes.length === 2 && + !utils.isArray(requestSizes[0]) + ) { + sizeObj.width = parseInt(requestSizes[0], 10); + sizeObj.height = parseInt(requestSizes[1], 10); + sizes.push(sizeObj); + } else if (typeof requestSizes === 'object') { + for (let i = 0; i < requestSizes.length; i++) { + let size = requestSizes[i]; + sizeObj = {}; + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + sizes.push(sizeObj); + } + } + + return sizes; +} + +export const normalAdSize = [ + { w: 300, h: 250 }, + { w: 300, h: 600 }, + { w: 728, h: 90 }, + { w: 970, h: 250 }, + { w: 320, h: 50 }, + { w: 160, h: 600 }, + { w: 320, h: 180 }, + { w: 320, h: 100 }, + { w: 336, h: 280 }, +]; diff --git a/libraries/smartyadsUtils/getAdUrlByRegion.js b/libraries/smartyadsUtils/getAdUrlByRegion.js new file mode 100644 index 00000000000..cad9055f671 --- /dev/null +++ b/libraries/smartyadsUtils/getAdUrlByRegion.js @@ -0,0 +1,32 @@ +const adUrls = { + US_EAST: 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + EU: 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + SGP: 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' +}; + +export function getAdUrlByRegion(bid) { + let adUrl; + + if (bid.params.region && adUrls[bid.params.region]) { + adUrl = adUrls[bid.params.region]; + } else { + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; + + switch (region) { + case 'Europe': + adUrl = adUrls['EU']; + break; + case 'Asia': + adUrl = adUrls['SGP']; + break; + default: adUrl = adUrls['US_EAST']; + } + } catch (err) { + adUrl = adUrls['US_EAST']; + } + } + + return adUrl; +}; diff --git a/libraries/targetVideoUtils/bidderUtils.js b/libraries/targetVideoUtils/bidderUtils.js new file mode 100644 index 00000000000..cf106566944 --- /dev/null +++ b/libraries/targetVideoUtils/bidderUtils.js @@ -0,0 +1,213 @@ +import {SYNC_URL} from './constants.js'; +import {VIDEO} from '../../src/mediaTypes.js'; +import {getRefererInfo} from '../../src/refererDetection.js'; +import {createTrackPixelHtml, deepAccess, getBidRequest, formatQS} from '../../src/utils.js'; + +export function getSizes(request) { + let sizes = request.sizes; + if (!sizes && request.mediaTypes && request.mediaTypes.banner && request.mediaTypes.banner.sizes) { + sizes = request.mediaTypes.banner.sizes; + } + if (Array.isArray(sizes) && !Array.isArray(sizes[0])) { + sizes = [sizes[0], sizes[1]]; + } + if (!Array.isArray(sizes) || !Array.isArray(sizes[0])) { + sizes = [[0, 0]]; + } + + return sizes; +} + +export function formatRequest({payload, url, bidderRequest, bidId}) { + const request = { + method: 'POST', + data: JSON.stringify(payload), + url, + options: { + withCredentials: true, + } + } + + if (bidderRequest) { + request.bidderRequest = bidderRequest; + } + + if (bidId) { + request.bidId = bidId; + } + + return request; +} + +export function createVideoTag(bid) { + const tag = {}; + tag.id = parseInt(bid.params.placementId, 10); + tag.gpid = 'targetVideo'; + tag.sizes = getSizes(bid); + tag.primary_size = tag.sizes[0]; + tag.ad_types = [VIDEO]; + tag.uuid = bid.bidId; + tag.allow_smaller_sizes = false; + tag.use_pmt_rule = false; + tag.prebid = true; + tag.disable_psa = true; + tag.hb_source = 1; + tag.require_asset_url = true; + tag.video = { + playback_method: 2, + skippable: true + }; + + return tag; +} + +export function bannerBid(serverBid, rtbBid, bidderRequest, margin) { + const bidRequest = getBidRequest(serverBid.uuid, [bidderRequest]); + const sizes = getSizes(bidRequest); + const bid = { + requestId: serverBid.uuid, + cpm: rtbBid.cpm / margin, + creativeId: rtbBid.creative_id, + dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + width: sizes[0][0], + height: sizes[0][1], + ttl: 300, + adUnitCode: bidRequest.adUnitCode, + appnexus: { + buyerMemberId: rtbBid.buyer_member_id, + dealPriority: rtbBid.deal_priority, + dealCode: rtbBid.deal_code + } + }; + + if (rtbBid.rtb.video) { + Object.assign(bid, { + vastImpUrl: rtbBid.notify_url, + ad: getBannerHtml(rtbBid.notify_url + '&redir=' + encodeURIComponent(rtbBid.rtb.video.asset_url)), + ttl: 3600 + }); + } + + return bid; +} + +export function videoBid(serverBid, requestId, currency, params, ttl) { + const {ad, adUrl, vastUrl, vastXml} = getAd(serverBid); + + const bid = { + requestId, + params, + currency, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.adid || serverBid.crid, + netRevenue: false, + ttl, + meta: { + advertiserDomains: serverBid.adomain || [] + } + }; + + if (vastUrl || vastXml) { + bid.mediaType = VIDEO; + if (vastUrl) bid.vastUrl = vastUrl; + if (vastXml) bid.vastXml = vastXml; + } else { + bid.ad = ad; + bid.adUrl = adUrl; + }; + + return bid; +} + +export function getRtbBid(tag) { + return tag && tag.ads && tag.ads.length && tag.ads.find(ad => ad.rtb); +} + +export function getBannerHtml(vastUrl) { + return ` + + + + + + + +
+ + + + `; +} + +export function getAd(bid) { + let ad, adUrl, vastXml, vastUrl; + + switch (deepAccess(bid, 'ext.prebid.type')) { + case VIDEO: + if (bid.adm.substr(0, 4) === 'http') { + vastUrl = bid.adm; + } else { + vastXml = bid.adm; + }; + break; + default: + if (bid.adm && bid.nurl) { + ad = bid.adm; + ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } else if (bid.adm) { + ad = bid.adm; + } else if (bid.nurl) { + adUrl = bid.nurl; + }; + } + + return {ad, adUrl, vastXml, vastUrl}; +} + +export function getSyncResponse(syncOptions, gdprConsent, uspConsent, gppConsent, endpoint) { + const params = { + endpoint + }; + + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + params.gdpr = (gdprConsent.gdprApplies ? 1 : 0); + params.gdpr_consent = encodeURIComponent(gdprConsent.consentString || ''); + } + + // CCPA + if (uspConsent && typeof uspConsent === 'string') { + params.us_privacy = encodeURIComponent(uspConsent); + } + + // GPP Consent + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + params.gpp = encodeURIComponent(gppConsent.gppString); + params.gpp_sid = encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + + const queryParams = Object.keys(params).length > 0 ? formatQS(params) : ''; + let response = []; + if (syncOptions.iframeEnabled) { + response = [{ + type: 'iframe', + url: SYNC_URL + 'load-cookie.html?' + queryParams + }]; + } + + return response; +} + +export function getSiteObj() { + const refInfo = (getRefererInfo && getRefererInfo()) || {}; + + return { + page: refInfo.page, + ref: refInfo.ref, + domain: refInfo.domain + } +} diff --git a/libraries/targetVideoUtils/constants.js b/libraries/targetVideoUtils/constants.js new file mode 100644 index 00000000000..33076b71e7d --- /dev/null +++ b/libraries/targetVideoUtils/constants.js @@ -0,0 +1,25 @@ +const SOURCE = 'pbjs'; +const GVLID = 786; +const MARGIN = 1.35; +const BIDDER_CODE = 'targetVideo'; + +const TIME_TO_LIVE = 300; +const BANNER_ENDPOINT_URL = 'https://ib.adnxs.com/ut/v3/prebid'; +const VIDEO_ENDPOINT_URL = 'https://pbs.prebrid.tv/openrtb2/auction'; +const SYNC_URL = 'https://bppb.link/static/'; +const VIDEO_PARAMS = [ + 'api', 'linearity', 'maxduration', 'mimes', 'minduration', + 'plcmt', 'playbackmethod', 'protocols', 'startdelay', 'placement' +]; + +export { + SOURCE, + GVLID, + MARGIN, + BIDDER_CODE, + SYNC_URL, + TIME_TO_LIVE, + BANNER_ENDPOINT_URL, + VIDEO_ENDPOINT_URL, + VIDEO_PARAMS +} diff --git a/libraries/teqblazeUtils/bidderUtils.js b/libraries/teqblazeUtils/bidderUtils.js new file mode 100644 index 00000000000..84010adbbd6 --- /dev/null +++ b/libraries/teqblazeUtils/bidderUtils.js @@ -0,0 +1,265 @@ +import { BANNER, NATIVE, VIDEO } from '../../src/mediaTypes.js'; +import { deepAccess } from '../../src/utils.js'; +import { config } from '../../src/config.js'; + +const PROTOCOL_PATTERN = /^[a-z0-9.+-]+:/i; + +const isBidResponseValid = (bid) => { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +}; + +const getBidFloor = (bid) => { + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + + return bidFloor?.floor; + } catch (err) { + return 0; + } +}; + +const createBasePlacement = (bid) => { + const { bidId, mediaTypes, transactionId, userIdAsEids } = bid; + const schain = bid.schain || {}; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.plcmt = mediaTypes[VIDEO].plcmt; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + if (transactionId) { + placement.ext = placement.ext || {}; + placement.ext.tid = transactionId; + } + + if (userIdAsEids && userIdAsEids.length) { + placement.eids = userIdAsEids; + } + + return placement; +}; + +const defaultPlacementType = (bid, bidderRequest, placement) => { + const { placementId, endpointId } = bid.params; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } +}; + +const checkIfObjectHasKey = (keys, obj, mode = 'some') => { + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const val = obj[key]; + + if (mode === 'some' && val) return true; + if (mode === 'every' && !val) return false; + } + + return mode === 'every'; +} + +export const isBidRequestValid = (keys = ['placementId', 'endpointId'], mode) => (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && checkIfObjectHasKey(keys, params, mode)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + + return valid; +}; + +/** + * @param {{ adUrl, validBidRequests, bidderRequest, placementProcessingFunction }} config + * @returns {function} + */ +export const buildRequestsBase = (config) => { + const { adUrl, validBidRequests, bidderRequest } = config; + const placementProcessingFunction = config.placementProcessingFunction || buildPlacementProcessingFunction(); + const device = deepAccess(bidderRequest, 'ortb2.device'); + const page = deepAccess(bidderRequest, 'refererInfo.page', ''); + + const proto = PROTOCOL_PATTERN.exec(page); + const protocol = proto?.[0]; + + const placements = []; + const request = { + deviceWidth: device?.w || 0, + deviceHeight: device?.h || 0, + language: device?.language?.split('-')[0] || '', + secure: protocol === 'https:' ? 1 : 0, + host: deepAccess(bidderRequest, 'refererInfo.domain', ''), + page, + placements, + coppa: deepAccess(bidderRequest, 'ortb2.regs.coppa') ? 1 : 0, + tmax: bidderRequest.timeout, + bcat: deepAccess(bidderRequest, 'ortb2.bcat'), + badv: deepAccess(bidderRequest, 'ortb2.badv'), + bapp: deepAccess(bidderRequest, 'ortb2.bapp'), + battr: deepAccess(bidderRequest, 'ortb2.battr') + }; + + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + + if (bidderRequest.gdprConsent) { + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; + } + + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + + if (bidderRequest?.ortb2?.device) { + request.device = bidderRequest.ortb2.device; + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(placementProcessingFunction(bid, bidderRequest)); + } + + return { + method: 'POST', + url: adUrl, + data: request + }; +}; + +export const buildRequests = (adUrl) => (validBidRequests = [], bidderRequest = {}) => { + const placementProcessingFunction = buildPlacementProcessingFunction(); + + return buildRequestsBase({ adUrl, validBidRequests, bidderRequest, placementProcessingFunction }); +}; + +export function interpretResponseBuilder({addtlBidValidation = (bid) => true} = {}) { + return function (serverResponse) { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem) && addtlBidValidation(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + + return response; + } +} + +export const interpretResponse = interpretResponseBuilder(); + +export const getUserSyncs = (syncUrl) => (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + const type = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let url = syncUrl + `/${type}?pbjs=1`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + url += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + url += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + + if (uspConsent && uspConsent.consentString) { + url += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + url += '&gpp=' + gppConsent.gppString; + url += '&gpp_sid=' + gppConsent.applicableSections.join(','); + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + url += `&coppa=${coppa}`; + + return [{ + type, + url + }]; +}; + +/** + * + * @param {{ addPlacementType?: function, addCustomFieldsToPlacement?: function }} [config] + * @returns {function(object, object): object} + */ +export const buildPlacementProcessingFunction = (config) => (bid, bidderRequest) => { + const addPlacementType = config?.addPlacementType ?? defaultPlacementType; + + const placement = createBasePlacement(bid); + + addPlacementType(bid, bidderRequest, placement); + + if (config?.addCustomFieldsToPlacement) { + config.addCustomFieldsToPlacement(bid, bidderRequest, placement); + } + + return placement; +}; diff --git a/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js b/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js new file mode 100644 index 00000000000..5d5330d8127 --- /dev/null +++ b/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js @@ -0,0 +1,37 @@ +/** + * Calculates the Time to First Byte (TTFB) for the given window object. + * + * This function attempts to use the Navigation Timing Level 2 API first, and falls back to + * the Navigation Timing Level 1 API if the former is not available. + * + * @param {Window} win - The window object from which to retrieve performance timing information. + * @returns {string} The TTFB in milliseconds as a string, or an empty string if the TTFB cannot be determined. + */ +export function getTimeToFirstByte(win) { + const performance = win.performance || win.webkitPerformance || win.msPerformance || win.mozPerformance; + + const ttfbWithTimingV2 = performance && + typeof performance.getEntriesByType === 'function' && + Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' && + performance.getEntriesByType('navigation')[0] && + performance.getEntriesByType('navigation')[0].responseStart && + performance.getEntriesByType('navigation')[0].requestStart && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && + Math.round( + performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart + ); + + if (ttfbWithTimingV2) { + return ttfbWithTimingV2.toString(); + } + + const ttfbWithTimingV1 = performance && + performance.timing.responseStart && + performance.timing.requestStart && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && + performance.timing.responseStart - performance.timing.requestStart; + + return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; +} diff --git a/libraries/timeoutQueue/timeoutQueue.js b/libraries/timeoutQueue/timeoutQueue.js new file mode 100644 index 00000000000..5046eed150b --- /dev/null +++ b/libraries/timeoutQueue/timeoutQueue.js @@ -0,0 +1,22 @@ +export function timeoutQueue() { + const queue = []; + return { + submit(timeout, onResume, onTimeout) { + const item = [ + onResume, + setTimeout(() => { + queue.splice(queue.indexOf(item), 1); + onTimeout(); + }, timeout) + ]; + queue.push(item); + }, + resume() { + while (queue.length) { + const [onResume, timerId] = queue.shift(); + clearTimeout(timerId); + onResume(); + } + } + } +} diff --git a/libraries/userAgentUtils/index.js b/libraries/userAgentUtils/index.js new file mode 100644 index 00000000000..7300bbd519a --- /dev/null +++ b/libraries/userAgentUtils/index.js @@ -0,0 +1,58 @@ +import { deviceTypes, browserTypes, osTypes } from './userAgentTypes.enums.js'; + +/** + * Get the approximate device type enum from the user agent + * @returns {number} + */ +export const getDeviceType = () => { + if ( + /ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test( + navigator.userAgent.toLowerCase() + ) + ) return deviceTypes.TABLET; + if ( + /iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test( + navigator.userAgent.toLowerCase() + ) + ) return deviceTypes.MOBILE; + return deviceTypes.DESKTOP; +}; + +/** + * Get the approximate browser type enum from the user agent (or vendor + * if available) + * @returns {number} + */ +export const getBrowser = () => { + if (/Edg/.test(navigator.userAgent)) return browserTypes.EDGE; + else if ( + /Chrome/.test(navigator.userAgent) && + /Google Inc/.test(navigator.vendor) + ) return browserTypes.CHROME; + else if (navigator.userAgent.match('CriOS')) return browserTypes.CHROME; + else if (/Firefox/.test(navigator.userAgent)) return browserTypes.FIREFOX; + else if ( + /Safari/.test(navigator.userAgent) && + /Apple Computer/.test(navigator.vendor) + ) return browserTypes.SAFARI; + else if ( + /Trident/.test(navigator.userAgent) || + /MSIE/.test(navigator.userAgent) + ) return browserTypes.INTERNET_EXPLORER; + else return browserTypes.OTHER; +}; + +/** + * Get the approximate OS enum from the user agent (or app version, + * if available) + * @returns {number} + */ +export const getOS = () => { + if (navigator.userAgent.indexOf('Android') != -1) return osTypes.ANDROID; + if (navigator.userAgent.indexOf('like Mac') != -1) return osTypes.IOS; + if (navigator.userAgent.indexOf('Win') != -1) return osTypes.WINDOWS; + if (navigator.userAgent.indexOf('Mac') != -1) return osTypes.MAC; + if (navigator.userAgent.indexOf('Linux') != -1) return osTypes.LINUX; + if (navigator.appVersion.indexOf('X11') != -1) return osTypes.UNIX; + return osTypes.OTHER; +}; diff --git a/libraries/userAgentUtils/userAgentTypes.enums.js b/libraries/userAgentUtils/userAgentTypes.enums.js new file mode 100644 index 00000000000..8a0255e88bf --- /dev/null +++ b/libraries/userAgentUtils/userAgentTypes.enums.js @@ -0,0 +1,22 @@ +export const deviceTypes = Object.freeze({ + DESKTOP: 0, + MOBILE: 1, + TABLET: 2, +}) +export const browserTypes = Object.freeze({ + CHROME: 0, + FIREFOX: 1, + SAFARI: 2, + EDGE: 3, + INTERNET_EXPLORER: 4, + OTHER: 5 +}) +export const osTypes = Object.freeze({ + WINDOWS: 0, + MAC: 1, + LINUX: 2, + UNIX: 3, + IOS: 4, + ANDROID: 5, + OTHER: 6 +}) diff --git a/libraries/userSyncUtils/userSyncUtils.js b/libraries/userSyncUtils/userSyncUtils.js new file mode 100644 index 00000000000..db8c77d307b --- /dev/null +++ b/libraries/userSyncUtils/userSyncUtils.js @@ -0,0 +1,24 @@ +export function getUserSyncParams(gdprConsent, uspConsent, gppConsent) { + let params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); + } + + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = gdprConsent.consentString; + } + } + + if (uspConsent) { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + if (gppConsent?.gppString) { + params['gpp'] = gppConsent.gppString; + params['gpp_sid'] = gppConsent.applicableSections?.toString(); + } + + return params; +} diff --git a/libraries/vastTrackers/vastTrackers.js b/libraries/vastTrackers/vastTrackers.js index b4ae98aba57..b74bdd1aac5 100644 --- a/libraries/vastTrackers/vastTrackers.js +++ b/libraries/vastTrackers/vastTrackers.js @@ -1,25 +1,50 @@ -import {addBidResponse} from '../../src/auction.js'; +import {callPrebidCache} from '../../src/auction.js'; import {VIDEO} from '../../src/mediaTypes.js'; import {logError} from '../../src/utils.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_REPORT_ANALYTICS} from '../../src/activities/activities.js'; import {activityParams} from '../../src/activities/activityParams.js'; +import {auctionManager} from '../../src/auctionManager.js'; const vastTrackers = []; +let enabled = false; -addBidResponse.before(function (next, adUnitcode, bidResponse, reject) { - if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) { - const vastTrackers = getVastTrackers(bidResponse); - if (vastTrackers) { - bidResponse.vastXml = insertVastTrackers(vastTrackers, bidResponse.vastXml); - const impTrackers = vastTrackers.get('impressions'); - if (impTrackers) { - bidResponse.vastImpUrl = [].concat(impTrackers).concat(bidResponse.vastImpUrl).filter(t => t); +export function reset() { + vastTrackers.length = 0; +} + +export function enable() { + if (!enabled) { + callPrebidCache.before(addTrackersToResponse); + enabled = true; + } +} + +export function disable() { + if (enabled) { + callPrebidCache.getHooks({hook: addTrackersToResponse}).remove(); + enabled = false; + } +} + +export function cacheVideoBidHook({index = auctionManager.index} = {}) { + return function addTrackersToResponse(next, auctionInstance, bidResponse, afterBidAdded, videoMediaType) { + if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) { + const vastTrackers = getVastTrackers(bidResponse, {index}); + if (vastTrackers) { + bidResponse.vastXml = insertVastTrackers(vastTrackers, bidResponse.vastXml); + const impTrackers = vastTrackers.get('impressions'); + if (impTrackers) { + bidResponse.vastImpUrl = [].concat([...impTrackers]).concat(bidResponse.vastImpUrl).filter(t => t); + } } } + next(auctionInstance, bidResponse, afterBidAdded, videoMediaType); } - next(adUnitcode, bidResponse, reject); -}); +} + +const addTrackersToResponse = cacheVideoBidHook(); +enable(); export function registerVastTrackers(moduleType, moduleName, trackerFn) { if (typeof trackerFn === 'function') { @@ -49,7 +74,7 @@ export function insertVastTrackers(trackers, vastXml) { return vastXml; } -export function getVastTrackers(bid) { +export function getVastTrackers(bid, {index = auctionManager.index}) { let trackers = []; vastTrackers.filter( ({ @@ -58,7 +83,9 @@ export function getVastTrackers(bid) { trackerFn }) => isActivityAllowed(ACTIVITY_REPORT_ANALYTICS, activityParams(moduleType, moduleName)) ).forEach(({trackerFn}) => { - let trackersToAdd = trackerFn(bid); + const auction = index.getAuction(bid).getProperties(); + const bidRequest = index.getBidRequest(bid); + let trackersToAdd = trackerFn(bid, {auction, bidRequest}); trackersToAdd.forEach(trackerToAdd => { if (isValidVastTracker(trackers, trackerToAdd)) { trackers.push(trackerToAdd); diff --git a/libraries/vidazooUtils/bidderUtils.js b/libraries/vidazooUtils/bidderUtils.js new file mode 100644 index 00000000000..83317f932b9 --- /dev/null +++ b/libraries/vidazooUtils/bidderUtils.js @@ -0,0 +1,502 @@ +import { + _each, + deepAccess, + formatQS, + isArray, + isFn, + parseSizesInput, + parseUrl, + triggerPixel, + uniques +} 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'; +import {bidderSettings} from '../../src/bidderSettings.js'; +import {config} from '../../src/config.js'; +import {BANNER, VIDEO} from '../../src/mediaTypes.js'; + +export function createSessionId() { + return 'wsid_' + parseInt(Date.now() * Math.random()); +} + +export function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function extractCID(params) { + return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; +} + +export function extractPID(params) { + return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; +} + +export function extractSubDomain(params) { + return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; +} + +export function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export function setStorageItem(storage, key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function getStorageItem(storage, key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key, null)); + } catch (e) { + } + + return null; +} + +export function getCacheOpt(storage, useKey) { + let data = storage.getDataFromLocalStorage(useKey, null); + if (!data) { + data = String(Date.now()); + storage.setDataInLocalStorage(useKey, data, null); + } + + return data; +} + +export function getUniqueDealId(storage, key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storage, storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storage, storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getNextDealId(storage, key, expiry = DEAL_ID_EXPIRY) { + try { + const data = getStorageItem(storage, key); + let currentValue = 0; + let timestamp; + + if (data && data.value && Date.now() - data.created < expiry) { + currentValue = data.value; + timestamp = data.created; + } + + const nextValue = currentValue + 1; + setStorageItem(storage, key, nextValue, timestamp); + return nextValue; + } catch (e) { + return 0; + } +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function onBidWon(bid) { + if (!bid.nurl) { + return; + } + const wonBid = { + adId: bid.adId, + creativeId: bid.creativeId, + auctionId: bid.auctionId, + transactionId: bid.transactionId, + adUnitCode: bid.adUnitCode, + cpm: bid.cpm, + currency: bid.currency, + originalCpm: bid.originalCpm, + originalCurrency: bid.originalCurrency, + netRevenue: bid.netRevenue, + mediaType: bid.mediaType, + timeToRespond: bid.timeToRespond, + status: bid.status, + }; + const qs = formatQS(wonBid); + const url = bid.nurl + (bid.nurl.indexOf('?') === -1 ? '?' : '&') + qs; + triggerPixel(url); +} + +/** + * Create the spec function for getting user syncs + * + * The options object accepts the following fields: + * + * - iframeSyncUrl + * - imageSyncUrl + * + * @param options + */ +export function createUserSyncGetter(options = { + iframeSyncUrl: '', + imageSyncUrl: '' +}) { + return function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { + const syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; + const coppa = config.getConfig('coppa') ? 1 : 0; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}&coppa=${encodeURIComponent((coppa))}`; + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + + if (iframeEnabled && options.iframeSyncUrl) { + syncs.push({ + type: 'iframe', + url: `${options.iframeSyncUrl}/${params}` + }); + } + if (pixelEnabled && options.imageSyncUrl) { + syncs.push({ + type: 'image', + url: `${options.imageSyncUrl}/${params}` + }); + } + return syncs; + } +} + +export function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +export function getVidazooSessionId(storage) { + return getStorageItem(storage, SESSION_ID_KEY) || ''; +} + +export function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout, storage, bidderVersion, bidderCode, getUniqueRequestData) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + const {ext} = params; + let {bidFloor} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueRequestData = isFn(getUniqueRequestData) ? getUniqueRequestData(hashUrl, bid) : {}; + const uniqueDealId = getUniqueDealId(storage, hashUrl); + const pId = extractPID(params); + const isStorageAllowed = bidderSettings.get(bidderCode, 'storageAllowed'); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', ''); + const cat = deepAccess(bidderRequest, 'ortb2.site.cat', []); + const pagecat = deepAccess(bidderRequest, 'ortb2.site.pagecat', []); + const contentData = deepAccess(bidderRequest, 'ortb2.site.content.data', []); + const userData = deepAccess(bidderRequest, 'ortb2.user.data', []); + const contentLang = deepAccess(bidderRequest, 'ortb2.site.content.language') || document.documentElement.lang; + const coppa = deepAccess(bidderRequest, 'ortb2.regs.coppa', 0); + const device = deepAccess(bidderRequest, 'ortb2.device', {}); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo?.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: bidderVersion, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + isStorageAllowed: isStorageAllowed, + gpid: gpid, + cat: cat, + contentData, + contentLang, + coppa, + userData: userData, + pagecat: pagecat, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout, + device, + ...uniqueRequestData + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + if (bidderRequest.paapi?.enabled) { + const fledge = deepAccess(bidderRequest, 'ortb2Imp.ext.ae'); + if (fledge) { + data.fledge = fledge; + } + } + + const api = deepAccess(mediaTypes, 'video.api', []); + if (api.includes(7)) { + const sourceExt = deepAccess(bidderRequest, 'ortb2.source.ext'); + if (sourceExt?.omidpv) { + data.omidpv = sourceExt.omidpv; + } + if (sourceExt?.omidpn) { + data.omidpn = sourceExt.omidpn; + } + } + + const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); + if (dsa) { + data.dsa = dsa; + } + + _each(ext, (value, key) => { + data['ext.' + key] = value; + }); + + return data; +} + +export function createInterpretResponseFn(bidderCode, allowSingleRequest) { + return function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + + const singleRequestMode = allowSingleRequest && config.getConfig(`${bidderCode}.singleRequest`); + const reqBidId = deepAccess(request, 'data.bidId'); + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach((result, i) => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + bidId, + nurl, + advertiserDomains, + metaData, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: (singleRequestMode && bidId) ? bidId : reqBidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (nurl) { + response.nurl = nurl; + } + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + + return output; + } catch (e) { + return []; + } + } +} + +export function createBuildRequestsFn(createRequestDomain, createUniqueRequestData, storage, bidderCode, bidderVersion, allowSingleRequest) { + function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const {params} = bid; + const cId = extractCID(params); + const subDomain = extractSubDomain(params); + const data = buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout, storage, bidderVersion, bidderCode, createUniqueRequestData); + const dto = { + method: 'POST', url: `${createRequestDomain(subDomain)}/prebid/multi/${cId}`, data: data + }; + return dto; + } + + function buildSingleRequest(bidRequests, bidderRequest, topWindowUrl, bidderTimeout) { + const {params} = bidRequests[0]; + const cId = extractCID(params); + const subDomain = extractSubDomain(params); + const data = bidRequests.map(bid => { + const sizes = parseSizesInput(bid.sizes); + return buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout, storage, bidderVersion, bidderCode, createUniqueRequestData) + }); + const chunkSize = Math.min(20, config.getConfig(`${bidderCode}.chunkSize`) || 10); + + const chunkedData = chunk(data, chunkSize); + return chunkedData.map(chunk => { + return { + method: 'POST', + url: `${createRequestDomain(subDomain)}/prebid/multi/${cId}`, + data: { + bids: chunk + } + }; + }); + } + + return function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = bidderRequest.timeout || config.getConfig('bidderTimeout'); + + const singleRequestMode = allowSingleRequest && config.getConfig(`${bidderCode}.singleRequest`); + + const requests = []; + + if (singleRequestMode) { + // banner bids are sent as a single request + const bannerBidRequests = validBidRequests.filter(bid => isArray(bid.mediaTypes) ? bid.mediaTypes.includes(BANNER) : bid.mediaTypes[BANNER] !== undefined); + if (bannerBidRequests.length > 0) { + const singleRequests = buildSingleRequest(bannerBidRequests, bidderRequest, topWindowUrl, bidderTimeout); + requests.push(...singleRequests); + } + + // video bids are sent as a single request for each bid + + const videoBidRequests = validBidRequests.filter(bid => bid.mediaTypes[VIDEO] !== undefined); + videoBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + } else { + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + } + return requests; + } +} diff --git a/libraries/vidazooUtils/constants.js b/libraries/vidazooUtils/constants.js new file mode 100644 index 00000000000..b1056c15899 --- /dev/null +++ b/libraries/vidazooUtils/constants.js @@ -0,0 +1,7 @@ +export const CURRENCY = 'USD'; +export const TTL_SECONDS = 60 * 5; +export const DEAL_ID_EXPIRY = 1000 * 60 * 15; +export const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 60; +export const SESSION_ID_KEY = 'vidSid'; +export const OPT_CACHE_KEY = 'vdzwopt'; +export const OPT_TIME_KEY = 'vdzHum'; diff --git a/libraries/video/constants/ortb.js b/libraries/video/constants/ortb.js index d67c8a5f393..86e7b499774 100644 --- a/libraries/video/constants/ortb.js +++ b/libraries/video/constants/ortb.js @@ -1,6 +1,6 @@ /** * @typedef {Object} OrtbParams - * @property {OrtbVideoParamst} video + * @property {OrtbVideoParams} video * @property {OrtbContentParams} content */ @@ -13,7 +13,8 @@ * @property {number} w - Width of the video player in device independent pixels (DIPS). * @property {number} h - Height of the video player in device independent pixels (DIPS). * @property {number|undefined} startdelay - Indicates the offset of the ad placement. - * @property {number|undefined} placement - Placement type for the impression. + * @property {number|undefined} placement - Legacy Placement type for the impression. + * @property {number|undefined} plcmt - Modern placement type for the impression. * @property {number|undefined} linearity - Indicates if the impression must be linear, nonlinear, etc. If omitted, assume all are allowed. * @property {number} skip - Indicates if the player can allow the video to be skipped, where 0 is no, 1 is yes. * @property {number|undefined} skipmin - Only ad creatives with a duration greater than this value can be skippable; only applicable if the ad is skippable. @@ -97,6 +98,18 @@ export const PLACEMENT = { INTERSTITIAL_SLIDER_FLOATING: 5 }; +/** + * ADCOM - https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/develop/AdCOM%20v1.0%20FINAL.md#list_plcmtsubtypesvideo + * @enum OrtbVideoParams.plcmt + */ +export const PLCMT = { + INSTREAM: 1, + ACCOMPANYING_CONTENT: 2, + INTERSTITIAL: 3, + OUTSTREAM: 4, + NO_CONTENT: 4 +}; + /** * ORTB 2.5 section 5.4 - Ad Position * @enum OrtbVideoParams.pos diff --git a/libraries/video/constants/vendorCodes.js b/libraries/video/constants/vendorCodes.js index 370c151b997..4e3550ce431 100644 --- a/libraries/video/constants/vendorCodes.js +++ b/libraries/video/constants/vendorCodes.js @@ -1,6 +1,7 @@ // Video Vendors export const JWPLAYER_VENDOR = 1; export const VIDEO_JS_VENDOR = 2; +export const AD_PLAYER_PRO_VENDOR = 3; // Ad Server Vendors export const GAM_VENDOR = 'gam'; diff --git a/libraries/video/shared/parentModule.js b/libraries/video/shared/parentModule.js index b040f39bcb8..36e5cdb54e9 100644 --- a/libraries/video/shared/parentModule.js +++ b/libraries/video/shared/parentModule.js @@ -3,7 +3,7 @@ * @summary abstraction for any module to store and reference its submodules * @param {SubmoduleBuilder} submoduleBuilder_ * @returns {ParentModule} - * @constructor + * @class */ export function ParentModule(submoduleBuilder_) { const submoduleBuilder = submoduleBuilder_; @@ -53,7 +53,7 @@ export function ParentModule(submoduleBuilder_) { * @param {vendorSubmoduleDirectory} submoduleDirectory_ * @param {Object|null|undefined} sharedUtils_ * @returns {SubmoduleBuilder} - * @constructor + * @class */ export function SubmoduleBuilder(submoduleDirectory_, sharedUtils_) { const submoduleDirectory = submoduleDirectory_; diff --git a/libraries/video/shared/vastXmlEditor.js b/libraries/video/shared/vastXmlEditor.js index b586e5b4c29..d5798bfc2ac 100644 --- a/libraries/video/shared/vastXmlEditor.js +++ b/libraries/video/shared/vastXmlEditor.js @@ -1,6 +1,6 @@ -import { getErrorNode, getImpressionNode, buildVastWrapper } from './vastXmlBuilder.js'; -export const XML_MIME_TYPE = 'application/xml'; +import XMLUtil from '../../xmlUtils/xmlUtils.js'; +import { getErrorNode, getImpressionNode, buildVastWrapper } from './vastXmlBuilder.js'; export function VastXmlEditor(xmlUtil_) { const xmlUtil = xmlUtil_; @@ -76,40 +76,6 @@ export function VastXmlEditor(xmlUtil_) { } } -function XMLUtil() { - let parser; - let serializer; - - function getParser() { - if (!parser) { - // DOMParser instantiation is costly; instantiate only once throughout Prebid lifecycle. - parser = new DOMParser(); - } - return parser; - } - - function getSerializer() { - if (!serializer) { - // XMLSerializer instantiation is costly; instantiate only once throughout Prebid lifecycle. - serializer = new XMLSerializer(); - } - return serializer; - } - - function parse(xmlString) { - return getParser().parseFromString(xmlString, XML_MIME_TYPE); - } - - function serialize(xmlDoc) { - return getSerializer().serializeToString(xmlDoc); - } - - return { - parse, - serialize - }; -} - export function vastXmlEditorFactory() { return VastXmlEditor(XMLUtil()); } diff --git a/libraries/viewport/viewport.js b/libraries/viewport/viewport.js new file mode 100644 index 00000000000..6ac1f839a7b --- /dev/null +++ b/libraries/viewport/viewport.js @@ -0,0 +1,23 @@ +import {getWinDimensions, getWindowTop} from '../../src/utils.js'; + +export function getViewportCoordinates() { + try { + const win = getWindowTop(); + let { scrollY: top, scrollX: left } = win; + const { height: innerHeight, width: innerWidth } = getViewportSize(); + return { top, right: left + innerWidth, bottom: top + innerHeight, left }; + } catch (e) { + return {}; + } +} + +export function getViewportSize() { + const windowDimensions = getWinDimensions(); + try { + const innerHeight = windowDimensions.innerHeight || windowDimensions.document.documentElement.clientHeight || windowDimensions.document.body.clientHeight || 0; + const innerWidth = windowDimensions.innerWidth || windowDimensions.document.documentElement.clientWidth || windowDimensions.document.body.clientWidth || 0; + return { width: innerWidth, height: innerHeight }; + } catch (e) { + return {}; + } +} diff --git a/libraries/weakStore/weakStore.js b/libraries/weakStore/weakStore.js new file mode 100644 index 00000000000..09606354dae --- /dev/null +++ b/libraries/weakStore/weakStore.js @@ -0,0 +1,15 @@ +import {auctionManager} from '../../src/auctionManager.js'; + +export function weakStore(get) { + const store = new WeakMap(); + return function (id, init = {}) { + const obj = get(id); + if (obj == null) return; + if (!store.has(obj)) { + store.set(obj, init); + } + return store.get(obj); + }; +} + +export const auctionStore = () => weakStore((auctionId) => auctionManager.index.getAuction({auctionId})); diff --git a/libraries/xeUtils/bidderUtils.js b/libraries/xeUtils/bidderUtils.js new file mode 100644 index 00000000000..7e742a66c5d --- /dev/null +++ b/libraries/xeUtils/bidderUtils.js @@ -0,0 +1,155 @@ +import {deepAccess, getBidIdParameter, isFn, logError, isArray, parseSizesInput, isPlainObject} from '../../src/utils.js'; +import {getAdUnitSizes} from '../sizeUtils/sizeUtils.js'; +import {findIndex} from '../../src/polyfill.js'; + +export function getBidFloor(bid, currency = 'USD') { + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency, + mediaType: '*', + size: '*' + }); + + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === currency) { + return floor.floor; + } + + return null; +} + +export function isBidRequestValid(bid) { + if (bid && typeof bid.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('env', bid.params) || !getBidIdParameter('pid', bid.params)) { + logError('Env or pid is not present in bidder params'); + return false; + } + + if (deepAccess(bid, 'mediaTypes.video') && !isArray(deepAccess(bid, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +export function buildRequests(validBidRequests, bidderRequest, endpoint) { + const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; + const requests = validBidRequests.map(req => { + const request = {}; + request.tmax = bidderRequest.timeout || 0; + request.bidId = req.bidId; + request.banner = deepAccess(req, 'mediaTypes.banner'); + request.auctionId = req.ortb2?.source?.tid; + request.transactionId = req.ortb2Imp?.ext?.tid; + request.sizes = parseSizesInput(getAdUnitSizes(req)); + request.schain = req.schain; + request.location = { + page: refererInfo.page, + location: refererInfo.location, + domain: refererInfo.domain, + whost: window.location.host, + ref: refererInfo.ref, + isAmp: refererInfo.isAmp + }; + request.device = { + ua: navigator.userAgent, + lang: navigator.language + }; + request.env = { + env: req.params.env, + pid: req.params.pid + }; + request.ortb2 = req.ortb2; + request.ortb2Imp = req.ortb2Imp; + request.tz = new Date().getTimezoneOffset(); + request.ext = req.params.ext; + request.bc = req.bidRequestsCount; + request.floor = getBidFloor(req); + + if (req.userIdAsEids && req.userIdAsEids.length !== 0) { + request.userEids = req.userIdAsEids; + } else { + request.userEids = []; + } + + request.gdprConsent = gdprConsent; + + if (uspConsent) { + request.usPrivacy = uspConsent; + } else { + request.usPrivacy = ''; + } + const video = deepAccess(req, 'mediaTypes.video'); + if (video) { + request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); + request.video = video; + } + + return request; + }); + + return { + method: 'POST', + url: endpoint + '/bid', + data: JSON.stringify(requests), + withCredentials: true, + bidderRequest, + options: { + contentType: 'application/json', + } + }; +} + +export function interpretResponse(serverResponse, {bidderRequest}) { + const response = []; + if (!isArray(deepAccess(serverResponse, 'body.data'))) { + return response; + } + + serverResponse.body.data.forEach(serverBid => { + const bidIndex = findIndex(bidderRequest.bids, (bidRequest) => { + return bidRequest.bidId === serverBid.requestId; + }); + + if (bidIndex !== -1) { + const bid = { + requestId: serverBid.requestId, + dealId: bidderRequest.dealId || null, + ...serverBid + }; + response.push(bid); + } + }); + + return response; +} + +export function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); + + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; + + pixels.forEach(pixel => { + const [type, url] = pixel; + const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; + if (type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push(sync) + } else if (type === 'image' && syncOptions.pixelEnabled) { + syncs.push(sync) + } + }); + } + + return syncs; +} diff --git a/libraries/xmlUtils/xmlUtils.js b/libraries/xmlUtils/xmlUtils.js new file mode 100644 index 00000000000..b29ff2d0e2a --- /dev/null +++ b/libraries/xmlUtils/xmlUtils.js @@ -0,0 +1,35 @@ +const XML_MIME_TYPE = 'application/xml'; + +export default function XMLUtil() { + let parser; + let serializer; + + function getParser() { + if (!parser) { + // DOMParser instantiation is costly; instantiate only once throughout Prebid lifecycle. + parser = new DOMParser(); + } + return parser; + } + + function getSerializer() { + if (!serializer) { + // XMLSerializer instantiation is costly; instantiate only once throughout Prebid lifecycle. + serializer = new XMLSerializer(); + } + return serializer; + } + + function parse(xmlString) { + return getParser().parseFromString(xmlString, XML_MIME_TYPE); + } + + function serialize(xmlDoc) { + return getSerializer().serializeToString(xmlDoc); + } + + return { + parse, + serialize + }; +} diff --git a/modules.json b/modules.json index 02663d55020..1db3e5d4321 100644 --- a/modules.json +++ b/modules.json @@ -1,5 +1,5 @@ [ - "consentManagement", + "consentManagementTcf", "rubiconBidAdapter", "appnexusBidAdapter", "openxBidAdapter", @@ -9,7 +9,6 @@ "criteoBidAdapter", "rtbhouseBidAdapter", "categoryTranslation", - "dfpAdServerVideo", "smartadserverBidAdapter", "prebidServerBidAdapter", "ringieraxelspringerAnalyticsAdapter", @@ -23,12 +22,16 @@ "teadsBidAdapter", "invibesBidAdapter", "carodaBidAdapter", - "fledgeForGpt", "adqueryIdSystem", "topicsFpdModule", "priceFloors", "visxBidAdapter", "currency", "justIdSystem", - "dasBidAdapter" + "dasBidAdapter", + "paapi", + "paapiForGpt", + "dfpAdServerVideo", + "criteoIdSystem", + "connectadBidAdapter" ] diff --git a/modules/.submodules.json b/modules/.submodules.json index 8409ea9918d..0165c9adc64 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -6,7 +6,6 @@ "adtelligentIdSystem", "adqueryIdSystem", "amxIdSystem", - "britepoolIdSystem", "connectIdSystem", "czechAdIdSystem", "criteoIdSystem", @@ -26,40 +25,48 @@ "justIdSystem", "kinessoIdSystem", "liveIntentIdSystem", + "lmpIdSystem", "lockrAIMIdSystem", "lotamePanoramaIdSystem", "merkleIdSystem", + "mobkoiIdSystem", "mwOpenLinkIdSystem", "mygaruIdSystem", "naveggIdSystem", "netIdSystem", "novatiqIdSystem", "oneKeyIdSystem", + "openPairIdSystem", "operaadsIdSystem", - "parrableIdSystem", + "permutiveIdentityManagerIdSystem", + "pubmaticIdSystem", "pubProvidedIdSystem", "publinkIdSystem", "quantcastIdSystem", + "rewardedInterestIdSystem", "sharedIdSystem", "tapadIdSystem", "teadsIdSystem", "tncIdSystem", - "utiqSystem", + "utiqIdSystem", "utiqMtpIdSystem", "uid2IdSystem", "euidIdSystem", "unifiedIdSystem", "verizonMediaIdSystem", - "zeotapIdPlusIdSystem" + "zeotapIdPlusIdSystem", + "yandexIdSystem" ], "adpod": [ "freeWheelAdserverVideo", - "dfpAdServerVideo" + "dfpAdpod" ], "rtdModule": [ "1plusXRtdProvider", + "51DegreesRtdProvider", "a1MediaRtdProvider", "aaxBlockmeterRtdProvider", + "adagioRtdProvider", "adlooxRtdProvider", "adnuntiusRtdProvider", "airgridRtdProvider", @@ -79,6 +86,7 @@ "greenbidsRtdProvider", "growthCodeRtdProvider", "hadronRtdProvider", + "humansecurityRtdProvider", "iasRtdProvider", "idWardRtdProvider", "imRtdProvider", @@ -86,18 +94,24 @@ "jwplayerRtdProvider", "medianetRtdProvider", "mgidRtdProvider", + "mobianRtdProvider", "neuwoRtdProvider", "oneKeyRtdProvider", + "optableRtdProvider", "optimeraRtdProvider", "oxxionRtdProvider", "permutiveRtdProvider", + "pubmaticRtdProvider", "pubxaiRtdProvider", "qortexRtdProvider", "reconciliationRtdProvider", "relevadRtdProvider", + "semantiqRtdProvider", "sirdataRtdProvider", + "symitriDapRtdProvider", "timeoutRtdProvider", - "weboramaRtdProvider" + "weboramaRtdProvider", + "wurflRtdProvider" ], "fpdModule": [ "validationFpdModule", @@ -105,10 +119,12 @@ ], "videoModule": [ "jwplayerVideoProvider", - "videojsVideoProvider" + "videojsVideoProvider", + "adplayerproVideoProvider" ], "paapi": [ - "fledgeForGpt" + "paapiForGpt", + "topLevelPaapi" ] } } diff --git a/modules/1plusXRtdProvider.js b/modules/1plusXRtdProvider.js index c5c4594ff22..88891b14a78 100644 --- a/modules/1plusXRtdProvider.js +++ b/modules/1plusXRtdProvider.js @@ -1,5 +1,7 @@ import { submodule } from '../src/hook.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import { ajax } from '../src/ajax.js'; +import { getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE } from '../src/storageManager.js'; import { logMessage, logError, deepAccess, deepSetValue, mergeDeep, @@ -13,6 +15,9 @@ const ORTB2_NAME = '1plusX.com' const PAPI_VERSION = 'v1.0'; const LOG_PREFIX = '[1plusX RTD Module]: '; const OPE_FPID = 'ope_fpid' + +export const fpidStorage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME }); + export const segtaxes = { // cf. https://github.com/InteractiveAdvertisingBureau/openrtb/pull/108 AUDIENCE: 526, @@ -53,7 +58,19 @@ export const extractConfig = (moduleConfig, reqBidsConfigObj) => { throw new Error('No bidRequestConfig bidder found in moduleConfig bidders'); } - return { customerId, timeout, bidders }; + const fpidStorageType = deepAccess(moduleConfig, 'params.fpidStorageType', + STORAGE_TYPE_LOCALSTORAGE) + + if ( + fpidStorageType !== STORAGE_TYPE_COOKIES && + fpidStorageType !== STORAGE_TYPE_LOCALSTORAGE + ) { + throw new Error( + `fpidStorageType must be ${STORAGE_TYPE_LOCALSTORAGE} or ${STORAGE_TYPE_COOKIES}` + ) + } + + return { customerId, timeout, bidders, fpidStorageType }; } /** @@ -81,16 +98,20 @@ export const extractConsent = ({ gdpr }) => { } /** - * Extracts the OPE first party id field from local storage + * Extracts the OPE first party id field + * @param {string} fpidStorageType indicates where fpid should be read from * @returns fpid string if found, else null */ -export const extractFpid = () => { +export const extractFpid = (fpidStorageType) => { try { - const fpid = window.localStorage.getItem(OPE_FPID); - if (fpid) { - return fpid; + switch (fpidStorageType) { + case STORAGE_TYPE_COOKIES: return fpidStorage.getCookie(OPE_FPID) + case STORAGE_TYPE_LOCALSTORAGE: return fpidStorage.getDataFromLocalStorage(OPE_FPID) + default: { + logError(`Got unknown fpidStorageType ${fpidStorageType}. Aborting...`) + return null + } } - return null; } catch (error) { return null; } @@ -231,10 +252,10 @@ const init = (config, userConsent) => { const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { try { // Get the required config - const { customerId, bidders } = extractConfig(moduleConfig, reqBidsConfigObj); + const { customerId, bidders, fpidStorageType } = extractConfig(moduleConfig, reqBidsConfigObj); const { ortb2Fragments: { bidder: biddersOrtb2 } } = reqBidsConfigObj; // Get PAPI URL - const papiUrl = getPapiUrl(customerId, extractConsent(userConsent) || {}, extractFpid()) + const papiUrl = getPapiUrl(customerId, extractConsent(userConsent) || {}, extractFpid(fpidStorageType)) // Call PAPI getTargetingDataFromPapi(papiUrl) .then((papiResponse) => { diff --git a/modules/1plusXRtdProvider.md b/modules/1plusXRtdProvider.md index 6a6211b37cc..c1e5a6f48a4 100644 --- a/modules/1plusXRtdProvider.md +++ b/modules/1plusXRtdProvider.md @@ -45,15 +45,16 @@ pbjs.setConfig({ ### Parameters -| Name | Type | Description | Default | -| :---------------- | :------------ | :--------------------------------------------------------------- |:----------------- | -| name | String | Real time data module name | Always '1plusX' | -| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | -| params | Object | | | -| params.customerId | String | Your 1plusX customer id | | -| params.bidders | Array | List of bidders for which you would like data to be set | | -| params.timeout | Integer | timeout (ms) | 1000ms | - +| Name | Type | Description | Default | +| :------------------------ | :------------ | :--------------------------------------------------------------- |:----------------- | +| name | String | Real time data module name | Always '1plusX' | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | | +| params.customerId | String | Your 1plusX customer id | | +| params.bidders | Array | List of bidders for which you would like data to be set | | +| params.timeout | Integer | timeout (ms) | 1000ms | +| params.fpidStorageType | String | Specifies where the 1plusX fpid should be read from. Either | html5 | +| | | "html5" (local storage) or "cookie" (first party cookie) | | ## Testing To view an example of how the 1plusX RTD module works : diff --git a/modules/33acrossAnalyticsAdapter.js b/modules/33acrossAnalyticsAdapter.js index 890c963fa71..2f5640d3c69 100644 --- a/modules/33acrossAnalyticsAdapter.js +++ b/modules/33acrossAnalyticsAdapter.js @@ -5,6 +5,7 @@ import adapterManager, { coppaDataHandler, gdprDataHandler, gppDataHandler, uspD * @typedef {typeof import('../src/constants.js').EVENTS} EVENTS */ import { EVENTS } from '../src/constants.js'; +import { sendBeacon } from '../src/ajax.js'; /** @typedef {'pending'|'available'|'targetingSet'|'rendered'|'timeout'|'rejected'|'noBid'|'error'} BidStatus */ /** @@ -154,12 +155,12 @@ class TransactionManager { } // gulp-eslint is using eslint 6, a version that doesn't support private method syntax - // eslint-disable-next-line no-dupe-class-members + #clearSendTimeout() { return clearTimeout(this.#sendTimeoutId); } - // eslint-disable-next-line no-dupe-class-members + #restartSendTimeout() { this.#clearSendTimeout(); @@ -629,7 +630,7 @@ function setCachedBidStatus(auctionId, bidId, status) { * @param {string} endpoint URL */ function sendReport(report, endpoint) { - if (navigator.sendBeacon(endpoint, JSON.stringify(report))) { + if (sendBeacon(endpoint, JSON.stringify(report))) { log.info(`Analytics report sent to ${endpoint}`, report); return; diff --git a/modules/33acrossAnalyticsAdapter.md b/modules/33acrossAnalyticsAdapter.md index c56059e5526..d093434dc97 100644 --- a/modules/33acrossAnalyticsAdapter.md +++ b/modules/33acrossAnalyticsAdapter.md @@ -49,7 +49,7 @@ by default when Prebid is downloaded. If you are compiling from source, this might look something like: ```sh -gulp bundle --modules=gptPreAuction,consentManagement,consentManagementGpp,consentManagementUsp,enrichmentFpdModule,gdprEnforcement,33acrossBidAdapter,33acrossIdSystem,33acrossAnalyticsAdapter +gulp bundle --modules=gptPreAuction,consentManagementTcf,consentManagementGpp,consentManagementUsp,tcfControl,33acrossBidAdapter,33acrossIdSystem,33acrossAnalyticsAdapter ``` Enable the 33Across Analytics Adapter in Prebid.js using the analytics provider `33across` diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 0e9beb22013..9feca97d425 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -2,6 +2,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import { deepAccess, + getWinDimensions, getWindowSelf, getWindowTop, isArray, @@ -14,8 +15,9 @@ import { } from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import { percentInView } from '../libraries/percentInView/percentInView.js'; -// **************************** UTILS *************************** // +// **************************** UTILS ************************** // const BIDDER_CODE = '33across'; const BIDDER_ALIASES = ['33across_mgni']; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; @@ -36,6 +38,7 @@ const VIDEO_ORTB_PARAMS = [ 'minduration', 'maxduration', 'placement', + 'plcmt', 'protocols', 'startdelay', 'skip', @@ -72,9 +75,7 @@ function isBidRequestValid(bid) { } function _validateBasic(bid) { - const invalidBidderName = bid.bidder !== BIDDER_CODE && !BIDDER_ALIASES.includes(bid.bidder); - - if (invalidBidderName || !bid.params) { + if (!bid.params) { return false; } @@ -140,10 +141,10 @@ function _validateVideo(bid) { } // If placement if defined, it must be a number - if ( - typeof videoParams.placement !== 'undefined' && - typeof videoParams.placement !== 'number' - ) { + if ([ videoParams.placement, videoParams.plcmt ].some(value => ( + typeof value !== 'undefined' && + typeof value !== 'number' + ))) { return false; } @@ -463,7 +464,7 @@ function _buildBannerORTB(bidRequest) { } // BUILD REQUESTS: VIDEO -// eslint-disable-next-line no-unused-vars + function _buildVideoORTB(bidRequest) { const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {}); const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); @@ -490,12 +491,24 @@ function _buildVideoORTB(bidRequest) { // Placement Inference Rules: // - If no placement is defined then default to 2 (In Banner) - // - If product is instream (for instream context) then override placement to 1 - video.placement = video.placement || 2; + // - If the old deprecated field is defined, use its value for the recent placement field + + const calculatePlacementValue = () => { + const IN_BANNER_PLACEMENT_VALUE = 2; + + if (video.placement) { + logWarn('[33Across Adapter] The ORTB field `placement` is deprecated, please use `plcmt` instead'); + + return video.placement; + } + + return IN_BANNER_PLACEMENT_VALUE; + } + + video.plcmt ??= calculatePlacementValue(); if (product === PRODUCT.INSTREAM) { video.startdelay = video.startdelay || 0; - video.placement = 1; } // bidfloors @@ -524,7 +537,7 @@ function _getBidFloors(bidRequest, size, mediaType) { size: [ size.w, size.h ] }); - if (!isNaN(bidFloors.floor) && (bidFloors.currency === CURRENCY)) { + if (!isNaN(bidFloors?.floor) && (bidFloors?.currency === CURRENCY)) { return bidFloors.floor; } } @@ -536,7 +549,7 @@ function _isViewabilityMeasurable(element) { function _getViewability(element, topWin, { w, h } = {}) { return topWin.document.visibilityState === 'visible' - ? _getPercentInView(element, topWin, { w, h }) + ? percentInView(element, { w, h }) : 0; } @@ -571,76 +584,6 @@ function _getMinSize(sizes) { return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); } -function _getBoundingBox(element, { w, h } = {}) { - let { width, height, left, top, right, bottom } = element.getBoundingClientRect(); - - if ((width === 0 || height === 0) && w && h) { - width = w; - height = h; - right = left + w; - bottom = top + h; - } - - return { width, height, left, top, right, bottom }; -} - -function _getIntersectionOfRects(rects) { - const bbox = { - left: rects[0].left, - right: rects[0].right, - top: rects[0].top, - bottom: rects[0].bottom - }; - - for (let i = 1; i < rects.length; ++i) { - bbox.left = Math.max(bbox.left, rects[i].left); - bbox.right = Math.min(bbox.right, rects[i].right); - - if (bbox.left >= bbox.right) { - return null; - } - - bbox.top = Math.max(bbox.top, rects[i].top); - bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); - - if (bbox.top >= bbox.bottom) { - return null; - } - } - - bbox.width = bbox.right - bbox.left; - bbox.height = bbox.bottom - bbox.top; - - return bbox; -} - -function _getPercentInView(element, topWin, { w, h } = {}) { - const elementBoundingBox = _getBoundingBox(element, { w, h }); - - // Obtain the intersection of the element and the viewport - const elementInViewBoundingBox = _getIntersectionOfRects([ { - left: 0, - top: 0, - right: topWin.innerWidth, - bottom: topWin.innerHeight - }, elementBoundingBox ]); - - let elementInViewArea, - elementTotalArea; - - if (elementInViewBoundingBox !== null) { - // Some or all of the element is in view - elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; - elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; - - return ((elementInViewArea / elementTotalArea) * 100); - } - - // No overlap between element and the viewport; therefore, the element - // lies completely out of view - return 0; -} - /** * Viewability contribution to request.. */ @@ -807,11 +750,7 @@ function getViewportDimensions() { } function getScreenDimensions() { - const { - innerWidth: windowWidth, - innerHeight: windowHeight, - screen - } = getWindowSelf(); + const { innerWidth: windowWidth, innerHeight: windowHeight, screen } = getWinDimensions(); const [biggerDimension, smallerDimension] = [ Math.max(screen.width, screen.height), diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index e0f7435a1ec..823025826f5 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -11,6 +11,7 @@ import { submodule } from '../src/hook.js'; import { uspDataHandler, coppaDataHandler, gppDataHandler } from '../src/adapterManager.js'; import { getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE } from '../src/storageManager.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { domainOverrideToRootDomain } from '../libraries/domainOverrideToRootDomain/index.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -25,9 +26,17 @@ const CALLER_NAME = 'pbjs'; const GVLID = 58; const STORAGE_FPID_KEY = '33acrossIdFp'; +const STORAGE_TPID_KEY = '33acrossIdTp'; +const STORAGE_HEM_KEY = '33acrossIdHm' +const DEFAULT_1PID_SUPPORT = true; +const DEFAULT_TPID_SUPPORT = true; export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); +export const domainUtils = { + domainOverride: domainOverrideToRootDomain(storage, MODULE_NAME) +}; + function calculateResponseObj(response) { if (!response.succeeded) { if (response.error == 'Cookied User') { @@ -46,11 +55,12 @@ function calculateResponseObj(response) { return { envelope: response.data.envelope, - fp: response.data.fp + fp: response.data.fp, + tp: response.data.tp }; } -function calculateQueryStringParams(pid, gdprConsentData, storageConfig) { +function calculateQueryStringParams({ pid, pubProvidedHem }, gdprConsentData, enabledStorageTypes) { const uspString = uspDataHandler.getConsentData(); const coppaValue = coppaDataHandler.getCoppa(); const gppConsent = gppDataHandler.getConsentData(); @@ -78,11 +88,21 @@ function calculateQueryStringParams(pid, gdprConsentData, storageConfig) { params.gdpr_consent = gdprConsentData.consentString; } - const fp = getStoredValue(STORAGE_FPID_KEY, storageConfig); + const fp = getStoredValue(STORAGE_FPID_KEY, enabledStorageTypes); if (fp) { params.fp = encodeURIComponent(fp); } + const tp = getStoredValue(STORAGE_TPID_KEY, enabledStorageTypes); + if (tp) { + params.tp = encodeURIComponent(tp); + } + + const hem = pubProvidedHem || getStoredValue(STORAGE_HEM_KEY, enabledStorageTypes); + if (hem) { + params.sha256 = encodeURIComponent(hem); + } + return params; } @@ -90,39 +110,90 @@ function deleteFromStorage(key) { if (storage.cookiesAreEnabled()) { const expiredDate = new Date(0).toUTCString(); - storage.setCookie(key, '', expiredDate, 'Lax'); + storage.setCookie(key, '', expiredDate, 'Lax', domainUtils.domainOverride()); } storage.removeDataFromLocalStorage(key); } -function storeValue(key, value, storageConfig = {}) { - if (storageConfig.type === STORAGE_TYPE_COOKIES && storage.cookiesAreEnabled()) { - const expirationInMs = 60 * 60 * 24 * 1000 * storageConfig.expires; - const expirationTime = new Date(Date.now() + expirationInMs); +function storeValue(key, value, { enabledStorageTypes, expires }) { + enabledStorageTypes.forEach(storageType => { + if (storageType === STORAGE_TYPE_COOKIES) { + const expirationInMs = 60 * 60 * 24 * 1000 * expires; + const expirationTime = new Date(Date.now() + expirationInMs); + + storage.setCookie(key, value, expirationTime.toUTCString(), 'Lax', domainUtils.domainOverride()); + } else if (storageType === STORAGE_TYPE_LOCALSTORAGE) { + storage.setDataInLocalStorage(key, value); + } + }); +} + +function getStoredValue(key, enabledStorageTypes) { + let storedValue; + + enabledStorageTypes.find(storageType => { + if (storageType === STORAGE_TYPE_COOKIES) { + storedValue = storage.getCookie(key); + } else if (storageType === STORAGE_TYPE_LOCALSTORAGE) { + storedValue = storage.getDataFromLocalStorage(key); + } - storage.setCookie(key, value, expirationTime.toUTCString(), 'Lax'); - } else if (storageConfig.type === STORAGE_TYPE_LOCALSTORAGE) { - storage.setDataInLocalStorage(key, value); + return !!storedValue; + }); + + return storedValue; +} + +function filterEnabledSupplementalIds({ tp, fp, hem }, { storeFpid, storeTpid, envelopeAvailable }) { + const ids = []; + + if (storeFpid) { + ids.push( + /** + * [ + * , + * < ID value to store or remove >, + * < clear flag: indicates if existing storage item should be removed or not based on certain condition> + * ] + */ + [STORAGE_FPID_KEY, fp, !fp], + [STORAGE_HEM_KEY, hem, !envelopeAvailable] // Clear hashed email if envelope is not available + ); } + + if (storeTpid) { + ids.push([STORAGE_TPID_KEY, tp, !tp]); + } + + return ids; } -function getStoredValue(key, storageConfig = {}) { - if (storageConfig.type === STORAGE_TYPE_COOKIES && storage.cookiesAreEnabled()) { - return storage.getCookie(key); - } else if (storageConfig.type === STORAGE_TYPE_LOCALSTORAGE) { - return storage.getDataFromLocalStorage(key); +function updateSupplementalIdStorage(supplementalId, storageConfig) { + const [ key, id, clear ] = supplementalId; + + if (clear) { + deleteFromStorage(key); + + return; + } + + if (id) { + storeValue(key, id, storageConfig); } } -function handleFpId(fpId, storageConfig = {}) { - fpId - ? storeValue(STORAGE_FPID_KEY, fpId, storageConfig) - : deleteFromStorage(STORAGE_FPID_KEY); +function handleSupplementalIds(ids, { enabledStorageTypes, expires, ...options }) { + filterEnabledSupplementalIds(ids, options).forEach((supplementalId) => { + updateSupplementalIdStorage(supplementalId, { + enabledStorageTypes, + expires + }) + }); } /** @type {Submodule} */ -export const thirthyThreeAcrossIdSubmodule = { +export const thirtyThreeAcrossIdSubmodule = { /** * used to link submodule with config * @type {string} @@ -151,7 +222,7 @@ export const thirthyThreeAcrossIdSubmodule = { * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId({ params = { }, storage: storageConfig }, gdprConsentData) { + getId({ params = { }, enabledStorageTypes = [], storage: storageConfig = {} }, {gdpr: gdprConsentData} = {}) { if (typeof params.pid !== 'string') { logError(`${MODULE_NAME}: Submodule requires a partner ID to be defined`); @@ -164,7 +235,13 @@ export const thirthyThreeAcrossIdSubmodule = { return; } - const { pid, storeFpid, apiUrl = API_URL } = params; + const { + storeFpid = DEFAULT_1PID_SUPPORT, + storeTpid = DEFAULT_TPID_SUPPORT, apiUrl = API_URL, + pid, + hem + } = params; + const pubProvidedHem = hem || window._33across?.hem?.sha256; return { callback(cb) { @@ -179,12 +256,22 @@ export const thirthyThreeAcrossIdSubmodule = { } if (!responseObj.envelope) { - deleteFromStorage(MODULE_NAME); + ['', '_last', '_exp', '_cst'].forEach(suffix => { + deleteFromStorage(`${MODULE_NAME}${suffix}`); + }); } - if (storeFpid) { - handleFpId(responseObj.fp, storageConfig); - } + handleSupplementalIds({ + fp: responseObj.fp, + tp: responseObj.tp, + hem: pubProvidedHem + }, { + storeFpid, + storeTpid, + envelopeAvailable: !!responseObj.envelope, + enabledStorageTypes, + expires: storageConfig.expires + }); cb(responseObj.envelope); }, @@ -193,10 +280,14 @@ export const thirthyThreeAcrossIdSubmodule = { cb(); } - }, calculateQueryStringParams(pid, gdprConsentData, storageConfig), { method: 'GET', withCredentials: true }); + }, calculateQueryStringParams({ pid, pubProvidedHem }, gdprConsentData, enabledStorageTypes), { + method: 'GET', + withCredentials: true + }); } }; }, + domainOverride: domainUtils.domainOverride, eids: { '33acrossId': { source: '33across.com', @@ -208,4 +299,4 @@ export const thirthyThreeAcrossIdSubmodule = { } }; -submodule('userId', thirthyThreeAcrossIdSubmodule); +submodule('userId', thirtyThreeAcrossIdSubmodule); diff --git a/modules/33acrossIdSystem.md b/modules/33acrossIdSystem.md index 8b73a43069d..7dabb08eebd 100644 --- a/modules/33acrossIdSystem.md +++ b/modules/33acrossIdSystem.md @@ -14,7 +14,7 @@ pbjs.setConfig({ name: "33acrossId", storage: { name: "33acrossId", - type: "html5", + type: "cookie&html5", expires: 30, refreshInSeconds: 8*3600 }, @@ -40,7 +40,7 @@ The following settings are available for the `storage` property in the `userSync | Param name | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | name | Required | String| Name of the cookie or HTML5 local storage where the user ID will be stored | `"33acrossId"` | -| type | Required | String | `"html5"` (preferred) or `"cookie"` | `"html5"` | +| type | Required | String | `"cookie&html5"` (preferred) or `"cookie"` or `"html5"` | `"cookie&html5"` | | expires | Strongly Recommended | Number | How long (in days) the user ID information will be stored. 33Across recommends `30`. | `30` | | refreshInSeconds | Strongly Recommended | Number | The interval (in seconds) for refreshing the user ID. 33Across recommends no more than 8 hours between refreshes. | `8*3600` | @@ -51,4 +51,11 @@ The following settings are available in the `params` property in `userSync.userI | Param name | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | pid | Required | String | Partner ID provided by 33Across | `"0010b00002GYU4eBAH"` | -| storeFpid | Optional | Boolean | Indicates whether a supplemental first-party ID may be stored to improve addressability | `false` (default) or `true` | +| hem | Optional | String | Hashed email address in sha256 format | `"ba4235544d6c91865fbf70fa1bdb70f2d375ded1b2b946b21c675dcbe9968cdc"` | +| storeFpid | Optional | Boolean | Indicates whether a supplemental first-party ID may be stored to improve addressability, this feature is enabled by default | `true` (default) or `false` | +| storeTpid | Optional | Boolean | Indicates whether a supplemental third-party ID may be stored to improve addressability, this feature is enabled by default | `true` (default) or `false` | + +### HEM Collection + +33Across ID System supports user's hashed emails (HEMs). HEMs could be collected from 3 different sources in following +priority order: `hem` configuration parameter, global `_33across.hem.sha256` field or from storage (cookie or local storage). diff --git a/modules/51DegreesRtdProvider.js b/modules/51DegreesRtdProvider.js new file mode 100644 index 00000000000..8251fd76e28 --- /dev/null +++ b/modules/51DegreesRtdProvider.js @@ -0,0 +1,335 @@ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {submodule} from '../src/hook.js'; +import { + deepAccess, + deepSetValue, + formatQS, + mergeDeep, + prefixLog, +} from '../src/utils.js'; + +const MODULE_NAME = '51Degrees'; +export const LOG_PREFIX = `[${MODULE_NAME} RTD Submodule]:`; +const {logMessage, logWarn, logError} = prefixLog(LOG_PREFIX); + +// ORTB device types +const ORTB_DEVICE_TYPE = { + UNKNOWN: 0, + MOBILE_TABLET: 1, + PERSONAL_COMPUTER: 2, + CONNECTED_TV: 3, + PHONE: 4, + TABLET: 5, + CONNECTED_DEVICE: 6, + SET_TOP_BOX: 7, + OOH_DEVICE: 8 +}; + +// Map of 51Degrees device types to ORTB device types. See +// https://51degrees.com/developers/property-dictionary?item=Device%7CDevice +// for available properties and values. +const ORTB_DEVICE_TYPE_MAP = new Map([ + ['Phone', ORTB_DEVICE_TYPE.PHONE], + ['Console', ORTB_DEVICE_TYPE.SET_TOP_BOX], + ['Desktop', ORTB_DEVICE_TYPE.PERSONAL_COMPUTER], + ['EReader', ORTB_DEVICE_TYPE.PERSONAL_COMPUTER], + ['IoT', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['Kiosk', ORTB_DEVICE_TYPE.OOH_DEVICE], + ['MediaHub', ORTB_DEVICE_TYPE.SET_TOP_BOX], + ['Mobile', ORTB_DEVICE_TYPE.MOBILE_TABLET], + ['Router', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['SmallScreen', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['SmartPhone', ORTB_DEVICE_TYPE.MOBILE_TABLET], + ['SmartSpeaker', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['SmartWatch', ORTB_DEVICE_TYPE.CONNECTED_DEVICE], + ['Tablet', ORTB_DEVICE_TYPE.TABLET], + ['Tv', ORTB_DEVICE_TYPE.CONNECTED_TV], + ['Vehicle Display', ORTB_DEVICE_TYPE.PERSONAL_COMPUTER] +]); + +/** + * Extracts the parameters for 51Degrees RTD module from the config object passed at instantiation + * @param {Object} moduleConfig Configuration object of the 51Degrees RTD module + * @param {Object} reqBidsConfigObj Configuration object for the bidders, currently not used + */ +export const extractConfig = (moduleConfig, reqBidsConfigObj) => { + // Resource key + let resourceKey = deepAccess(moduleConfig, 'params.resourceKey'); + // On-premise JS URL + let onPremiseJSUrl = deepAccess(moduleConfig, 'params.onPremiseJSUrl'); + + // Trim the values + if (typeof resourceKey === 'string') { + resourceKey = resourceKey.trim(); + } + if (typeof onPremiseJSUrl === 'string') { + onPremiseJSUrl = onPremiseJSUrl.trim(); + } + + // If this module is configured via a 3rd party wrapper, both form inputs + // might be mandatory. To handle this, 0 can be used as a value to skip + // the parameter. + if (typeof resourceKey === 'string' && resourceKey.trim() === '0') { + resourceKey = undefined; + } + if (typeof onPremiseJSUrl === 'string' && onPremiseJSUrl.trim() === '0') { + onPremiseJSUrl = undefined; + } + + // Verify that onPremiseJSUrl is a valid URL: either a full URL, relative + // path (/path/to/file.js), or a protocol-relative URL (//example.com/path/to/file.js) + if (typeof onPremiseJSUrl === 'string' && onPremiseJSUrl.length && !( + onPremiseJSUrl.startsWith('https://') || + onPremiseJSUrl.startsWith('http://') || + onPremiseJSUrl.startsWith('/')) + ) { + throw new Error(LOG_PREFIX + ' Invalid URL format for onPremiseJSUrl in moduleConfig'); + } + + // Verify that one of the parameters is provided, + // but not both at the same time + if (!resourceKey && !onPremiseJSUrl) { + throw new Error(LOG_PREFIX + ' Missing parameter resourceKey or onPremiseJSUrl in moduleConfig'); + } else if (resourceKey && onPremiseJSUrl) { + throw new Error(LOG_PREFIX + ' Only one of resourceKey or onPremiseJSUrl should be provided in moduleConfig'); + } + + // Verify that the resource key is not the one provided as an example + if (resourceKey === '') { + throw new Error(LOG_PREFIX + ' replace in configuration with a resource key obtained from https://configure.51degrees.com/HNZ75HT1'); + } + + return {resourceKey, onPremiseJSUrl}; +} + +/** + * Gets 51Degrees JS URL + * @param {Object} pathData API path data + * @param {string} [pathData.resourceKey] Resource key + * @param {string} [pathData.onPremiseJSUrl] On-premise JS URL + * @param {Object} [pathData.hev] High entropy values + * @param {Window} [win] Window object (mainly for testing) + * @returns {string} 51Degrees JS URL + */ +export const get51DegreesJSURL = (pathData, win) => { + const _window = win || window; + const baseURL = pathData.onPremiseJSUrl || `https://cloud.51degrees.com/api/v4/${pathData.resourceKey}.js`; + + const queryPrefix = baseURL.includes('?') ? '&' : '?'; + const qs = {}; + + deepSetNotEmptyValue( + qs, + '51D_GetHighEntropyValues', + pathData.hev && Object.keys(pathData.hev).length ? btoa(JSON.stringify(pathData.hev)) : null, + ); + deepSetNotEmptyValue(qs, '51D_ScreenPixelsHeight', _window?.screen?.height); + deepSetNotEmptyValue(qs, '51D_ScreenPixelsWidth', _window?.screen?.width); + deepSetNotEmptyValue(qs, '51D_PixelRatio', _window?.devicePixelRatio); + + const _qs = formatQS(qs); + const _qsString = _qs ? `${queryPrefix}${_qs}` : ''; + + return `${baseURL}${_qsString}`; +} + +/** + * Retrieves high entropy values from `navigator.userAgentData` if available + * + * @param {Array} hints - An array of hints indicating which high entropy values to retrieve + * @returns {Promise>} A promise that resolves to an object containing high entropy values if supported, or `undefined` if not + */ +export const getHighEntropyValues = async (hints) => { + return navigator?.userAgentData?.getHighEntropyValues?.(hints); +}; + +/** + * Check if meta[http-equiv="Delegate-CH"] tag is present in the document head and points to 51Degrees cloud + * + * The way to delegate processing User-Agent Client Hints to a 3rd party is either + * via setting Permissions-Policy + Accept-CH response headers or Delegate-CH meta-http equiv. + * Of those two, Delegate-CH meta http-equiv is an easier and more performant option + * (client hints are sent on the very first request without a round trip required). + * Using the getHighEntropyValues() API is an alternative; + * however, Google is likely to restrict it as part of the Privacy Sandbox in future + * versions of Chrome, so we want to be future-proof and transparent here. + * Hence, a check that would output the warning if the user does not have proper delegation of UA-CH. + * + * @returns {boolean} True if 51Degrees meta is present + * @returns {boolean} False if 51Degrees meta is not present + */ +export const is51DegreesMetaPresent = () => { + const meta51 = document.head.querySelectorAll('meta[http-equiv="Delegate-CH"]'); + if (!meta51.length) { + return false; + } + return Array.from(meta51).some( + meta => !meta.content + ? false + : meta.content.includes('cloud.51degrees') + ); +} + +/** + * Sets the value of a key in the ORTB2 object if the value is not empty + * + * @param {Object} obj The object to set the key in + * @param {string} key The key to set + * @param {any} value The value to set + */ +export const deepSetNotEmptyValue = (obj, key, value) => { + if (!key) { + throw new Error(LOG_PREFIX + ' Key is required'); + } + + if (value) { + deepSetValue(obj, key, value); + } +} + +/** + * Converts all 51Degrees data to ORTB2 format + * + * @param {Object} data51 Response from 51Degrees API + * @param {Object} [data51.device] Device data + * + * @returns {Object} Enriched ORTB2 object + */ +export const convert51DegreesDataToOrtb2 = (data51) => { + let ortb2Data = {}; + + if (!data51) { + return ortb2Data; + } + + ortb2Data = convert51DegreesDeviceToOrtb2(data51.device); + + // placeholder for the next 51Degrees RTD submodule update + + return ortb2Data; +}; + +/** + * Converts 51Degrees device data to ORTB2 format + * + * @param {Object} device 51Degrees device object + * @param {string} [device.deviceid] Device ID (unique 51Degrees identifier) + * @param {string} [device.devicetype] Device type + * @param {string} [device.hardwarevendor] Hardware vendor + * @param {string} [device.hardwaremodel] Hardware model + * @param {string[]} [device.hardwarename] Hardware name + * @param {string} [device.platformname] Platform name + * @param {string} [device.platformversion] Platform version + * @param {number} [device.screenpixelsheight] Screen height in pixels + * @param {number} [device.screenpixelswidth] Screen width in pixels + * @param {number} [device.screenpixelsphysicalheight] Screen physical height in pixels + * @param {number} [device.screenpixelsphysicalwidth] Screen physical width in pixels + * @param {number} [device.pixelratio] Pixel ratio + * @param {number} [device.screeninchesheight] Screen height in inches + * + * @returns {Object} Enriched ORTB2 object + */ +export const convert51DegreesDeviceToOrtb2 = (device) => { + const ortb2Device = {}; + + if (!device) { + return ortb2Device; + } + + const deviceModel = + device.hardwaremodel || ( + device.hardwarename && device.hardwarename.length + ? device.hardwarename.join(',') + : null + ); + + const devicePhysicalPPI = device.screenpixelsphysicalheight && device.screeninchesheight + ? Math.round(device.screenpixelsphysicalheight / device.screeninchesheight) + : null; + + const devicePPI = device.screenpixelsheight && device.screeninchesheight + ? Math.round(device.screenpixelsheight / device.screeninchesheight) + : null; + + deepSetNotEmptyValue(ortb2Device, 'devicetype', ORTB_DEVICE_TYPE_MAP.get(device.devicetype)); + deepSetNotEmptyValue(ortb2Device, 'make', device.hardwarevendor); + deepSetNotEmptyValue(ortb2Device, 'model', deviceModel); + deepSetNotEmptyValue(ortb2Device, 'os', device.platformname); + deepSetNotEmptyValue(ortb2Device, 'osv', device.platformversion); + deepSetNotEmptyValue(ortb2Device, 'h', device.screenpixelsphysicalheight || device.screenpixelsheight); + deepSetNotEmptyValue(ortb2Device, 'w', device.screenpixelsphysicalwidth || device.screenpixelswidth); + deepSetNotEmptyValue(ortb2Device, 'pxratio', device.pixelratio); + deepSetNotEmptyValue(ortb2Device, 'ppi', devicePhysicalPPI || devicePPI); + deepSetNotEmptyValue(ortb2Device, 'ext.fiftyonedegrees_deviceId', device.deviceid); + + return {device: ortb2Device}; +} + +/** + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} callback Called on completion + * @param {Object} moduleConfig Configuration for 1plusX RTD module + * @param {Object} userConsent + */ +export const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { + try { + // Get the required config + const {resourceKey, onPremiseJSUrl} = extractConfig(moduleConfig, reqBidsConfigObj); + logMessage('Resource key: ', resourceKey); + logMessage('On-premise JS URL: ', onPremiseJSUrl); + + // Check if 51Degrees meta is present (cloud only) + if (resourceKey) { + logMessage('Checking if 51Degrees meta is present in the document head'); + if (!is51DegreesMetaPresent()) { + logWarn('Delegate-CH meta tag is not present in the document head'); + } + } + + getHighEntropyValues(['model', 'platform', 'platformVersion', 'fullVersionList']).then((hev) => { + // Get 51Degrees JS URL, which is either cloud or on-premise + const scriptURL = get51DegreesJSURL({resourceKey, onPremiseJSUrl, hev}); + logMessage('URL of the script to be injected: ', scriptURL); + + // Inject 51Degrees script, get device data and merge it into the ORTB2 object + loadExternalScript(scriptURL, MODULE_TYPE_RTD, MODULE_NAME, () => { + logMessage('Successfully injected 51Degrees script'); + const fod = /** @type {Object} */ (window.fod); + // Convert and merge device data in the callback + fod.complete((data) => { + logMessage('51Degrees raw data: ', data); + mergeDeep( + reqBidsConfigObj.ortb2Fragments.global, + convert51DegreesDataToOrtb2(data), + ); + logMessage('reqBidsConfigObj: ', reqBidsConfigObj); + callback(); + }); + }, document, {crossOrigin: 'anonymous'}); + }); + } catch (error) { + // In case of an error, log it and continue + logError(error); + callback(); + } +} + +/** + * Init + * @param {Object} config Module configuration + * @param {boolean} userConsent User consent + * @returns true + */ +const init = (config, userConsent) => { + return true; +} + +// 51Degrees RTD submodule object to be registered +export const fiftyOneDegreesSubmodule = { + name: MODULE_NAME, + init, + getBidRequestData, +} + +submodule('realTimeData', fiftyOneDegreesSubmodule); diff --git a/modules/51DegreesRtdProvider.md b/modules/51DegreesRtdProvider.md new file mode 100644 index 00000000000..76fa73803c9 --- /dev/null +++ b/modules/51DegreesRtdProvider.md @@ -0,0 +1,162 @@ +# 51Degrees RTD Submodule + +## Overview + + Module Name: 51Degrees RTD Provider + Module Type: RTD Provider + Maintainer: support@51degrees.com + +## Description + +The 51Degrees module enriches an OpenRTB request with [51Degrees Device Data](https://51degrees.com/documentation/index.html). + +The 51Degrees module sets the following fields of the device object: `devicetype`, `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pxratio`. Interested bidder adapters may use these fields as needed. In addition, the module sets `device.ext.fiftyonedegrees_deviceId` to a permanent device ID, which can be rapidly looked up in on-premise data, exposing over 250 properties, including device age, chipset, codec support, price, operating system and app/browser versions, age, and embedded features. + +The module supports on-premise and cloud device detection services, with free options for both. + +A free resource key for use with 51Degrees cloud service can be obtained from [51Degrees cloud configuration](https://configure.51degrees.com/HNZ75HT1). This is the simplest approach to trial the module. + +An interface-compatible self-hosted service can be used with .NET, Java, Node, PHP, and Python. See [51Degrees examples](https://51degrees.com/documentation/_examples__device_detection__getting_started__web__on_premise.html). + +Free cloud and on-premise solutions can be expanded to support unlimited requests, additional properties, and automatic daily on-premise data updates via a [subscription](https://51degrees.com/pricing). + +## Usage + +### Integration + +Compile the 51Degrees RTD Module with other modules and adapters into your Prebid.js build: + +``` +gulp build --modules=rtdModule,51DegreesRtdProvider,appnexusBidAdapter,... +``` + +> Note that the 51Degrees RTD module is dependent on the global real-time data module, `rtdModule`. + +### Prerequisites + +#### Resource Key + +In order to use the module, please first obtain a Resource Key using the [Configurator tool](https://configure.51degrees.com/HNZ75HT1) - choose the following properties: + +* DeviceId +* DeviceType +* HardwareVendor +* HardwareName +* HardwareModel +* PlatformName +* PlatformVersion +* ScreenPixelsHeight +* ScreenPixelsWidth +* ScreenPixelsPhysicalHeight +* ScreenPixelsPhysicalWidth +* ScreenInchesHeight +* ScreenInchesWidth +* PixelRatio + +The Cloud API is **free** to integrate and use. To increase limits, please check [51Degrees pricing](https://51degrees.com/pricing). + +#### User Agent Client Hint (UA-CH) Permissions + +Some UA-CH headers are not available to third parties. To allow the 51Degrees cloud service to access these headers for more accurate detection and lower latency, it is highly recommended to set `Permissions-Policy` in one of two ways: + +In the HTML of the publisher's web page where the Prebid.js wrapper is integrated: + +```html + +``` + +Or in the Response Headers of the publisher's web server: + +```http +Permissions-Policy: ch-ua-arch=(self "https://cloud.51degrees.com"), ch-ua-full-version=(self "https://cloud.51degrees.com"), ch-ua-full-version-list=(self "https://cloud.51degrees.com"), ch-ua-model=(self "https://cloud.51degrees.com"), ch-ua-platform=(self "https://cloud.51degrees.com"), ch-ua-platform-version=(self "https://cloud.51degrees.com") + +Accept-CH: sec-ch-ua-arch, sec-ch-ua-full-version, sec-ch-ua-full-version-list, sec-ch-ua-model, sec-ch-ua-platform, sec-ch-ua-platform-version +``` + +See the [51Degrees documentation](https://51degrees.com/documentation/_device_detection__features__u_a_c_h__overview.html) for more information concerning UA-CH and permissions. + +##### Why not use the GetHighEntropyValues API instead? + +Thanks for asking. + +The script this module injects has a fallback to the GetHighEntropyValues API but does not rely on it as a first (or only) choice route. Please see the illustrative cases below. Although it seems easier, the GHEV API is not supported by all browsers (so the decision to call it should be conditional). Also, even in Chrome, this API will likely be subject to the Privacy Budget in the future. + +In summary, we recommend using `Delegate-CH` http-equiv as the preferred method of obtaining the necessary evidence because it is the fastest and most future-proof method. + +##### Illustrative Cases + +* If the device is iPhone/iPad, there is no point in checking for or calling GetHighEntropyValues at the moment because iOS does not support this API. However, this might change in the future. Platforms like iOS require additional techniques to identify the model, which are not covered via a single API call, and change from version to version of the operating system and browser rendering engine. **When used with iOS, 51Degrees resolves the [iPhone/iPad model groups](https://51degrees.com/documentation/4.4/_device_detection__features__apple_device_table.html) using these techniques.** That is one of the benefits the module brings to the Prebid community, as most solutions do not resolve iPhone/iPad model groups. More on Apple Device Detection [here](https://51degrees.com/documentation/4.4/_device_detection__features__apple_detection.html). + +* If the browser is Firefox on Android or Desktop, there is similarly no point in requesting GHEV, as the API is not supported. + +* If the browser is Chrome, the `Delegate-CH`, if enabled by the publisher, would allow the browser to provide the necessary evidence. However, if this is not implemented, then the dynamic script would fall back to GHEV, which is slower. + +### Configuration + +This module is configured as part of the `realTimeData.dataProviders`. We recommend setting `auctionDelay` to at least 250 ms and ensuring `waitForIt` is set to `true` for the `51Degrees` RTD provider. + +```javascript +pbjs.setConfig({ + debug: false, // turn on for testing, remove in production + realTimeData: { + auctionDelay: 250, + dataProviders: [ + { + name: '51Degrees', + waitForIt: true, // should be true, otherwise the auctionDelay will be ignored + params: { + resourceKey: '', + // Get your resource key from https://configure.51degrees.com/HNZ75HT1 + // alternatively, you can use the on-premise version of the 51Degrees service and connect to your chosen endpoint + // onPremiseJSUrl: 'https://localhost/51Degrees.core.js' + }, + }, + ], + }, +}); +``` + +### Parameters + +> Note that `resourceKey` and `onPremiseJSUrl` are mutually exclusive parameters. Use strictly one of them: either a `resourceKey` for cloud integration or `onPremiseJSUrl` for the on-premise self-hosted integration. + +| Name | Type | Description | Default | +|:----------------------|:--------|:---------------------------------------------------------------------------------------------|:-------------------| +| name | String | Real-time data module name | Always '51Degrees' | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (mandatory) | `false` | +| params | Object | | | +| params.resourceKey | String | Your 51Degrees Cloud Resource Key | | +| params.onPremiseJSUrl | String | Direct URL to your self-hosted on-premise JS file (e.g. https://localhost/51Degrees.core.js) | | + +> Note: if you use a third-party Prebid.js wrapper, there might be a chance that the UI will force you to input both `resourceKey` and `onPremiseJSUrl`. In this case, you can set a redundant parameter to a string equal to "0", which will be ignored by the module. + +## Example + +> Note: you need to have a valid resource key to run the example.\ +> It should be set in the configuration instead of ``.\ +> It is located in the `integrationExamples/gpt/51DegreesRtdProvider_example.html` file. + +If you want to see an example of how the 51Degrees RTD module works,\ +run the following command: + +`gulp serve --modules=rtdModule,51DegreesRtdProvider,appnexusBidAdapter` + +and then open the following URL in your browser: + +`http://localhost:9999/integrationExamples/gpt/51DegreesRtdProvider_example.html` + +Open the browser console to see the logs. + +## Customer Notices + +When using the 51Degrees cloud service, publishers need to reference the 51Degrees [client services privacy policy](https://51degrees.com/terms/client-services-privacy-policy) in their customer notices. + +## Optimisation + +To reduce latency when loading the 51Degrees cloud service script, it's recommended to preconnect to the 51Degrees domain. This will establish an early connection, allowing the browser to resolve DNS, set up TCP, and perform the TLS handshake ahead of time, speeding up the script download. + +To enable `preconnect`, add the following in the `` of your HTML: + +```html + +``` diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/AsteriobidPbmAnalyticsAdapter.js similarity index 94% rename from modules/prebidmanagerAnalyticsAdapter.js rename to modules/AsteriobidPbmAnalyticsAdapter.js index 858e30068db..7f76a96101f 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/AsteriobidPbmAnalyticsAdapter.js @@ -1,18 +1,19 @@ -import { generateUUID, getParameterByName, logError, parseUrl, logInfo } from '../src/utils.js'; +import { deepClone, generateUUID, getParameterByName, hasNonSerializableProperty, logError, parseUrl, logInfo } from '../src/utils.js'; import {ajaxBuilder} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import { EVENTS } from '../src/constants.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; /** * prebidmanagerAnalyticsAdapter.js - analytics adapter for prebidmanager */ -export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'prebidmanager'}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'asteriobidpbm'}); const DEFAULT_EVENT_URL = 'https://endpt.prebidmanager.com/endpoint'; const analyticsType = 'endpoint'; -const analyticsName = 'Prebid Manager Analytics'; +const analyticsName = 'Asteriobid PBM Analytics'; let ajax = ajaxBuilder(0); @@ -25,12 +26,7 @@ let flushInterval; var pmAnalyticsEnabled = false; const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; -var w = window; -var d = document; -var e = d.documentElement; -var g = d.getElementsByTagName('body')[0]; -var x = (w && w.innerWidth) || (e && e.clientWidth) || (g && g.clientWidth); -var y = (w && w.innerHeight) || (e && e.clientHeight) || (g && g.clientHeight); +const {width: x, height: y} = getViewportSize(); var _pageView = { eventType: 'pageView', @@ -199,10 +195,10 @@ function trimBidderRequest(bidderRequest) { } function handleEvent(eventType, eventArgs) { - try { - eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {}; - } catch (e) { - // keep eventArgs as is + if (eventArgs) { + eventArgs = hasNonSerializableProperty(eventArgs) ? eventArgs : deepClone(eventArgs) + } else { + eventArgs = {} } const pmEvent = {}; diff --git a/modules/AsteriobidPbmAnalyticsAdapter.md b/modules/AsteriobidPbmAnalyticsAdapter.md new file mode 100644 index 00000000000..0331a71b17c --- /dev/null +++ b/modules/AsteriobidPbmAnalyticsAdapter.md @@ -0,0 +1,9 @@ +# Overview + +Module Name: Asteriobid PBM Analytics Adapter +Module Type: Analytics Adapter +Maintainer: admin@prebidmanager.com + +# Description + +Analytics adapter for Asteriobid PBM. Contact admin@prebidmanager.com for information. diff --git a/modules/a1MediaRtdProvider.js b/modules/a1MediaRtdProvider.js index 445ed47181d..1fbe88ecfa0 100644 --- a/modules/a1MediaRtdProvider.js +++ b/modules/a1MediaRtdProvider.js @@ -39,7 +39,7 @@ function loadLbScript(tagname) { linkback.l = true; const scriptUrl = `${SCRIPT_URL}/${tagname}`; - loadExternalScript(scriptUrl, MODULE_NAME); + loadExternalScript(scriptUrl, MODULE_TYPE_RTD, MODULE_NAME); } } diff --git a/modules/aaxBlockmeterRtdProvider.js b/modules/aaxBlockmeterRtdProvider.js index a3b7b4812a7..0a72e4e36f1 100644 --- a/modules/aaxBlockmeterRtdProvider.js +++ b/modules/aaxBlockmeterRtdProvider.js @@ -1,6 +1,7 @@ import {isEmptyStr, isStr, logError, isFn, logWarn} from '../src/utils.js'; import {submodule} from '../src/hook.js'; import { loadExternalScript } from '../src/adloader.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; export const _config = { MODULE: 'aaxBlockmeter', @@ -28,7 +29,7 @@ function loadBlockmeter(_rtdConfig) { } const scriptUrl = `https://${url}&${params.join('&')}`; - loadExternalScript(scriptUrl, _config.MODULE); + loadExternalScript(scriptUrl, MODULE_TYPE_RTD, _config.MODULE); return true; } diff --git a/modules/ablidaBidAdapter.js b/modules/ablidaBidAdapter.js index 175d5ff7c72..3881d06f81a 100644 --- a/modules/ablidaBidAdapter.js +++ b/modules/ablidaBidAdapter.js @@ -2,6 +2,7 @@ import {triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -95,7 +96,6 @@ export const spec = { function getDevice() { const ua = navigator.userAgent; - const topWindow = window.top; if ((/(ipad|xoom|sch-i800|playbook|silk|tablet|kindle)|(android(?!.*mobi))/i).test(ua)) { return 'tablet'; } @@ -105,7 +105,7 @@ function getDevice() { if ((/Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|NetFront|Windows\sCE|Silk-Accelerated|(hpw|web)OS|Fennec|Minimo|Opera M(obi|ini)|Blazer|Dolfin|Dolphin|Skyfire|Zune/i).test(ua)) { return 'smartphone'; } - const width = topWindow.innerWidth || topWindow.document.documentElement.clientWidth || topWindow.document.body.clientWidth; + const { width } = getViewportSize(); if (width > 320) { return 'desktop'; } diff --git a/modules/acuityadsBidAdapter.js b/modules/acuityadsBidAdapter.js index 5b12eb2133b..b94234c2c26 100644 --- a/modules/acuityadsBidAdapter.js +++ b/modules/acuityadsBidAdapter.js @@ -1,216 +1,21 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'acuityads'; +const GVLID = 231; const AD_URL = 'https://prebid.admanmedia.com/pbjs'; const SYNC_URL = 'https://cs.admanmedia.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - placement.placementId = placementId; - placement.type = 'publisher'; - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && params.placementId); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - // Add GPP consent - if (bidderRequest.gppConsent) { - request.gpp = bidderRequest.gppConsent.gppString; - request.gpp_sid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - request.gpp = bidderRequest.ortb2.regs.gpp; - request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/adagioAnalyticsAdapter.js b/modules/adagioAnalyticsAdapter.js index bb5de41d3ce..410accc946a 100644 --- a/modules/adagioAnalyticsAdapter.js +++ b/modules/adagioAnalyticsAdapter.js @@ -2,13 +2,15 @@ * Analytics Adapter for Adagio */ +import { _ADAGIO, getBestWindowForAdagio } from '../libraries/adagioUtils/adagioUtils.js'; +import { deepAccess, logError, logInfo, logWarn, isPlainObject } from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { EVENTS } from '../src/constants.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import { EVENTS } from '../src/constants.js'; import { ajax } from '../src/ajax.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { getWindowTop, getWindowSelf, deepAccess, logInfo, logError } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; +import { subscribeToGamSlotRenderEndedEvent, SlotRenderEndedEvent } from '../libraries/gptUtils/gptUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; @@ -19,6 +21,14 @@ const PREBID_VERSION = '$prebid.version$'; const ENDPOINT = 'https://c.4dex.io/pba.gif'; const CURRENCY_USD = 'USD'; const ADAGIO_CODE = 'adagio'; + +export const _internal = { + getAdagioNs: function() { + return _ADAGIO; + }, + gamSlotCallback +}; + const cache = { auctions: {}, getAuction: function(auctionId, adUnitCode) { @@ -44,37 +54,22 @@ const cache = { }, getAdagioAuctionId(auctionId) { return this.auctionIdReferences[auctionId]; - } -}; -const enc = window.encodeURIComponent; - -/** -/* BEGIN ADAGIO.JS CODE - */ + }, -function canAccessTopWindow() { - try { - if (getWindowTop().location.href) { - return true; + // Map adunitcode with prebid auction ID + auctionByAdunit: {}, + getAuctionIdByAdunit(adUnitPath, adSlotElementId) { + if (cache.auctionByAdunit[adUnitPath]) { + return { auctionId: cache.auctionByAdunit[adUnitPath], adUnitCode: adUnitPath } + } + if (cache.auctionByAdunit[adSlotElementId]) { + return { auctionId: cache.auctionByAdunit[adSlotElementId], adUnitCode: adSlotElementId } } - } catch (error) { - return false; + return { auctionId: null, adUnitCode: null } } }; -function getCurrentWindow() { - return currentWindow; -}; - -let currentWindow; - -const adagioEnqueue = function adagioEnqueue(action, data) { - getCurrentWindow().ADAGIO.queue.push({ action, data, ts: Date.now() }); -}; - -/** - * END ADAGIO.JS CODE - */ +const enc = window.encodeURIComponent; /** * UTILS FUNCTIONS @@ -150,6 +145,10 @@ function getCurrencyData(bid) { * @param {Object} qp */ function sendRequest(qp) { + if (!qp.org_id || !qp.site) { + logInfo('request is missing org_id or site, skipping beacon.'); + return; + } // Removing null values qp = Object.keys(qp).reduce((acc, key) => { if (qp[key] !== null) { @@ -194,37 +193,36 @@ function getTargetedAuctionId(bid) { */ function handlerAuctionInit(event) { - const w = getCurrentWindow(); + const w = getBestWindowForAdagio(); const prebidAuctionId = event.auctionId; - const adUnitCodes = removeDuplicates(event.adUnitCodes, adUnitCode => adUnitCode); + + // adUnitCodes come from `event.bidderRequests` to be sure to keep the ad-units that are valid and will be effectively used during the auction. + // This array can be different than `event.adUnitCodes` because of the usage of conditionnal ad-units (see: https://docs.prebid.org/dev-docs/conditional-ad-units.html) + const adUnitCodes = new Set( + event.bidderRequests + .map(br => br.bids.map(bid => bid.adUnitCode)) + .flat() + ); // Check if Adagio is on the bid requests. - // If not, we don't need to track the auction. const adagioBidRequest = event.bidderRequests.find(bidRequest => isAdagio(bidRequest.bidderCode)); - if (!adagioBidRequest) { - logInfo(`Adagio is not on the bid requests for auction '${prebidAuctionId}'`) - return; - } + + const rtdUid = deepAccess(event.bidderRequests[0], 'ortb2.site.ext.data.adg_rtd.uid'); + cache.addPrebidAuctionIdRef(prebidAuctionId, rtdUid); cache.auctions[prebidAuctionId] = {}; adUnitCodes.forEach(adUnitCode => { + // event.adUnits are splitted by mediatypes + // having twin ad-unit codes is ok: https://docs.prebid.org/dev-docs/adunit-reference.html#twin-adunit-codes const adUnits = event.adUnits.filter(adUnit => adUnit.code === adUnitCode); - // Get all bidders configures for the ad unit. - const bidders = removeDuplicates( - adUnits.map(adUnit => adUnit.bids.map(bid => ({bidder: bid.bidder, params: bid.params}))).flat(), - bidder => bidder.bidder - ); - - // Check if Adagio is configured for the ad unit. - // If not, we don't need to track the ad unit. - const adagioBidder = bidders.find(bidder => isAdagio(bidder.bidder)); - if (!adagioBidder) { - logInfo(`Adagio is not configured for ad unit '${adUnitCode}'`); - return; - } + // Get all bidders configured for the ad unit. + // AdUnits with the same code can have a different bidder list, aggregate all of them. + const biddersAggregate = adUnits.reduce((bidders, adUnit) => bidders.concat(adUnit.bids.map(bid => bid.bidder)), []) + // remove duplicates + const bidders = [...new Set(biddersAggregate)]; // Get all media types and banner sizes configured for the ad unit. const mediaTypes = adUnits.map(adUnit => adUnit.mediaTypes); @@ -233,52 +231,69 @@ function handlerAuctionInit(event) { mediaTypeKey => mediaTypeKey ).map(mediaType => getMediaTypeAlias(mediaType)).sort(); const bannerSizes = removeDuplicates( - mediaTypes.filter(mediaType => mediaType.hasOwnProperty(BANNER)) + mediaTypes.filter(mediaType => mediaType.hasOwnProperty(BANNER) && mediaType[BANNER].hasOwnProperty('sizes')) .map(mediaType => mediaType[BANNER].sizes.map(size => size.join('x'))) .flat(), bannerSize => bannerSize ).sort(); - // Get all Adagio bids for the ad unit from the bidRequest. - // If no bids, we don't need to track the ad unit. - const adagioAdUnitBids = adagioBidRequest.bids.filter(bid => bid.adUnitCode === adUnitCode); - if (deepAccess(adagioAdUnitBids, 'length', 0) <= 0) { - logInfo(`Adagio is not on the bid requests for ad unit '${adUnitCode}' and auction '${prebidAuctionId}'`) - return; + const sortedBidderNames = bidders.sort(); + + const bidSrcMapper = (bidder) => { + // bidderCode in the context of the bidderRequest is the name given to the bidder in the adunit. + // It is not always the "true" bidder code, it can also be its alias + const request = event.bidderRequests.find(br => br.bidderCode === bidder) + return request ? request.bids[0].src : null } - // Get Adagio params from the first bid. - // We assume that all Adagio bids for a same adunit have the same params. - const params = adagioAdUnitBids[0].params; - const adagioAuctionId = params.adagioAuctionId; - cache.addPrebidAuctionIdRef(prebidAuctionId, adagioAuctionId); + const biddersSrc = sortedBidderNames.map(bidSrcMapper).join(','); + const biddersCode = sortedBidderNames.map(bidder => adapterManager.resolveAlias(bidder)).join(','); - // Get all media types requested for Adagio. - const adagioMediaTypes = removeDuplicates( - adagioAdUnitBids.map(bid => Object.keys(bid.mediaTypes)).flat(), - mediaTypeKey => mediaTypeKey - ).flat().map(mediaType => getMediaTypeAlias(mediaType)).sort(); + // if adagio was involved in the auction we identified it with rtdUid, if not use the prebid auctionId + const auctionId = rtdUid || prebidAuctionId; + + const adgRtdSession = deepAccess(event.bidderRequests[0], 'ortb2.site.ext.data.adg_rtd.session', {}); const qp = { + org_id: adagioAdapter.options.organizationId, + site: adagioAdapter.options.site, v: 0, pbjsv: PREBID_VERSION, - org_id: params.organizationId, - site: params.site, - pv_id: params.pageviewId, - auct_id: adagioAuctionId, + pv_id: _internal.getAdagioNs().pageviewId, + auct_id: auctionId, adu_code: adUnitCode, url_dmn: w.location.hostname, - pgtyp: params.pagetype, - plcmt: params.placement, - t_n: params.testName || null, - t_v: params.testVersion || null, mts: mediaTypesKeys.join(','), ban_szs: bannerSizes.join(','), - bdrs: bidders.map(bidder => bidder.bidder).sort().join(','), - adg_mts: adagioMediaTypes.join(',') + bdrs: sortedBidderNames.join(','), + pgtyp: deepAccess(event.bidderRequests[0], 'ortb2.site.ext.data.pagetype', null), + plcmt: deepAccess(adUnits[0], 'ortb2Imp.ext.data.placement', null), + t_n: adgRtdSession.testName || null, + t_v: adgRtdSession.testVersion || null, + s_id: adgRtdSession.id || null, + s_new: adgRtdSession.new || null, + bdrs_src: biddersSrc, + bdrs_code: biddersCode, }; + if (adagioBidRequest && adagioBidRequest.bids) { + const adagioAdUnitBids = adagioBidRequest.bids.filter(bid => bid.adUnitCode === adUnitCode); + if (adagioAdUnitBids.length > 0) { + // Get all media types requested for Adagio. + const adagioMediaTypes = removeDuplicates( + adagioAdUnitBids.map(bid => Object.keys(bid.mediaTypes)).flat(), + mediaTypeKey => mediaTypeKey + ).flat().map(mediaType => getMediaTypeAlias(mediaType)).sort(); + + qp.adg_mts = adagioMediaTypes.join(','); + // 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; + } + } + cache.auctions[prebidAuctionId][adUnitCode] = qp; + cache.auctionByAdunit[adUnitCode] = prebidAuctionId; sendNewBeacon(prebidAuctionId, adUnitCode); }); }; @@ -324,13 +339,26 @@ function handlerAuctionEnd(event) { return bid ? getCurrencyData(bid).netCpm : null } + const perfNavigation = performance.getEntriesByType('navigation')[0]; + + const auction = cache.getAuction(auctionId, adUnitCode); + const bdrs = auction.bdrs.split(','); + const bdrsTimeout = auction.bdrs_timeout || []; + cache.updateAuction(auctionId, adUnitCode, { bdrs_bid: cache.getBiddersFromAuction(auctionId, adUnitCode).map(bidResponseMapper).join(','), - bdrs_cpm: cache.getBiddersFromAuction(auctionId, adUnitCode).map(bidCpmMapper).join(',') + bdrs_cpm: cache.getBiddersFromAuction(auctionId, adUnitCode).map(bidCpmMapper).join(','), + // check timings at the end of the auction to leave time to the browser to update it + dom_i: Math.round(perfNavigation['domInteractive']) || null, + dom_c: Math.round(perfNavigation['domComplete']) || null, + loa_e: Math.round(perfNavigation['loadEventEnd']) || null, + bdrs_timeout: bdrs.map(b => bdrsTimeout.includes(b) ? '1' : '0').join(','), }); + sendNewBeacon(auctionId, adUnitCode); }); } + function handlerBidWon(event) { let auctionId = getTargetedAuctionId(event); @@ -345,6 +373,8 @@ function handlerBidWon(event) { ? cache.getAdagioAuctionId(event.auctionId) : null); + const perfNavigation = performance.getEntriesByType('navigation')[0]; + cache.updateAuction(auctionId, event.adUnitCode, { win_bdr: event.bidder, win_mt: getMediaTypeAlias(event.mediaType), @@ -353,6 +383,11 @@ function handlerBidWon(event) { win_net_cpm: currencyData.netCpm, win_og_cpm: currencyData.orginalCpm, + // check timings at the end of the auction to leave time to the browser to update it + dom_i: Math.round(perfNavigation['domInteractive']) || null, + dom_c: Math.round(perfNavigation['domComplete']) || null, + loa_e: Math.round(perfNavigation['loadEventEnd']) || null, + // cache bid id auct_id_c: adagioAuctionCacheId, }); @@ -373,14 +408,84 @@ function handlerAdRender(event, isSuccess) { sendNewBeacon(auctionId, adUnitCode); }; +function handlerBidTimeout(args) { + args.forEach(event => { + const auction = cache.getAuction(event.auctionId, event.adUnitCode); + if (!auction) { + logWarn(`bid timeout on auction ${event.auctionId}, with adunitCode ${event.adUnitCode}: could not retrieve auction from cache`); + return; + } + + // an array of bidder names is first created + // in AUCTION_END handler, this array is sorted + // and transformed in a comma-separated list. + const bdrsTimeout = auction.bdrs_timeout || []; + bdrsTimeout.push(event.bidder); + auction.bdrs_timeout = bdrsTimeout; + }); +}; + +/** + * handlerPbsAnalytics add to the cache data coming from Adagio PBS AdResponse. + * The data is retrieved from an AnalyticsTag (set by a custom PBS module named `adg-pba`), + * located in the AdResponse at `response.ext.prebid.analytics.tags[].pba`. + */ +function handlerPbsAnalytics(event) { + const pbaByAdUnit = event.atag.find(e => { + return e.module === 'adg-pba' + })?.pba; + + if (!pbaByAdUnit) { + return; + } + + const adUnitCodes = cache.getAllAdUnitCodes(event.auctionId); + + adUnitCodes.forEach(adUnitCode => { + const pba = pbaByAdUnit[adUnitCode] + + if (isPlainObject(pba)) { + cache.updateAuction(event.auctionId, adUnitCode, { + ...addKeyPrefix(pba, 'e_') + }); + } + }) +} + /** * END HANDLERS */ +/** + * @param {SlotRenderEndedEvent} event + * @returns {void} + */ +function gamSlotCallback(event) { + const { auctionId, adUnitCode } = cache.getAuctionIdByAdunit(event.slot.getAdUnitPath(), event.slot.getSlotElementId()); + if (!auctionId) { + const slotName = `${event.slot.getAdUnitPath()} - ${event.slot.getSlotElementId()}`; + logWarn('Could not find configured ad unit matching GAM render of slot: ' + slotName); + return; + } + + cache.updateAuction(auctionId, adUnitCode, { + adsrv: 'gam', + adsrv_empty: event.isEmpty + }); + + // This event can be triggered after AUCTION_END + // To make sure the data is sent, we must send a new beacon version. + const auction = cache.getAuction(auctionId, adUnitCode) + if (auction?.loa_e !== undefined) { + // loa_e = loadEventEnd + // It means the AUCTION_END has already been sent. + sendNewBeacon(auctionId, adUnitCode); + } +} + let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { track: function(event) { const { eventType, args } = event; - try { switch (eventType) { case EVENTS.AUCTION_INIT: @@ -400,6 +505,12 @@ let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { case EVENTS.AD_RENDER_FAILED: handlerAdRender(args, eventType === EVENTS.AD_RENDER_SUCCEEDED); break; + case EVENTS.PBS_ANALYTICS: + handlerPbsAnalytics(args); + break; + case EVENTS.BID_TIMEOUT: + handlerBidTimeout(args); + break; } } catch (error) { logError('Error on Adagio Analytics Adapter', error); @@ -407,7 +518,11 @@ let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { try { if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { - adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + _internal.getAdagioNs().queue.push({ + action: 'pb-analytics-event', + data: { eventName: eventType, args }, + ts: Date.now() + }); } } catch (error) { logError('Error on Adagio Analytics Adapter - adagio.js', error); @@ -418,15 +533,29 @@ let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { adagioAdapter.originEnableAnalytics = adagioAdapter.enableAnalytics; adagioAdapter.enableAnalytics = config => { - const w = (canAccessTopWindow()) ? getWindowTop() : getWindowSelf(); - currentWindow = w; + _internal.getAdagioNs().versions.adagioAnalyticsAdapter = VERSION; - w.ADAGIO = w.ADAGIO || {}; - w.ADAGIO.queue = w.ADAGIO.queue || []; - w.ADAGIO.versions = w.ADAGIO.versions || {}; - w.ADAGIO.versions.adagioAnalyticsAdapter = VERSION; + let modules = getGlobal().installedModules; + if (modules && (!modules.length || modules.indexOf('adagioRtdProvider') === -1 || modules.indexOf('rtdModule') === -1)) { + logError('Adagio Analytics Adapter requires rtdModule & adagioRtdProvider modules which are not installed. No beacon will be sent'); + return; + } + adagioAdapter.options = config.options || {}; + if (!adagioAdapter.options.organizationId) { + logWarn('Adagio Analytics Adapter: organizationId is required and is missing will try to fallback on params.'); + } else { + adagioAdapter.options.organizationId = adagioAdapter.options.organizationId.toString(); // allows publisher to pass it as a number + } + if (!adagioAdapter.options.site) { + logWarn('Adagio Analytics Adapter: site is required and is missing will try to fallback on params.'); + } else if (typeof adagioAdapter.options.site !== 'string') { + logWarn('Adagio Analytics Adapter: site should be a string will try to fallback on params.'); + adagioAdapter.options.site = undefined; + } adagioAdapter.originEnableAnalytics(config); + + subscribeToGamSlotRenderEndedEvent(gamSlotCallback) } adapterManager.registerAnalyticsAdapter({ diff --git a/modules/adagioAnalyticsAdapter.md b/modules/adagioAnalyticsAdapter.md index 9fc2cb0bb88..916f9ec9c58 100644 --- a/modules/adagioAnalyticsAdapter.md +++ b/modules/adagioAnalyticsAdapter.md @@ -13,5 +13,9 @@ Analytics adapter for Adagio ```js pbjs.enableAnalytics({ provider: 'adagio', + options: { + organizationId: '1000', // Required. Provided by Adagio + site: 'my-website', // Required. Provided by Adagio + } }); ``` diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 6e3c38e4e85..6b1c182e7e6 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,277 +1,71 @@ -import {find} from '../src/polyfill.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { + canAccessWindowTop, cleanObj, deepAccess, deepClone, generateUUID, - getDNT, - getUniqueIdentifierStr, getWindowSelf, - getWindowTop, - inIframe, isArray, isFn, - isInteger, isNumber, - isArrayOfNums, + isStr, logError, logInfo, logWarn, mergeDeep, - isStr, } from '../src/utils.js'; -import {config} from '../src/config.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {loadExternalScript} from '../src/adloader.js'; -import {verify} from 'criteo-direct-rsa-validate/build/verify.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {getRefererInfo, parseDomain} from '../src/refererDetection.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {Renderer} from '../src/Renderer.js'; -import {OUTSTREAM} from '../src/video.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import { getRefererInfo, parseDomain } from '../src/refererDetection.js'; +import { OUTSTREAM, validateOrtbVideoFields } from '../src/video.js'; +import { Renderer } from '../src/Renderer.js'; +import { _ADAGIO } from '../libraries/adagioUtils/adagioUtils.js'; +import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { find } from '../src/polyfill.js'; +import { getGptSlotInfoForAdUnitCode } from '../libraries/gptUtils/gptUtils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { userSync } from '../src/userSync.js'; -import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; -const FEATURES_VERSION = '1'; export const ENDPOINT = 'https://mp.4dex.io/prebid'; const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; -const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; -const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; const GVLID = 617; -export const storage = getStorageManager({bidderCode: BIDDER_CODE}); const BB_PUBLICATION = 'adagio'; const BB_RENDERER_DEFAULT = 'renderer'; export const BB_RENDERER_URL = `https://${BB_PUBLICATION}.bbvms.com/r/$RENDERER.js`; -const MAX_SESS_DURATION = 30 * 60 * 1000; -const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0ki9JJI2uYm/6VEYo8TJED9WfMkiJ4vf02CW3RvSWwc35bif2SK1L8Nn/GfFYr/2/GG/Rm0vUsv+vBHky6nuuYls20Og0HDhMgaOlXoQ/cxMuiy5QSktp'; -const ADAGIO_PUBKEY_E = 65537; const CURRENCY = 'USD'; -// This provide a whitelist and a basic validation of OpenRTB 2.5 options used by the Adagio SSP. -// Accept all options but 'protocol', 'companionad', 'companiontype', 'ext' -// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf -export const ORTB_VIDEO_PARAMS = { - 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), - 'minduration': (value) => isInteger(value), - 'maxduration': (value) => isInteger(value), - 'protocols': (value) => isArrayOfNums(value), - 'w': (value) => isInteger(value), - 'h': (value) => isInteger(value), - 'startdelay': (value) => isInteger(value), - 'placement': (value) => isInteger(value), - 'linearity': (value) => isInteger(value), - 'skip': (value) => [1, 0].includes(value), - 'skipmin': (value) => isInteger(value), - 'skipafter': (value) => isInteger(value), - 'sequence': (value) => isInteger(value), - 'battr': (value) => isArrayOfNums(value), - 'maxextended': (value) => isInteger(value), - 'minbitrate': (value) => isInteger(value), - 'maxbitrate': (value) => isInteger(value), - 'boxingallowed': (value) => isInteger(value), - 'playbackmethod': (value) => isArrayOfNums(value), - 'playbackend': (value) => isInteger(value), - 'delivery': (value) => isArrayOfNums(value), - 'pos': (value) => isInteger(value), - 'api': (value) => isArrayOfNums(value) -}; - -let currentWindow; - -export const GlobalExchange = (function() { - let features; - let exchangeData = {}; - - return { - clearFeatures: function() { - features = undefined; - }, - - clearExchangeData: function() { - exchangeData = {}; - }, - - getOrSetGlobalFeatures: function () { - if (!features) { - features = { - page_dimensions: getPageDimensions().toString(), - viewport_dimensions: getViewPortDimensions().toString(), - user_timestamp: getTimestampUTC().toString(), - dom_loading: getDomLoadingDuration().toString(), - } - } - return features; - }, - - prepareExchangeData(storageValue) { - const adagioStorage = JSON.parse(storageValue, function(name, value) { - if (name.charAt(0) !== '_' || name === '') { - return value; - } - }); - let random = deepAccess(adagioStorage, 'session.rnd'); - let newSession = false; - - if (internal.isNewSession(adagioStorage)) { - newSession = true; - random = Math.random(); - } - - const data = { - session: { - new: newSession, - rnd: random - } - } - - mergeDeep(exchangeData, adagioStorage, data); - - internal.enqueue({ - action: 'session', - ts: Date.now(), - data: exchangeData - }); - }, - - getExchangeData() { - return exchangeData - } - }; -})(); - -export function adagioScriptFromLocalStorageCb(ls) { - try { - if (!ls) { - logWarn(`${LOG_PREFIX} script not found.`); - return; - } - - const hashRgx = /^(\/\/ hash: (.+)\n)(.+\n)$/; - - if (!hashRgx.test(ls)) { - logWarn(`${LOG_PREFIX} no hash found.`); - storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); - } else { - const r = ls.match(hashRgx); - const hash = r[2]; - const content = r[3]; - - if (verify(content, hash, ADAGIO_PUBKEY, ADAGIO_PUBKEY_E)) { - logInfo(`${LOG_PREFIX} start script.`); - Function(ls)(); // eslint-disable-line no-new-func - } else { - logWarn(`${LOG_PREFIX} invalid script found.`); - storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); - } - } - } catch (err) { - logError(LOG_PREFIX, err); - } -} - -export function getAdagioScript() { - storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY, (ls) => { - internal.adagioScriptFromLocalStorageCb(ls); - }); - - storage.localStorageIsEnabled(isValid => { - if (isValid) { - loadExternalScript(ADAGIO_TAG_URL, BIDDER_CODE); - } else { - // Try-catch to avoid error when 3rd party cookies is disabled (e.g. in privacy mode) - try { - // ensure adagio removing for next time. - // It's an antipattern regarding the TCF2 enforcement logic - // but it's the only way to respect the user choice update. - window.localStorage.removeItem(ADAGIO_LOCALSTORAGE_KEY); - // Extra data from external script. - // This key is removed only if localStorage is not accessible. - window.localStorage.removeItem('adagio'); - } catch (e) { - logInfo(`${LOG_PREFIX} unable to clear Adagio scripts from localstorage.`); - } - } - }); -} +/** + * Get device data object, with some properties + * deviated from the OpenRTB spec. + * @param {Object} ortb2Data + * @returns {Object} Device data object + */ +function getDevice(ortb2Data) { + const _device = {}; -function canAccessTopWindow() { - try { - if (getWindowTop().location.href) { - return true; - } - } catch (error) { - return false; + // Merge the device object from ORTB2 data. + if (ortb2Data?.device) { + mergeDeep(_device, ortb2Data.device); } -} - -function getCurrentWindow() { - return currentWindow || getWindowSelf(); -} - -function isSafeFrameWindow() { - const ws = getWindowSelf(); - return !!(ws.$sf && ws.$sf.ext); -} -function initAdagio() { - if (canAccessTopWindow()) { - currentWindow = (canAccessTopWindow()) ? getWindowTop() : getWindowSelf(); + // If the geo object is not defined, create it. + if (!_device.geo) { + _device.geo = {}; } - const w = internal.getCurrentWindow(); - - w.ADAGIO = w.ADAGIO || {}; - w.ADAGIO.adUnits = w.ADAGIO.adUnits || {}; - w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits || []; - w.ADAGIO.queue = w.ADAGIO.queue || []; - w.ADAGIO.versions = w.ADAGIO.versions || {}; - w.ADAGIO.versions.pbjs = '$prebid.version$'; - w.ADAGIO.isSafeFrameWindow = isSafeFrameWindow(); - - storage.getDataFromLocalStorage('adagio', (storageData) => { - try { - GlobalExchange.prepareExchangeData(storageData); - } catch (e) { - logError(LOG_PREFIX, e); - } - }); - - getAdagioScript(); -} - -function enqueue(ob) { - const w = internal.getCurrentWindow(); - - w.ADAGIO = w.ADAGIO || {}; - w.ADAGIO.queue = w.ADAGIO.queue || []; - w.ADAGIO.queue.push(ob); -}; - -function getPageviewId() { - const w = internal.getCurrentWindow(); - - w.ADAGIO = w.ADAGIO || {}; - w.ADAGIO.pageviewId = w.ADAGIO.pageviewId || generateUUID(); - - return w.ADAGIO.pageviewId; -}; - -function getDevice() { const language = navigator.language ? 'language' : 'userLanguage'; - return { + mergeDeep(_device, { userAgent: navigator.userAgent, language: navigator[language], - dnt: getDNT() ? 1 : 0, - geo: {}, js: 1 - }; -}; + }); + + return _device; +} function getSite(bidderRequest) { const { refererInfo } = bidderRequest; @@ -283,30 +77,6 @@ function getSite(bidderRequest) { }; }; -function getElementFromTopWindow(element, currentWindow) { - try { - if (getWindowTop() === currentWindow) { - if (!element.getAttribute('id')) { - element.setAttribute('id', `adg-${getUniqueIdentifierStr()}`); - } - return element; - } else { - const frame = currentWindow.frameElement; - const frameClientRect = frame.getBoundingClientRect(); - const elementClientRect = element.getBoundingClientRect(); - - if (frameClientRect.width !== elementClientRect.width || frameClientRect.height !== elementClientRect.height) { - return false; - } - - return getElementFromTopWindow(frame, currentWindow.parent); - } - } catch (err) { - logWarn(`${LOG_PREFIX}`, err); - return false; - } -}; - function autoDetectAdUnitElementIdFromGpt(adUnitCode) { const autoDetectedAdUnit = getGptSlotInfoForAdUnitCode(adUnitCode); @@ -331,42 +101,28 @@ function isRendererPreferredFromPublisher(bidRequest) { } /** - * - * @param {object} adagioStorage - * @returns {boolean} + * Check if the publisher has defined its own video player and uses it for all ad-units. + * If not or if the `backupOnly` flag is true, this means we use our own player (BlueBillywig) defined in this adapter. */ -function isNewSession(adagioStorage) { - const now = Date.now(); - const { lastActivityTime, vwSmplg } = deepAccess(adagioStorage, 'session', {}); - return ( - !isNumber(lastActivityTime) || - !isNumber(vwSmplg) || - (now - lastActivityTime) > MAX_SESS_DURATION - ) +function getPlayerName(bidRequest) { + return _internal.isRendererPreferredFromPublisher(bidRequest) ? 'other' : 'adagio'; ; } -function setPlayerName(bidRequest) { - const playerName = (internal.isRendererPreferredFromPublisher(bidRequest)) ? 'other' : 'adagio'; - - if (playerName === 'other') { - logWarn(`${LOG_PREFIX} renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.`); - } - - return playerName; -} +function hasRtd() { + const rtdConfigs = config.getConfig('realTimeData.dataProviders') || []; + return rtdConfigs.find(provider => provider.name === 'adagio'); +}; -export const internal = { - enqueue, - getPageviewId, +export const _internal = { + canAccessWindowTop, + getAdagioNs: function() { + return _ADAGIO; + }, getDevice, getSite, - getElementFromTopWindow, getRefererInfo, - adagioScriptFromLocalStorageCb, - getCurrentWindow, - canAccessTopWindow, + hasRtd, isRendererPreferredFromPublisher, - isNewSession }; function _getGdprConsent(bidderRequest) { @@ -399,17 +155,6 @@ function _getUspConsent(bidderRequest) { return (deepAccess(bidderRequest, 'uspConsent')) ? { uspConsent: bidderRequest.uspConsent } : false; } -function _getGppConsent(bidderRequest) { - let gpp = deepAccess(bidderRequest, 'gppConsent.gppString') - let gppSid = deepAccess(bidderRequest, 'gppConsent.applicableSections') - - if (!gpp || !gppSid) { - gpp = deepAccess(bidderRequest, 'ortb2.regs.gpp', '') - gppSid = deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', []) - } - return { gpp, gppSid } -} - function _getSchain(bidRequest) { return deepAccess(bidRequest, 'schain'); } @@ -420,6 +165,12 @@ function _getEids(bidRequest) { } } +/** + * Merge and compute video params set at mediaTypes and bidder params level + * + * @param {object} bidRequest - copy of the original bidRequest object. + * @returns {void} + */ function _buildVideoBidRequest(bidRequest) { const videoAdUnitParams = deepAccess(bidRequest, 'mediaTypes.video', {}); const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); @@ -440,22 +191,11 @@ function _buildVideoBidRequest(bidRequest) { }; if (videoParams.context && videoParams.context === OUTSTREAM) { - bidRequest.mediaTypes.video.playerName = setPlayerName(bidRequest); + videoParams.playerName = getPlayerName(bidRequest); } - // Only whitelisted OpenRTB options need to be validated. - // Other options will still remain in the `mediaTypes.video` object - // sent in the ad-request, but will be ignored by the SSP. - Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => { - if (videoParams.hasOwnProperty(paramName)) { - if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) { - bidRequest.mediaTypes.video[paramName] = videoParams[paramName]; - } else { - delete bidRequest.mediaTypes.video[paramName]; - logWarn(`${LOG_PREFIX} The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`); - } - } - }); + bidRequest.mediaTypes.video = videoParams; + validateOrtbVideoFields(bidRequest); } function _parseNativeBidResponse(bid) { @@ -587,7 +327,7 @@ function _getFloors(bidRequest) { floors.push(cleanObj({ mt: mediaType, s: isArray(size) ? `${size[0]}x${size[1]}` : undefined, - f: (!isNaN(info.floor) && info.currency === CURRENCY) ? info.floor : undefined + f: (!isNaN(info?.floor) && info?.currency === CURRENCY) ? info?.floor : undefined })); } @@ -660,13 +400,14 @@ function autoFillParams(bid) { bid.params.site = adgGlobalConf.siteId.split(':')[1]; } - // Edge case. Useful when Prebid Manager cannot handle properly params setting… - if (adgGlobalConf.useAdUnitCodeAsPlacement === true || bid.params.useAdUnitCodeAsPlacement === true) { + // `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; } - bid.params.adUnitElementId = deepAccess(bid, 'ortb2Imp.ext.data.elementId', null) || bid.params.adUnitElementId; - + bid.params.adUnitElementId = deepAccess(bid, 'ortb2Imp.ext.data.divId', bid.params.adUnitElementId); if (!bid.params.adUnitElementId) { if (adgGlobalConf.useAdUnitCodeAsAdUnitElementId === true || bid.params.useAdUnitCodeAsAdUnitElementId === true) { bid.params.adUnitElementId = bid.adUnitCode; @@ -680,209 +421,6 @@ function autoFillParams(bid) { setExtraParam(bid, 'category'); } -function getPageDimensions() { - if (isSafeFrameWindow() || !canAccessTopWindow()) { - return ''; - } - - // the page dimension can be computed on window.top only. - const wt = getWindowTop(); - const body = wt.document.querySelector('body'); - - if (!body) { - return ''; - } - const html = wt.document.documentElement; - const pageWidth = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth); - const pageHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); - - return `${pageWidth}x${pageHeight}`; -} - -/** - * @todo Move to prebid Core as Utils. - * @returns - */ -function getViewPortDimensions() { - if (!isSafeFrameWindow() && !canAccessTopWindow()) { - return ''; - } - - const viewportDims = { w: 0, h: 0 }; - - if (isSafeFrameWindow()) { - const ws = getWindowSelf(); - - if (typeof ws.$sf.ext.geom !== 'function') { - logWarn(LOG_PREFIX, 'Unable to compute from safeframe api.'); - return ''; - } - - const sfGeom = ws.$sf.ext.geom(); - - if (!sfGeom || !sfGeom.win) { - logWarn(LOG_PREFIX, 'Unable to compute from safeframe api. Missing `geom().win` property'); - return ''; - } - - viewportDims.w = Math.round(sfGeom.w); - viewportDims.h = Math.round(sfGeom.h); - } else { - // window.top based computing - const wt = getWindowTop(); - viewportDims.w = wt.innerWidth; - viewportDims.h = wt.innerHeight; - } - - return `${viewportDims.w}x${viewportDims.h}`; -} - -function getSlotPosition(adUnitElementId) { - if (!adUnitElementId) { - return ''; - } - - if (!isSafeFrameWindow() && !canAccessTopWindow()) { - return ''; - } - - const position = { x: 0, y: 0 }; - - if (isSafeFrameWindow()) { - const ws = getWindowSelf(); - - if (typeof ws.$sf.ext.geom !== 'function') { - logWarn(LOG_PREFIX, 'Unable to compute from safeframe api.'); - return ''; - } - - const sfGeom = ws.$sf.ext.geom(); - - if (!sfGeom || !sfGeom.self) { - logWarn(LOG_PREFIX, 'Unable to compute from safeframe api. Missing `geom().self` property'); - return ''; - } - - position.x = Math.round(sfGeom.t); - position.y = Math.round(sfGeom.l); - } else if (canAccessTopWindow()) { - try { - // window.top based computing - const wt = getWindowTop(); - const d = wt.document; - - let domElement; - - if (inIframe() === true) { - const ws = getWindowSelf(); - const currentElement = ws.document.getElementById(adUnitElementId); - domElement = internal.getElementFromTopWindow(currentElement, ws); - } else { - domElement = wt.document.getElementById(adUnitElementId); - } - - if (!domElement) { - return ''; - } - - let box = domElement.getBoundingClientRect(); - - const docEl = d.documentElement; - const body = d.body; - const clientTop = d.clientTop || body.clientTop || 0; - const clientLeft = d.clientLeft || body.clientLeft || 0; - const scrollTop = wt.pageYOffset || docEl.scrollTop || body.scrollTop; - const scrollLeft = wt.pageXOffset || docEl.scrollLeft || body.scrollLeft; - - const elComputedStyle = wt.getComputedStyle(domElement, null); - const mustDisplayElement = elComputedStyle.display === 'none'; - - if (mustDisplayElement) { - logWarn(LOG_PREFIX, 'The element is hidden. The slot position cannot be computed.'); - } - - position.x = Math.round(box.left + scrollLeft - clientLeft); - position.y = Math.round(box.top + scrollTop - clientTop); - } catch (err) { - logError(LOG_PREFIX, err); - return ''; - } - } else { - return ''; - } - - return `${position.x}x${position.y}`; -} - -function getTimestampUTC() { - // timestamp returned in seconds - return Math.floor(new Date().getTime() / 1000) - new Date().getTimezoneOffset() * 60; -} - -function getPrintNumber(adUnitCode, bidderRequest) { - if (!bidderRequest.bids || !bidderRequest.bids.length) { - return 1; - } - const adagioBid = find(bidderRequest.bids, bid => bid.adUnitCode === adUnitCode); - return adagioBid.bidderRequestsCount || 1; -} - -/** - * domLoading feature is computed on window.top if reachable. - */ -function getDomLoadingDuration() { - let domLoadingDuration = -1; - let performance; - - performance = (canAccessTopWindow()) ? getWindowTop().performance : getWindowSelf().performance; - - if (performance && performance.timing && performance.timing.navigationStart > 0) { - const val = performance.timing.domLoading - performance.timing.navigationStart; - if (val > 0) { - domLoadingDuration = val; - } - } - - return domLoadingDuration; -} - -function storeRequestInAdagioNS(bidRequest) { - const w = getCurrentWindow(); - // Store adUnits config. - // If an adUnitCode has already been stored, it will be replaced. - w.ADAGIO = w.ADAGIO || {}; - w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits.filter((adUnit) => adUnit.code !== bidRequest.adUnitCode); - - let printNumber - if (bidRequest.features && bidRequest.features.print_number) { - printNumber = bidRequest.features.print_number; - } else if (bidRequest.params.features && bidRequest.params.features.print_number) { - printNumber = bidRequest.params.features.print_number; - } - - w.ADAGIO.pbjsAdUnits.push({ - code: bidRequest.adUnitCode, - mediaTypes: bidRequest.mediaTypes || {}, - sizes: (bidRequest.mediaTypes && bidRequest.mediaTypes.banner && Array.isArray(bidRequest.mediaTypes.banner.sizes)) ? bidRequest.mediaTypes.banner.sizes : bidRequest.sizes, - bids: [{ - bidder: bidRequest.bidder, - params: bidRequest.params // use the updated bid.params object with auto-detected params - }], - auctionId: bidRequest.auctionId, // this auctionId has been generated by adagioBidAdapter - pageviewId: internal.getPageviewId(), - printNumber, - localPbjs: '$$PREBID_GLOBAL$$', - localPbjsRef: getGlobal() - }); - - // (legacy) Store internal adUnit information - w.ADAGIO.adUnits[bidRequest.adUnitCode] = { - auctionId: bidRequest.auctionId, // this auctionId has been generated by adagioBidAdapter - pageviewId: internal.getPageviewId(), - printNumber, - }; -} - // See https://support.bluebillywig.com/developers/vast-renderer/ const OUTSTREAM_RENDERER = { bootstrapPlayer: function(bid) { @@ -964,9 +502,9 @@ export const spec = { autoFillParams(bid); + // Note: `bid.params.placement` is not related to the video param `placement`. if (!(bid.params.organizationId && bid.params.site && bid.params.placement)) { logWarn(`${LOG_PREFIX} at least one required param is missing.`); - // internal.enqueue(debugData()); return false; } @@ -978,22 +516,30 @@ export const spec = { validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); const secure = (location.protocol === 'https:') ? 1 : 0; - const device = internal.getDevice(); - const site = internal.getSite(bidderRequest); - const pageviewId = internal.getPageviewId(); + const device = _internal.getDevice(bidderRequest?.ortb2); + const site = _internal.getSite(bidderRequest); + const pageviewId = _internal.getAdagioNs().pageviewId; const gdprConsent = _getGdprConsent(bidderRequest) || {}; const uspConsent = _getUspConsent(bidderRequest) || {}; const coppa = _getCoppa(); - const gppConsent = _getGppConsent(bidderRequest) + const { gpp, gpp_sid: gppSid } = deepAccess(bidderRequest, 'ortb2.regs', {}); const schain = _getSchain(validBidRequests[0]); const eids = _getEids(validBidRequests[0]) || []; const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled') - const usIfr = syncEnabled && userSync.canBidderRegisterSync('iframe', 'adagio') + const canSyncWithIframe = syncEnabled && userSync.canBidderRegisterSync('iframe', 'adagio') // We don't validate the dsa object in adapter and let our server do it. const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); - const aucId = generateUUID() + // If no session data is provided, we always generate a new one. + const sessionData = deepAccess(bidderRequest, 'ortb2.site.ext.data.adg_rtd.session', {}); + if (!Object.keys(sessionData).length) { + logInfo(LOG_PREFIX, 'No session data provided. A new session is be generated.') + sessionData.new = true; + sessionData.rnd = Math.random() + } + + const aucId = deepAccess(bidderRequest, 'ortb2.site.ext.data.adg_rtd.uid') || generateUUID() const adUnits = validBidRequests.map(rawBidRequest => { const bidRequest = deepClone(rawBidRequest); @@ -1001,13 +547,6 @@ export const spec = { // Fix https://github.com/prebid/Prebid.js/issues/9781 bidRequest.auctionId = aucId - const globalFeatures = GlobalExchange.getOrSetGlobalFeatures(); - const features = { - ...globalFeatures, - print_number: getPrintNumber(bidRequest.adUnitCode, bidderRequest).toString(), - adunit_position: getSlotPosition(bidRequest.params.adUnitElementId) // adUnitElementId à déplacer ??? - }; - // Force the Split Keyword to be a String if (bidRequest.params.splitKeyword) { if (isStr(bidRequest.params.splitKeyword) || isNumber(bidRequest.params.splitKeyword)) { @@ -1051,24 +590,6 @@ export const spec = { } } - Object.keys(features).forEach((prop) => { - if (features[prop] === '') { - delete features[prop]; - } - }); - - bidRequest.features = features; - - internal.enqueue({ - action: 'features', - ts: Date.now(), - data: { - features: bidRequest.features, - params: bidRequest.params, - adUnitCode: bidRequest.adUnitCode - } - }); - // Handle priceFloors module // We need to use `rawBidRequest` as param because: // - adagioBidAdapter generates its own auctionId due to transmitTid activity limitation (see https://github.com/prebid/Prebid.js/pull/10079) @@ -1122,8 +643,23 @@ export const spec = { bidRequest.gpid = gpid; } - // store the whole bidRequest (adUnit) object in the ADAGIO namespace. - storeRequestInAdagioNS(bidRequest); + let instl = deepAccess(bidRequest, 'ortb2Imp.instl'); + if (instl !== undefined) { + bidRequest.instl = instl === 1 || instl === '1' ? 1 : undefined; + } + let rwdd = deepAccess(bidRequest, 'ortb2Imp.rwdd'); + if (rwdd !== undefined) { + bidRequest.rwdd = rwdd === 1 || rwdd === '1' ? 1 : undefined; + } + + // features are added by the adagioRtdProvider. + const rawFeatures = { + ...deepAccess(bidRequest, 'ortb2.site.ext.data.adg_rtd.features', {}), + print_number: (bidRequest.bidderRequestsCount || 1).toString(), + adunit_position: deepAccess(bidRequest, 'ortb2Imp.ext.data.adg_rtd.adunit_position', null) + } + // Clean the features object from null or undefined values. + bidRequest.features = Object.entries(rawFeatures).reduce((a, [k, v]) => (v == null ? a : (a[k] = v, a)), {}) // Remove some params that are not needed on the server side. delete bidRequest.params.siteId; @@ -1141,6 +677,8 @@ export const spec = { nativeParams: bidRequest.nativeParams, score: bidRequest.score, transactionId: bidRequest.transactionId, + instl: bidRequest.instl, + rwdd: bidRequest.rwdd, } return adUnit; @@ -1160,7 +698,6 @@ export const spec = { // Those params are not sent to the server. // They are used for further operations on analytics adapter. validBidRequests.forEach(rawBidRequest => { - rawBidRequest.params.adagioAuctionId = aucId rawBidRequest.params.pageviewId = pageviewId }); @@ -1171,18 +708,21 @@ export const spec = { url: ENDPOINT, data: { organizationId: organizationId, + hasRtd: _internal.hasRtd() ? 1 : 0, secure: secure, device: device, site: site, pageviewId: pageviewId, adUnits: groupedAdUnits[organizationId], - data: GlobalExchange.getExchangeData(), + data: { + session: sessionData + }, regs: { gdpr: gdprConsent, coppa: coppa, ccpa: uspConsent, - gpp: gppConsent.gpp, - gppSid: gppConsent.gppSid, + gpp: gpp || '', + gppSid: gppSid || [], dsa: dsa // populated if exists }, schain: schain, @@ -1190,9 +730,7 @@ export const spec = { eids: eids }, prebidVersion: '$prebid.version$', - featuresVersion: FEATURES_VERSION, - usIfr: usIfr, - adgjs: storage.localStorageIsEnabled() + usIfr: canSyncWithIframe }, options: { contentType: 'text/plain' @@ -1209,11 +747,13 @@ export const spec = { const response = serverResponse.body; if (response) { if (response.data) { - internal.enqueue({ - action: 'ssp-data', - ts: Date.now(), - data: response.data - }); + if (_internal.hasRtd()) { + _internal.getAdagioNs().queue.push({ + action: 'ssp-data', + ts: Date.now(), + data: response.data + }); + } } if (response.bids) { response.bids.forEach(bidObj => { @@ -1275,22 +815,6 @@ export const spec = { return syncs; }, - - /** - * Handle custom logic in s2s context - * - * @param {*} params - * @param {boolean} isOrtb Is an s2s context - * @param {*} adUnit - * @param {*} bidRequests - * @returns {object} updated params - */ - transformBidParams(params, isOrtb, adUnit, bidRequests) { - // We do not have a prebid server adapter. So let's return unchanged params. - return params; - } }; -initAdagio(); - registerBidder(spec); diff --git a/modules/adagioRtdProvider.js b/modules/adagioRtdProvider.js new file mode 100644 index 00000000000..9c0e8aabed9 --- /dev/null +++ b/modules/adagioRtdProvider.js @@ -0,0 +1,749 @@ +/** + * This module adds the adagio provider to the Real Time Data module (rtdModule). + * The {@link module:modules/realTimeData} module is required. + * @module modules/adagioRtdProvider + * @requires module:modules/realTimeData + */ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import adapterManager from '../src/adapterManager.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { submodule } from '../src/hook.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { + canAccessWindowTop, + deepAccess, + deepSetValue, + generateUUID, + getDomLoadingDuration, + getSafeframeGeometry, + getUniqueIdentifierStr, + getWinDimensions, + getWindowSelf, + getWindowTop, + inIframe, + isNumber, + isSafeFrameWindow, + isStr, + prefixLog +} from '../src/utils.js'; +import { _ADAGIO, getBestWindowForAdagio } from '../libraries/adagioUtils/adagioUtils.js'; +import { getGptSlotInfoForAdUnitCode } from '../libraries/gptUtils/gptUtils.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('../modules/rtdModule/index.js').adUnit} adUnit + */ +const SUBMODULE_NAME = 'adagio'; +const ADAGIO_BIDDER_CODE = 'adagio'; +const GVLID = 617; +const SCRIPT_URL = 'https://script.4dex.io/a/latest/adagio.js'; +const LATEST_ABTEST_VERSION = 2; +export const PLACEMENT_SOURCES = { + ORTB: 'ortb', // implicit default, not used atm. + ADUNITCODE: 'code', + GPID: 'gpid' +}; +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); + +const { logError, logWarn } = prefixLog('AdagioRtdProvider:'); + +// Guard to avoid storing the same bid data several times. +const guard = new Set(); + +/** + * Store the sampling data. + * This data is used to determine if beacons should be sent to adagio. + * The sampling data + */ +const _SESSION = (function() { + /** + * @type {SessionData} + */ + const data = { + session: {} + }; + + return { + init: () => { + // helper function to determine if the session is new. + const isNewSession = (expiry) => { + return (!isNumber(expiry) || Date.now() > expiry); + }; + + storage.getDataFromLocalStorage('adagio', (storageValue) => { + // session can be an empty object + const { rnd, vwSmplg, vwSmplgNxt, expiry, lastActivityTime, id, pages, testName: legacyTestName, testVersion: legacyTestVersion } = _internal.getSessionFromLocalStorage(storageValue); + + const isNewSess = isNewSession(expiry); + + const abTest = _internal.getAbTestFromLocalStorage(storageValue); + + // if abTest is defined it means that the website is using the new version of the snippet + const v = abTest ? LATEST_ABTEST_VERSION : undefined; + + data.session = { + rnd, + pages: pages || 1, + new: isNewSess, // legacy: `new` was used but the choosen name is not good. + // Don't use values if they are not defined. + ...(v !== undefined && { v }), + ...(vwSmplg !== undefined && { vwSmplg }), + ...(vwSmplgNxt !== undefined && { vwSmplgNxt }), + ...(expiry !== undefined && { expiry }), + ...(lastActivityTime !== undefined && { lastActivityTime }), // legacy: used by older version of the snippet + ...(id !== undefined && { id }), + }; + + if (isNewSess) { + data.session.new = true; + data.session.id = generateUUID(); + data.session.rnd = Math.random(); + } + + if (v === LATEST_ABTEST_VERSION) { + const { testName, testVersion, expiry: abTestExpiry, sessionId } = abTest; + if (abTestExpiry && abTestExpiry > Date.now() && (!sessionId || sessionId === data.session.id)) { // if AbTest didn't set a session id, it's probably because it's a new one and it didn't retrieve it yet, assume it's okay to get test Name and Version. + if (testName && testVersion) { + data.session.testName = testName; + data.session.testVersion = testVersion; + } + } + } else { + if (legacyTestName && legacyTestVersion) { + data.session.testName = legacyTestName; + data.session.testVersion = legacyTestVersion; + } + } + + _internal.getAdagioNs().queue.push({ + action: 'session', + ts: Date.now(), + data: { + session: { + ...data.session + } + } + }); + }); + }, + get: function() { + return data.session; + } + }; +})(); + +const _FEATURES = (function() { + /** + * @type {Features} + */ + const features = { + initialized: false, + data: {}, + }; + + return { + // reset is used for testing purpose + reset: function() { + features.initialized = false; + features.data = {}; + }, + get: function() { + const w = getBestWindowForAdagio(); + + if (!features.initialized) { + features.data = { + page_dimensions: getPageDimensions().toString(), + viewport_dimensions: getViewPortDimensions().toString(), + user_timestamp: getTimestampUTC().toString(), + dom_loading: getDomLoadingDuration(w).toString(), + }; + features.initialized = true; + } + + return { ...features.data }; + } + }; +})(); + +export const _internal = { + getAdagioNs: function() { + return _ADAGIO; + }, + + getSession: function() { + return _SESSION; + }, + + getFeatures: function() { + return _FEATURES; + }, + + getGuard: function() { + return guard; + }, + + /** + * Ensure that the bidder is Adagio. + * + * @param {string} alias + * @returns {boolean} + */ + isAdagioBidder: function (alias) { + if (!alias) { + return false; + } + return (alias + adapterManager.aliasRegistry[alias]).toLowerCase().includes(ADAGIO_BIDDER_CODE); + }, + + /** + * Returns the session data from the localStorage. + * + * @param {string} storageValue - The value stored in the localStorage. + * @returns {Session} + */ + getSessionFromLocalStorage: function(storageValue) { + const _default = { + new: true, + rnd: Math.random() + }; + + const obj = this.getObjFromStorageValue(storageValue); + + return (!obj || !obj.session) ? _default : obj.session; + }, + + /** + * Returns the abTest data from the localStorage. + * + * @param {string} storageValue - The value stored in the localStorage. + * @returns {AbTest} + */ + getAbTestFromLocalStorage: function(storageValue) { + const obj = this.getObjFromStorageValue(storageValue); + + return (!obj || !obj.abTest) ? null : obj.abTest; + }, + + /** + * Returns the parsed data from the localStorage. + * + * @param {string} storageValue - The value stored in the localStorage. + * @returns {Object} + */ + getObjFromStorageValue: function(storageValue) { + return JSON.parse(storageValue, function(name, value) { + if (name.charAt(0) !== '_' || name === '') { + return value; + } + }); + } +}; + +function loadAdagioScript(config) { + storage.localStorageIsEnabled(isValid => { + if (!isValid) { + return; + } + + loadExternalScript(SCRIPT_URL, MODULE_TYPE_RTD, SUBMODULE_NAME, undefined, undefined, { + id: `adagiojs-${getUniqueIdentifierStr()}`, + 'data-pid': config.params.organizationId + }); + }); +} + +/** + * Initialize the Adagio RTD Module. + * @param {Object} config + * @param {Object} _userConsent + * @returns {boolean} + */ +function init(config, _userConsent) { + if (!isStr(config.params?.organizationId) || !isStr(config.params?.site)) { + logError('organizationId is required and must be a string.'); + return false; + } + + _internal.getAdagioNs().hasRtd = true; + + _internal.getSession().init(); + + registerEventsForAdServers(config); + + loadAdagioScript(config); + + return true; +} + +/** + * onBidRequest is called for each bidder during an auction and contains the bids for that bidder. + * + * @param {*} bidderRequest + * @param {*} config + * @param {*} _userConsent + */ +function onBidRequest(bidderRequest, config, _userConsent) { + // setTimeout trick to ensure that the `bidderRequest.params` values updated by a bidder adapter are taken into account. + // @todo: Check why we have to do it like this, and if there is a better way. Check how the event is dispatched in rtdModule/index.js + setTimeout(() => { + bidderRequest.bids.forEach(bid => { + const uid = deepAccess(bid, 'ortb2.site.ext.data.adg_rtd.uid'); + if (!uid) { + logError('The `uid` is required to store the request in the ADAGIO namespace.'); + return; + } + + // No need to store the same info several times. + // `uid` is unique as it is generated by the RTD module itself for each auction. + const key = `${bid.adUnitCode}-${uid}`; + if (_internal.getGuard().has(key)) { + return; + } + + _internal.getGuard().add(key); + storeRequestInAdagioNS(bid, config); + }); + }, 1); +} + +/** + * onGetBidRequestData is called once per auction. + * Update both the `ortb2Fragments` and `ortb2Imp` objects with features computed for Adagio. + * + * @param {*} bidReqConfig + * @param {*} callback + * @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 = { + uid: generateUUID(), + pageviewId: _ADAGIO.pageviewId, + features: { ...features }, + session: { ..._SESSION.get() } + }; + + deepSetValue(ortb2Site, `ext.data.adg_rtd`, ext); + + const adUnits = bidReqConfig.adUnits || getGlobal().adUnits || []; + adUnits.forEach(adUnit => { + adUnit.ortb2Imp = adUnit.ortb2Imp || {}; + const ortb2Imp = deepAccess(adUnit, 'ortb2Imp'); + + // A divId is required to compute the slot position and later to track viewability. + // If nothing has been explicitly set, we try to get the divId from the GPT slot and fallback to the adUnit code in last resort. + let divId = deepAccess(ortb2Imp, 'ext.data.divId') + if (!divId) { + divId = getGptSlotInfoForAdUnitCode(adUnit.code).divId; + deepSetValue(ortb2Imp, `ext.data.divId`, divId || adUnit.code); + } + + 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 + let mustWarnOrtb2 = false; + if (!deepAccess(ortb2Site, 'ext.data.pagetype') && adagioBid.params.pagetype) { + deepSetValue(ortb2Site, 'ext.data.pagetype', adagioBid.params.pagetype); + mustWarnOrtb2 = true; + } + if (!deepAccess(ortb2Site, 'ext.data.category') && adagioBid.params.category) { + 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) { + logWarn('`pagetype` and `category` must be defined in the FPD `ortb2.site.ext.data` object. Relying on `adUnits[].bids.adagio.params` is deprecated.'); + } + if (mustWarnOrtb2Imp) { + logWarn('`placement` must be defined in the FPD `adUnits[].ortb2Imp.ext.data` object. Relying on `adUnits[].bids.adagio.params` is deprecated.'); + } + } + }); + + callback(); +} + +export const adagioRtdSubmodule = { + name: SUBMODULE_NAME, + gvlid: GVLID, + init: init, + getBidRequestData: onGetBidRequestData, + onBidRequestEvent: onBidRequest, +}; + +submodule('realTimeData', adagioRtdSubmodule); + +// --- +// +// internal functions moved from adagioBidAdapter.js to adagioRtdProvider.js. +// +// Several of these functions could be redistribued in Prebid.js core or in a library +// +// --- + +/** + * storeRequestInAdagioNS store ad-units in the ADAGIO namespace for further usage. + * Not all the properties are stored, only the ones that are useful for adagio.js. + * + * @param {*} bid - The bid object. Correspond to the bidRequest.bids[i] object. + * @param {*} config - The RTD module configuration. + * @returns {void} + */ +function storeRequestInAdagioNS(bid, config) { + try { + const { bidder, adUnitCode, mediaTypes, params, auctionId, bidderRequestsCount, ortb2, ortb2Imp } = bid; + + const { organizationId, site } = config.params; + + const ortb2Data = deepAccess(ortb2, 'site.ext.data', {}); + const ortb2ImpData = deepAccess(ortb2Imp, 'ext.data', {}); + + // TODO: `bidderRequestsCount` must be incremented with s2s context, actually works only for `client` context + // see: https://github.com/prebid/Prebid.js/pull/11295/files#diff-d5c9b255c545e5097d1cd2f49e7dad309b731e34d788f9c28432ad43ebcd7785L114 + const data = { + bidder, + adUnitCode, + mediaTypes, + params, + auctionId, + bidderRequestsCount, + ortb2: ortb2Data, + ortb2Imp: ortb2ImpData, + localPbjs: '$$PREBID_GLOBAL$$', + localPbjsRef: getGlobal(), + organizationId, + site + }; + + _internal.getAdagioNs().queue.push({ + action: 'store', + ts: Date.now(), + data + }); + } catch (e) { + logError(e); + } +} + +function getElementFromTopWindow(element, currentWindow) { + try { + if (getWindowTop() === currentWindow) { + if (!element.getAttribute('id')) { + element.setAttribute('id', `adg-${getUniqueIdentifierStr()}`); + } + return element; + } else { + const frame = currentWindow.frameElement; + const frameClientRect = getBoundingClientRect(frame); + const elementClientRect = getBoundingClientRect(element); + + if (frameClientRect.width !== elementClientRect.width || frameClientRect.height !== elementClientRect.height) { + return false; + } + + return getElementFromTopWindow(frame, currentWindow.parent); + } + } catch (err) { + logWarn(err); + return false; + } +}; + +function getSlotPosition(divId) { + if (!isSafeFrameWindow() && !canAccessWindowTop()) { + return ''; + } + + const position = { x: 0, y: 0 }; + + if (isSafeFrameWindow()) { + const { self } = getSafeframeGeometry() || {}; + + if (!self) { + return ''; + } + + position.x = Math.round(self.t); + position.y = Math.round(self.l); + } else { + try { + // window.top based computing + const wt = getWindowTop(); + const d = wt.document; + + let domElement; + + if (inIframe() === true) { + const ws = getWindowSelf(); + const currentElement = ws.document.getElementById(divId); + domElement = getElementFromTopWindow(currentElement, ws); + } else { + domElement = wt.document.getElementById(divId); + } + + if (!domElement) { + return ''; + } + + let box = getBoundingClientRect(domElement); + + const windowDimensions = getWinDimensions(); + + const body = d.body; + const clientTop = d.clientTop || body.clientTop || 0; + const clientLeft = d.clientLeft || body.clientLeft || 0; + const scrollTop = wt.pageYOffset || windowDimensions.document.documentElement.scrollTop || windowDimensions.document.body.scrollTop; + const scrollLeft = wt.pageXOffset || windowDimensions.document.documentElement.scrollLeft || windowDimensions.document.body.scrollLeft; + + const elComputedStyle = wt.getComputedStyle(domElement, null); + const mustDisplayElement = elComputedStyle.display === 'none'; + + if (mustDisplayElement) { + logWarn('The element is hidden. The slot position cannot be computed.'); + } + + position.x = Math.round(box.left + scrollLeft - clientLeft); + position.y = Math.round(box.top + scrollTop - clientTop); + } catch (err) { + logError(err); + return ''; + } + } + + return `${position.x}x${position.y}`; +} + +function getPageDimensions() { + if (isSafeFrameWindow() || !canAccessWindowTop()) { + return ''; + } + + // the page dimension can be computed on window.top only. + const wt = getWindowTop(); + const body = wt.document.querySelector('body'); + + if (!body) { + return ''; + } + const html = wt.document.documentElement; + const pageWidth = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth); + const pageHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); + + return `${pageWidth}x${pageHeight}`; +} + +function getViewPortDimensions() { + if (!isSafeFrameWindow() && !canAccessWindowTop()) { + return ''; + } + + const viewportDims = { w: 0, h: 0 }; + + if (isSafeFrameWindow()) { + const { win } = getSafeframeGeometry() || {}; + + if (!win) { + return ''; + } + + viewportDims.w = Math.round(win.w); + viewportDims.h = Math.round(win.h); + } else { + // window.top based computing + const { innerWidth, innerHeight } = getWinDimensions(); + viewportDims.w = innerWidth; + viewportDims.h = innerHeight; + } + + return `${viewportDims.w}x${viewportDims.h}`; +} + +function getTimestampUTC() { + // timestamp returned in seconds + return Math.floor(new Date().getTime() / 1000) - new Date().getTimezoneOffset() * 60; +} + +/** + * registerEventsForAdServers bind adagio listeners to ad-server events. + * Theses events are used to track the viewability and attention. + * + * @param {*} config + * @returns {void} + */ +function registerEventsForAdServers(config) { + const GPT_EVENTS = new Set([ + 'impressionViewable', + 'slotRenderEnded', + 'slotVisibilityChanged', + ]); + + const SAS_EVENTS = new Set([ + 'noad', + 'setHeaderBiddingWinner', + ]); + + const AST_EVENTS = new Set([ + 'adLoaded', + ]); + + // Listen to ad-server events in current window + // as we can be safe in a Post-Bid scenario. + const ws = getWindowSelf(); + + // Keep a reference to the window on which the listener is attached. + // this is used to avoid to bind event several times. + if (!Array.isArray(_internal.getAdagioNs().windows)) { + _internal.getAdagioNs().windows = []; + } + + let selfStoredWindow = _internal.getAdagioNs().windows.find(_w => _w.self === ws); + if (!selfStoredWindow) { + selfStoredWindow = { self: ws }; + _internal.getAdagioNs().windows.push(selfStoredWindow); + } + + const register = (namespace, command, selfWindow, adserver, cb) => { + try { + if (selfWindow.adserver === adserver) { + return; + } + ws[namespace] = ws[namespace] || {}; + ws[namespace][command] = ws[namespace][command] || []; + cb(); + } catch (e) { + logError(e); + } + }; + + register('googletag', 'cmd', ws, 'gpt', () => { + ws.googletag.cmd.push(() => { + GPT_EVENTS.forEach(eventName => { + ws.googletag.pubads().addEventListener(eventName, (args) => { + _internal.getAdagioNs().queue.push({ + action: 'gpt-event', + data: { eventName, args, _window: ws }, + ts: Date.now(), + }); + }); + }); + selfStoredWindow.adserver = 'gpt'; + }); + }); + + register('sas', 'cmd', ws, 'sas', () => { + ws.sas.cmd.push(() => { + SAS_EVENTS.forEach(eventName => { + ws.sas.events.on(eventName, (args) => { + _internal.getAdagioNs().queue.push({ + action: 'sas-event', + data: { eventName, args, _window: ws }, + ts: Date.now(), + }); + }); + }); + selfStoredWindow.adserver = 'sas'; + }); + }); + + // https://learn.microsoft.com/en-us/xandr/seller-tag/on-event + register('apntag', 'anq', ws, 'ast', () => { + ws.apntag.anq.push(() => { + AST_EVENTS.forEach(eventName => { + ws.apntag.onEvent(eventName, function () { + _internal.getAdagioNs().queue.push({ + action: 'ast-event', + data: { eventName, args: arguments, _window: ws }, + ts: Date.now(), + }); + }); + }); + selfStoredWindow.adserver = 'ast'; + }); + }); +}; + +// --- end of internal functions ----- // + +/** + * @typedef {Object} AdagioWindow + * @property {Window} self + * @property {string} adserver - 'gpt', 'sas', 'ast' + */ + +/** + * @typedef {Object} AdagioGlobal + * @property {Object} adUnits + * @property {Array} pbjsAdUnits + * @property {Array} queue + * @property {Array} windows + */ + +/** + * @typedef {Object} Session + * @property {string} id - uuid of the session. + * @property {boolean} new - True if the session is new. + * @property {number} rnd - Random number used to determine if the session is new. + * @property {number} vwSmplg - View sampling rate. + * @property {number} vwSmplgNxt - Next view sampling rate. + * @property {number} expiry - Timestamp after which session should be considered expired. + * @property {number} lastActivityTime - Last activity time. + * @property {number} pages - current number of pages seen. + * @property {string} testName - The test name defined by the publisher. Legacy only present for websites with older abTest snippet. + * @property {string} testVersion - 'clt', 'srv'. Legacy only present for websites with older abTest snippet. + */ + +/** + * @typedef {Object} AbTest + * @property {string} testName - The test name defined by the publisher. + * @property {string} testVersion - 'clt', 'srv'. + * @property {string} sessionId - uuid of the session. + * @property {number} expiry - Timestamp after which session should be considered expired. + */ + +/** + * @typedef {Object} SessionData + * @property {Session} session - the session data. + */ + +/** + * @typedef {Object} Features + * @property {boolean} initialized - True if the features are initialized. + * @property {Object} data - the features data. + */ diff --git a/modules/adagioRtdProvider.md b/modules/adagioRtdProvider.md new file mode 100644 index 00000000000..a51137d571f --- /dev/null +++ b/modules/adagioRtdProvider.md @@ -0,0 +1,38 @@ +# Overview + +Module Name: Adagio Rtd Provider +Module Type: Rtd Provider +Maintainer: dev@adagio.io + +# Description + +This module is exclusively used in combination with Adagio Bidder Adapter (SSP) and/or with Adagio prebid server endpoint, and mandatory for Adagio customers. +It computes and collects data required to leverage Adagio viewability and attention prediction engine. + +Features are computed for the Adagio bidder only and placed into `ortb2.ext` and `AdUnit.ortb2Imp.ext.data`. + +To collect data, an external script is loaded by the provider. +It relies on the listening of ad-server events. +Supported ad-servers are GAM, Smart Ad Server, Xandr. Custom ad-server can also be used, +please contact [contact@adagio.io](contact@adagio.io) for more information. + +# Integration + +```bash +gulp build --modules=adagioBidAdapter,rtdModule,adagioRtdProvider +``` + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders:[{ + name: 'adagio', + params: { + organizationId: '1000' // Required. Provided by Adagio + site: 'my-site' // Required. Provided by Adagio + placementSource: 'ortb' // Optional. Where to find the "placement" value. Possible values: 'ortb' | 'code' | 'gpid' + } + }] + } +}); +``` diff --git a/modules/adbookpspBidAdapter.js b/modules/adbookpspBidAdapter.js deleted file mode 100644 index cb03f2ffc17..00000000000 --- a/modules/adbookpspBidAdapter.js +++ /dev/null @@ -1,830 +0,0 @@ -import {find, includes} from '../src/polyfill.js'; -import {config} from '../src/config.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {getStorageManager} from '../src/storageManager.js'; -import { - deepAccess, - deepSetValue, - flatten, - generateUUID, - inIframe, - isArray, - isEmptyStr, - isNumber, - isPlainObject, - isStr, - logError, - logWarn, - triggerPixel, - uniques -} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - -/** - * CONSTANTS - */ - -export const VERSION = '1.0.0'; -const EXCHANGE_URL = 'https://ex.fattail.com/openrtb2'; -const WIN_TRACKING_URL = 'https://ev.fattail.com/wins'; -const BIDDER_CODE = 'adbookpsp'; -const USER_ID_KEY = 'hb_adbookpsp_uid'; -const USER_ID_COOKIE_EXP = 2592000000; // lasts 30 days -const BID_TTL = 300; -const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; -const DEFAULT_CURRENCY = 'USD'; -const VIDEO_PARAMS = [ - 'mimes', - 'minduration', - 'maxduration', - 'protocols', - 'w', - 'h', - 'startdelay', - 'placement', - 'linearity', - 'skip', - 'skipmin', - 'skipafter', - 'sequence', - 'battr', - 'maxextended', - 'minbitrate', - 'maxbitrate', - 'boxingallowed', - 'playbackmethod', - 'playbackend', - 'delivery', - 'pos', - 'companionad', - 'api', - 'companiontype', - 'ext', -]; -const TARGETING_VALUE_SEPARATOR = ','; - -export const DEFAULT_BIDDER_CONFIG = { - bidTTL: BID_TTL, - defaultCurrency: DEFAULT_CURRENCY, - exchangeUrl: EXCHANGE_URL, - winTrackingEnabled: true, - winTrackingUrl: WIN_TRACKING_URL, - orgId: null, -}; - -config.setDefaults({ - adbookpsp: DEFAULT_BIDDER_CONFIG, -}); - -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: SUPPORTED_MEDIA_TYPES, - - buildRequests, - getUserSyncs, - interpretResponse, - isBidRequestValid, - onBidWon, -}; - -registerBidder(spec); - -/** - * BID REQUEST - */ - -function isBidRequestValid(bidRequest) { - return ( - hasRequiredParams(bidRequest) && - (isValidBannerRequest(bidRequest) || isValidVideoRequest(bidRequest)) - ); -} - -function buildRequests(validBidRequests, bidderRequest) { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - const requests = []; - - if (validBidRequests.length > 0) { - requests.push({ - method: 'POST', - url: getBidderConfig('exchangeUrl'), - options: { - contentType: 'application/json', - withCredentials: true, - }, - data: buildRequest(validBidRequests, bidderRequest), - }); - } - - return requests; -} - -function buildRequest(validBidRequests, bidderRequest) { - const request = { - id: bidderRequest.bidderRequestId, - tmax: bidderRequest.timeout, - site: { - domain: bidderRequest.refererInfo.domain, - page: bidderRequest.refererInfo.page, - ref: bidderRequest.refererInfo.ref, - }, - source: buildSource(validBidRequests, bidderRequest), - device: buildDevice(), - regs: buildRegs(bidderRequest), - user: buildUser(bidderRequest), - imp: validBidRequests.map(buildImp), - ext: { - adbook: { - config: getBidderConfig(), - version: { - prebid: '$prebid.version$', - adapter: VERSION, - }, - }, - }, - }; - - return JSON.stringify(request); -} - -function buildDevice() { - const { innerWidth, innerHeight } = common.getWindowDimensions(); - - const device = { - w: innerWidth, - h: innerHeight, - js: true, - ua: navigator.userAgent, - dnt: - navigator.doNotTrack === 'yes' || - navigator.doNotTrack == '1' || - navigator.msDoNotTrack == '1' - ? 1 - : 0, - }; - - const deviceConfig = common.getConfig('device'); - - if (isPlainObject(deviceConfig)) { - return { ...device, ...deviceConfig }; - } - - return device; -} - -function buildRegs(bidderRequest) { - const regs = { - coppa: common.getConfig('coppa') === true ? 1 : 0, - }; - - if (bidderRequest.gdprConsent) { - deepSetValue( - regs, - 'ext.gdpr', - bidderRequest.gdprConsent.gdprApplies ? 1 : 0 - ); - deepSetValue( - regs, - 'ext.gdprConsentString', - bidderRequest.gdprConsent.consentString || '' - ); - } - - if (bidderRequest.uspConsent) { - deepSetValue(regs, 'ext.us_privacy', bidderRequest.uspConsent); - } - - return regs; -} - -function buildSource(bidRequests, bidderRequest) { - const source = { - fd: 1, - tid: bidderRequest.ortb2.source.tid, - }; - const schain = deepAccess(bidRequests, '0.schain'); - - if (schain) { - deepSetValue(source, 'ext.schain', schain); - } - - return source; -} - -function buildUser(bidderRequest) { - const user = { - id: getUserId(), - }; - - if (bidderRequest.gdprConsent) { - user.gdprConsentString = bidderRequest.gdprConsent.consentString || ''; - } - - return user; -} - -function buildImp(bidRequest) { - let impBase = { - id: bidRequest.bidId, - tagid: bidRequest.adUnitCode, - ext: buildImpExt(bidRequest), - }; - - return Object.keys(bidRequest.mediaTypes) - .filter((mediaType) => includes(SUPPORTED_MEDIA_TYPES, mediaType)) - .reduce((imp, mediaType) => { - return { - ...imp, - [mediaType]: buildMediaTypeObject(mediaType, bidRequest), - }; - }, impBase); -} - -function buildMediaTypeObject(mediaType, bidRequest) { - switch (mediaType) { - case BANNER: - return buildBannerObject(bidRequest); - case VIDEO: - return buildVideoObject(bidRequest); - default: - logWarn(`${BIDDER_CODE}: Unsupported media type ${mediaType}!`); - } -} - -function buildBannerObject(bidRequest) { - const format = bidRequest.mediaTypes.banner.sizes.map((size) => { - const [w, h] = size; - - return { w, h }; - }); - const { w, h } = format[0]; - - return { - pos: 0, - topframe: inIframe() ? 0 : 1, - format, - w, - h, - }; -} - -function buildVideoObject(bidRequest) { - const { w, h } = getVideoSize(bidRequest); - let videoObj = { - w, - h, - }; - - for (const param of VIDEO_PARAMS) { - const paramsValue = deepAccess(bidRequest, `params.video.${param}`); - const mediaTypeValue = deepAccess( - bidRequest, - `mediaTypes.video.${param}` - ); - - if (paramsValue || mediaTypeValue) { - videoObj[param] = paramsValue || mediaTypeValue; - } - } - - return videoObj; -} - -function getVideoSize(bidRequest) { - const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize', [[]]); - const { w, h } = deepAccess(bidRequest, 'mediaTypes.video', {}); - - if (isNumber(w) && isNumber(h)) { - return { w, h }; - } - - return { - w: playerSize[0][0], - h: playerSize[0][1], - } -} - -function buildImpExt(validBidRequest) { - const defaultOrgId = getBidderConfig('orgId'); - const { orgId, placementId } = validBidRequest.params || {}; - const effectiverOrgId = orgId || defaultOrgId; - const ext = {}; - - if (placementId) { - deepSetValue(ext, 'adbook.placementId', placementId); - } - - if (effectiverOrgId) { - deepSetValue(ext, 'adbook.orgId', effectiverOrgId); - } - - return ext; -} - -/** - * BID RESPONSE - */ - -function interpretResponse(bidResponse, bidderRequest) { - const bidderRequestBody = safeJSONparse(bidderRequest.data); - - if ( - deepAccess(bidderRequestBody, 'id') != - deepAccess(bidResponse, 'body.id') - ) { - logError( - `${BIDDER_CODE}: Bid response id does not match bidder request id` - ); - - return []; - } - - const referrer = deepAccess(bidderRequestBody, 'site.ref', ''); - const incomingBids = deepAccess(bidResponse, 'body.seatbid', []) - .filter((seat) => isArray(seat.bid)) - .reduce((bids, seat) => bids.concat(seat.bid), []) - .filter(validateBid(bidderRequestBody)); - const targetingMap = buildTargetingMap(incomingBids); - - return impBidsToPrebidBids( - incomingBids, - bidderRequestBody, - bidResponse.body.cur, - referrer, - targetingMap - ); -} - -function impBidsToPrebidBids( - incomingBids, - bidderRequestBody, - bidResponseCurrency, - referrer, - targetingMap -) { - return incomingBids - .map( - impToPrebidBid( - bidderRequestBody, - bidResponseCurrency, - referrer, - targetingMap - ) - ) - .filter((i) => i !== null); -} - -const impToPrebidBid = - (bidderRequestBody, bidResponseCurrency, referrer, targetingMap) => (bid, bidIndex) => { - try { - const bidRequest = findBidRequest(bidderRequestBody, bid); - - if (!bidRequest) { - logError(`${BIDDER_CODE}: Could not match bid to bid request`); - - return null; - } - const categories = deepAccess(bid, 'cat', []); - const mediaType = getMediaType(bid.adm); - let prebidBid = { - ad: bid.adm, - adId: bid.adid, - adserverTargeting: targetingMap[bidIndex], - adUnitCode: bidRequest.tagid, - bidderRequestId: bidderRequestBody.id, - bidId: bid.id, - cpm: bid.price, - creativeId: bid.crid || bid.id, - currency: bidResponseCurrency || getBidderConfig('defaultCurrency'), - height: bid.h, - lineItemId: deepAccess(bid, 'ext.liid'), - mediaType, - meta: { - advertiserDomains: bid.adomain, - mediaType, - primaryCatId: categories[0], - secondaryCatIds: categories.slice(1), - }, - netRevenue: true, - nurl: bid.nurl, - referrer: referrer, - requestId: bid.impid, - ttl: getBidderConfig('bidTTL'), - width: bid.w, - }; - - if (mediaType === VIDEO) { - prebidBid = { - ...prebidBid, - ...getVideoSpecificParams(bidRequest, bid), - }; - } - - if (deepAccess(bid, 'ext.pa_win') === true) { - prebidBid.auctionWinner = true; - } - return prebidBid; - } catch (error) { - logError(`${BIDDER_CODE}: Error while building bid`, error); - - return null; - } - }; - -function getVideoSpecificParams(bidRequest, bid) { - return { - height: bid.h || bidRequest.video.h, - vastXml: bid.adm, - width: bid.w || bidRequest.video.w, - }; -} - -function buildTargetingMap(bids) { - const impIds = bids.map(({ impid }) => impid).filter(uniques); - const values = impIds.reduce((result, id) => { - result[id] = { - lineItemIds: [], - orderIds: [], - dealIds: [], - adIds: [], - adAndOrderIndexes: [] - }; - - return result; - }, {}); - - bids.forEach((bid, bidIndex) => { - let impId = bid.impid; - values[impId].lineItemIds.push(bid.ext.liid); - values[impId].dealIds.push(bid.dealid); - values[impId].adIds.push(bid.adid); - - if (deepAccess(bid, 'ext.ordid')) { - values[impId].orderIds.push(bid.ext.ordid); - bid.ext.ordid.split(TARGETING_VALUE_SEPARATOR).forEach((ordid, ordIndex) => { - let adIdIndex = values[impId].adIds.indexOf(bid.adid); - values[impId].adAndOrderIndexes.push(adIdIndex + '_' + ordIndex) - }) - } - }); - - const targetingMap = {}; - - bids.forEach((bid, bidIndex) => { - let id = bid.impid; - - targetingMap[bidIndex] = { - hb_liid_adbookpsp: values[id].lineItemIds.join(TARGETING_VALUE_SEPARATOR), - hb_deal_adbookpsp: values[id].dealIds.join(TARGETING_VALUE_SEPARATOR), - hb_ad_ord_adbookpsp: values[id].adAndOrderIndexes.join(TARGETING_VALUE_SEPARATOR), - hb_adid_c_adbookpsp: values[id].adIds.join(TARGETING_VALUE_SEPARATOR), - hb_ordid_adbookpsp: values[id].orderIds.join(TARGETING_VALUE_SEPARATOR), - }; - }) - return targetingMap; -} - -/** - * VALIDATION - */ - -function hasRequiredParams(bidRequest) { - const value = - deepAccess(bidRequest, 'params.placementId') != null || - deepAccess(bidRequest, 'params.orgId') != null || - getBidderConfig('orgId') != null; - - if (!value) { - logError(`${BIDDER_CODE}: missing orgId and placementId parameter`); - } - - return value; -} - -function isValidBannerRequest(bidRequest) { - const value = validateSizes( - deepAccess(bidRequest, 'mediaTypes.banner.sizes', []) - ); - - return value; -} - -function isValidVideoRequest(bidRequest) { - const value = - isArray(deepAccess(bidRequest, 'mediaTypes.video.mimes')) && - validateVideoSizes(bidRequest); - - return value; -} - -function validateSize(size) { - return isArray(size) && size.length === 2 && size.every(isNumber); -} - -function validateSizes(sizes) { - return isArray(sizes) && sizes.length > 0 && sizes.every(validateSize); -} - -function validateVideoSizes(bidRequest) { - const { w, h } = deepAccess(bidRequest, 'mediaTypes.video', {}); - - return ( - validateSizes( - deepAccess(bidRequest, 'mediaTypes.video.playerSize') - ) || - (isNumber(w) && isNumber(h)) - ); -} - -function validateBid(bidderRequestBody) { - return function (bid) { - const mediaType = getMediaType(bid.adm); - const bidRequest = findBidRequest(bidderRequestBody, bid); - let validators = commonBidValidators; - - if (mediaType === BANNER) { - validators = [...commonBidValidators, ...bannerBidValidators]; - } - - const value = validators.every((validator) => validator(bid, bidRequest)); - - if (!value) { - logWarn(`${BIDDER_CODE}: Invalid bid`, bid); - } - - return value; - }; -} - -const commonBidValidators = [ - (bid) => isPlainObject(bid), - (bid) => isNonEmptyStr(bid.adid), - (bid) => isNonEmptyStr(bid.adm), - (bid) => isNonEmptyStr(bid.id), - (bid) => isNonEmptyStr(bid.impid), - (bid) => isNonEmptyStr(deepAccess(bid, 'ext.liid')), - (bid) => isNumber(bid.price), -]; - -const bannerBidValidators = [ - validateBannerDimension('w'), - validateBannerDimension('h'), -]; - -function validateBannerDimension(dimension) { - return function (bid, bidRequest) { - if (bid[dimension] == null) { - return bannerHasSingleSize(bidRequest); - } - - return isNumber(bid[dimension]); - }; -} - -function bannerHasSingleSize(bidRequest) { - return deepAccess(bidRequest, 'banner.format', []).length === 1; -} - -/** - * USER SYNC - */ - -export const storage = getStorageManager({bidderCode: BIDDER_CODE}); - -function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { - return responses - .map((response) => deepAccess(response, 'body.ext.sync')) - .filter(isArray) - .reduce(flatten, []) - .filter(validateSync(syncOptions)) - .map(applyConsents(gdprConsent, uspConsent)); -} - -const validateSync = (syncOptions) => (sync) => { - return ( - ((sync.type === 'image' && syncOptions.pixelEnabled) || - (sync.type === 'iframe' && syncOptions.iframeEnabled)) && - sync.url - ); -}; - -const applyConsents = (gdprConsent, uspConsent) => (sync) => { - const url = getUrlBuilder(sync.url); - - if (gdprConsent) { - url.set('gdpr', gdprConsent.gdprApplies ? 1 : 0); - url.set('consentString', gdprConsent.consentString || ''); - } - if (uspConsent) { - url.set('us_privacy', encodeURIComponent(uspConsent)); - } - if (common.getConfig('coppa') === true) { - url.set('coppa', 1); - } - - return { ...sync, url: url.toString() }; -}; - -function getUserId() { - const id = getUserIdFromStorage() || common.generateUUID(); - - setUserId(id); - - return id; -} - -function getUserIdFromStorage() { - const id = storage.localStorageIsEnabled() - ? storage.getDataFromLocalStorage(USER_ID_KEY) - : storage.getCookie(USER_ID_KEY); - - if (!validateUUID(id)) { - return; - } - - return id; -} - -function setUserId(userId) { - if (storage.localStorageIsEnabled()) { - storage.setDataInLocalStorage(USER_ID_KEY, userId); - } - - if (storage.cookiesAreEnabled()) { - const expires = new Date(Date.now() + USER_ID_COOKIE_EXP).toISOString(); - - storage.setCookie(USER_ID_KEY, userId, expires); - } -} - -function validateUUID(uuid) { - return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( - uuid - ); -} - -/** - * EVENT TRACKING - */ - -function onBidWon(bid) { - if (!getBidderConfig('winTrackingEnabled')) { - return; - } - - const wurl = buildWinUrl(bid); - - if (wurl !== null) { - triggerPixel(wurl); - } - - if (isStr(bid.nurl)) { - triggerPixel(bid.nurl); - } -} - -function buildWinUrl(bid) { - try { - const url = getUrlBuilder(getBidderConfig('winTrackingUrl')); - - url.set('impId', bid.requestId); - url.set('reqId', bid.bidderRequestId); - url.set('bidId', bid.bidId); - - return url.toString(); - } catch (_) { - logError( - `${BIDDER_CODE}: Could not build win tracking URL with %s`, - getBidderConfig('winTrackingUrl') - ); - - return null; - } -} - -/** - * COMMON - */ - -const VAST_REGEXP = /VAST\s+version/; - -function getMediaType(adm) { - const videoRegex = new RegExp(VAST_REGEXP); - - if (videoRegex.test(adm)) { - return VIDEO; - } - - const markup = safeJSONparse(adm.replace(/\\/g, '')); - - if (markup && isPlainObject(markup.native)) { - return NATIVE; - } - - return BANNER; -} - -function safeJSONparse(...args) { - try { - return JSON.parse(...args); - } catch (_) { - return undefined; - } -} - -function isNonEmptyStr(value) { - return isStr(value) && !isEmptyStr(value); -} - -function findBidRequest(bidderRequest, bid) { - return find(bidderRequest.imp, (imp) => imp.id === bid.impid); -} - -function getBidderConfig(property) { - if (!property) { - return common.getConfig(`${BIDDER_CODE}`); - } - - return common.getConfig(`${BIDDER_CODE}.${property}`); -} - -const getUrlBase = function (url) { - return url.split('?')[0]; -}; - -const getUrlQuery = function (url) { - const query = url.split('?')[1]; - - if (!query) { - return; - } - - return '?' + query.split('#')[0]; -}; - -const getUrlHash = function (url) { - const hash = url.split('#')[1]; - - if (!hash) { - return; - } - - return '#' + hash; -}; - -const getUrlBuilder = function (url) { - const hash = getUrlHash(url); - const base = getUrlBase(url); - const query = getUrlQuery(url); - const pairs = []; - - function set(key, value) { - pairs.push([key, value]); - - return { - set, - toString, - }; - } - - function toString() { - if (!pairs.length) { - return url; - } - - const queryString = pairs - .map(function (pair) { - return pair.join('='); - }) - .join('&'); - - if (!query) { - return base + '?' + queryString + (hash || ''); - } - - return base + query + '&' + queryString + (hash || ''); - } - - return { - set, - toString, - }; -}; - -export const common = { - generateUUID: function () { - return generateUUID(); - }, - getConfig: function (property) { - return config.getConfig(property); - }, - getWindowDimensions: function () { - return { - innerWidth: window.innerWidth, - innerHeight: window.innerHeight, - }; - }, -}; diff --git a/modules/adbookpspBidAdapter.md b/modules/adbookpspBidAdapter.md deleted file mode 100644 index e258b1fd7c3..00000000000 --- a/modules/adbookpspBidAdapter.md +++ /dev/null @@ -1,191 +0,0 @@ -### Overview - -``` -Module Name: AdbookPSP Bid Adapter -Module Type: Bidder Adapter -Maintainer: hbsupport@fattail.com -``` - -### Description - -Prebid.JS adapter that connects to the AdbookPSP demand sources. - -*NOTE*: The AdBookPSP Bidder Adapter requires setup and approval before use. The adapter uses custom targeting keys that require a dedicated Google Ad Manager setup to work. Please reach out to your AdbookPSP representative for more details. - -### Bidder parameters - -Each adUnit with `adbookpsp` adapter has to have either `placementId` or `orgId` set. - -```js -var adUnits = [ - { - bids: [ - { - bidder: 'adbookpsp', - params: { - placementId: 'example-placement-id', - orgId: 'example-org-id', - }, - }, - ], - }, -]; -``` - -Alternatively, `orgId` can be set globally while configuring prebid.js: - -```js -pbjs.setConfig({ - adbookpsp: { - orgId: 'example-org-id', - }, -}); -``` - -*NOTE*: adUnit orgId will take precedence over the globally set orgId. - -#### Banner parameters - -Required: - -- sizes - -Example configuration: - -```js -var adUnits = [ - { - code: 'div-1', - mediaTypes: { - banner: { - sizes: [[300, 250]], - }, - } - }, -]; -``` - -#### Video parameters - -Required: - -- context -- mimes -- playerSize - -Additionaly, all `Video` object parameters described in chapter `3.2.7` of the [OpenRTB 2.5 specification](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf) can be passed as bidder params. - -Example configuration: - -```js -var adUnits = [ - { - code: 'div-1', - mediaTypes: { - video: { - context: 'outstream', - mimes: ['video/mp4', 'video/x-flv'], - playerSize: [400, 300], - protocols: [2, 3], - }, - }, - bids: [ - { - bidder: 'adbookpsp', - params: { - placementId: 'example-placement-id', - video: { - placement: 2, - }, - }, - }, - ], - }, -]; -``` - -*NOTE*: Supporting outstream video requires the publisher to set up a renderer as described [in the Prebid docs](https://docs.prebid.org/dev-docs/show-outstream-video-ads.html). - -#### Testing params - -To test the adapter, either `placementId: 'example-placement-id'` or `orgId: 'example-org-id'` can be used. - -*NOTE*: If any adUnit uses the testing params, all adUnits will receive testing responses. - -Example adUnit configuration: - -```js -var adUnits = [ - { - code: 'div-1', - mediaTypes: { - banner: { - sizes: [[300, 250]], - }, - }, - bids: [ - { - bidder: 'adbookpsp', - params: { - placementId: 'example-placement-id', - }, - }, - ], - }, -]; -``` - -Example google publisher tag configuration: - -```js -googletag - .defineSlot('/22094606581/example-adbookPSP', sizes, 'div-1') - .addService(googletag.pubads()); -``` - -### Configuration - -Setting of the `orgId` can be done in the `pbjs.setConfig()` call. If this is the case, both `orgId` and `placementId` become optional. Remember to only call `pbjs.setConfig()` once as each call overwrites anything set in previous calls. - -Enabling iframe based user syncs is also encouraged. - -```javascript -pbjs.setConfig({ - adbookpsp: { - orgId: 'example-org-id', - winTrackingEnabled: true, - }, - userSync: { - filterSettings: { - iframe: { - bidders: '*', - filter: 'include', - }, - }, - }, -}); -``` - -### Privacy - -GDPR and US Privacy are both supported by default. - -#### Event tracking - -This adapter tracks win events for it’s bids. This functionality can be disabled by adding `winTrackingEnabled: false` to the adapter configuration: - -```js -pbjs.setConfig({ - adbookpsp: { - winTrackingEnabled: false, - }, -}); -``` - -#### COPPA support - -COPPA support can be enabled for all the visitors by changing the config value: - -```js -config.setConfig({ coppa: true }); -``` diff --git a/modules/addefendBidAdapter.js b/modules/addefendBidAdapter.js index dbb186fdc86..a646400b083 100644 --- a/modules/addefendBidAdapter.js +++ b/modules/addefendBidAdapter.js @@ -1,5 +1,4 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getGlobal} from '../src/prebidGlobal.js'; const BIDDER_CODE = 'addefend'; @@ -17,7 +16,7 @@ export const spec = { }, buildRequests: function(validBidRequests, bidderRequest) { let bid = { - v: getGlobal().version, + v: 'v' + '$prebid.version$', auctionId: false, pageId: false, gdpr_applies: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? bidderRequest.gdprConsent.gdprApplies : 'true', diff --git a/modules/adfBidAdapter.js b/modules/adfBidAdapter.js index 881b1adfcc4..d3e8e05848b 100644 --- a/modules/adfBidAdapter.js +++ b/modules/adfBidAdapter.js @@ -3,9 +3,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {deepAccess, deepClone, deepSetValue, mergeDeep, parseSizesInput} from '../src/utils.js'; +import {deepAccess, deepClone, deepSetValue, getWinDimensions, mergeDeep, parseSizesInput, setOnAny} from '../src/utils.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; const { getConfig } = config; @@ -32,7 +33,7 @@ export const spec = { let app, site; const commonFpd = bidderRequest.ortb2 || {}; - let { user } = commonFpd; + let user = commonFpd.user || {}; if (typeof getConfig('app') === 'object') { app = getConfig('app') || {}; @@ -50,21 +51,36 @@ export const spec = { } } - const device = getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; + let device = getConfig('device') || {}; + if (commonFpd.device) { + mergeDeep(device, commonFpd.device); + } + const { innerWidth, innerHeight } = getWinDimensions(); + device.w = device.w || innerWidth; + device.h = device.h || innerHeight; device.ua = device.ua || navigator.userAgent; + let source = commonFpd.source || {}; + source.fd = 1; + + let regs = commonFpd.regs || {}; + const adxDomain = setOnAny(validBidRequests, 'params.adxDomain') || 'adx.adform.net'; const pt = setOnAny(validBidRequests, 'params.pt') || setOnAny(validBidRequests, 'params.priceType') || 'net'; - const tid = bidderRequest.ortb2?.source?.tid; const test = setOnAny(validBidRequests, 'params.test'); - const currency = getConfig('currency.adServerCurrency'); + const currency = getCurrencyFromBidderRequest(bidderRequest); const cur = currency && [ currency ]; const eids = setOnAny(validBidRequests, 'userIdAsEids'); const schain = setOnAny(validBidRequests, 'schain'); - const dsa = commonFpd.regs?.ext?.dsa; + + if (eids) { + deepSetValue(user, 'ext.eids', eids); + } + + if (schain) { + deepSetValue(source, 'ext.schain', schain); + } const imp = validBidRequests.map((bid, id) => { bid.netRevenue = pt; @@ -75,9 +91,10 @@ export const spec = { mediaType: '*' }) : {}; - const bidfloor = floorInfo.floor; - const bidfloorcur = floorInfo.currency; + const bidfloor = floorInfo?.floor; + const bidfloorcur = floorInfo?.currency; const { mid, inv, mname } = bid.params; + const impExtData = bid.ortb2Imp?.ext?.data; const imp = { id: id + 1, @@ -85,6 +102,7 @@ export const spec = { bidfloor, bidfloorcur, ext: { + data: impExtData, bidder: { inv, mname @@ -148,10 +166,11 @@ export const spec = { app, user, device, - source: { tid, fd: 1 }, + source, ext: { pt }, cur, - imp + imp, + regs }; if (test) { @@ -159,31 +178,6 @@ export const spec = { request.test = 1; } - if (config.getConfig('coppa')) { - deepSetValue(request, 'regs.coppa', 1); - } - - if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') !== undefined) { - deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); - } - - if (bidderRequest.uspConsent) { - deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (eids) { - deepSetValue(request, 'user.ext.eids', eids); - } - - if (schain) { - deepSetValue(request, 'source.ext.schain', schain); - } - - if (dsa) { - deepSetValue(request, 'regs.ext.dsa', dsa); - } - return { method: 'POST', url: 'https://' + adxDomain + '/adx/openrtb', @@ -221,7 +215,9 @@ export const spec = { meta: { mediaType, advertiserDomains: bidResponse.adomain, - dsa + dsa, + primaryCatId: bidResponse.cat?.[0], + secondaryCatIds: bidResponse.cat?.slice(1) } }; @@ -253,15 +249,6 @@ export const spec = { registerBidder(spec); -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; - } - } -} - function flatten(arr) { return [].concat(...arr); } diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index 16375d92194..95eef114f32 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -1,10 +1,11 @@ -import {deepAccess, getBidIdParameter} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; -import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; -import {escapeUnsafeChars} from '../libraries/htmlEscape/htmlEscape.js'; +import { escapeUnsafeChars } from '../libraries/htmlEscape/htmlEscape.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { tryAppendQueryString } from '../libraries/urlUtils/urlUtils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import { getBidIdParameter, deepSetValue, prefixLog } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +const adgLogger = prefixLog('Adgeneration: '); /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -15,6 +16,31 @@ import {escapeUnsafeChars} from '../libraries/htmlEscape/htmlEscape.js'; */ const ADG_BIDDER_CODE = 'adgeneration'; +const ADGENE_PREBID_VERSION = '1.6.4'; +const DEBUG_URL = 'https://api-test.scaleout.jp/adgen/prebid'; +const URL = 'https://d.socdm.com/adgen/prebid'; + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 30// default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext.params', bidRequest.params); + deepSetValue(imp, 'ext.mediaTypes', bidRequest.mediaTypes); + deepSetValue(imp, 'ext.novatiqSyncResponse', bidRequest?.userId?.novatiq?.snowflake?.syncResponse); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + return request; + }, + bidResponse(buildBidResponse, bid, context) { + return buildBidResponse(bid, context) + } +}); export const spec = { code: ADG_BIDDER_CODE, @@ -36,68 +62,61 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const ADGENE_PREBID_VERSION = '1.6.3'; - let serverRequests = []; - for (let i = 0, len = validBidRequests.length; i < len; i++) { - const validReq = validBidRequests[i]; - const DEBUG_URL = 'https://api-test.scaleout.jp/adsv/v1'; - const URL = 'https://d.socdm.com/adsv/v1'; - const url = validReq.params.debug ? DEBUG_URL : URL; - const criteoId = getCriteoId(validReq); - const id5id = getId5Id(validReq); - const id5LinkType = getId5LinkType(validReq); - const imuid = deepAccess(validReq, 'userId.imuid'); - const gpid = deepAccess(validReq, 'ortb2Imp.ext.gpid'); - const sua = deepAccess(validReq, 'ortb2.device.sua'); - const uid2 = deepAccess(validReq, 'userId.uid2.id'); - let data = ``; - data = tryAppendQueryString(data, 'posall', 'SSPLOC'); - const id = getBidIdParameter('id', validReq.params); - data = tryAppendQueryString(data, 'id', id); - data = tryAppendQueryString(data, 'sdktype', '0'); - data = tryAppendQueryString(data, 'hb', 'true'); - data = tryAppendQueryString(data, 't', 'json3'); - data = tryAppendQueryString(data, 'transactionid', validReq.ortb2Imp?.ext?.tid); - data = tryAppendQueryString(data, 'sizes', getSizes(validReq)); - data = tryAppendQueryString(data, 'currency', getCurrencyType()); - data = tryAppendQueryString(data, 'pbver', '$prebid.version$'); - data = tryAppendQueryString(data, 'sdkname', 'prebidjs'); - data = tryAppendQueryString(data, 'adapterver', ADGENE_PREBID_VERSION); - data = tryAppendQueryString(data, 'adgext_criteo_id', criteoId); - data = tryAppendQueryString(data, 'adgext_id5_id', id5id); - data = tryAppendQueryString(data, 'adgext_id5_id_link_type', id5LinkType); - data = tryAppendQueryString(data, 'adgext_imuid', imuid); - data = tryAppendQueryString(data, 'adgext_uid2', uid2); - data = tryAppendQueryString(data, 'gpid', gpid); - data = tryAppendQueryString(data, 'uach', sua ? JSON.stringify(sua) : null); - data = tryAppendQueryString(data, 'schain', validReq.schain ? JSON.stringify(validReq.schain) : null); + const ortbObj = converter.toORTB({bidRequests: validBidRequests, bidderRequest}); + adgLogger.logInfo('ortbObj', ortbObj); + const {imp, ...rest} = ortbObj + const requests = imp.map((impObj) => { + const customParams = impObj?.ext?.params; + const id = getBidIdParameter('id', customParams); + const additionalParams = JSON.parse(JSON.stringify(rest)); - // native以外にvideo等の対応が入った場合は要修正 - if (!validReq.mediaTypes || !validReq.mediaTypes.native) { - data = tryAppendQueryString(data, 'imark', '1'); + // hyperIDが有効ではない場合、パラメータから削除する + if (!impObj?.ext?.novatiqSyncResponse || impObj?.ext?.novatiqSyncResponse !== 1) { + if (additionalParams?.user?.ext?.eids && Array.isArray(additionalParams?.user?.ext?.eids)) { + additionalParams.user.ext.eids = additionalParams?.user?.ext?.eids.filter((eid) => eid?.source !== 'novatiq.com'); + } } - data = tryAppendQueryString(data, 'tp', bidderRequest.refererInfo.page); + let urlParams = ``; + urlParams = tryAppendQueryString(urlParams, 'id', id); + urlParams = tryAppendQueryString(urlParams, 'posall', 'SSPLOC');// not reaquired + urlParams = tryAppendQueryString(urlParams, 'sdktype', '0'); - const hyperId = getHyperId(validReq); - if (hyperId != null) { - data = tryAppendQueryString(data, 'hyper_id', hyperId); + // remove the trailing "&" + if (urlParams.lastIndexOf('&') === urlParams.length - 1) { + urlParams = urlParams.substring(0, urlParams.length - 1); } - // remove the trailing "&" - if (data.lastIndexOf('&') === data.length - 1) { - data = data.substring(0, data.length - 1); + const urlBase = customParams.debug ? (customParams.debug_url ? customParams.debug_url : DEBUG_URL) : URL + const url = `${urlBase}?${urlParams}`; + + let data = { + currency: getCurrencyType(bidderRequest), + pbver: '$prebid.version$', + sdkname: 'prebidjs', + adapterver: ADGENE_PREBID_VERSION, + ortb: { + imp: [impObj], + ...additionalParams + } + } + + // native以外にvideo等の対応が入った場合は要修正 + if (!impObj?.ext?.mediaTypes || !impObj?.ext?.mediaTypes.native) { + data.imark = 1; } - serverRequests.push({ - method: 'GET', + + return { + method: 'POST', url: url, - data: data, - bidRequest: validBidRequests[i] - }); - } - return serverRequests; + data, + options: { + withCredentials: true, + crossOrigin: true + }, + } + }) + return requests; }, /** * Unpack the response from the server into a list of bids. @@ -107,33 +126,42 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function (serverResponse, bidRequests) { + adgLogger.logInfo('serverResponse', JSON.parse(JSON.stringify(serverResponse))); const body = serverResponse.body; if (!body.results || body.results.length < 1) { return []; } - const bidRequest = bidRequests.bidRequest; + + if (!bidRequests?.data?.ortb?.imp || bidRequests?.data?.ortb?.imp.length < 1) { + return []; + } + + const adResult = body?.results[0]; + const targetImp = bidRequests?.data?.ortb?.imp[0]; + const requestId = targetImp?.id; + const bidResponse = { - requestId: bidRequest.bidId, - cpm: body.cpm || 0, - width: body.w ? body.w : 1, - height: body.h ? body.h : 1, - creativeId: body.creativeid || '', - dealId: body.dealid || '', - currency: getCurrencyType(), + requestId: requestId, + cpm: adResult.cpm || 0, + width: adResult.w ? adResult.w : 1, + height: adResult.h ? adResult.h : 1, + creativeId: adResult.creativeid || '', + dealId: adResult.dealid || '', + currency: getCurrencyType(bidRequests.bidderRequest), netRevenue: true, - ttl: body.ttl || 10, + ttl: adResult.ttl || 10, }; - if (body.adomain && Array.isArray(body.adomain) && body.adomain.length) { + if (adResult.adomain && Array.isArray(adResult.adomain) && adResult.adomain.length) { bidResponse.meta = { - advertiserDomains: body.adomain + advertiserDomains: adResult.adomain } } - if (isNative(body)) { - bidResponse.native = createNativeAd(body); + if (isNative(adResult)) { + bidResponse.native = createNativeAd(adResult.native, adResult.beaconurl); bidResponse.mediaType = NATIVE; } else { // banner - bidResponse.ad = createAd(body, bidRequest); + bidResponse.ad = createAd(adResult, body?.location_params, targetImp.ext.params, requestId); } return [bidResponse]; }, @@ -151,37 +179,38 @@ export const spec = { } }; -function createAd(body, bidRequest) { - let ad = body.ad; - if (body.vastxml && body.vastxml.length > 0) { - if (isUpperBillboard(body)) { - const marginTop = bidRequest.params.marginTop ? bidRequest.params.marginTop : '0'; - ad = `${createADGBrowserMTag()}${insertVASTMethodForADGBrowserM(body.vastxml, marginTop)}`; +function createAd(adResult, locationPrams, bidParams, requestId) { + adgLogger.logInfo('params', bidParams); + let ad = adResult.ad; + if (adResult.vastxml && adResult.vastxml.length > 0) { + if (isUpperBillboard(locationPrams)) { + const marginTop = bidParams.marginTop ? bidParams.marginTop : '0'; + ad = `${createADGBrowserMTag()}${insertVASTMethodForADGBrowserM(adResult.vastxml, marginTop)}`; } else { - ad = `
${createAPVTag()}${insertVASTMethodForAPV(bidRequest.bidId, body.vastxml)}`; + ad = `
${createAPVTag()}${insertVASTMethodForAPV(requestId, adResult.vastxml)}`; } } - ad = appendChildToBody(ad, body.beacon); + ad = appendChildToBody(ad, adResult.beacon); if (removeWrapper(ad)) return removeWrapper(ad); return ad; } -function isUpperBillboard(body) { - if (body.location_params && body.location_params.option && body.location_params.option.ad_type) { - return body.location_params.option.ad_type === 'upper_billboard'; +function isUpperBillboard(locationParams) { + if (locationParams && locationParams.option && locationParams.option.ad_type) { + return locationParams.option.ad_type === 'upper_billboard'; } return false; } -function isNative(body) { - if (!body) return false; - return body.native_ad && body.native_ad.assets.length > 0; +function isNative(adResult) { + if (!adResult) return false; + return adResult.native && adResult.native.assets.length > 0; } -function createNativeAd(body) { +function createNativeAd(nativeAd, beaconUrl) { let native = {}; - if (body.native_ad && body.native_ad.assets.length > 0) { - const assets = body.native_ad.assets; + if (nativeAd && nativeAd.assets.length > 0) { + const assets = nativeAd.assets; for (let i = 0, len = assets.length; i < len; i++) { switch (assets[i].id) { case 1: @@ -215,11 +244,11 @@ function createNativeAd(body) { break; } } - native.clickUrl = body.native_ad.link.url; - native.clickTrackers = body.native_ad.link.clicktrackers || []; - native.impressionTrackers = body.native_ad.imptrackers || []; - if (body.beaconurl && body.beaconurl != '') { - native.impressionTrackers.push(body.beaconurl); + native.clickUrl = nativeAd.link.url; + native.clickTrackers = nativeAd.link.clicktrackers || []; + native.impressionTrackers = nativeAd.imptrackers || []; + if (beaconUrl && beaconUrl != '') { + native.impressionTrackers.push(beaconUrl); } } return native; @@ -281,60 +310,12 @@ function removeWrapper(ad) { return ad.substr(bodyIndex, lastBodyIndex).replace('', '').replace('', ''); } -/** - * request - * @param validReq request - * @returns {?string} 300x250,320x50... - */ -function getSizes(validReq) { - const sizes = validReq.sizes; - if (!sizes || sizes.length < 1) return null; - let sizesStr = ''; - for (const i in sizes) { - const size = sizes[i]; - if (size.length !== 2) return null; - sizesStr += `${size[0]}x${size[1]},`; - } - if (sizesStr || sizesStr.lastIndexOf(',') === sizesStr.length - 1) { - sizesStr = sizesStr.substring(0, sizesStr.length - 1); - } - return sizesStr; -} - /** * @return {?string} USD or JPY */ -function getCurrencyType() { - if (config.getConfig('currency.adServerCurrency') && config.getConfig('currency.adServerCurrency').toUpperCase() === 'USD') return 'USD'; - return 'JPY'; -} - -/** - * - * @param validReq request - * @return {null|string} - */ -function getCriteoId(validReq) { - return (validReq.userId && validReq.userId.criteoId) ? validReq.userId.criteoId : null -} - -function getId5Id(validReq) { - return validId5(validReq) ? validReq.userId.id5id.uid : null -} - -function getId5LinkType(validReq) { - return validId5(validReq) ? validReq.userId.id5id.ext.linkType : null -} - -function validId5(validReq) { - return validReq.userId && validReq.userId.id5id && validReq.userId.id5id.uid && validReq.userId.id5id.ext.linkType -} - -function getHyperId(validReq) { - if (validReq.userId && validReq.userId.novatiq && validReq.userId.novatiq.snowflake.syncResponse === 1) { - return validReq.userId.novatiq.snowflake.id; - } - return null; +function getCurrencyType(bidderRequest) { + const adServerCurrency = getCurrencyFromBidderRequest(bidderRequest) || '' + return adServerCurrency.toUpperCase() === 'USD' ? 'USD' : 'JPY' } registerBidder(spec); diff --git a/modules/adgridBidAdapter.js b/modules/adgridBidAdapter.js new file mode 100644 index 00000000000..427eada4c50 --- /dev/null +++ b/modules/adgridBidAdapter.js @@ -0,0 +1,206 @@ +import { _each, isEmpty, deepAccess } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER = Object.freeze({ + CODE: 'adgrid', + HOST: 'https://api-prebid.adgrid.io', + REQUEST_METHOD: 'POST', + REQUEST_ENDPOINT: '/api/v1/auction', + SUPPORTED_MEDIA_TYPES: [BANNER, VIDEO], +}); + +const CURRENCY = Object.freeze({ + KEY: 'currency', + US_DOLLAR: 'USD', +}); + +function isBidRequestValid(bid) { + if (!bid || !bid.params) { + return false; + } + + return !!bid.params.domainId && !!bid.params.placement; +} + +/** + * Return some extra params + */ +function getAudience(validBidRequests, bidderRequest) { + const params = { + domain: deepAccess(bidderRequest, 'refererInfo.page') + }; + + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { + params.gdpr = 1; + params.gdprConsent = deepAccess(bidderRequest, 'gdprConsent.consentString'); + } + + if (deepAccess(bidderRequest, 'uspConsent')) { + params.usp = deepAccess(bidderRequest, 'uspConsent'); + } + + if (deepAccess(validBidRequests[0], 'schain')) { + params.schain = deepAccess(validBidRequests[0], 'schain'); + } + + if (deepAccess(validBidRequests[0], 'userId')) { + params.userIds = deepAccess(validBidRequests[0], 'userId'); + } + + if (deepAccess(validBidRequests[0], 'userIdAsEids')) { + params.userEids = deepAccess(validBidRequests[0], 'userIdAsEids'); + } + + if (bidderRequest.gppConsent) { + params.gpp = bidderRequest.gppConsent.gppString; + params.gppSid = bidderRequest.gppConsent.applicableSections?.toString(); + } else if (bidderRequest.ortb2?.regs?.gpp) { + params.gpp = bidderRequest.ortb2.regs.gpp; + params.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + return params; +} + +function buildRequests(validBidRequests, bidderRequest) { + const currencyObj = config.getConfig(CURRENCY.KEY); + const currency = (currencyObj && currencyObj.adServerCurrency) ? currencyObj.adServerCurrency : 'USD'; + const bids = []; + + _each(validBidRequests, bid => { + bids.push(getBidData(bid)) + }); + + const bidsParams = Object.assign({}, { + url: window.location.href, + timeout: bidderRequest.timeout, + ts: new Date().getTime(), + device: { + size: [ + window.screen.width, + window.screen.height + ] + }, + bids + }); + + // Add currency if not USD + if (currency != null && currency != CURRENCY.US_DOLLAR) { + bidsParams.cur = currency; + } + + bidsParams.audience = getAudience(validBidRequests, bidderRequest); + + // Passing geo location data if found in prebid config + bidsParams.geodata = config.getConfig('adgGeodata') || {}; + + return Object.assign({}, bidderRequest, { + method: BIDDER.REQUEST_METHOD, + url: `${BIDDER.HOST}${BIDDER.REQUEST_ENDPOINT}`, + data: bidsParams, + currency: currency, + options: { + withCredentials: false, + contentType: 'application/json' + } + }); +} + +function interpretResponse(response, bidRequest) { + let bids = response.body; + const bidResponses = []; + + if (isEmpty(bids)) { + return bidResponses; + } + + if (typeof bids !== 'object') { + return bidResponses; + } + + bids = bids.bids; + + bids.forEach((adUnit) => { + const bidResponse = { + requestId: adUnit.bidId, + cpm: Number(adUnit.cpm), + width: adUnit.width, + height: adUnit.height, + ttl: 300, + creativeId: adUnit.creativeId, + netRevenue: true, + currency: adUnit.currency || bidRequest.currency, + mediaType: adUnit.mediaType + }; + + if (adUnit.mediaType == 'video') { + if (adUnit.admUrl) { + bidResponse.vastUrl = adUnit.admUrl; + } else { + bidResponse.vastXml = adUnit.adm; + } + } else { + bidResponse.ad = adUnit.ad; + } + + bidResponses.push(bidResponse); + }); + + return bidResponses; +} + +function getBidData(bid) { + const bidData = { + requestId: bid.bidId, + tid: bid.ortb2Imp?.ext?.tid, + deviceW: bid.ortb2?.device?.w, + deviceH: bid.ortb2?.device?.h, + deviceUa: bid.ortb2?.device?.ua, + domain: bid.ortb2?.site?.publisher?.domain, + domainId: bid.params.domainId, + placement: bid.params.placement, + code: bid.adUnitCode + }; + + if (bid.mediaTypes != null) { + if (bid.mediaTypes.banner != null) { + bidData.mediaType = 'banner'; + bidData.sizes = bid.mediaTypes.banner.sizes; + } else if (bid.mediaTypes.video != null) { + bidData.mediaType = 'video'; + bidData.sizes = bid.mediaTypes.video.playerSize; + bidData.videoData = bid.mediaTypes.video; + bidData.videoParams = bid.params.video; + } + } + + return bidData; +} + +/** + * Register the user sync pixels/iframe which should be dropped after the auction. + */ +function getUserSyncs(syncOptions, response, gdprConsent, uspConsent) { + if (typeof response !== 'object' || response === null || response.length === 0) { + return []; + } + + if (response[0]?.body?.ext?.cookies && typeof response[0].body.ext.cookies === 'object') { + return response[0].body.ext.cookies.slice(0, 5); + } else { + return []; + } +}; + +export const spec = { + code: BIDDER.CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + supportedMediaTypes: BIDDER.SUPPORTED_MEDIA_TYPES, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/adgridBidAdapter.md b/modules/adgridBidAdapter.md new file mode 100644 index 00000000000..205c6ca31bf --- /dev/null +++ b/modules/adgridBidAdapter.md @@ -0,0 +1,47 @@ +# Overview + +**Module Name**: AdGrid Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: support@adgrid.io + +# Description + +The AdGrid Bidding Adapter requires setup and approval before beginning. Please reach out to for more details. + +# Test Parameters + +```javascript +var adUnits = [ + // Banner adUnit + { + code: 'test-div-1', + mediaTypes:{ + banner:{ + sizes: [[300, 250]] + } + } + bids: [{ + bidder: 'adgrid', + params: { + domainId: 12345, + placement: 'leaderboard' + } + }] + }, + { + code: 'test-div-2', + mediaTypes:{ + banner:{ + sizes: [[728, 90], [320, 50]] + } + } + bids: [{ + bidder: 'adgrid', + params: { + domainId: 67890, + placement: 'adhesion' + } + }] + } +]; +``` diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index 96e93883de6..7eb91dfcd52 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -7,6 +7,7 @@ const VERSION = '3.6'; const BAD_WORD_STEP = 0.1; const BAD_WORD_MIN = 0.2; const ADHASH_BIDDER_CODE = 'adhash'; +const storage = getStorageManager({ bidderCode: ADHASH_BIDDER_CODE }); /** * Function that checks the page where the ads are being served for brand safety. @@ -120,7 +121,7 @@ function brandSafety(badWords, maxScore) { .replaceAll(/\s\s+/g, ' ') .toLowerCase() .trim(); - const content = window.top.document.body.innerText.toLowerCase(); + const content = window.top.document.body.textContent.toLowerCase(); // \p{L} matches a single unicode code point in the category 'letter'. Matches any kind of letter from any language. const regexp = new RegExp('[\\p{L}]+', 'gu'); const wordsMatched = content.match(regexp); @@ -171,7 +172,6 @@ export const spec = { }, buildRequests: (validBidRequests, bidderRequest) => { - const storage = getStorageManager({ bidderCode: ADHASH_BIDDER_CODE }); const { gdprConsent } = bidderRequest; const bidRequests = []; const body = document.body; @@ -199,9 +199,11 @@ export const spec = { position: validBidRequests[i].adUnitCode }; let recentAds = []; + let recentAdsPrebid = []; if (storage.localStorageIsEnabled()) { const prefix = validBidRequests[i].params.prefix || 'adHash'; recentAds = JSON.parse(storage.getDataFromLocalStorage(prefix + 'recentAds') || '[]'); + recentAdsPrebid = JSON.parse(storage.getDataFromLocalStorage(prefix + 'recentAdsPrebid') || '[]'); } // Needed for the ad density calculation @@ -237,6 +239,7 @@ export const spec = { blockedCreatives: [], currentTimestamp: (new Date().getTime() / 1000) | 0, recentAds: recentAds, + recentAdsPrebid: recentAdsPrebid, GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null, GDPR: gdprConsent ? gdprConsent.consentString : null, servedAdsCount: window.adsCount, @@ -263,6 +266,19 @@ export const spec = { return []; } + if (storage.localStorageIsEnabled()) { + const prefix = request.bidRequest.params.prefix || 'adHash'; + let recentAdsPrebid = JSON.parse(storage.getDataFromLocalStorage(prefix + 'recentAdsPrebid') || '[]'); + recentAdsPrebid.push([ + (new Date().getTime() / 1000) | 0, + responseBody.creatives[0].advertiserId, + responseBody.creatives[0].budgetId, + responseBody.creatives[0].expectedHashes.length ? responseBody.creatives[0].expectedHashes[0] : '', + ]); + let recentAdsPrebidFinal = JSON.stringify(recentAdsPrebid.slice(-100)); + storage.setDataInLocalStorage(prefix + 'recentAdsPrebid', recentAdsPrebidFinal); + } + const publisherURL = JSON.stringify(request.bidRequest.params.platformURL); const bidderURL = request.bidRequest.params.bidderURL || 'https://bidder.adhash.com'; const oneTimeId = request.bidRequest.adUnitCode + Math.random().toFixed(16).replace('0.', '.'); diff --git a/modules/adipoloBidAdapter.js b/modules/adipoloBidAdapter.js new file mode 100644 index 00000000000..f6638c25eb8 --- /dev/null +++ b/modules/adipoloBidAdapter.js @@ -0,0 +1,37 @@ +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {buildRequests, getUserSyncs, interpretResponse} from '../libraries/xeUtils/bidderUtils.js'; +import {deepAccess, getBidIdParameter, isArray, logError} from '../src/utils.js'; + +const BIDDER_CODE = 'adipolo'; +const ENDPOINT = 'https://prebid.adipolo.live'; + +function isBidRequestValid(bid) { + if (bid && typeof bid.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('pid', bid.params)) { + logError('Pid is not present in bidder params'); + return false; + } + + if (deepAccess(bid, 'mediaTypes.video') && !isArray(deepAccess(bid, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests: (validBidRequests, bidderRequest) => buildRequests(validBidRequests, bidderRequest, ENDPOINT), + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/adipoloBidAdapter.md b/modules/adipoloBidAdapter.md new file mode 100644 index 00000000000..bebb770f0e7 --- /dev/null +++ b/modules/adipoloBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: Adipolo Bidder Adapter +Module Type: Adipolo Bidder Adapter +Maintainer: support@adipolo.com +``` + +# Description + +Module that connects to adipolo.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'adipolo', + params: { + env: 'adipolo', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'adipolo', + params: { + env: 'adipolo', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index a32a97af3c6..c4a612ce95a 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -339,7 +339,7 @@ export function getUmtSource(pageUrl, referrer) { * Expiring queue implementation. Fires callback on elapsed timeout since last update or creation. * @param callback * @param ttl - * @constructor + * @class */ export function ExpiringQueue(callback, ttl) { let queue = []; diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js index 81067a3efcf..ae5528f2aeb 100644 --- a/modules/adkernelAdnBidAdapter.js +++ b/modules/adkernelAdnBidAdapter.js @@ -2,6 +2,7 @@ import {deepAccess, deepSetValue, isArray, isNumber, isStr, logInfo, parseSizesI import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; +import {getBidFloor} from '../libraries/adkernelUtils/adkernelUtils.js' const DEFAULT_ADKERNEL_DSP_DOMAIN = 'tag.adkernel.com'; const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; @@ -143,18 +144,6 @@ function fillBidMeta(bid, tag) { } } -function getBidFloor(bid, mediaType, sizes) { - var floor; - var size = sizes.length === 1 ? sizes[0] : '*'; - if (typeof bid.getFloor === 'function') { - const floorInfo = bid.getFloor({currency: 'USD', mediaType, size}); - if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { - floor = parseFloat(floorInfo.floor); - } - } - return floor; -} - export const spec = { code: 'adkernelAdn', gvlid: GVLID, diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 0c353e4332a..2120a73dabd 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -20,6 +20,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {find} from '../src/polyfill.js'; import {config} from '../src/config.js'; import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {getBidFloor} from '../libraries/adkernelUtils/adkernelUtils.js' /** * In case you're AdKernel whitelable platform's client who needs branded adapter to @@ -83,7 +84,6 @@ export const spec = { {code: 'unibots'}, {code: 'ergadx'}, {code: 'turktelekom'}, - {code: 'felixads'}, {code: 'motionspots'}, {code: 'sonic_twist'}, {code: 'displayioads'}, @@ -94,7 +94,16 @@ export const spec = { {code: 'adpluto'}, {code: 'headbidder'}, {code: 'digiad'}, - {code: 'monetix'} + {code: 'monetix'}, + {code: 'hyperbrainz'}, + {code: 'voisetech'}, + {code: 'global_sun'}, + {code: 'rxnetwork'}, + {code: 'revbid'}, + {code: 'spinx', gvlid: 1308}, + {code: 'oppamedia'}, + {code: 'pixelpluses', gvlid: 1209}, + {code: 'urekamedia'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -110,7 +119,9 @@ export const spec = { !isNaN(Number(bidRequest.params.zoneId)) && bidRequest.params.zoneId > 0 && bidRequest.mediaTypes && - (bidRequest.mediaTypes.banner || bidRequest.mediaTypes.video || (bidRequest.mediaTypes.native && validateNativeAdUnit(bidRequest.mediaTypes.native))); + (bidRequest.mediaTypes.banner || bidRequest.mediaTypes.video || + (bidRequest.mediaTypes.native && validateNativeAdUnit(bidRequest.mediaTypes.native)) + ); }, /** @@ -245,18 +256,6 @@ function groupImpressionsByHostZone(bidRequests, refererInfo) { ); } -function getBidFloor(bid, mediaType, sizes) { - var floor; - var size = sizes.length === 1 ? sizes[0] : '*'; - if (typeof bid.getFloor === 'function') { - const floorInfo = bid.getFloor({currency: 'USD', mediaType, size}); - if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { - floor = parseFloat(floorInfo.floor); - } - } - return floor; -} - /** * Builds rtb imp object(s) for single adunit * @param bidRequest {BidRequest} @@ -268,7 +267,7 @@ function buildImps(bidRequest, secure) { 'tagid': bidRequest.adUnitCode }; if (secure) { - imp.secure = 1; + imp.secure = bidRequest.ortb2Imp?.secure ?? 1; } var sizes = []; let mediaTypes = bidRequest.mediaTypes; diff --git a/modules/adlooxAnalyticsAdapter.js b/modules/adlooxAnalyticsAdapter.js index e31ea290e11..c7321799f3c 100644 --- a/modules/adlooxAnalyticsAdapter.js +++ b/modules/adlooxAnalyticsAdapter.js @@ -28,6 +28,7 @@ import { parseUrl } from '../src/utils.js'; import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js'; const MODULE = 'adlooxAnalyticsAdapter'; @@ -80,8 +81,6 @@ const PARAMS_DEFAULT = { 'id11': '$ADLOOX_WEBSITE' }; -const NOOP = function() {}; - let analyticsAdapter = Object.assign(adapter({ analyticsType: 'endpoint' }), { track({ eventType, args }) { if (!analyticsAdapter[`handle_${eventType}`]) return; @@ -108,6 +107,10 @@ analyticsAdapter.enableAnalytics = function(config) { logError(MODULE, 'invalid js options value'); return; } + if (isStr(config.options.js) && !/\.adlooxtracking\.(com|ru)$/.test(parseUrl(config.options.js, { 'noDecodeWholeURL': true }).host)) { + logError(MODULE, "invalid js options value, must be a sub-domain of 'adlooxtracking.com'"); + return; + } if (!(config.options.toselector === undefined || isFn(config.options.toselector))) { logError(MODULE, 'invalid toselector options value'); return; @@ -220,19 +223,24 @@ analyticsAdapter.url = function(url, args, bid) { return url + a2qs(args); } +const preloaded = {}; analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = function(auctionDetails) { if (!(auctionDetails.auctionStatus == AUCTION_COMPLETED && auctionDetails.bidsReceived.length > 0)) return; - analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = NOOP; - - logMessage(MODULE, 'preloading verification JS'); const uri = parseUrl(analyticsAdapter.url(`${analyticsAdapter.context.js}#`)); + const href = `${uri.protocol}://${uri.host}${uri.pathname}`; + if (preloaded[href]) return; + + logMessage(MODULE, 'preloading verification JS'); const link = document.createElement('link'); - link.setAttribute('href', `${uri.protocol}://${uri.host}${uri.pathname}`); + link.setAttribute('href', href); link.setAttribute('rel', 'preload'); link.setAttribute('as', 'script'); + // TODO fix rules violation insertElement(link); + + preloaded[href] = true; } analyticsAdapter[`handle_${EVENTS.BID_WON}`] = function(bid) { @@ -261,7 +269,7 @@ analyticsAdapter[`handle_${EVENTS.BID_WON}`] = function(bid) { [ 'creatype', '%%creatype%%' ] ]); - loadExternalScript(analyticsAdapter.url(`${analyticsAdapter.context.js}#`, params, bid), 'adloox'); + loadExternalScript(analyticsAdapter.url(`${analyticsAdapter.context.js}#`, params, bid), MODULE_TYPE_ANALYTICS, 'adloox'); } adapterManager.registerAnalyticsAdapter({ diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index 727dc84e399..975aa5e254a 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -9,7 +9,6 @@ * @optional module:modules/intersectionRtdProvider */ -/* eslint standard/no-callback-literal: "off" */ /* eslint prebid/validate-imports: "off" */ import {auctionManager} from '../src/auctionManager.js'; @@ -106,7 +105,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { // buildUrl creates PHP style multi-parameters and includes undefined... (╯°□°)╯ ┻━┻ const url = buildUrl(mergeDeep(parseUrl(`${API_ORIGIN}/q`), { search: { - 'v': `pbjs-${getGlobal().version}`, + 'v': 'pbjs-v' + '$prebid.version$', 'c': config.params.clientid, 'p': config.params.platformid, 't': config.params.tagid, diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index b78737722bd..6778e536a1b 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -1,207 +1,51 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { isFn, deepAccess, logMessage } from '../src/utils.js'; -import {config} from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { deepAccess } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { + isBidRequestValid, + buildRequestsBase, + interpretResponse, + getUserSyncs, + buildPlacementProcessingFunction +} from '../libraries/teqblazeUtils/bidderUtils.js'; const GVLID = 149; const BIDDER_CODE = 'adman'; const AD_URL = 'https://pub.admanmedia.com/?c=o&m=multi'; -const URL_SYNC = 'https://sync.admanmedia.com'; +const SYNC_URL = 'https://sync.admanmedia.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; - } - switch (bid['mediaType']) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl); - case NATIVE: - return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); - default: - return false; - } -} +const addCustomFieldsToPlacement = (bid, bidderRequest, placement) => { + placement.traffic = placement.adFormat; -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); + if (placement.adFormat === VIDEO) { + placement.wPlayer = placement.playerSize?.[0]?.[0]; + placement.hPlayer = placement.playerSize?.[0]?.[1]; } +}; - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} +const placementProcessingFunction = buildPlacementProcessingFunction({ addCustomFieldsToPlacement }); + +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + const request = buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest, placementProcessingFunction }); + const content = deepAccess(bidderRequest, 'ortb2.site.content', config.getAnyConfig('ortb2.site.content')); -function getUserId(eids, id, source, uidExt) { - if (id) { - var uid = { id }; - if (uidExt) { - uid.ext = uidExt; - } - eids.push({ - source, - uids: [ uid ] - }); + if (content) { + request.data.content = content; } -} + + return request; +}; export const spec = { code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(bid.params.placementId)); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const content = deepAccess(bidderRequest, 'ortb2.site.content', config.getAnyConfig('ortb2.site.content')); - - let winTop = window; - let location; - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - let placements = []; - let request = { - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language : '', - 'secure': 1, - 'host': location.host, - 'page': location.pathname, - 'placements': placements - }; - request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = { - consentString: bidderRequest.gdprConsent.consentString - }; - } - if (content) { - request.content = content; - } - } - const len = validBidRequests.length; - - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - const { params, bidId, mediaTypes } = bid; - - const placement = { - placementId: params.placementId, - bidId, - eids: [], - bidFloor: getBidFloor(bid) - } - - if (bid.transactionId) { - placement.ext = placement.ext || {}; - placement.ext.tid = bid.transactionId; - } - - if (bid.schain) { - placement.schain = bid.schain; - } - - if (bid.userId) { - getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); - getUserId(placement.eids, bid.userId.lotamePanoramaId, 'lotame.com'); - getUserId(placement.eids, bid.userId.idx, 'idx.lat'); - } - - if (mediaTypes?.[BANNER]) { - placement.traffic = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes?.[VIDEO]) { - placement.traffic = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } - - placements.push(placement); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - serverResponse = serverResponse.body; - for (let i = 0; i < serverResponse.length; i++) { - let resItem = serverResponse[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = URL_SYNC + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } - + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests, + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index 6c268b2d382..9cc2182c6bf 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -1,8 +1,10 @@ -import {getValue, formatQS, logError, deepAccess, isArray, getBidIdParameter} from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; import { Renderer } from '../src/Renderer.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { deepAccess, getBidIdParameter, getValue, isArray, logError } from '../src/utils.js'; +import { getUserSyncParams } from '../libraries/userSyncUtils/userSyncUtils.js'; +import { interpretNativeAd } from '../libraries/precisoUtils/bidNativeUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -10,29 +12,7 @@ import { Renderer } from '../src/Renderer.js'; * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest */ -export const OPENRTB = { - NATIVE: { - IMAGE_TYPE: { - ICON: 1, - MAIN: 3, - }, - ASSET_ID: { - TITLE: 1, - IMAGE: 2, - ICON: 3, - BODY: 4, - SPONSORED: 5, - CTA: 6 - }, - DATA_ASSET_TYPE: { - SPONSORED: 1, - DESC: 2, - CTA_TEXT: 12, - }, - } -}; - -let SYNC_URL = ''; +let SYNC_URL = 'https://static.cdn.admatic.com.tr/sync.html'; const BIDDER_CODE = 'admatic'; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; @@ -40,7 +20,12 @@ export const spec = { code: BIDDER_CODE, gvlid: 1281, aliases: [ - {code: 'pixad', gvlid: 1281} + { code: 'admaticde', gvlid: 1281 }, + { code: 'pixad', gvlid: 1281 }, + { code: 'monetixads', gvlid: 1281 }, + { code: 'netaddiction', gvlid: 1281 }, + { code: 'adt', gvlid: 779 }, + { code: 'yobee', gvlid: 1281 } ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** @@ -71,8 +56,8 @@ export const spec = { const bids = validBidRequests.map(buildRequestObject); const ortb = bidderRequest.ortb2; const networkId = getValue(validBidRequests[0].params, 'networkId'); - const host = getValue(validBidRequests[0].params, 'host'); - const currency = config.getConfig('currency.adServerCurrency') || 'TRY'; + let host = getValue(validBidRequests[0].params, 'host'); + const currency = getCurrencyFromBidderRequest(bidderRequest) || null; const bidderName = validBidRequests[0].bidder; const payload = { @@ -87,7 +72,6 @@ export const spec = { }, imp: bids, ext: { - cur: currency, bidder: bidderName }, schain: {}, @@ -102,6 +86,8 @@ export const spec = { tmax: parseInt(tmax) }; + payload.ext.cur = currency; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { const consentStr = (bidderRequest.gdprConsent.consentString) ? bidderRequest.gdprConsent.consentString.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : ''; @@ -139,48 +125,35 @@ export const spec = { } if (payload) { - switch (bidderName) { - case 'pixad': - SYNC_URL = 'https://static.cdn.pixad.com.tr/sync.html'; - break; - default: - SYNC_URL = 'https://cdn.serve.admatic.com.tr/showad/sync.html'; - break; + const domain = {}; + domain.parts = host.split('rtb.'); + if (domain.parts.length > 1) { + domain.url = domain.parts[1]; } + SYNC_URL = `https://static.cdn.${domain.url}/${bidderName}/sync.html`; + host = host.replace('https://', '').replace('http://', '').replace('/', ''); return { method: 'POST', url: `https://${host}/pb`, data: payload, options: { contentType: 'application/json' } }; } }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { if (!hasSynced && syncOptions.iframeEnabled) { - // data is only assigned if params are available to pass to syncEndpoint - let params = {}; - - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - params['gdpr'] = Number(gdprConsent.gdprApplies); - } - if (typeof gdprConsent.consentString === 'string') { - params['gdpr_consent'] = gdprConsent.consentString; - } - } + // Retrieve the sync parameters + const params = getUserSyncParams(gdprConsent, uspConsent, gppConsent); - if (uspConsent) { - params['us_privacy'] = encodeURIComponent(uspConsent); - } + // Create a URL object from SYNC_URL + const urlObj = new URL(SYNC_URL); - if (gppConsent?.gppString) { - params['gpp'] = gppConsent.gppString; - params['gpp_sid'] = gppConsent.applicableSections?.toString(); - } - - params = Object.keys(params).length ? `?${formatQS(params)}` : ''; + // Append each parameter from the params object to the URL's search parameters + Object.keys(params).forEach(key => { + urlObj.searchParams.append(key, params[key]); + }); hasSynced = true; return { type: 'iframe', - url: SYNC_URL + params + url: urlObj.toString() }; } }, @@ -203,7 +176,7 @@ export const spec = { cpm: bid.price, width: bid.width, height: bid.height, - currency: body.cur || 'TRY', + currency: body.cur, netRevenue: true, creativeId: bid.creative_id, meta: { @@ -282,7 +255,7 @@ function isUrl(str) { } }; -function outstreamRender (bid) { +function outstreamRender(bid) { bid.renderer.push(() => { window.ANOutstreamVideo.renderAd({ targetId: bid.adUnitCode, @@ -379,7 +352,7 @@ function buildRequestObject(bid) { } if (bid.mediaTypes?.native) { reqObj.type = 'native'; - reqObj.size = [{w: 1, h: 1}]; + reqObj.size = [{ w: 1, h: 1 }]; reqObj.mediatype = bid.mediaTypes.native; } @@ -407,7 +380,7 @@ function concatSizes(bid) { if (isArray(bannerSizes) || isArray(playerSize) || isArray(videoSizes)) { let mediaTypesSizes = [bannerSizes, videoSizes, nativeSizes, playerSize]; return mediaTypesSizes - .reduce(function(acc, currSize) { + .reduce(function (acc, currSize) { if (isArray(currSize)) { if (isArray(currSize[0])) { currSize.forEach(function (childSize) { @@ -420,45 +393,6 @@ function concatSizes(bid) { } } -function interpretNativeAd(adm) { - const native = JSON.parse(adm).native; - const result = { - clickUrl: encodeURI(native.link.url), - impressionTrackers: native.imptrackers - }; - native.assets.forEach(asset => { - switch (asset.id) { - case OPENRTB.NATIVE.ASSET_ID.TITLE: - result.title = asset.title.text; - break; - case OPENRTB.NATIVE.ASSET_ID.IMAGE: - result.image = { - url: encodeURI(asset.img.url), - width: asset.img.w, - height: asset.img.h - }; - break; - case OPENRTB.NATIVE.ASSET_ID.ICON: - result.icon = { - url: encodeURI(asset.img.url), - width: asset.img.w, - height: asset.img.h - }; - break; - case OPENRTB.NATIVE.ASSET_ID.BODY: - result.body = asset.data.value; - break; - case OPENRTB.NATIVE.ASSET_ID.SPONSORED: - result.sponsoredBy = asset.data.value; - break; - case OPENRTB.NATIVE.ASSET_ID.CTA: - result.cta = asset.data.value; - break; - } - }); - return result; -} - function _validateId(id) { return (parseInt(id) > 0); } diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index f5f0b5bf665..1570a36c5f0 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -1,4 +1,4 @@ -import {isStr, logError} from '../src/utils.js'; +import {isStr, logError, isFn, deepAccess} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; @@ -10,11 +10,10 @@ const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.2.aspx'; const ALIASES = [ {code: 'go2net', endpoint: 'https://ads.go2net.com.ua/prebid.1.2.aspx'}, 'adblender', - {code: 'adsyield', endpoint: 'https://ads.adsyield.com/prebid.1.2.aspx'}, {code: 'futureads', endpoint: 'https://ads.futureads.io/prebid.1.2.aspx'}, {code: 'smn', endpoint: 'https://ads.smn.rs/prebid.1.2.aspx'}, {code: 'admixeradx', endpoint: 'https://inv-nets.admixer.net/adxprebid.1.2.aspx'}, - {code: 'admixerwl', endpoint: 'https://inv-nets-adxwl.admixer.com/adxwlprebid.aspx'}, + 'rtbstack' ]; export const spec = { code: BIDDER_CODE, @@ -24,8 +23,8 @@ export const spec = { * Determines whether or not the given bid request is valid. */ isBidRequestValid: function (bid) { - return bid.bidder === 'admixerwl' - ? !!bid.params.clientId && !!bid.params.endpointId + return bid.bidder === 'rtbstack' + ? !!bid.params.tagId : !!bid.params.zone; }, /** @@ -52,8 +51,12 @@ export const spec = { }; let endpointUrl; if (bidderRequest) { - const {bidderCode} = bidderRequest; - endpointUrl = config.getConfig(`${bidderCode}.endpoint_url`); + // checks if there is specified any endpointUrl in bidder config + endpointUrl = config.getConfig('bidderURL'); + if (!endpointUrl && bidderRequest.bidderCode === 'rtbstack') { + logError('The bidderUrl config is required for RTB Stack bids. Please set it with setBidderConfig() for "rtbstack".'); + return; + } // TODO: is 'page' the right value here? if (bidderRequest.refererInfo?.page) { payload.referrer = encodeURIComponent(bidderRequest.refererInfo.page); @@ -68,22 +71,22 @@ export const spec = { if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } - let bidFloor = getBidFloor(bidderRequest); - if (bidFloor) { - payload.bidFloor = bidFloor; - } } validRequest.forEach((bid) => { let imp = {}; Object.keys(bid).forEach(key => imp[key] = bid[key]); imp.ortb2 && delete imp.ortb2; + let bidFloor = getBidFloor(bid); + if (bidFloor) { + imp.bidFloor = bidFloor; + } payload.imps.push(imp); }); let urlForRequest = endpointUrl || getEndpointUrl(bidderRequest.bidderCode) return { method: 'POST', - url: bidderRequest.bidderCode === 'admixerwl' ? `${urlForRequest}?client=${payload.imps[0]?.params?.clientId}` : urlForRequest, + url: urlForRequest, data: payload, }; }, @@ -114,19 +117,26 @@ export const spec = { return pixels; } }; + function getEndpointUrl(code) { return find(ALIASES, (val) => val.code === code)?.endpoint || ENDPOINT_URL; } + function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + try { const bidFloor = bid.getFloor({ currency: 'USD', mediaType: '*', size: '*', }); - return bidFloor.floor; + return bidFloor?.floor; } catch (_) { return 0; } } + registerBidder(spec); diff --git a/modules/admixerBidAdapter.md b/modules/admixerBidAdapter.md index 64f8dd64ee4..097e7feb95e 100644 --- a/modules/admixerBidAdapter.md +++ b/modules/admixerBidAdapter.md @@ -51,7 +51,7 @@ Please use ```admixer``` as the bidder code. ]; ``` -### AdmixerWL Test Parameters +### RTB Stack Test Parameters ``` var adUnits = [ { @@ -59,10 +59,9 @@ Please use ```admixer``` as the bidder code. sizes: [[300, 250]], // a display size bids: [ { - bidder: "admixer", + bidder: "rtbstack", params: { - endpointId: 41512, - clientId: 62 + tagId: 41512 } } ] @@ -71,10 +70,9 @@ Please use ```admixer``` as the bidder code. sizes: [[300, 50]], // a mobile size bids: [ { - bidder: "admixer", + bidder: "rtbstack", params: { - endpointId: 41512, - clientId: 62 + tagId: 41512 } } ] @@ -84,10 +82,9 @@ Please use ```admixer``` as the bidder code. mediaType: 'video', bids: [ { - bidder: "admixer", + bidder: "rtbstack", params: { - endpointId: 41512, - clientId: 62 + tagId: 41512 } } ] diff --git a/modules/admixerIdSystem.js b/modules/admixerIdSystem.js index cb7248c9537..04628d2356e 100644 --- a/modules/admixerIdSystem.js +++ b/modules/admixerIdSystem.js @@ -49,7 +49,7 @@ export const admixerIdSubmodule = { * @param {ConsentData} [consentData] * @returns {IdResponse|undefined} */ - getId(config, consentData) { + getId(config, {gdpr: consentData} = {}) { const {e, p, pid} = (config && config.params) || {}; if (!pid || typeof pid !== 'string') { logError('admixerId submodule requires partner id to be defined'); diff --git a/modules/adnuntiusAnalyticsAdapter.js b/modules/adnuntiusAnalyticsAdapter.js new file mode 100644 index 00000000000..ed5535d96d1 --- /dev/null +++ b/modules/adnuntiusAnalyticsAdapter.js @@ -0,0 +1,407 @@ +import { timestamp, logInfo } from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import { EVENTS, STATUS } from '../src/constants.js'; +import adapterManager from '../src/adapterManager.js'; + +const URL = 'https://analytics.adnuntius.com/prebid'; +const REQUEST_SENT = 1; +const RESPONSE_SENT = 2; +const WIN_SENT = 4; +const TIMEOUT_SENT = 8; +const AD_RENDER_FAILED_SENT = 16; + +let initOptions; +export const BID_WON_TIMEOUT = 500; + +const cache = { + auctions: {} +}; + +const adnAnalyticsAdapter = Object.assign(adapter({url: '', analyticsType: 'endpoint'}), { + track({eventType, args}) { + const time = timestamp(); + logInfo('ADN_EVENT:', [eventType, args]); + + switch (eventType) { + case EVENTS.AUCTION_INIT: + logInfo('ADN_AUCTION_INIT:', args); + cache.auctions[args.auctionId] = {bids: {}, bidAdUnits: {}}; + break; + case EVENTS.BID_REQUESTED: + logInfo('ADN_BID_REQUESTED:', args); + cache.auctions[args.auctionId].timeStamp = args.start; + + args.bids.forEach(function(bidReq) { + cache.auctions[args.auctionId].gdprApplies = args.gdprConsent ? args.gdprConsent.gdprApplies : undefined; + cache.auctions[args.auctionId].gdprConsent = args.gdprConsent ? args.gdprConsent.consentString : undefined; + + const container = document.getElementById(bidReq.adUnitCode); + const containerAttr = container ? container.getAttribute('data-adunitid') : undefined; + const adUnitId = containerAttr || undefined; + + cache.auctions[args.auctionId].bids[bidReq.bidId] = { + bidder: bidReq.bidder, + adUnit: bidReq.adUnitCode, + adUnitId: adUnitId, + isBid: false, + won: false, + timeout: false, + sendStatus: 0, + readyToSend: 0, + start: args.start, + auc: bidReq.auc, + buc: bidReq.buc, + lw: bidReq.lw + }; + + logInfo(bidReq); + }); + logInfo(adnAnalyticsAdapter.requestEvents); + break; + case EVENTS.BID_RESPONSE: + logInfo('ADN_BID_RESPONSE:', args); + + const bidResp = cache.auctions[args.auctionId].bids[args.requestId]; + bidResp.isBid = args.getStatusCode() === STATUS.GOOD; + bidResp.width = args.width; + bidResp.height = args.height; + bidResp.cpm = args.cpm; + bidResp.currency = args.currency; + bidResp.originalCpm = args.originalCpm; + bidResp.originalCurrency = args.originalCurrency; + bidResp.ttr = args.timeToRespond; + bidResp.readyToSend = 1; + bidResp.mediaType = args.mediaType === 'native' ? 2 : (args.mediaType === 'video' ? 4 : 1); + bidResp.meta = args.meta; + + if (!bidResp.ttr) { + bidResp.ttr = time - bidResp.start; + } + if (!cache.auctions[args.auctionId].bidAdUnits[bidResp.adUnit]) { + cache.auctions[args.auctionId].bidAdUnits[bidResp.adUnit] = + { + sent: 0, + lw: bidResp.lw, + adUnitId: bidResp.adUnitId, + timeStamp: cache.auctions[args.auctionId].timeStamp + }; + } + break; + case EVENTS.BIDDER_DONE: + logInfo('ADN_BIDDER_DONE:', args); + args.bids.forEach(doneBid => { + let bid = cache.auctions[doneBid.auctionId].bids[doneBid.bidId || doneBid.requestId]; + if (!bid.ttr) { + bid.ttr = time - bid.start; + } + bid.readyToSend = 1; + }); + break; + case EVENTS.BID_WON: + logInfo('ADN_BID_WON:', args); + const wonBid = cache.auctions[args.auctionId].bids[args.requestId]; + wonBid.won = true; + wonBid.rUp = args.rUp; + wonBid.meta = args.meta; + wonBid.dealId = args.dealId; + if (wonBid.sendStatus !== 0) { + adnAnalyticsAdapter.sendEvents(); + } + break; + case EVENTS.AD_RENDER_SUCCEEDED: + logInfo('ADN_AD_RENDER_SUCCEEDED:', args); + const adRenderSucceeded = cache.auctions[args.bid.auctionId].bids[args.bid.requestId]; + adRenderSucceeded.renderedTimestamp = Date.now(); + break; + case EVENTS.AD_RENDER_FAILED: + logInfo('ADN_AD_RENDER_FAILED:', args); + const adRenderFailedBid = cache.auctions[args.bid.auctionId].bids[args.bid.requestId]; + adRenderFailedBid.adRenderFailed = true; + adRenderFailedBid.reason = args.reason; + adRenderFailedBid.message = args.message; + if (adRenderFailedBid.sendStatus !== 0) { + adnAnalyticsAdapter.sendEvents(); + } + break; + case EVENTS.BID_TIMEOUT: + logInfo('ADN_BID_TIMEOUT:', args); + args.forEach(timeout => { + cache.auctions[timeout.auctionId].bids[timeout.bidId].timeout = true; + }); + break; + case EVENTS.AUCTION_END: + logInfo('ADN_AUCTION_END:', args); + setTimeout(() => { + adnAnalyticsAdapter.sendEvents(); + }, BID_WON_TIMEOUT); + break; + } + } +}); + +// save the base class function +adnAnalyticsAdapter.originEnableAnalytics = adnAnalyticsAdapter.enableAnalytics; +adnAnalyticsAdapter.allRequestEvents = []; + +// override enableAnalytics so we can get access to the config passed in from the page +adnAnalyticsAdapter.enableAnalytics = function (config) { + initOptions = config.options; + adnAnalyticsAdapter.originEnableAnalytics(config); +}; + +adnAnalyticsAdapter.sendEvents = function() { + const sentRequests = getSentRequests(); + const events = { + publisherId: initOptions.publisherId, + gdpr: sentRequests.gdpr, + auctionIds: sentRequests.auctionIds, + requests: sentRequests.sentRequests, + responses: getResponses(sentRequests.gdpr, sentRequests.auctionIds), + wins: getWins(sentRequests.gdpr, sentRequests.auctionIds), + timeouts: getTimeouts(sentRequests.gdpr, sentRequests.auctionIds), + bidAdUnits: getBidAdUnits(), + rf: getAdRenderFailed(sentRequests.auctionIds), + ext: initOptions.ext + }; + + if (events.requests.length === 0 && events.responses.length === 0 && events.wins.length === 0 && events.timeouts.length === 0 && events.rf.length === 0) { + return; + } + + ajax(initOptions.endPoint || URL, undefined, JSON.stringify(events), {method: 'POST'}); +}; + +function getSentRequests() { + const sentRequests = []; + const gdpr = []; + const auctionIds = []; + + Object.keys(cache.auctions).forEach(auctionId => { + const auction = cache.auctions[auctionId]; + const gdprPos = getGdprPos(gdpr, auction); + const auctionIdPos = getAuctionIdPos(auctionIds, auctionId); + + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + let bid = auction.bids[bidId]; + if (!(bid.sendStatus & REQUEST_SENT)) { + bid.sendStatus |= REQUEST_SENT; + + sentRequests.push({ + adUnit: bid.adUnit, + adUnitId: bid.adUnitId, + bidder: bid.bidder, + timeStamp: auction.timeStamp, + gdpr: gdprPos, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc, + lw: bid.lw + }); + } + }); + }); + + return {gdpr: gdpr, auctionIds: auctionIds, sentRequests: sentRequests}; +} + +function getResponses(gdpr, auctionIds) { + const responses = []; + + Object.keys(cache.auctions).forEach(auctionId => { + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + let auction = cache.auctions[auctionId]; + let gdprPos = getGdprPos(gdpr, auction); + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId) + let bid = auction.bids[bidId]; + if (bid.readyToSend && !(bid.sendStatus & RESPONSE_SENT) && !bid.timeout) { + bid.sendStatus |= RESPONSE_SENT; + + let response = getResponseObject(auction, bid, gdprPos, auctionIdPos); + + responses.push(response); + } + }); + }); + + return responses; +} + +function getWins(gdpr, auctionIds) { + const wins = []; + + Object.keys(cache.auctions).forEach(auctionId => { + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + const auction = cache.auctions[auctionId]; + const gdprPos = getGdprPos(gdpr, auction); + const auctionIdPos = getAuctionIdPos(auctionIds, auctionId); + const bid = auction.bids[bidId]; + + if (!(bid.sendStatus & WIN_SENT) && bid.won) { + bid.sendStatus |= WIN_SENT; + + wins.push({ + adUnit: bid.adUnit, + adUnitId: bid.adUnitId, + bidder: bid.bidder, + timeStamp: auction.timeStamp, + width: bid.width, + height: bid.height, + cpm: bid.cpm, + currency: bid.currency, + originalCpm: bid.originalCpm, + originalCurrency: bid.originalCurrency, + mediaType: bid.mediaType, + gdpr: gdprPos, + auctionId: auctionIdPos, + auc: bid.auc, + lw: bid.lw, + buc: bid.buc, + rUp: bid.rUp, + meta: bid.meta, + dealId: bid.dealId + }); + } + }); + }); + + return wins; +} + +function getGdprPos(gdpr, auction) { + let gdprPos; + for (gdprPos = 0; gdprPos < gdpr.length; gdprPos++) { + if (gdpr[gdprPos].gdprApplies === auction.gdprApplies && + gdpr[gdprPos].gdprConsent === auction.gdprConsent) { + break; + } + } + + if (gdprPos === gdpr.length) { + gdpr[gdprPos] = {gdprApplies: auction.gdprApplies, gdprConsent: auction.gdprConsent}; + } + + return gdprPos; +} + +function getAuctionIdPos(auIds, auId) { + let auctionIdPos; + for (auctionIdPos = 0; auctionIdPos < auIds.length; auctionIdPos++) { + if (auIds[auctionIdPos] === auId) { + break; + } + } + + if (auctionIdPos === auIds.length) { + auIds[auctionIdPos] = auId; + } + + return auctionIdPos; +} + +function getResponseObject(auction, bid, gdprPos, auctionIdPos) { + return { + adUnit: bid.adUnit, + adUnitId: bid.adUnitId, + bidder: bid.bidder, + timeStamp: auction.timeStamp, + renderedTimestamp: bid.renderedTimestamp, + width: bid.width, + height: bid.height, + cpm: bid.cpm, + currency: bid.currency, + originalCpm: bid.originalCpm, + originalCurrency: bid.originalCurrency, + ttr: bid.ttr, + isBid: bid.isBid, + mediaType: bid.mediaType, + gdpr: gdprPos, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc, + lw: bid.lw, + meta: bid.meta + }; +} + +function getTimeouts(gdpr, auctionIds) { + const timeouts = []; + + Object.keys(cache.auctions).forEach(auctionId => { + const auctionIdPos = getAuctionIdPos(auctionIds, auctionId); + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + const auction = cache.auctions[auctionId]; + const gdprPos = getGdprPos(gdpr, auction); + const bid = auction.bids[bidId]; + if (!(bid.sendStatus & TIMEOUT_SENT) && bid.timeout) { + bid.sendStatus |= TIMEOUT_SENT; + + let timeout = getResponseObject(auction, bid, gdprPos, auctionIdPos); + + timeouts.push(timeout); + } + }); + }); + + return timeouts; +} + +function getAdRenderFailed(auctionIds) { + const adRenderFails = []; + + Object.keys(cache.auctions).forEach(auctionId => { + const auctionIdPos = getAuctionIdPos(auctionIds, auctionId); + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { + const auction = cache.auctions[auctionId]; + const bid = auction.bids[bidId]; + if (!(bid.sendStatus & AD_RENDER_FAILED_SENT) && bid.adRenderFailed) { + bid.sendStatus |= AD_RENDER_FAILED_SENT; + + adRenderFails.push({ + bidder: bid.bidder, + adUnit: bid.adUnit, + adUnitId: bid.adUnitId, + timeStamp: auction.timeStamp, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc, + lw: bid.lw, + rsn: bid.reason, + msg: bid.message + }); + } + }); + }); + + return adRenderFails; +} + +function getBidAdUnits() { + const bidAdUnits = []; + + Object.keys(cache.auctions).forEach(auctionId => { + const auction = cache.auctions[auctionId]; + Object.keys(auction.bidAdUnits).forEach(adUnit => { + const bidAdUnit = auction.bidAdUnits[adUnit]; + if (!bidAdUnit.sent) { + bidAdUnit.sent = 1; + + bidAdUnits.push({ + adUnit: adUnit, + adUnitId: bidAdUnit.adUnitId, + timeStamp: bidAdUnit.timeStamp, + lw: bidAdUnit.lw + }); + } + }); + }); + + return bidAdUnits; +} + +adapterManager.registerAnalyticsAdapter({ + adapter: adnAnalyticsAdapter, + code: 'adnuntius' +}); + +export default adnAnalyticsAdapter; diff --git a/modules/adnuntiusAnalyticsAdapter.md b/modules/adnuntiusAnalyticsAdapter.md new file mode 100644 index 00000000000..a0d931529ad --- /dev/null +++ b/modules/adnuntiusAnalyticsAdapter.md @@ -0,0 +1,22 @@ +# Overview +Module Name: Adnuntius Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: ops@adnuntius.com + +# Description + +Analytics adapter for Adnuntius AS. Contact Adnuntius AS in order to use the adapter. + +# Test Parameters + +``` +{ + provider: 'adnuntius', + options : { + publisherId: "contact-adnuntius-for-this-id" + } +} + +``` diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index 9c8aa5dd5c4..ceeb3ae1d39 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -1,27 +1,65 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { isStr, deepAccess } from '../src/utils.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; +import {isStr, isEmpty, deepAccess, getUnixTimestampFromNow, convertObjectToArray, getWindowTop, deepClone, getWinDimensions} from '../src/utils.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +import {toLegacyResponse, toOrtbNativeRequest} from '../src/native.js'; const BIDDER_CODE = 'adnuntius'; const BIDDER_CODE_DEAL_ALIAS_BASE = 'adndeal'; const BIDDER_CODE_DEAL_ALIASES = [1, 2, 3, 4, 5].map(num => { return BIDDER_CODE_DEAL_ALIAS_BASE + num; }); -const ENDPOINT_URL = 'https://ads.adnuntius.delivery/i'; -const ENDPOINT_URL_EUROPE = 'https://europe.delivery.adnuntius.com/i'; const GVLID = 855; -const DEFAULT_VAST_VERSION = 'vast4' +const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO, NATIVE]; const MAXIMUM_DEALS_LIMIT = 5; const VALID_BID_TYPES = ['netBid', 'grossBid']; const METADATA_KEY = 'adn.metaData'; const METADATA_KEY_SEPARATOR = '@@@'; +const ENVS = { + localhost: { + id: 'localhost', + as: 'localhost:8078' + }, + lcl: { + id: 'lcl', + as: 'adserver.dev.lcl.test' + }, + andemu: { + id: 'andemu', + as: '10.0.2.2:8078' + }, + dev: { + id: 'dev', + as: 'adserver.dev.adnuntius.com' + }, + staging: { + id: 'staging', + as: 'adserver.staging.adnuntius.com' + }, + production: { + id: 'production', + as: 'ads.adnuntius.delivery', + asEu: 'europe.delivery.adnuntius.com' + }, + cloudflare: { + id: 'cloudflare', + as: 'ads.adnuntius.delivery' + }, + limited: { + id: 'limited', + as: 'limited.delivery.adnuntius.com' + } +}; + export const misc = { - getUnixTimestamp: function (addDays, asMinutes) { - const multiplication = addDays / (asMinutes ? 1440 : 1); - return Date.now() + (addDays && addDays > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0); + findHighestPrice: function(arr, bidType) { + return arr.reduce((highest, cur) => { + const currentBid = cur[bidType]; + const highestBid = highest[bidType] + return currentBid.currency === highestBid.currency && currentBid.amount > highestBid.amount ? cur : highest; + }, arr[0]); } }; @@ -50,11 +88,11 @@ const storageTool = (function () { if (datum.key === 'voidAuIds' && Array.isArray(datum.value)) { return true; } - return datum.key && datum.value && datum.exp && datum.exp > misc.getUnixTimestamp() && (!network || network === datum.network); + return datum.key && datum.value && datum.exp && datum.exp > getUnixTimestampFromNow() && (!network || network === datum.network); }) : []; const voidAuIdsEntry = filteredEntries.find(entry => entry.key === 'voidAuIds'); if (voidAuIdsEntry) { - const now = misc.getUnixTimestamp(); + const now = getUnixTimestampFromNow(); voidAuIdsEntry.value = voidAuIdsEntry.value.filter(voidAuId => voidAuId.auId && voidAuId.exp > now); if (!voidAuIdsEntry.value.length) { filteredEntries = filteredEntries.filter(entry => entry.key !== 'voidAuIds'); @@ -73,7 +111,7 @@ const storageTool = (function () { const notNewExistingAuIds = currentVoidAuIds.filter(auIdObj => { return newAuIds.indexOf(auIdObj.value) < -1; }) || []; - const oneDayFromNow = misc.getUnixTimestamp(1); + const oneDayFromNow = getUnixTimestampFromNow(1); const apiIdsArray = newAuIds.map(auId => { return { exp: oneDayFromNow, auId: auId }; }) || []; @@ -86,7 +124,7 @@ const storageTool = (function () { if (key !== 'voidAuIds') { metaAsObj[key + METADATA_KEY_SEPARATOR + network] = { value: apiRespMetadata[key], - exp: misc.getUnixTimestamp(100), + exp: getUnixTimestampFromNow(100), network: network } } @@ -130,22 +168,6 @@ const storageTool = (function () { return (meta && meta.usi) ? meta.usi : false } - const getSegmentsFromOrtb = function (ortb2) { - const userData = deepAccess(ortb2, 'user.data'); - let segments = []; - if (userData) { - userData.forEach(userdat => { - if (userdat.segment) { - segments.push(...userdat.segment.map((segment) => { - if (isStr(segment)) return segment; - if (isStr(segment.id)) return segment.id; - }).filter((seg) => !!seg)); - } - }); - } - return segments - } - return { refreshStorage: function (bidderRequest) { const ortb2 = bidderRequest.ortb2 || {}; @@ -162,24 +184,79 @@ const storageTool = (function () { return voidAuId.auId; }); } - metaInternal.segments = getSegmentsFromOrtb(ortb2); }, saveToStorage: function (serverData, network) { setMetaInternal(serverData, network); }, getUrlRelatedData: function () { // getting the URL information is theoretically not network-specific - const { segments, usi, voidAuIdsArray } = metaInternal; - return { segments, usi, voidAuIdsArray }; + const { usi, voidAuIdsArray } = metaInternal; + return { usi, voidAuIdsArray }; }, getPayloadRelatedData: function (network) { // getting the payload data should be network-specific - const { segments, usi, userId, voidAuIdsArray, voidAuIds, ...payloadRelatedData } = getMetaDataFromLocalStorage(network).reduce((a, entry) => ({...a, [entry.key]: entry.value}), {}); + const { segments, usi, userId, voidAuIdsArray, voidAuIds, ...payloadRelatedData } = getMetaDataFromLocalStorage(network).reduce((a, entry) => ({ ...a, [entry.key]: entry.value }), {}); return payloadRelatedData; } }; })(); +const targetingTool = (function() { + const getSegmentsFromOrtb = function(bidderRequest) { + const userData = deepAccess(bidderRequest.ortb2 || {}, 'user.data'); + let segments = []; + if (userData && Array.isArray(userData)) { + userData.forEach(userdat => { + if (userdat.segment) { + segments.push(...userdat.segment.map((segment) => { + if (isStr(segment)) return segment; + if (isStr(segment.id)) return segment.id; + }).filter((seg) => !!seg)); + } + }); + } + return segments + }; + + const getKvsFromOrtb = function(bidderRequest, path) { + return deepAccess(bidderRequest.ortb2 || {}, path); + }; + + return { + addSegmentsToUrlData: function (validBids, bidderRequest, existingUrlRelatedData) { + let segments = getSegmentsFromOrtb(bidderRequest || {}); + + for (let i = 0; i < validBids.length; i++) { + const bid = validBids[i]; + const targeting = bid.params.targeting || {}; + if (Array.isArray(targeting.segments)) { + segments = segments.concat(targeting.segments); + delete bid.params.targeting.segments; + } + } + + existingUrlRelatedData.segments = segments; + }, + mergeKvsFromOrtb: function(bidTargeting, bidderRequest) { + const siteKvs = getKvsFromOrtb(bidderRequest || {}, 'site.ext.data'); + const userKvs = getKvsFromOrtb(bidderRequest || {}, 'user.ext.data'); + if (isEmpty(siteKvs) && isEmpty(userKvs)) { + return; + } + if (bidTargeting.kv && !Array.isArray(bidTargeting.kv)) { + bidTargeting.kv = convertObjectToArray(bidTargeting.kv); + } + bidTargeting.kv = bidTargeting.kv || []; + if (!isEmpty(siteKvs)) { + bidTargeting.kv = bidTargeting.kv.concat(convertObjectToArray(siteKvs)); + } + if (!isEmpty(userKvs)) { + bidTargeting.kv = bidTargeting.kv.concat(convertObjectToArray(userKvs)); + } + } + } +})(); + const validateBidType = function (bidTypeOption) { return VALID_BID_TYPES.indexOf(bidTypeOption || '') > -1 ? bidTypeOption : 'bid'; } @@ -190,7 +267,7 @@ export const spec = { code: BIDDER_CODE, aliases: BIDDER_CODE_DEAL_ALIASES, gvlid: GVLID, - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, isBidRequestValid: function (bid) { // The auId MUST be a hexadecimal string const validAuId = AU_ID_REGEX.test(bid.params.auId); @@ -208,6 +285,16 @@ export const spec = { queryParamsAndValues.push('consentString=' + consentString); queryParamsAndValues.push('gdpr=' + flag); } + const win = getWindowTop() || window; + if (win.screen && win.screen.availHeight) { + queryParamsAndValues.push('screen=' + win.screen.availWidth + 'x' + win.screen.availHeight); + } + + const { innerWidth, innerHeight } = getWinDimensions(); + + if (innerWidth) { + queryParamsAndValues.push('viewport=' + innerWidth + 'x' + innerHeight); + } const searchParams = new URLSearchParams(window.location.search); if (searchParams.has('script-override')) { @@ -217,11 +304,13 @@ export const spec = { storageTool.refreshStorage(bidderRequest); const urlRelatedMetaData = storageTool.getUrlRelatedData(); + targetingTool.addSegmentsToUrlData(validBidRequests, bidderRequest, urlRelatedMetaData); if (urlRelatedMetaData.segments.length > 0) queryParamsAndValues.push('segments=' + urlRelatedMetaData.segments.join(',')); if (urlRelatedMetaData.usi) queryParamsAndValues.push('userId=' + urlRelatedMetaData.usi); const bidderConfig = config.getConfig(); if (bidderConfig.useCookie === false) queryParamsAndValues.push('noCookies=true'); + if (bidderConfig.advertiserTransparency === true) queryParamsAndValues.push('advertiserTransparency=true'); if (bidderConfig.maxDeals > 0) queryParamsAndValues.push('ds=' + Math.min(bidderConfig.maxDeals, MAXIMUM_DEALS_LIMIT)); const bidRequests = {}; @@ -235,38 +324,87 @@ export const spec = { } let network = bid.params.network || 'network'; - if (bid.mediaTypes && bid.mediaTypes.video && bid.mediaTypes.video.context !== 'outstream') { - network += '_video' - } - bidRequests[network] = bidRequests[network] || []; bidRequests[network].push(bid); networks[network] = networks[network] || {}; networks[network].adUnits = networks[network].adUnits || []; - if (bidderRequest && bidderRequest.refererInfo) networks[network].context = bidderRequest.refererInfo.page; + + const refererInfo = bidderRequest && bidderRequest.refererInfo ? bidderRequest.refererInfo : {}; + if (refererInfo.page) { + networks[network].context = bidderRequest.refererInfo.page; + } + if (refererInfo.canonicalUrl) { + networks[network].canonical = bidderRequest.refererInfo.canonicalUrl; + } const payloadRelatedData = storageTool.getPayloadRelatedData(bid.params.network); if (Object.keys(payloadRelatedData).length > 0) { networks[network].metaData = payloadRelatedData; } - const targeting = bid.params.targeting || {}; - const adUnit = { ...targeting, auId: bid.params.auId, targetId: bid.params.targetId || bid.bidId }; - const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); - if (maxDeals > 0) { - adUnit.maxDeals = maxDeals; + const bidTargeting = {...bid.params.targeting || {}}; + targetingTool.mergeKvsFromOrtb(bidTargeting, bidderRequest); + const mediaTypes = bid.mediaTypes || {}; + const validMediaTypes = SUPPORTED_MEDIA_TYPES.filter(mt => { + return mediaTypes[mt]; + }) || []; + if (validMediaTypes.length === 0) { + // banner ads by default if nothing specified, dimensions to be derived from the ad unit within adnuntius system + validMediaTypes.push(BANNER); } - if (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) adUnit.dimensions = bid.mediaTypes.banner.sizes - networks[network].adUnits.push(adUnit); + const isSingleFormat = validMediaTypes.length === 1; + validMediaTypes.forEach(mediaType => { + const mediaTypeData = mediaTypes[mediaType]; + if (mediaType === VIDEO && mediaTypeData && mediaTypeData.context === 'outstream') { + return; + } + const targetId = (bid.params.targetId || bid.bidId) + (isSingleFormat || mediaType === BANNER ? '' : ('-' + mediaType)); + const adUnit = {...bidTargeting, auId: bid.params.auId, targetId: targetId}; + if (mediaType === VIDEO) { + adUnit.adType = 'VAST'; + } else if (mediaType === NATIVE) { + adUnit.adType = 'NATIVE'; + if (!mediaTypeData.ortb) { + // assume it's using old format if ortb not specified + const legacyStyleNativeRequest = deepClone(mediaTypeData); + const nativeOrtb = toOrtbNativeRequest(legacyStyleNativeRequest); + // add explicit event tracker requests for impressions and viewable impressions, which do not exist in legacy format + nativeOrtb.eventtrackers = [ + { + 'event': 1, + 'methods': [1] + }, + { + 'event': 2, + 'methods': [1] + } + ]; + adUnit.nativeRequest = {ortb: nativeOrtb} + } else { + adUnit.nativeRequest = {ortb: mediaTypeData.ortb}; + } + } + const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); + if (maxDeals > 0) { + adUnit.maxDeals = maxDeals; + } + if (mediaType !== VIDEO && mediaTypeData && mediaTypeData.sizes) { + adUnit.dimensions = mediaTypeData.sizes; + } + networks[network].adUnits.push(adUnit); + }); } const requests = []; const networkKeys = Object.keys(networks); for (let j = 0; j < networkKeys.length; j++) { const network = networkKeys[j]; - if (network.indexOf('_video') > -1) { queryParamsAndValues.push('tt=' + DEFAULT_VAST_VERSION) } - const requestURL = gdprApplies ? ENDPOINT_URL_EUROPE : ENDPOINT_URL + let requestURL = gdprApplies ? ENVS.production.asEu : ENVS.production.as; + if (bidderConfig.env && ENVS[bidderConfig.env]) { + requestURL = ENVS[bidderConfig.env][bidderConfig.endPointType || 'as']; + } + requestURL = (bidderConfig.protocol || 'https') + '://' + requestURL + '/i'; requests.push({ method: 'POST', url: requestURL + '?' + queryParamsAndValues.join('&'), @@ -282,7 +420,7 @@ export const spec = { if (serverResponse.body.metaData) { storageTool.saveToStorage(serverResponse.body.metaData, serverResponse.body.network); } - const adUnits = serverResponse.body.adUnits; + const responseAdUnits = serverResponse.body.adUnits; let validatedBidType = validateBidType(config.getConfig().bidType); if (bidRequest.bid) { @@ -293,11 +431,13 @@ export const spec = { }); } - function buildAdResponse(bidderCode, ad, adUnit, dealCount) { - const destinationUrls = ad.destinationUrls || {}; - const advertiserDomains = []; - for (const value of Object.values(destinationUrls)) { - advertiserDomains.push(value.split('/')[2]) + function buildAdResponse(bidderCode, ad, adUnit, dealCount, bidOnRequest) { + const advertiserDomains = ad.advertiserDomains || []; + if (advertiserDomains.length === 0) { + const destinationUrls = ad.destinationUrls || {}; + for (const value of Object.values(destinationUrls)) { + advertiserDomains.push(value.split('/')[2]) + } } const adResponse = { bidderCode: bidderCode, @@ -320,23 +460,59 @@ export const spec = { const isDeal = dealCount > 0; const renderSource = isDeal ? ad : adUnit; if (renderSource.vastXml) { - adResponse.vastXml = renderSource.vastXml - adResponse.mediaType = VIDEO + adResponse.vastXml = renderSource.vastXml; + adResponse.mediaType = VIDEO; + } else if (renderSource.nativeJson) { + adResponse.mediaType = NATIVE; + if (bidOnRequest.mediaTypes?.native && !bidOnRequest.mediaTypes?.native?.ortb) { + adResponse.native = toLegacyResponse(renderSource.nativeJson.ortb, toOrtbNativeRequest(bidOnRequest.mediaTypes.native)); + } else { + adResponse.native = renderSource.nativeJson; + } } else { - adResponse.ad = renderSource.html + adResponse.ad = renderSource.html; } return adResponse; } - const bidsById = bidRequest.bid.reduce((response, bid) => { + const highestYieldingAdUnits = []; + if (responseAdUnits.length === 1) { + highestYieldingAdUnits.push(responseAdUnits[0]); + } else if (responseAdUnits.length > 1) { + bidRequest.bid.forEach((resp) => { + const multiFormatAdUnits = []; + SUPPORTED_MEDIA_TYPES.forEach((mediaType) => { + const suffix = mediaType === BANNER ? '' : '-' + mediaType; + const targetId = (resp?.params?.targetId || resp.bidId) + suffix; + + const au = responseAdUnits.find((rAu) => { + return rAu.targetId === targetId && rAu.matchedAdCount > 0; + }); + if (au) { + multiFormatAdUnits.push(au); + } + }); + if (multiFormatAdUnits.length > 0) { + const highestYield = multiFormatAdUnits.length === 1 ? multiFormatAdUnits[0] : multiFormatAdUnits.reduce((highest, cur) => { + const highestBid = misc.findHighestPrice(highest.ads, validatedBidType)[validatedBidType]; + const curBid = misc.findHighestPrice(cur.ads, validatedBidType)[validatedBidType]; + return curBid.currency === highestBid.currency && curBid.amount > highestBid.amount ? cur : highest; + }, multiFormatAdUnits[0]); + highestYield.targetId = resp.bidId; + highestYieldingAdUnits.push(highestYield); + } + }); + } + + const bidRequestsById = bidRequest.bid.reduce((response, bid) => { return { ...response, [bid.bidId]: bid }; }, {}); - const hasBidAdUnits = adUnits.filter((au) => { - const bid = bidsById[au.targetId]; + const hasBidAdUnits = highestYieldingAdUnits.filter((au) => { + const bid = bidRequestsById[au.targetId]; if (bid && bid.bidder && BIDDER_CODE_DEAL_ALIASES.indexOf(bid.bidder) < 0) { return au.matchedAdCount > 0; } else { @@ -345,24 +521,24 @@ export const spec = { return false; } }); - const hasDealsAdUnits = adUnits.filter((au) => { + const hasDealsAdUnits = highestYieldingAdUnits.filter((au) => { return au.deals && au.deals.length > 0; }); const dealAdResponses = hasDealsAdUnits.reduce((response, au) => { - const bid = bidsById[au.targetId]; - if (bid) { + const selBidRequest = bidRequestsById[au.targetId]; + if (selBidRequest) { (au.deals || []).forEach((deal, i) => { - response.push(buildAdResponse(bid.bidder, deal, au, i + 1)); + response.push(buildAdResponse(selBidRequest.bidder, deal, au, i + 1, selBidRequest)); }); } return response; }, []); const bidAdResponses = hasBidAdUnits.reduce((response, au) => { - const bid = bidsById[au.targetId]; - if (bid) { - response.push(buildAdResponse(bid.bidder, au.ads[0], au, 0)); + const selBidRequest = bidRequestsById[au.targetId]; + if (selBidRequest) { + response.push(buildAdResponse(selBidRequest.bidder, au.ads[0], au, 0, selBidRequest)); } return response; }, []); diff --git a/modules/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js deleted file mode 100644 index d6e1547cce8..00000000000 --- a/modules/adomikAnalyticsAdapter.js +++ /dev/null @@ -1,262 +0,0 @@ -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; -import {EVENTS} from '../src/constants.js'; -import adapterManager from '../src/adapterManager.js'; -import {logInfo} from '../src/utils.js'; -import {find, findIndex} from '../src/polyfill.js'; - -// Events used in adomik analytics adapter. -const auctionInit = EVENTS.AUCTION_INIT; -const auctionEnd = EVENTS.AUCTION_END; -const bidRequested = EVENTS.BID_REQUESTED; -const bidResponse = EVENTS.BID_RESPONSE; -const bidWon = EVENTS.BID_WON; -const bidTimeout = EVENTS.BID_TIMEOUT; -const ua = navigator.userAgent; - -var _sampled = true; - -let adomikAdapter = Object.assign(adapter({}), - { - // Track every event needed - track({ eventType, args }) { - switch (eventType) { - case auctionInit: - adomikAdapter.initializeBucketEvents() - adomikAdapter.currentContext.id = args.auctionId - break; - - case bidTimeout: - adomikAdapter.currentContext.timeouted = true; - break; - - case bidResponse: - adomikAdapter.saveBidResponse(args); - break; - - case bidWon: - args.id = args.adId; - args.placementCode = args.adUnitCode; - adomikAdapter.sendWonEvent(args); - break; - - case bidRequested: - args.bids.forEach(function(bid) { - adomikAdapter.bucketEvents.push({ - type: 'request', - event: { - bidder: bid.bidder.toUpperCase(), - placementCode: bid.adUnitCode - } - }); - }); - break; - - case auctionEnd: - if (adomikAdapter.bucketEvents.length > 0) { - adomikAdapter.sendTypedEvent(); - } - break; - } - } - } -); - -adomikAdapter.initializeBucketEvents = function() { - adomikAdapter.bucketEvents = []; -} - -adomikAdapter.saveBidResponse = function(args) { - let responseSaved = adomikAdapter.bucketEvents.find((bucketEvent) => - bucketEvent.type == 'response' && bucketEvent.event.id == args.id - ); - if (responseSaved) { return true; } - adomikAdapter.bucketEvents.push({ - type: 'response', - event: adomikAdapter.buildBidResponse(args) - }); -} - -adomikAdapter.maxPartLength = function () { - return (ua.includes(' MSIE ')) ? 1600 : 60000; -}; - -adomikAdapter.sendTypedEvent = function() { - let [testId, testValue] = adomikAdapter.getKeyValues(); - const groupedTypedEvents = adomikAdapter.buildTypedEvents(); - - const bulkEvents = { - testId: testId, - testValue: testValue, - uid: adomikAdapter.currentContext.uid, - ahbaid: adomikAdapter.currentContext.id, - hostname: window.location.hostname, - sampling: adomikAdapter.currentContext.sampling, - eventsByPlacementCode: groupedTypedEvents.map(function(typedEventsByType) { - let sizes = []; - const eventKeys = ['request', 'response', 'winner']; - let events = {}; - - eventKeys.forEach((eventKey) => { - events[`${eventKey}s`] = []; - if (typedEventsByType[eventKey] !== undefined) { - typedEventsByType[eventKey].forEach((typedEvent) => { - if (typedEvent.event.size !== undefined) { - const size = adomikAdapter.sizeUtils.handleSize(sizes, typedEvent.event.size); - if (size !== null) { - sizes = [...sizes, size]; - } - } - events[`${eventKey}s`] = [...events[`${eventKey}s`], typedEvent.event]; - }); - } - }); - - return { - placementCode: typedEventsByType.placementCode, - sizes, - events - }; - }) - }; - - const stringBulkEvents = JSON.stringify(bulkEvents) - logInfo('Events sent to adomik prebid analytic ' + stringBulkEvents); - - const encodedBuf = window.btoa(stringBulkEvents); - - const encodedUri = encodeURIComponent(encodedBuf); - const maxLength = adomikAdapter.maxPartLength(); - const splittedUrl = encodedUri.match(new RegExp(`.{1,${maxLength}}`, 'g')); - - splittedUrl.forEach((split, i) => { - const partUrl = `${split}&id=${adomikAdapter.currentContext.id}&part=${i}&on=${splittedUrl.length - 1}`; - const img = new Image(1, 1); - img.src = 'https://' + adomikAdapter.currentContext.url + '/?q=' + partUrl; - }) -}; - -adomikAdapter.sendWonEvent = function (wonEvent) { - let [testId, testValue] = adomikAdapter.getKeyValues(); - let keyValues = { testId: testId, testValue: testValue }; - let samplingInfo = { sampling: adomikAdapter.currentContext.sampling }; - wonEvent = { ...adomikAdapter.buildBidResponse(wonEvent), ...keyValues, ...samplingInfo }; - - const stringWonEvent = JSON.stringify(wonEvent); - logInfo('Won event sent to adomik prebid analytic ' + stringWonEvent); - - const encodedBuf = window.btoa(stringWonEvent); - const encodedUri = encodeURIComponent(encodedBuf); - const img = new Image(1, 1); - img.src = `https://${adomikAdapter.currentContext.url}/?q=${encodedUri}&id=${adomikAdapter.currentContext.id}&won=true`; -} - -adomikAdapter.buildBidResponse = function (bid) { - return { - bidder: bid.bidderCode.toUpperCase(), - placementCode: bid.adUnitCode, - id: bid.adId, - status: (bid.statusMessage === 'Bid available') ? 'VALID' : 'EMPTY_OR_ERROR', - cpm: parseFloat(bid.cpm), - size: { - width: Number(bid.width), - height: Number(bid.height) - }, - timeToRespond: bid.timeToRespond, - afterTimeout: adomikAdapter.currentContext.timeouted - }; -} - -adomikAdapter.sizeUtils = { - sizeAlreadyExists: (sizes, typedEventSize) => { - return find(sizes, (size) => size.height === typedEventSize.height && size.width === typedEventSize.width); - }, - formatSize: (typedEventSize) => { - return { - width: Number(typedEventSize.width), - height: Number(typedEventSize.height) - }; - }, - handleSize: (sizes, typedEventSize) => { - let formattedSize = null; - if (adomikAdapter.sizeUtils.sizeAlreadyExists(sizes, typedEventSize) === undefined) { - formattedSize = adomikAdapter.sizeUtils.formatSize(typedEventSize); - } - return formattedSize; - } -}; - -adomikAdapter.buildTypedEvents = function () { - const groupedTypedEvents = []; - adomikAdapter.bucketEvents.forEach(function(typedEvent, i) { - const [placementCode, type] = [typedEvent.event.placementCode, typedEvent.type]; - let existTypedEvent = findIndex(groupedTypedEvents, (groupedTypedEvent) => groupedTypedEvent.placementCode === placementCode); - - if (existTypedEvent === -1) { - groupedTypedEvents.push({ - placementCode: placementCode, - [type]: [typedEvent] - }); - existTypedEvent = groupedTypedEvents.length - 1; - } - - if (groupedTypedEvents[existTypedEvent][type]) { - groupedTypedEvents[existTypedEvent][type] = [...groupedTypedEvents[existTypedEvent][type], typedEvent]; - } else { - groupedTypedEvents[existTypedEvent][type] = [typedEvent]; - } - }); - - return groupedTypedEvents; -} - -adomikAdapter.getKeyValues = function () { - let preventTest = sessionStorage.getItem(window.location.hostname + '_NoAdomikTest') - let inScope = sessionStorage.getItem(window.location.hostname + '_AdomikTestInScope') - let keyValues = JSON.parse(sessionStorage.getItem(window.location.hostname + '_AdomikTest')) - let testId; - let testValue; - if (typeof (keyValues) === 'object' && keyValues != undefined && !preventTest && inScope) { - testId = keyValues.testId - testValue = keyValues.testOptionLabel - } - return [testId, testValue] -} - -adomikAdapter.enable = function(options) { - adomikAdapter.currentContext = { - uid: options.id, - url: options.url, - id: '', - timeouted: false, - sampling: options.sampling - } - logInfo('Adomik Analytics enabled with config', options); - adomikAdapter.adapterEnableAnalytics(options); -}; - -adomikAdapter.checkOptions = function(options) { - if (typeof options !== 'undefined') { - if (options.id && options.url) { adomikAdapter.enable(options); } else { logInfo('Adomik Analytics disabled because id and/or url is missing from config', options); } - } else { logInfo('Adomik Analytics disabled because config is missing'); } -}; - -adomikAdapter.checkSampling = function(options) { - _sampled = typeof options === 'undefined' || - typeof options.sampling === 'undefined' || - (options.sampling > 0 && Math.random() < parseFloat(options.sampling)); - if (_sampled) { adomikAdapter.checkOptions(options) } else { logInfo('Adomik Analytics ignored for sampling', options.sampling); } -}; - -adomikAdapter.adapterEnableAnalytics = adomikAdapter.enableAnalytics; - -adomikAdapter.enableAnalytics = function ({ provider, options }) { - logInfo('Adomik Analytics enableAnalytics', provider); - adomikAdapter.checkSampling(options); -}; - -adapterManager.registerAnalyticsAdapter({ - adapter: adomikAdapter, - code: 'adomik' -}); - -export default adomikAdapter; diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index 9f2810e13df..3924537061c 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -1,11 +1,13 @@ -import {Renderer} from '../src/Renderer.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {isArray, isBoolean, isFn, isPlainObject, isStr, logError, replaceAuctionPrice} from '../src/utils.js'; -import {find} from '../src/polyfill.js'; -import {config} from '../src/config.js'; -import {OUTSTREAM} from '../src/video.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { Renderer } from '../src/Renderer.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { find } from '../src/polyfill.js'; +import { isArray, isBoolean, isFn, isPlainObject, isStr, logError, replaceAuctionPrice } from '../src/utils.js'; +import { OUTSTREAM } from '../src/video.js'; +import { NATIVE_ASSETS_IDS as NATIVE_ID_MAPPING, NATIVE_ASSETS as NATIVE_PLACEMENTS } from '../libraries/braveUtils/nativeAssets.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -24,6 +26,37 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; * @typedef {import('../src/adapters/bidderFactory.js').Imp} Imp */ +/** + * @typedef {Object} OpenRtbRequest + * @property {string} id - Unique request ID + * @property {Array} imp - List of impression objects + * @property {Site} site - Site information + * @property {Device} device - Device information + * @property {User} user - User information + * @property {object} regs - Regulatory data, including GDPR and COPPA + * @property {object} ext - Additional extensions, such as custom data for the bid request + * @property {number} at - Auction type, typically first-price or second-price + */ + +/** + * @typedef {Object} OpenRtbBid + * @property {string} impid - ID of the impression this bid relates to + * @property {number} price - Bid price for the impression + * @property {string} adid - Ad ID for the bid + * @property {number} [crid] - Creative ID, if available + * @property {string} [dealid] - Deal ID if the bid is part of a private marketplace deal + * @property {object} [ext] - Additional bid-specific extensions, such as media type + * @property {string} [adm] - Ad markup if it’s directly included in the bid response + * @property {string} [nurl] - Notification URL to be called when the bid wins + */ + +/** + * @typedef {Object} OpenRtbBidResponse + * @property {string} id - ID of the bid response + * @property {Array<{bid: Array}>} seatbid - Array of seat bids, each containing a list of bids + * @property {string} cur - Currency in which bid amounts are expressed + */ + const BIDDER_CODE = 'adot'; const ADAPTER_VERSION = 'v2.0.0'; const GVLID = 272; @@ -32,15 +65,6 @@ const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidreq const REQUIRED_VIDEO_PARAMS = ['mimes', 'protocols']; const FIRST_PRICE = 1; const IMP_BUILDER = { banner: buildBanner, video: buildVideo, native: buildNative }; -const NATIVE_PLACEMENTS = { - title: { id: 1, name: 'title' }, - icon: { id: 2, type: 1, name: 'img' }, - image: { id: 3, type: 3, name: 'img' }, - sponsoredBy: { id: 4, name: 'data', type: 1 }, - body: { id: 5, name: 'data', type: 2 }, - cta: { id: 6, type: 12, name: 'data' } -}; -const NATIVE_ID_MAPPING = { 1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta' }; const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; const BID_RESPONSE_NET_REVENUE = true; const BID_RESPONSE_TTL = 10; @@ -197,7 +221,7 @@ function buildVideo(video) { mimes: video.mimes, minduration: video.minduration, maxduration: video.maxduration, - placement: video.placement, + placement: video.plcmt, playbackmethod: video.playbackmethod, pos: video.position || 0, protocols: video.protocols, @@ -287,7 +311,7 @@ function buildImpFromAdUnit(adUnit, bidderRequest) { if (!mediaType) return null; const media = IMP_BUILDER[mediaType](mediaTypes[mediaType], bidderRequest, adUnit) - const currency = config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; + const currency = getCurrencyFromBidderRequest(bidderRequest) || DEFAULT_CURRENCY; const bidfloor = getMainFloor(adUnit, media.format, mediaType, currency); return { @@ -618,7 +642,7 @@ function getFloor(adUnit, size, mediaType, currency) { const floorResult = adUnit.getFloor({ currency, mediaType, size }); - return floorResult.currency === currency ? floorResult.floor : 0; + return floorResult?.currency === currency ? floorResult?.floor : 0; } /** diff --git a/modules/adpartnerBidAdapter.js b/modules/adpartnerBidAdapter.js index 471a0bba64a..504809afa87 100644 --- a/modules/adpartnerBidAdapter.js +++ b/modules/adpartnerBidAdapter.js @@ -1,6 +1,5 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { buildUrl } from '../src/utils.js' -import {ajax} from '../src/ajax.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { buildBidRequestsAndParams, postRequest, buildEndpointUrl } from '../libraries/mediaImpactUtils/index.js'; const BIDDER_CODE = 'adpartner'; export const ENDPOINT_PROTOCOL = 'https'; @@ -15,74 +14,25 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - // TODO does it make sense to fall back to window.location.href? const referer = bidderRequest?.refererInfo?.page || window.location.href; - let bidRequests = []; - let beaconParams = { - tag: [], - partner: [], - sizes: [], - referer: '' - }; - - validBidRequests.forEach(function(validBidRequest) { - let bidRequestObject = { - adUnitCode: validBidRequest.adUnitCode, - sizes: validBidRequest.sizes, - bidId: validBidRequest.bidId, - referer: referer - }; - - if (parseInt(validBidRequest.params.unitId)) { - bidRequestObject.unitId = parseInt(validBidRequest.params.unitId); - beaconParams.tag.push(validBidRequest.params.unitId); - } - - if (parseInt(validBidRequest.params.partnerId)) { - bidRequestObject.unitId = 0; - bidRequestObject.partnerId = parseInt(validBidRequest.params.partnerId); - beaconParams.partner.push(validBidRequest.params.partnerId); - } - - bidRequests.push(bidRequestObject); + // Use the common function to build bidRequests and beaconParams + const { bidRequests, beaconParams } = buildBidRequestsAndParams(validBidRequests, referer); - beaconParams.sizes.push(spec.joinSizesToString(validBidRequest.sizes)); - beaconParams.referer = encodeURIComponent(referer); - }); - - if (beaconParams.partner.length > 0) { - beaconParams.partner = beaconParams.partner.join(','); - } else { - delete beaconParams.partner; - } - - beaconParams.tag = beaconParams.tag.join(','); - beaconParams.sizes = beaconParams.sizes.join(','); - - let adPartnerRequestUrl = buildUrl({ - protocol: ENDPOINT_PROTOCOL, - hostname: ENDPOINT_DOMAIN, - pathname: ENDPOINT_PATH, - search: beaconParams - }); + const adPartnerRequestUrl = buildEndpointUrl( + ENDPOINT_PROTOCOL, + ENDPOINT_DOMAIN, + ENDPOINT_PATH, + beaconParams + ); return { method: 'POST', url: adPartnerRequestUrl, - data: JSON.stringify(bidRequests) + data: JSON.stringify(bidRequests), }; }, - joinSizesToString: function(sizes) { - let res = []; - sizes.forEach(function(size) { - res.push(size.join('x')); - }); - - return res.join('|'); - }, - interpretResponse: function (serverResponse, bidRequest) { const validBids = JSON.parse(bidRequest.data); @@ -91,15 +41,12 @@ export const spec = { } return validBids - .map(bid => ({ - bid: bid, - ad: serverResponse.body[bid.adUnitCode] - })) + .map(bid => ({ bid: bid, ad: serverResponse.body[bid.adUnitCode] })) .filter(item => item.ad) .map(item => spec.adResponse(item.bid, item.ad)); }, - adResponse: function(bid, ad) { + adResponse: function (bid, ad) { const bidObject = { requestId: bid.bidId, ad: ad.ad, @@ -110,38 +57,30 @@ export const spec = { creativeId: ad.creativeId, netRevenue: ad.netRevenue, currency: ad.currency, - winNotification: ad.winNotification - } - - bidObject.meta = {}; - if (ad.adomain && ad.adomain.length > 0) { - bidObject.meta.advertiserDomains = ad.adomain; - } + winNotification: ad.winNotification, + meta: ad.adomain && ad.adomain.length > 0 ? { advertiserDomains: ad.adomain } : {}, + }; return bidObject; }, - onBidWon: function(data) { - data.winNotification.forEach(function(unitWon) { - let adPartnerBidWonUrl = buildUrl({ - protocol: ENDPOINT_PROTOCOL, - hostname: ENDPOINT_DOMAIN, - pathname: unitWon.path - }); + onBidWon: function (data) { + data.winNotification.forEach(function (unitWon) { + const adPartnerBidWonUrl = buildEndpointUrl( + ENDPOINT_PROTOCOL, + ENDPOINT_DOMAIN, + unitWon.path + ); if (unitWon.method === 'POST') { - spec.postRequest(adPartnerBidWonUrl, JSON.stringify(unitWon.data)); + postRequest(adPartnerBidWonUrl, JSON.stringify(unitWon.data)); } }); return true; }, - postRequest(endpoint, data) { - ajax(endpoint, null, data, {method: 'POST'}); - }, - - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { const syncs = []; if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { @@ -205,7 +144,6 @@ export const spec = { return syncs; }, - -} +}; registerBidder(spec); diff --git a/modules/adplayerproVideoProvider.js b/modules/adplayerproVideoProvider.js new file mode 100644 index 00000000000..ca6fb58a4b8 --- /dev/null +++ b/modules/adplayerproVideoProvider.js @@ -0,0 +1,456 @@ +import { + API_FRAMEWORKS, + PLACEMENT, + PLAYBACK_METHODS, + PLCMT, + PROTOCOLS, + VIDEO_MIME_TYPE, + VPAID_MIME_TYPE +} from '../libraries/video/constants/ortb.js'; +import { + AD_CLICK, + AD_COMPLETE, + AD_ERROR, + AD_IMPRESSION, + AD_LOADED, + AD_PAUSE, + AD_PLAY, + AD_REQUEST, + AD_SKIPPED, + AD_STARTED, + DESTROYED, + ERROR, + MUTE, + PLAYER_RESIZE, + SETUP_COMPLETE, + SETUP_FAILED, + VOLUME +} from '../libraries/video/constants/events.js'; +import {AD_PLAYER_PRO_VENDOR} from '../libraries/video/constants/vendorCodes.js'; +import {getEventHandler} from '../libraries/video/shared/eventHandler.js'; +import {submodule} from '../src/hook.js'; + +const setupFailMessage = 'Failed to instantiate the player'; + +/** + * @class + * @param {Object} config - videoProviderConfig + * @param {function} adPlayerPro_ + * @param {CallbackStorage} callbackStorage_ + * @param {Object} utils + * @returns {Object} - VideoProvider + */ +export function AdPlayerProProvider(config, adPlayerPro_, callbackStorage_, utils) { + const adPlayerPro = adPlayerPro_; + let player = null; + let playerVersion = null; + const playerConfig = config.playerConfig; + const divId = config.divId; + let callbackStorage = callbackStorage_; + let supportedMediaTypes = null; + let setupCompleteCallbacks = []; + let setupFailedCallbacks = []; + const MEDIA_TYPES = [ + VIDEO_MIME_TYPE.MP4, + VIDEO_MIME_TYPE.OGG, + VIDEO_MIME_TYPE.WEBM, + VIDEO_MIME_TYPE.AAC, + VIDEO_MIME_TYPE.HLS + ]; + + function init() { + if (!adPlayerPro) { + triggerSetupFailure(-1, setupFailMessage + ': player not present'); + return; + } + + // if (playerVersion < minimumSupportedPlayerVersion) { + // triggerSetupFailure(-2, setupFailMessage + ': player version not supported'); + // return; + // } + + if (!document.getElementById(divId)) { + triggerSetupFailure(-3, setupFailMessage + ': No div found with id ' + divId); + return; + } + + if (!playerConfig || !playerConfig.placementId) { + triggerSetupFailure(-4, setupFailMessage + ': placementId is required in playerConfig'); + return; + } + + triggerSetupComplete(); + } + + function getId() { + return divId; + } + + function getOrtbVideo() { + supportedMediaTypes = supportedMediaTypes || utils.getSupportedMediaTypes(MEDIA_TYPES); + + const video = { + mimes: supportedMediaTypes, + protocols: [ + PROTOCOLS.VAST_2_0, + PROTOCOLS.VAST_3_0, + PROTOCOLS.VAST_4_0, + PROTOCOLS.VAST_2_0_WRAPPER, + PROTOCOLS.VAST_3_0_WRAPPER, + PROTOCOLS.VAST_4_0_WRAPPER + ], + // h: player.getHeight(), + // w: player.getWidth(), + placement: utils.getPlacement(playerConfig.advertising), + maxextended: -1, // extension is allowed, and there is no time limit imposed. + boxingallowed: 1, + playbackmethod: [utils.getPlaybackMethod(config)], + playbackend: 1, + api: [ + API_FRAMEWORKS.VPAID_2_0, + API_FRAMEWORKS.OMID_1_0 + ], + plcmt: utils.getPlcmt(playerConfig) + }; + + return video; + } + + function getOrtbContent() { + } + + function setAdTagUrl(adTagUrl, options) { + setupPlayer(playerConfig, adTagUrl || options.adXml) + } + + function setAdXml(vastXml) { + setupPlayer(playerConfig, vastXml); + } + + function onEvent(externalEventName, callback, basePayload) { + if (externalEventName === SETUP_COMPLETE) { + setupCompleteCallbacks.push(callback); + return; + } + + if (externalEventName === SETUP_FAILED) { + setupFailedCallbacks.push(callback); + return; + } + + let getEventPayload; + + switch (externalEventName) { + case AD_REQUEST: + case AD_PLAY: + case AD_PAUSE: + case AD_LOADED: + case AD_STARTED: + case AD_IMPRESSION: + case AD_CLICK: + case AD_SKIPPED: + case AD_ERROR: + case AD_COMPLETE: + case MUTE: + case VOLUME: + case ERROR: + case PLAYER_RESIZE: + getEventPayload = e => ({ + height: player.getAdHeight(), + width: player.getAdWidth(), + }); + break; + default: + return; + } + + + const playerEventName = utils.getPlayerEvent(externalEventName); + const eventHandler = getEventHandler(externalEventName, callback, basePayload, getEventPayload) + player && player.on(playerEventName, eventHandler); + callbackStorage.storeCallback(playerEventName, eventHandler, callback); + } + + function offEvent(event, callback) { + const playerEventName = utils.getPlayerEvent(event); + const eventHandler = callbackStorage.getCallback(playerEventName, callback); + if (eventHandler) { + player && player.off(playerEventName, eventHandler); + } else { + player && player.off(playerEventName); + } + callbackStorage.clearCallback(playerEventName, callback); + } + + function destroy() { + if (!player) { + return; + } + player.remove(); + player = null; + } + + return { + init, + getId, + getOrtbVideo, + getOrtbContent, + setAdTagUrl, + setAdXml, + onEvent, + offEvent, + destroy + }; + + function setupPlayer(config, urlOrXml) { + if (!config || player) { + return; + } + const playerConfig = utils.getConfig(config, urlOrXml); + + if (!playerConfig) { + return; + } + + player = adPlayerPro(divId); + callbackStorage.addAllCallbacks(player.on); + player.on('AdStopped', () => player = null); + player.setup(playerConfig); + } + + function triggerSetupComplete() { + if (!setupCompleteCallbacks.length) { + return; + } + + const payload = getSetupCompletePayload(); + setupCompleteCallbacks.forEach(callback => callback(SETUP_COMPLETE, payload)); + setupCompleteCallbacks = []; + } + + function getSetupCompletePayload() { + return { + divId, + playerVersion, + type: SETUP_COMPLETE + }; + } + + function triggerSetupFailure(errorCode, msg, sourceError) { + if (!setupFailedCallbacks.length) { + return; + } + + const payload = { + divId, + playerVersion, + type: SETUP_FAILED, + errorCode: errorCode, + errorMessage: msg, + sourceError: sourceError + }; + + setupFailedCallbacks.forEach(callback => callback(SETUP_FAILED, payload)); + setupFailedCallbacks = []; + } +} + +/** + * @param {Object} config - videoProviderConfig + * @param {sharedUtils} sharedUtils + * @returns {Object} - VideoProvider + */ +const adPlayerProSubmoduleFactory = function (config, sharedUtils) { + const callbackStorage = callbackStorageFactory(); + return AdPlayerProProvider(config, window.playerPro, callbackStorage, utils); +} + +adPlayerProSubmoduleFactory.vendorCode = AD_PLAYER_PRO_VENDOR; +submodule('video', adPlayerProSubmoduleFactory); +export default adPlayerProSubmoduleFactory; + +// HELPERS + +export const utils = { + getConfig: function (config, urlOrXml) { + if (!config || !urlOrXml) { + return; + } + + const params = config.params || {}; + params.placementId = config.placementId; + params.advertising = params.advertising || {}; + params.advertising.tag = params.advertising.tag || {}; + + params._pType = 'pbjs'; + params.advertising.tag.url = urlOrXml; + return params; + }, + + getPlayerEvent: function (eventName) { + switch (eventName) { + case DESTROYED: + return 'AdStopped'; + + case AD_REQUEST: + return 'AdRequest'; + case AD_LOADED: + return 'AdLoaded'; + case AD_STARTED: + return 'AdStarted'; + case AD_IMPRESSION: + return 'AdImpression'; + case AD_PLAY: + return 'AdPlaying'; + case AD_PAUSE: + return 'AdPaused'; + case AD_CLICK: + return 'AdClickThru'; + case AD_SKIPPED: + return 'AdSkipped'; + case AD_ERROR: + return 'AdError'; + case AD_COMPLETE: + return 'AdCompleted'; + case VOLUME: + return 'AdVolumeChange'; + case PLAYER_RESIZE: + return 'AdSizeChange'; + // case FULLSCREEN: + // return FULLSCREEN; + default: + return eventName; + } + }, + + getSupportedMediaTypes: function (mediaTypes = []) { + const el = document.createElement('video'); + return mediaTypes + .filter(mediaType => el.canPlayType(mediaType)) + .concat(VPAID_MIME_TYPE); // Always allow VPAIDs. + }, + + /** + * Determine the ad placement + * @param {Object} adConfig + * @return {PLACEMENT|undefined} + */ + getPlacement: function (adConfig) { + adConfig = adConfig || {}; + + switch (adConfig.type) { + case 'inPage': + return PLACEMENT.ARTICLE; + case 'rewarded': + case 'inView': + return PLACEMENT.INTERSTITIAL_SLIDER_FLOATING; + default: + return PLACEMENT.BANNER; + } + }, + + getPlaybackMethod: function ({autoplay, mute}) { + if (autoplay) { + return mute ? PLAYBACK_METHODS.AUTOPLAY_MUTED : PLAYBACK_METHODS.AUTOPLAY; + } + return PLAYBACK_METHODS.CLICK_TO_PLAY; + }, + + getPlcmt: function ({type, autoplay, muted, file}) { + type = type || 'inStream'; + if (!file) { + // INTERSTITIAL: primary focus of the page and take up the majority of the viewport and cannot be scrolled out of view. + return type === 'rewarded' || type === 'inView' ? PLCMT.INTERSTITIAL : PLCMT.OUTSTREAM; + } + // INSTREAM must be set to “sound on” by default at player start + return type === 'inStream' && (!muted || !autoplay) ? PLCMT.INSTREAM : PLCMT.ACCOMPANYING_CONTENT; + } +} + +/** + * Tracks which functions are attached to events + * @typedef CallbackStorage + * @function storeCallback + * @function getCallback + * @function clearCallback + * @function addAllCallbacks + * @function clearStorage + */ + +/** + * @returns {CallbackStorage} + */ +export function callbackStorageFactory() { + let storage = {}; + let storageHandlers = {}; + + function storeCallback(eventType, eventHandler, callback) { + let eventHandlers = storage[eventType]; + if (!eventHandlers) { + eventHandlers = storage[eventType] = {}; + } + + eventHandlers[callback] = eventHandler; + addHandler(eventType, eventHandler); + } + + function getCallback(eventType, callback) { + let eventHandlers = storage[eventType]; + if (eventHandlers) { + return eventHandlers[callback]; + } + } + + function clearCallback(eventType, callback) { + if (!callback) { + delete storage[eventType]; + delete storageHandlers[eventType]; + return; + } + let eventHandlers = storage[eventType]; + if (eventHandlers) { + const eventHandler = eventHandlers[callback]; + if (eventHandler) { + delete eventHandlers[callback]; + clearHandler(eventType, eventHandler); + } + } + } + + function clearStorage() { + storage = {}; + storageHandlers = {}; + } + + function addHandler(eventType, eventHandler) { + let eventHandlers = storageHandlers[eventType]; + if (!eventHandlers) { + eventHandlers = storageHandlers[eventType] = []; + } + eventHandlers.push(eventHandler); + } + + function clearHandler(eventType, eventHandler) { + let eventHandlers = storageHandlers[eventType]; + eventHandlers = eventHandlers.filter(handler => handler !== eventHandler); + if (eventHandlers.length) { + storageHandlers[eventType] = eventHandlers; + } else { + delete storageHandlers[eventType]; + } + } + + function addAllCallbacks(functionOnPlayer) { + for (let eventType in storageHandlers) { + storageHandlers[eventType].forEach(handler => functionOnPlayer(eventType, handler)); + } + } + + return { + storeCallback, + getCallback, + clearCallback, + addAllCallbacks, + clearStorage, + } +} diff --git a/modules/adplayerproVideoProvider.md b/modules/adplayerproVideoProvider.md new file mode 100644 index 00000000000..d0b0601afeb --- /dev/null +++ b/modules/adplayerproVideoProvider.md @@ -0,0 +1,54 @@ +# Overview + +Module Name: AdPlayer.Pro Video Provider +Module Type: Video Submodule +Video Player: AdPlayer.Pro +Player website: https://adplayer.pro +Maintainer: support@adplayer.pro + +# Description + +Video provider to connect the Prebid Video Module to AdPlayer.Pro. + +# Requirements + +Your page must embed a build of AdPlayer.Pro. +i.e. +```html + + + +``` + +# Configuration + +The AdPlayer.Pro Video Provider requires the following configuration: + +```javascript +pbjs.setConfig({ + video: { + providers: [{ + divId: 'player', // required, this is the id of the div element where the player will be placed + vendorCode: 3, // AdPlayer.Pro vendorCode + playerConfig: { + placementId: 'c9gebfehcqjE', // required, this placementId is only for demo purposes + params: { + 'type': 'inView', + 'muted': true, + 'autoStart': true, + 'advertising': { + 'controls': true, + 'closeButton': true, + }, + 'width': '600', + 'height': '300' + } + }, + }] + } +}); +``` + +[Additional embed instructions](https://docs.adplayer.pro) + +[Obtaining a license](https://adplayer.pro/contacts) diff --git a/modules/adpod.js b/modules/adpod.js index b6d13673178..35a78766979 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -451,7 +451,6 @@ export function callPrebidCacheAfterAuction(bids, callback) { /** * Compare function to be used in sorting long-form bids. This will compare bids on price per second. * @param {Object} bid - * @param {Object} bid */ export function sortByPricePerSecond(a, b) { if (a.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / a.video.durationBucket < b.adserverTargeting[TARGETING_KEYS.PRICE_BUCKET] / b.video.durationBucket) { @@ -465,10 +464,10 @@ export function sortByPricePerSecond(a, b) { /** * This function returns targeting keyvalue pairs for long-form adserver modules. Freewheel and GAM are currently supporting Prebid long-form - * @param {Object} options - * @param {Array[string]} codes - * @param {function} callback - * @returns targeting kvs for adUnitCodes + * @param {Object} options - Options for targeting. + * @param {Array} options.codes - Array of ad unit codes. + * @param {function} options.callback - Callback function to handle the targeting key-value pairs. + * @returns {Object} Targeting key-value pairs for ad unit codes. */ export function getTargeting({ codes, callback } = {}) { if (!callback) { diff --git a/modules/adprimeBidAdapter.js b/modules/adprimeBidAdapter.js index 55ee1f0900c..e40d20356af 100644 --- a/modules/adprimeBidAdapter.js +++ b/modules/adprimeBidAdapter.js @@ -1,183 +1,46 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { isFn, deepAccess, logMessage } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import { getAllOrtbKeywords } from '../libraries/keywords/keywords.js'; +import { + isBidRequestValid, + buildRequestsBase, + interpretResponse, + getUserSyncs, + buildPlacementProcessingFunction +} from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'adprime'; const AD_URL = 'https://delta.adprime.com/pbjs'; const SYNC_URL = 'https://sync.adprime.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency || !bid.meta) { - return false; +const addCustomFieldsToPlacement = (bid, bidderRequest, placement) => { + if (placement.adFormat === VIDEO) { + placement.wPlayer = placement.playerSize?.[0]?.[0]; + placement.hPlayer = placement.playerSize?.[0]?.[1]; } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl); - case NATIVE: - return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); - default: - return false; + if (bid.userId && bid.userId.idl_env) { + placement.identeties = {}; + placement.identeties.identityLink = bid.userId.idl_env; } -} -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } + placement.keywords = getAllOrtbKeywords(bidderRequest.ortb2, bid.params.keywords); + placement.audiences = bid.params.audiences || []; +}; - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} +const placementProcessingFunction = buildPlacementProcessingFunction({ addCustomFieldsToPlacement }); + +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + return buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest, placementProcessingFunction }); +}; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && bid.params.placementId); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let winTop = window; - let location; - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - let placements = []; - let request = { - deviceWidth: winTop.screen.width, - deviceHeight: winTop.screen.height, - language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - secure: 1, - host: location.host, - page: location.pathname, - placements: placements - }; - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent - } - } - const len = validBidRequests.length; - - for (let i = 0; i < len; i++) { - let bid = validBidRequests[i]; - const { mediaTypes } = bid; - const placement = {}; - let sizes - let identeties = {} - if (mediaTypes) { - if (mediaTypes[BANNER] && mediaTypes[BANNER].sizes) { - placement.adFormat = BANNER; - sizes = mediaTypes[BANNER].sizes - } else if (mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize) { - placement.adFormat = VIDEO; - sizes = mediaTypes[VIDEO].playerSize - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else { - placement.adFormat = NATIVE; - placement.native = mediaTypes[NATIVE]; - } - } - if (bid.userId && bid.userId.idl_env) { - identeties.identityLink = bid.userId.idl_env - } - - placements.push({ - ...placement, - placementId: bid.params.placementId, - bidId: bid.bidId, - sizes: sizes || [], - wPlayer: sizes ? sizes[0] : 0, - hPlayer: sizes ? sizes[1] : 0, - schain: bid.schain || {}, - keywords: getAllOrtbKeywords(bidderRequest.ortb2, bid.params.keywords), - audiences: bid.params.audiences || [], - identeties, - bidFloor: getBidFloor(bid) - }); - } - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests, + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index 68cd859e24e..7d18135f2e8 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -18,9 +18,7 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; -import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; import {chunk} from '../libraries/chunk/chunk.js'; /** @@ -187,28 +185,6 @@ export const spec = { } return bids; - }, - - transformBidParams: function(params, isOpenRtb) { - params = convertTypes({ - 'placementId': 'number', - 'keywords': transformBidderParamKeywords - }, params); - - if (isOpenRtb) { - params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; - if (params.usePaymentRule) { delete params.usePaymentRule; } - - Object.keys(params).forEach(paramKey => { - let convertedKey = convertCamelToUnderscore(paramKey); - if (convertedKey !== paramKey) { - params[convertedKey] = params[paramKey]; - delete params[paramKey]; - } - }); - } - - return params; } }; diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index 5bce315f572..7ac8e4fb728 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,5 +1,5 @@ // ADRIVER BID ADAPTER for Prebid 1.13 -import {logInfo, getWindowLocation, _each, getBidIdParameter} from '../src/utils.js'; +import {logInfo, getWindowLocation, _each, getBidIdParameter, isPlainObject} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -188,12 +188,12 @@ function _getFloor(bid, currencyPar, sizes) { size: isSize ? sizes : '*' }); - if (typeof floorInfo === 'object' && - !isNaN(parseFloat(floorInfo.floor))) { + if (isPlainObject(floorInfo) && + !isNaN(parseFloat(floorInfo?.floor))) { floor = floorInfo.floor; } - if (typeof floorInfo === 'object' && floorInfo.currency) { + if (isPlainObject(floorInfo) && floorInfo.currency) { currencyResult = floorInfo.currency; } } diff --git a/modules/ads_interactiveBidAdapter.js b/modules/ads_interactiveBidAdapter.js new file mode 100644 index 00000000000..9a827cb4914 --- /dev/null +++ b/modules/ads_interactiveBidAdapter.js @@ -0,0 +1,21 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'ads_interactive'; +const AD_URL = 'https://bntb.adsinteractive.com/pbjs'; +const SYNC_URL = 'https://cstb.adsinteractive.com'; +const GVLID = 1212; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/ads_interactiveBidAdapter.md b/modules/ads_interactiveBidAdapter.md new file mode 100755 index 00000000000..43c4727db45 --- /dev/null +++ b/modules/ads_interactiveBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: AdsInteractive Bidder Adapter +Module Type: AdsInteractive Bidder Adapter +Maintainer: it@adsinteractive.com +``` + +# Description + +Connects to AdsInteractive exchange for bids. +AdsInteractive 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: 'ads_interactive', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'ads_interactive', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'ads_interactive', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/adspiritBidAdapter.js b/modules/adspiritBidAdapter.js index c39ceca8600..b7647a7d491 100644 --- a/modules/adspiritBidAdapter.js +++ b/modules/adspiritBidAdapter.js @@ -1,7 +1,8 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; - +import { getGlobal } from '../src/prebidGlobal.js'; +const { getWinDimensions } = utils; const RTB_URL = '/rtb/getbid.php?rtbprovider=prebid'; const SCRIPT_URL = '/adasync.min.js'; @@ -18,92 +19,251 @@ export const spec = { } return true; }, - + getScriptUrl: function () { + return SCRIPT_URL; + }, buildRequests: function (validBidRequests, bidderRequest) { let requests = []; + let prebidVersion = getGlobal().version; + const win = getWinDimensions(); + for (let i = 0; i < validBidRequests.length; i++) { let bidRequest = validBidRequests[i]; bidRequest.adspiritConId = spec.genAdConId(bidRequest); let reqUrl = spec.getBidderHost(bidRequest); let placementId = utils.getBidIdParameter('placementId', bidRequest.params); - reqUrl = '//' + reqUrl + RTB_URL + '&pid=' + placementId + + + reqUrl = '//' + reqUrl + RTB_URL + + '&pid=' + placementId + '&ref=' + encodeURIComponent(bidderRequest.refererInfo.topmostLocation) + - '&scx=' + (screen.width) + - '&scy=' + (screen.height) + - '&wcx=' + (window.innerWidth || document.documentElement.clientWidth) + - '&wcy=' + (window.innerHeight || document.documentElement.clientHeight) + + '&scx=' + (win.screen?.width || 0) + + '&scy=' + (win.screen?.height || 0) + + '&wcx=' + win.innerWidth + + '&wcy=' + win.innerHeight + '&async=' + bidRequest.adspiritConId + '&t=' + Math.round(Math.random() * 100000); - let data = {}; + let gdprApplies = bidderRequest.gdprConsent ? (bidderRequest.gdprConsent.gdprApplies ? 1 : 0) : 0; + let gdprConsentString = bidderRequest.gdprConsent ? encodeURIComponent(bidderRequest.gdprConsent.consentString) : ''; - if (bidderRequest && bidderRequest.gdprConsent) { - const gdprConsentString = bidderRequest.gdprConsent.consentString; - reqUrl += '&gdpr=' + encodeURIComponent(gdprConsentString); + if (bidderRequest.gdprConsent) { + reqUrl += '&gdpr=' + gdprApplies + '&gdpr_consent=' + gdprConsentString; } - if (bidRequest.schain && bidderRequest.schain) { - data.schain = bidRequest.schain; + let openRTBRequest = { + id: bidderRequest.auctionId, + at: 1, + cur: ['EUR'], + imp: [{ + id: bidRequest.bidId, + bidfloor: bidRequest.params.bidfloor !== undefined ? parseFloat(bidRequest.params.bidfloor) : 0, + bidfloorcur: 'EUR', + secure: 1, + banner: (bidRequest.mediaTypes.banner && bidRequest.mediaTypes.banner.sizes?.length > 0) ? { + format: bidRequest.mediaTypes.banner.sizes.map(size => ({ + w: size[0], + h: size[1] + })) + } : undefined, + native: (bidRequest.mediaTypes.native) ? { + request: JSON.stringify({ + ver: '1.2', + assets: bidRequest.mediaTypes.native.ortb?.assets?.length + ? bidRequest.mediaTypes.native.ortb.assets + : [ + { id: 1, required: 1, title: { len: 100 } }, + { id: 2, required: 1, img: { type: 3, wmin: 1200, hmin: 627, mimes: ['image/png', 'image/gif', 'image/jpeg'] } }, + { id: 4, required: 1, data: {type: 2, len: 150} }, + { id: 3, required: 0, data: {type: 12, len: 50} }, + { id: 6, required: 0, data: {type: 1, len: 50} }, + { id: 5, required: 0, img: { type: 1, wmin: 50, hmin: 50, mimes: ['image/png', 'image/gif', 'image/jpeg'] } } + + ] + }) + } : undefined, + ext: { + placementId: bidRequest.params.placementId + } + }], + + site: { + id: bidRequest.params.siteId || '', + domain: new URL(bidderRequest.refererInfo.topmostLocation).hostname, + page: bidderRequest.refererInfo.topmostLocation, + publisher: { + id: bidRequest.params.publisherId || '', + name: bidRequest.params.publisherName || '' + } + }, + user: { + id: bidRequest.userId || '', + data: bidRequest.userData || [], + ext: { + consent: gdprConsentString || '' + } + }, + device: { + ua: navigator.userAgent, + language: (navigator.language || '').split('-')[0], + w: win.innerWidth, + h: win.innerHeight, + geo: { + lat: bidderRequest?.geo?.lat || 0, + lon: bidderRequest?.geo?.lon || 0, + country: bidderRequest?.geo?.country || '' + } + }, + regs: { + ext: { + gdpr: gdprApplies ? 1 : 0, + gdpr_consent: gdprConsentString || '' + } + }, + ext: { + oat: 1, + prebidVersion: prebidVersion, + adUnitCode: { + prebidVersion: prebidVersion, + code: bidRequest.adUnitCode, + mediaTypes: bidRequest.mediaTypes + } + } + }; + + if (bidRequest.schain) { + openRTBRequest.source = { + ext: { + schain: bidRequest.schain + } + }; } requests.push({ - method: 'GET', + method: 'POST', url: reqUrl, - data: data, + data: JSON.stringify(openRTBRequest), + headers: { 'Content-Type': 'application/json' }, bidRequest: bidRequest }); } + return requests; }, - interpretResponse: function(serverResponse, bidRequest) { + interpretResponse: function (serverResponse, bidRequest) { const bidResponses = []; - let bidObj = bidRequest.bidRequest; + const bidObj = bidRequest.bidRequest; + let host = spec.getBidderHost(bidObj); - if (!serverResponse || !serverResponse.body || !bidObj) { - utils.logWarn(`No valid bids from ${spec.code} bidder!`); + if (!serverResponse || !serverResponse.body) { + utils.logWarn(`adspirit: Empty response from bidder`); return []; } - let adData = serverResponse.body; - let cpm = adData.cpm; + if (serverResponse.body.seatbid) { + serverResponse.body.seatbid.forEach(seat => { + seat.bid.forEach(bid => { + const bidResponse = { + requestId: bidObj.bidId, + cpm: bid.price, + width: bid.w || 1, + height: bid.h || 1, + creativeId: bid.crid || bid.impid, + currency: serverResponse.body.cur || 'EUR', + netRevenue: true, + ttl: bid.exp || 300, + meta: { + advertiserDomains: bid.adomain || [] + } + }; - if (!cpm) { - return []; - } + let adm = bid.adm; + if (typeof adm === 'string' && adm.trim().startsWith('{')) { + adm = JSON.parse(adm || '{}'); + if (typeof adm !== 'object') adm = null; + } - let host = spec.getBidderHost(bidObj); + if (adm?.native?.assets) { + const getAssetValue = (id, type) => { + const assetList = adm.native.assets.filter(a => a.id === id); + if (assetList.length === 0) return ''; + return assetList[0][type]?.text || assetList[0][type]?.value || assetList[0][type]?.url || ''; + }; - const bidResponse = { - requestId: bidObj.bidId, - cpm: cpm, - width: adData.w, - height: adData.h, - creativeId: bidObj.params.placementId, - currency: 'EUR', - netRevenue: true, - ttl: 300, - meta: { - advertiserDomains: bidObj && bidObj.adomain ? bidObj.adomain : [] - } - }; - - if ('mediaTypes' in bidObj && 'native' in bidObj.mediaTypes) { - bidResponse.native = { - title: adData.title, - body: adData.body, - cta: adData.cta, - image: { url: adData.image }, - clickUrl: adData.click, - impressionTrackers: [adData.view] - }; - bidResponse.mediaType = NATIVE; + const duplicateTracker = {}; + + bidResponse.native = { + title: getAssetValue(1, 'title'), + body: getAssetValue(4, 'data'), + cta: getAssetValue(3, 'data'), + image: { url: getAssetValue(2, 'img') || '' }, + icon: { url: getAssetValue(5, 'img') || '' }, + sponsoredBy: getAssetValue(6, 'data'), + clickUrl: adm.native.link?.url || '', + impressionTrackers: Array.isArray(adm.native.imptrackers) ? adm.native.imptrackers : [] + }; + + const predefinedAssetIds = Object.entries(bidResponse.native) + .filter(([key, value]) => key !== 'clickUrl' && key !== 'impressionTrackers') + .map(([key, value]) => adm.native.assets.find(asset => + typeof value === 'object' ? value.url === asset?.img?.url : value === asset?.data?.value + )?.id) + .filter(id => id !== undefined); + + adm.native.assets.forEach(asset => { + const type = Object.keys(asset).find(k => k !== 'id'); + + if (!duplicateTracker[asset.id]) { + duplicateTracker[asset.id] = 1; + } else { + duplicateTracker[asset.id]++; + } + + if (predefinedAssetIds.includes(asset.id) && duplicateTracker[asset.id] === 1) return; + + if (type && asset[type]) { + const value = asset[type].text || asset[type].value || asset[type].url || ''; + + if (type === 'img') { + bidResponse.native[`image_${asset.id}_extra${duplicateTracker[asset.id] - 1}`] = { + url: value, width: asset.img.w || null, height: asset.img.h || null + }; + } else { + bidResponse.native[`data_${asset.id}_extra${duplicateTracker[asset.id] - 1}`] = value; + } + } + }); + + bidResponse.mediaType = NATIVE; + } + + bidResponses.push(bidResponse); + }); + }); } else { + let adData = serverResponse.body; + let cpm = adData.cpm; + + if (!cpm) return []; + const bidResponse = { + requestId: bidObj.bidId, + cpm: cpm, + width: adData.w, + height: adData.h, + creativeId: bidObj.params.placementId, + currency: 'EUR', + netRevenue: true, + ttl: 300, + meta: { + advertiserDomains: adData.adomain || [] + } + }; let adm = '' + adData.adm; bidResponse.ad = adm; bidResponse.mediaType = BANNER; + + bidResponses.push(bidResponse); } - bidResponses.push(bidResponse); return bidResponses; }, getBidderHost: function (bid) { diff --git a/modules/adspiritBidAdapter.md b/modules/adspiritBidAdapter.md index 698ed9b4a0e..ea21dbe70e5 100644 --- a/modules/adspiritBidAdapter.md +++ b/modules/adspiritBidAdapter.md @@ -23,31 +23,74 @@ Each adunit with `adspirit` adapter has to have `placementId` and `host`. ## Sample Banner Ad Unit ```javascript - var adUnits = [ - { - code: 'display-div', - - mediaTypes: { - banner: { - sizes: [[300, 250]] //a display size - } - }, - - bids: [ - { - bidder: "adspirit", - params: { - placementId: '7', //Please enter your placementID - host: 'test.adspirit.de' //your host details from Adspirit - } - } - ] - } - ]; - + var adUnits = [ + // Banner Ad Unit + { + code: 'display-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] // A display size + } + }, + bids: [ + { + bidder: "adspirit", + params: { + placementId: '7', // Please enter your placementID + host: 'test.adspirit.de' // Your host details from Adspirit + } + } + ] + }, + // Native Ad Unit + { + code: 'native-div', + mediaTypes: { + native: { + ortb: { + request: { + ver: "1.2", + assets: [ + { id: 1, required: 1, title: { len: 100 } }, // Title + { id: 2, required: 1, img: { type: 3, wmin: 1200, hmin: 627, mimes: ["image/png", "image/gif", "image/jpeg"] } }, // Main Image + { id: 4, required: 1, data: { type: 2, len: 150 } }, // Body Text + { id: 3, required: 0, data: { type: 12, len:50 } }, // CTA Text + { id: 6, required: 0, data: { type: 1, len:50 } }, // Sponsored By + { id: 5, required: 0, img: { type: 1, wmin: 50, hmin: 50, mimes: ["image/png", "image/gif", "image/jpeg"] } } // Icon Image + ] + } + } + } + }, + bids: [ + { + bidder: 'adspirit', + params: { + placementId: '99', + host: 'test.adspirit.de', + bidfloor: 0.1 + } + } + ] + } +]; ``` +### Short description in five points for native +1. Title (id:1): This is the main heading of the ad, and it should be mandatory with a maximum length of 100 characters. + +2. Main Image (id:2): This is the main image that represents the ad content and should be in PNG, GIF, or JPEG format, with the following dimensions: wmin: 1200 and hmin: 627. + +3. Body Text (id:4): A brief description of the ad. The Body Text should have a maximum length of 150 characters. + +4. CTA (Call to Action) (id:3): A short phrase prompting user action, such as "Shop Now", "Get More Info", etc. + +5. Sponsored By (id:6): The advertiser or brand name promoting the ad. + +6. Click URL: This is the landing page URL where the user will be redirected after clicking the ad. + +In the Adspirit adapter, Title, Main Image, and Body Text are mandatory fields. ### Privacy Policies General Data Protection Regulation(GDPR) is supported by default. diff --git a/modules/adtargetBidAdapter.js b/modules/adtargetBidAdapter.js index a1dec5a420f..4b20ff06dca 100644 --- a/modules/adtargetBidAdapter.js +++ b/modules/adtargetBidAdapter.js @@ -4,6 +4,11 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; import {chunk} from '../libraries/chunk/chunk.js'; +import { + createTag, getUserSyncsFn, + isBidRequestValid, + supportedMediaTypes +} from '../libraries/adtelligentUtils/adtelligentUtils.js'; const ENDPOINT = 'https://ghb.console.adtarget.com.tr/v2/auction/'; const BIDDER_CODE = 'adtarget'; @@ -12,50 +17,11 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [VIDEO, BANNER], - isBidRequestValid: function (bid) { - return !!deepAccess(bid, 'params.aid'); - }, + gvlid: 779, + supportedMediaTypes, + isBidRequestValid, getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; - - function addSyncs(bid) { - const uris = bid.cookieURLs; - const types = bid.cookieURLSTypes || []; - - if (Array.isArray(uris)) { - uris.forEach((uri, i) => { - const type = types[i] || 'image'; - - if ((!syncOptions.pixelEnabled && type === 'image') || - (!syncOptions.iframeEnabled && type === 'iframe') || - syncsCache[uri]) { - return; - } - - syncsCache[uri] = true; - syncs.push({ - type: type, - url: uri - }) - }) - } - } - - if (syncOptions.pixelEnabled || syncOptions.iframeEnabled) { - isArray(serverResponses) && serverResponses.forEach((response) => { - if (response.body) { - if (isArray(response.body)) { - response.body.forEach(b => { - addSyncs(b); - }) - } else { - addSyncs(response.body) - } - } - }) - } - return syncs; + return getUserSyncsFn(syncOptions, serverResponses, syncsCache) }, buildRequests: function (bidRequests, adapterRequest) { @@ -117,26 +83,7 @@ function parseResponse(serverResponse, adapterRequest) { } function bidToTag(bidRequests, adapterRequest) { - const tag = { - // TODO: is 'page' the right value here? - Domain: deepAccess(adapterRequest, 'refererInfo.page') - }; - if (config.getConfig('coppa') === true) { - tag.Coppa = 1; - } - if (deepAccess(adapterRequest, 'gdprConsent.gdprApplies')) { - tag.GDPR = 1; - tag.GDPRConsent = deepAccess(adapterRequest, 'gdprConsent.consentString'); - } - if (deepAccess(adapterRequest, 'uspConsent')) { - tag.USP = deepAccess(adapterRequest, 'uspConsent'); - } - if (deepAccess(bidRequests[0], 'schain')) { - tag.Schain = deepAccess(bidRequests[0], 'schain'); - } - if (deepAccess(bidRequests[0], 'userId')) { - tag.UserIds = deepAccess(bidRequests[0], 'userId'); - } + const tag = createTag(bidRequests, adapterRequest); const bids = []; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index a95b9ed5652..f8ad874ab4d 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -4,8 +4,12 @@ import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; import {find} from '../src/polyfill.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; import {chunk} from '../libraries/chunk/chunk.js'; +import { + createTag, getUserSyncsFn, + isBidRequestValid, + supportedMediaTypes +} from '../libraries/adtelligentUtils/adtelligentUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid @@ -26,8 +30,8 @@ const HOST_GETTERS = { janet: () => 'ghb.bidder.jmgads.com', ocm: () => 'ghb.cenarius.orangeclickmedia.com', '9dotsmedia': () => 'ghb.platform.audiodots.com', - copper6: () => 'ghb.app.copper6.com', indicue: () => 'ghb.console.indicue.com', + stellormedia: () => 'ghb.ads.stellormedia.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -49,53 +53,13 @@ export const spec = { { code: 'selectmedia', gvlid: 775 }, { code: 'ocm', gvlid: 1148 }, '9dotsmedia', - 'copper6', 'indicue', + 'stellormedia' ], - supportedMediaTypes: [VIDEO, BANNER], - isBidRequestValid: function (bid) { - return !!deepAccess(bid, 'params.aid'); - }, + supportedMediaTypes, + isBidRequestValid, getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; - - function addSyncs(bid) { - const uris = bid.cookieURLs; - const types = bid.cookieURLSTypes || []; - - if (Array.isArray(uris)) { - uris.forEach((uri, i) => { - const type = types[i] || 'image'; - - if ((!syncOptions.pixelEnabled && type === 'image') || - (!syncOptions.iframeEnabled && type === 'iframe') || - syncsCache[uri]) { - return; - } - - syncsCache[uri] = true; - syncs.push({ - type: type, - url: uri - }) - }) - } - } - - if (syncOptions.pixelEnabled || syncOptions.iframeEnabled) { - isArray(serverResponses) && serverResponses.forEach((response) => { - if (response.body) { - if (isArray(response.body)) { - response.body.forEach(b => { - addSyncs(b); - }) - } else { - addSyncs(response.body) - } - } - }) - } - return syncs; + return getUserSyncsFn(syncOptions, serverResponses, syncsCache) }, /** * Make a server request from the list of BidRequests @@ -138,11 +102,6 @@ export const spec = { return bids; }, - transformBidParams(params) { - return convertTypes({ - 'aid': 'number', - }, params); - } }; function parseRTBResponse(serverResponse, adapterRequest) { @@ -170,29 +129,7 @@ function parseRTBResponse(serverResponse, adapterRequest) { function bidToTag(bidRequests, adapterRequest) { // start publisher env - const tag = { - // TODO: is 'page' the right value here? - Domain: deepAccess(adapterRequest, 'refererInfo.page') - }; - if (config.getConfig('coppa') === true) { - tag.Coppa = 1; - } - if (deepAccess(adapterRequest, 'gdprConsent.gdprApplies')) { - tag.GDPR = 1; - tag.GDPRConsent = deepAccess(adapterRequest, 'gdprConsent.consentString'); - } - if (deepAccess(adapterRequest, 'uspConsent')) { - tag.USP = deepAccess(adapterRequest, 'uspConsent'); - } - if (deepAccess(bidRequests[0], 'schain')) { - tag.Schain = deepAccess(bidRequests[0], 'schain'); - } - if (deepAccess(bidRequests[0], 'userId')) { - tag.UserIds = deepAccess(bidRequests[0], 'userId'); - } - if (deepAccess(bidRequests[0], 'userIdAsEids')) { - tag.UserEids = deepAccess(bidRequests[0], 'userIdAsEids'); - } + const tag = createTag(bidRequests, adapterRequest); if (window.adtDmp && window.adtDmp.ready) { tag.DMPId = window.adtDmp.getUID(); } @@ -315,6 +252,7 @@ function createBid(bidResponse, bidRequest) { /** * Create Adtelligent renderer * @param requestId + * @param bidderParams * @returns {*} */ function newRenderer(requestId, bidderParams) { diff --git a/modules/adtelligentIdSystem.js b/modules/adtelligentIdSystem.js index 76713f29775..bb5e1f82129 100644 --- a/modules/adtelligentIdSystem.js +++ b/modules/adtelligentIdSystem.js @@ -72,7 +72,7 @@ export const adtelligentIdModule = { * @param {ConsentData} [consentData] * @returns {IdResponse} */ - getId(config, consentData) { + getId(config, {gdpr: consentData} = {}) { const gdpr = consentData && consentData.gdprApplies ? 1 : 0; const gdprConsent = gdpr ? consentData.consentString : ''; const url = buildUrl({ diff --git a/modules/adtrgtmeBidAdapter.js b/modules/adtrgtmeBidAdapter.js index 4dc95ce6bc1..9432031e77e 100644 --- a/modules/adtrgtmeBidAdapter.js +++ b/modules/adtrgtmeBidAdapter.js @@ -1,333 +1,295 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { deepAccess, isFn, isStr, isNumber, isArray, isEmpty, isPlainObject, generateUUID, logWarn } from '../src/utils.js'; +import { + isFn, + isStr, + isNumber, + isEmpty, + isPlainObject, + generateUUID, + logWarn, +} from '../src/utils.js'; import { config } from '../src/config.js'; -import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { hasPurpose1Consent } from '../src/utils/gdpr.js'; -const INTEGRATION_METHOD = 'prebid.js'; const BIDDER_CODE = 'adtrgtme'; -const ENDPOINT = 'https://z.cdn.adtarget.market/ssp?prebid&s='; -const ADAPTER_VERSION = '1.0.0'; -const PREBID_VERSION = '$prebid.version$'; -const DEFAULT_BID_TTL = 300; -const DEFAULT_CURRENCY = 'USD'; - -function transformSizes(sizes) { - const getSize = (size) => { - return { - w: parseInt(size[0]), - h: parseInt(size[1]) - } - } - if (isArray(sizes) && sizes.length === 2 && !isArray(sizes[0])) { - return [ getSize(sizes) ]; - } - return sizes.map(getSize); +const BIDDER_VERSION = '1.0.6'; +const BIDDER_URL = 'https://z.cdn.adtarget.market/ssp?prebid&s='; +const PREBIDJS_VERSION = '$prebid.version$'; +const DEFAULT_TTL = 300; +const DEFAULT_CUR = 'USD'; + +function getFormat(s) { + const parseSize = ([w, h]) => ({ w: parseInt(w, 10), h: parseInt(h, 10) }); + return Array.isArray(s) && s.length === 2 && !Array.isArray(s[0]) + ? [parseSize(s)] + : s.map(parseSize); } -function extractUserSyncUrls(syncOptions, pixels) { - let itemsRegExp = /(img|iframe)[\s\S]*?src\s*=\s*("|')(.*?)\2/gi; - let tagNameRegExp = /\w*(?=\s)/; - let srcRegExp = /src=("|')(.*?)\1/; - let userSyncObjects = []; - - if (pixels) { - let matchedItems = pixels.match(itemsRegExp); - if (matchedItems) { - matchedItems.forEach(item => { - let tagName = item.match(tagNameRegExp)[0]; - let url = item.match(srcRegExp)[2]; - if (tagName && url) { - let tagType = tagName.toLowerCase() === 'img' ? 'image' : 'iframe'; - if ((!syncOptions.iframeEnabled && tagType === 'iframe') || - (!syncOptions.pixelEnabled && tagType === 'image')) { - return; - } - userSyncObjects.push({ - type: tagType, - url: url - }); - } - }); - } - } - return userSyncObjects; +function getType(b) { + return b?.mediaTypes?.banner ? BANNER : false; } -function isSecure(bid) { - return deepAccess(bid, 'params.bidOverride.imp.secure') || (document.location.protocol === 'https:') ? 1 : 0; -}; - -function getMediaType(bid) { - return deepAccess(bid, 'mediaTypes.banner') ? BANNER : false; +function getBidfloor(b) { + return isFn(b.getFloor) + ? b.getFloor({ + size: '*', + currency: b?.params?.bidOverride?.cur ?? DEFAULT_CUR, + mediaType: BANNER, + }) + : false; } -function validateAppendObject(validationFunction, allowedKeys, inputObject, appendToObject) { - const outputObject = { - ...appendToObject - }; - if (allowedKeys.length > 0 && typeof validationFunction === 'function') { - for (const objectKey in inputObject) { - if (allowedKeys.indexOf(objectKey) !== -1 && validationFunction(inputObject[objectKey])) { - outputObject[objectKey] = inputObject[objectKey] - } - } - } - return outputObject; -}; - -function getTtl(bidderRequest) { - const ttl = config.getConfig('adtrgtme.ttl'); - const validateTTL = (ttl) => { - return (isNumber(ttl) && ttl > 0 && ttl < 3600) ? ttl : DEFAULT_BID_TTL - }; - return ttl ? validateTTL(ttl) : validateTTL(deepAccess(bidderRequest, 'params.ttl')); -}; - -function getFloorModuleData(bid) { - const getFloorRequestObject = { - currency: deepAccess(bid, 'params.bidOverride.cur') || DEFAULT_CURRENCY, - mediaType: BANNER, - size: '*' - }; - return (isFn(bid.getFloor)) ? bid.getFloor(getFloorRequestObject) : false; -}; +function getTtl(b) { + const t = config.getConfig('adtrgtme.ttl'); + const validate = (t) => (isNumber(t) && t > 0 && t < 3000 ? t : DEFAULT_TTL); + return t ? validate(t) : validate(b?.params?.ttl); +} -function generateOpenRtbObject(bidderRequest, bid) { - if (bidderRequest) { - let outBoundBidRequest = { - id: generateUUID(), - cur: [getFloorModuleData(bidderRequest).currency || deepAccess(bid, 'params.bidOverride.cur') || DEFAULT_CURRENCY], - imp: [], - site: { - page: deepAccess(bidderRequest, 'refererInfo.page') - }, - device: { - dnt: 0, - ua: navigator.userAgent, - ip: deepAccess(bid, 'params.bidOverride.device.ip') || deepAccess(bid, 'params.ext.ip') || undefined +function createORTB(bR, bid) { + if (!bR || !bid) return; + + const { currency = bid.params?.bidOverride?.cur || DEFAULT_CUR } = + getBidfloor(bR); + const ip = + bid.params?.bidOverride?.device?.ip || + bid.ortb2?.device?.ip || + bid.params?.ext?.ip; + const site = bid.ortb2?.site || undefined; + const user = bid.ortb2?.user || undefined; + const gdpr = bR.gdprConsent?.gdprApplies ? 1 : 0; + const consentString = gdpr ? bR.gdprConsent?.consentString : ''; + const usPrivacy = bR.uspConsent || ''; + + let oR = { + id: generateUUID(), + cur: [currency], + imp: [], + site: { + id: String(bid.params?.sid), + page: bR.refererInfo?.page || '', + ...site, + }, + device: { + dnt: bid?.params?.dnt ? 1 : 0, + ua: bid?.params?.ua || navigator.userAgent, + ip, + }, + regs: { + ext: { + us_privacy: usPrivacy, + gdpr, }, - regs: { - ext: { - 'us_privacy': bidderRequest.uspConsent ? bidderRequest.uspConsent : '', - gdpr: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? 1 : 0 - } + }, + source: { + ext: { + hb: 1, + bidderver: BIDDER_VERSION, + prebidjsver: PREBIDJS_VERSION, + ...(bid?.schain && { schain: bid.schain }), }, - source: { - ext: { - hb: 1, - adapterver: ADAPTER_VERSION, - prebidver: PREBID_VERSION, - integration: { - name: INTEGRATION_METHOD, - ver: PREBID_VERSION - } - }, - fd: 1 + fd: 1, + }, + user: { + ...user, + ext: { + consent: consentString, + ...(user?.ext || {}), }, - user: { - ext: { - consent: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies - ? bidderRequest.gdprConsent.consentString : '' - } - } - }; - - outBoundBidRequest.site.id = bid.params.sid; - - if (bidderRequest.ortb2) { - outBoundBidRequest = appendFirstPartyData(outBoundBidRequest, bid); - }; - - if (deepAccess(bid, 'schain')) { - outBoundBidRequest.source.ext.schain = bid.schain; - outBoundBidRequest.source.ext.schain.nodes[0].rid = outBoundBidRequest.id; - }; - - return outBoundBidRequest; + }, }; -}; - -function appendImpObject(bid, openRtbObject) { - const mediaTypeMode = getMediaType(bid); - - if (openRtbObject && bid) { - const impObject = { - id: bid.bidId, - secure: isSecure(bid), - bidfloor: getFloorModuleData(bid).floor || deepAccess(bid, 'params.bidOverride.imp.bidfloor') || 0.000001 - }; - - if (mediaTypeMode === BANNER) { - impObject.banner = { - mimes: bid.mediaTypes.banner.mimes || ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], - format: transformSizes(bid.sizes) - }; - if (bid.mediaTypes.banner.pos) { - impObject.banner.pos = bid.mediaTypes.banner.pos; - }; - }; - - impObject.ext = { - dfp_ad_unit_code: bid.adUnitCode - }; - if (deepAccess(bid, 'params.zid')) { - impObject.tagid = bid.params.zid; - } - - if (deepAccess(bid, 'ortb2Imp.ext.data') && isPlainObject(bid.ortb2Imp.ext.data)) { - impObject.ext.data = bid.ortb2Imp.ext.data; - }; - - if (deepAccess(bid, 'ortb2Imp.instl') && isNumber(bid.ortb2Imp.instl) && (bid.ortb2Imp.instl === 1)) { - impObject.instl = bid.ortb2Imp.instl; - }; - - openRtbObject.imp.push(impObject); - }; -}; - -function appendFirstPartyData(outBoundBidRequest, bid) { - const ortb2Object = bid.ortb2; - const siteObject = deepAccess(ortb2Object, 'site') || undefined; - const siteContentObject = deepAccess(siteObject, 'content') || undefined; - const userObject = deepAccess(ortb2Object, 'user') || undefined; + if (bid?.schain) { + oR.source.ext.schain.nodes[0].rid = oR.id; + } - if (siteObject && isPlainObject(siteObject)) { - const allowedSiteStringKeys = ['name', 'domain', 'page', 'ref', 'keywords']; - const allowedSiteArrayKeys = ['cat', 'sectioncat', 'pagecat']; - const allowedSiteObjectKeys = ['ext']; - outBoundBidRequest.site = validateAppendObject(isStr, allowedSiteStringKeys, siteObject, outBoundBidRequest.site); - outBoundBidRequest.site = validateAppendObject(isArray, allowedSiteArrayKeys, siteObject, outBoundBidRequest.site); - outBoundBidRequest.site = validateAppendObject(isPlainObject, allowedSiteObjectKeys, siteObject, outBoundBidRequest.site); - }; + return oR; +} - if (siteContentObject && isPlainObject(siteContentObject)) { - const allowedContentStringKeys = ['id', 'title', 'language']; - const allowedContentArrayKeys = ['cat']; - outBoundBidRequest.site.content = validateAppendObject(isStr, allowedContentStringKeys, siteContentObject, outBoundBidRequest.site.content); - outBoundBidRequest.site.content = validateAppendObject(isArray, allowedContentArrayKeys, siteContentObject, outBoundBidRequest.site.content); +function appendImp(bid, oRtb) { + if (!oRtb || !bid) return; + + const type = getType(bid); + const { floor: bidfloor = 0, currency: bidfloorcur = '' } = getBidfloor(bid); + + const impObject = { + id: bid.bidId, + secure: 1, + bidfloor: bid?.params?.bidOverride?.imp?.bidfloor || bidfloor, + bidfloorcur: bid?.params?.bidOverride?.imp?.bidfloorcur || bidfloorcur, + ext: { + dfp_ad_unit_code: bid.adUnitCode, + ...(bid?.ortb2Imp?.ext?.data && + isPlainObject(bid.ortb2Imp.ext.data) && { + data: bid.ortb2Imp.ext.data, + }), + }, + ...(bid?.params?.zid && { tagid: String(bid.params.zid) }), + ...(bid?.ortb2Imp?.instl === 1 && { instl: 1 }), }; - if (userObject && isPlainObject(userObject)) { - const allowedUserStrings = ['id', 'buyeruid', 'gender', 'keywords', 'customdata']; - const allowedUserObjects = ['ext']; - outBoundBidRequest.user = validateAppendObject(isStr, allowedUserStrings, userObject, outBoundBidRequest.user); - outBoundBidRequest.user.ext = validateAppendObject(isPlainObject, allowedUserObjects, userObject, outBoundBidRequest.user.ext); - }; + if (type === BANNER) { + impObject.banner = { + mimes: bid.mediaTypes.banner.mimes || [ + 'text/html', + 'text/javascript', + 'application/javascript', + 'image/jpg', + ], + format: getFormat(bid.sizes), + ...(bid.mediaTypes.banner.pos && { pos: bid.mediaTypes.banner.pos }), + }; + } - return outBoundBidRequest; -}; + oRtb.imp.push(impObject); +} -function generateServerRequest({payload, requestOptions, bidderRequest}) { +function createRequest({ data, options, bidderRequest }) { return { - url: (config.getConfig('adtrgtme.endpoint') || ENDPOINT) + (payload.site.id || ''), + url: `${config.getConfig('adtrgtme.endpoint') || BIDDER_URL}${ + data.site?.id || '' + }`, method: 'POST', - data: payload, - options: requestOptions, - bidderRequest: bidderRequest + data, + options, + bidderRequest, }; -}; +} export const spec = { code: BIDDER_CODE, aliases: [], supportedMediaTypes: [BANNER], - isBidRequestValid: function(bid) { + isBidRequestValid: function (bid) { const params = bid.params; - if (isPlainObject(params) && isNumber(params.sid)) { + if ( + isPlainObject(params) && + isStr(params.sid) && + !isEmpty(params.sid) && + params.sid.length > 0 && + (isEmpty(params.zid) || + isNumber(params.zid) || + (isStr(params.zid) && !isNaN(parseInt(params.zid)))) + ) { return true; } else { - logWarn('Adtrgtme bidder params missing or incorrect'); + logWarn('Adtrgtme request invalid'); return false; } }, - buildRequests: function(validBidRequests, bidderRequest) { - if (isEmpty(validBidRequests) || isEmpty(bidderRequest)) { + buildRequests: function (bR, aR) { + if (isEmpty(bR) || isEmpty(aR)) { logWarn('Adtrgtme Adapter: buildRequests called with empty request'); return undefined; - }; + } - const requestOptions = { + const options = { contentType: 'application/json', - customHeaders: { - 'x-openrtb-version': '2.5' - } + withCredentials: hasPurpose1Consent(aR.gdprConsent), }; - requestOptions.withCredentials = hasPurpose1Consent(bidderRequest.gdprConsent); - if (config.getConfig('adtrgtme.singleRequestMode') === true) { - const payload = generateOpenRtbObject(bidderRequest, validBidRequests[0]); - validBidRequests.forEach(bid => { - appendImpObject(bid, payload); + const data = createORTB(aR, bR[0]); + bR.forEach((bid) => { + appendImp(bid, data); }); - return generateServerRequest({payload, requestOptions, bidderRequest}); + return createRequest({ data, options, bidderRequest: aR }); } - return validBidRequests.map(bid => { - const payloadClone = generateOpenRtbObject(bidderRequest, bid); - appendImpObject(bid, payloadClone); + return bR.map((b) => { + const data = createORTB(aR, b); + appendImp(b, data); - return generateServerRequest({payload: payloadClone, requestOptions, bidderRequest: bid}); + return createRequest({ + data, + options, + bidderRequest: b, + }); }); }, - interpretResponse: function(serverResponse, { data, bidderRequest }) { - const response = []; - if (!serverResponse.body || !Array.isArray(serverResponse.body.seatbid)) { - return response; + interpretResponse: function (sR, { data, bidderRequest }) { + const res = []; + if (!sR.body || !Array.isArray(sR.body.seatbid)) { + return res; } - let seatbids = serverResponse.body.seatbid; - seatbids.forEach(seatbid => { - let bid; - + sR.body.seatbid.forEach((sb) => { try { - bid = seatbid.bid[0]; + let b = sb.bid[0]; + + res.push({ + adId: b?.adId ? b.adId : b.impid || b.crid, + ad: b.adm, + adUnitCode: bidderRequest.adUnitCode, + requestId: b.impid, + cpm: b.price, + width: b.w, + height: b.h, + mediaType: BANNER, + creativeId: b.crid || 0, + currency: b.cur || DEFAULT_CUR, + dealId: b.dealid ? b.dealid : null, + netRevenue: true, + ttl: getTtl(bidderRequest), + meta: { + advertiserDomains: b.adomain || [], + mediaType: BANNER, + }, + }); } catch (e) { - return response; + return res; } - - let cpm = bid.price; - - let bidResponse = { - adId: deepAccess(bid, 'adId') ? bid.adId : bid.impid || bid.crid, - ad: bid.adm, - adUnitCode: bidderRequest.adUnitCode, - requestId: bid.impid, - cpm: cpm, - width: bid.w, - height: bid.h, - creativeId: bid.crid || 0, - currency: bid.cur || DEFAULT_CURRENCY, - dealId: bid.dealid ? bid.dealid : null, - netRevenue: true, - ttl: getTtl(bidderRequest), - mediaType: BANNER, - meta: { - advertiserDomains: bid.adomain, - mediaType: BANNER, - } - }; - - response.push(bidResponse); }); - return response; + return res; }, - - getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { - const bidResponse = !isEmpty(serverResponses) && serverResponses[0].body; - if (bidResponse && bidResponse.ext && bidResponse.ext.pixels) { - return extractUserSyncUrls(syncOptions, bidResponse.ext.pixels); + getUserSyncs: function (options, res, gdprConsent, uspConsent, gppConsent) { + const s = []; + if (!options.pixelEnabled && !options.iframeEnabled) { + return s; } - return []; - } + if (Array.isArray(res)) { + res.forEach((response) => { + const p = response.body?.ext?.pixels; + if (Array.isArray(p)) { + p.forEach(([stype, url]) => { + const type = stype.toLowerCase(); + if ( + typeof url === 'string' && + url.startsWith('http') && + (((type === 'image' || type === 'img') && options.pixelEnabled) || + (type === 'iframe' && options.iframeEnabled)) + ) { + s.push({ type, url: addConsentParams(url) }); + } + }); + } + }); + } + function addConsentParams(url) { + if (gdprConsent) { + url += `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}&gdpr_consent=${ + encodeURIComponent(gdprConsent.consentString) || '' + }`; + } + if (uspConsent) { + url += `&us_privacy=${encodeURIComponent(uspConsent)}`; + } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + url += `&gpp=${encodeURIComponent( + gppConsent.gppString + )}&gpp_sid=${encodeURIComponent( + gppConsent.applicableSections?.join(',') + )}`; + } + return url; + } + return s; + }, }; registerBidder(spec); diff --git a/modules/adtrgtmeBidAdapter.md b/modules/adtrgtmeBidAdapter.md index d136b17067d..b1a01e2e7b7 100644 --- a/modules/adtrgtmeBidAdapter.md +++ b/modules/adtrgtmeBidAdapter.md @@ -31,18 +31,23 @@ const adUnits = [{ { bidder: 'adtrgtme', params: { - sid: 1220291391, // Site/App ID provided from SSP + sid: '1220291391', // Site/App ID provided from SSP } } ] }]; ``` -# Optional: Price floors module & bidfloor -The adtargerme adapter supports the Prebid.org Price Floors module and will use it to define the outbound bidfloor and currency. +# Optional +## Price floors module & bidfloor +The adapter supports the Prebid.org Price Floors module and will use it to define the outbound bidfloor and currency. By default the adapter will always check the existance of Module price floor. -If a module price floor does not exist you can set a custom bid floor for your impression using "params.bidOverride.imp.bidfloor". +If a module price floor does not exist you can set a custom bid floor for your impression using "params.bidOverride.imp.bidfloor" and "params.bidOverride.imp.bidfloorcur". +## Strict placement identification +It's possible to use params.zid for strict identification for placement id provided from SSP like tagid. + +## Example: ```javascript const adUnits = [{ code: 'your-placement', @@ -56,10 +61,12 @@ const adUnits = [{ bids: [{ bidder: 'adtrgtme', params: { - sid: 1220291391, + sid: '1220291391', + zid: '1836455615', bidOverride :{ imp: { - bidfloor: 5.00 // bidOverride bidfloor + bidfloor: 5.00, // bidOverride bidfloor + bidfloorcur: 'USD' // bidOverride currency } } } diff --git a/modules/adtrueBidAdapter.js b/modules/adtrueBidAdapter.js index 389986eb586..a6186d6129f 100644 --- a/modules/adtrueBidAdapter.js +++ b/modules/adtrueBidAdapter.js @@ -43,6 +43,7 @@ const VIDEO_CUSTOM_PARAMS = { 'battr': DATA_TYPES.ARRAY, 'linearity': DATA_TYPES.NUMBER, 'placement': DATA_TYPES.NUMBER, + 'plcmt': DATA_TYPES.NUMBER, 'minbitrate': DATA_TYPES.NUMBER, 'maxbitrate': DATA_TYPES.NUMBER }; diff --git a/modules/advRedAnalyticsAdapter.js b/modules/advRedAnalyticsAdapter.js new file mode 100644 index 00000000000..8ad30ed351d --- /dev/null +++ b/modules/advRedAnalyticsAdapter.js @@ -0,0 +1,198 @@ +import {generateUUID, logInfo} from '../src/utils.js' +import {ajaxBuilder} from '../src/ajax.js' +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js' +import adapterManager from '../src/adapterManager.js' +import {EVENTS} from '../src/constants.js' +import {getRefererInfo} from '../src/refererDetection.js'; + +/** + * advRedAnalyticsAdapter.js - analytics adapter for AdvRed + */ +const DEFAULT_EVENT_URL = 'https://api.adv.red/api/event' + +let ajax = ajaxBuilder(10000) +let pwId +let initOptions +let flushInterval +let queue = [] + +let advRedAnalytics = Object.assign(adapter({url: DEFAULT_EVENT_URL, analyticsType: 'endpoint'}), { + track({eventType, args}) { + handleEvent(eventType, args) + } +}) + +function sendEvents() { + if (queue.length > 0) { + const message = { + pwId: pwId, + publisherId: initOptions.publisherId, + events: queue, + pageUrl: getRefererInfo().page + } + queue = [] + + const url = initOptions.url ? initOptions.url : DEFAULT_EVENT_URL + ajax( + url, + () => logInfo('AdvRed Analytics sent ' + queue.length + ' events'), + JSON.stringify(message), + { + method: 'POST', + contentType: 'application/json', + withCredentials: true + } + ) + } +} + +function convertAdUnit(adUnit) { + if (!adUnit) return adUnit + + const shortAdUnit = {} + shortAdUnit.code = adUnit.code + shortAdUnit.sizes = adUnit.sizes + return shortAdUnit +} + +function convertBid(bid) { + if (!bid) return bid + + const shortBid = {} + shortBid.adUnitCode = bid.adUnitCode + shortBid.bidder = bid.bidder + shortBid.cpm = bid.cpm + shortBid.currency = bid.currency + shortBid.mediaTypes = bid.mediaTypes + shortBid.sizes = bid.sizes + shortBid.serverResponseTimeMs = bid.serverResponseTimeMs + return shortBid +} + +function convertAuctionInit(origEvent) { + let shortEvent = {} + shortEvent.auctionId = origEvent.auctionId + shortEvent.timeout = origEvent.timeout + shortEvent.adUnits = origEvent.adUnits && origEvent.adUnits.map(convertAdUnit) + return shortEvent +} + +function convertBidRequested(origEvent) { + let shortEvent = {} + shortEvent.bidderCode = origEvent.bidderCode + shortEvent.bids = origEvent.bids && origEvent.bids.map(convertBid) + shortEvent.timeout = origEvent.timeout + return shortEvent +} + +function convertBidTimeout(origEvent) { + let shortEvent = {} + shortEvent.bids = origEvent && origEvent.map ? origEvent.map(convertBid) : origEvent + return shortEvent +} + +function convertBidderError(origEvent) { + let shortEvent = {} + shortEvent.bids = origEvent.bidderRequest && origEvent.bidderRequest.bids && origEvent.bidderRequest.bids.map(convertBid) + return shortEvent +} + +function convertAuctionEnd(origEvent) { + let shortEvent = {} + shortEvent.adUnitCodes = origEvent.adUnitCodes + shortEvent.bidsReceived = origEvent.bidsReceived && origEvent.bidsReceived.map(convertBid) + shortEvent.noBids = origEvent.noBids && origEvent.noBids.map(convertBid) + return shortEvent +} + +function convertBidWon(origEvent) { + let shortEvent = {} + shortEvent.adUnitCode = origEvent.adUnitCode + shortEvent.bidderCode = origEvent.bidderCode + shortEvent.mediaType = origEvent.mediaType + shortEvent.netRevenue = origEvent.netRevenue + shortEvent.cpm = origEvent.cpm + shortEvent.size = origEvent.size + shortEvent.currency = origEvent.currency + return shortEvent +} + +function handleEvent(eventType, origEvent) { + try { + origEvent = origEvent ? JSON.parse(JSON.stringify(origEvent)) : {} + } catch (e) { + } + + let shortEvent + switch (eventType) { + case EVENTS.AUCTION_INIT: { + shortEvent = convertAuctionInit(origEvent) + break + } + case EVENTS.BID_REQUESTED: { + shortEvent = convertBidRequested(origEvent) + break + } + case EVENTS.BID_TIMEOUT: { + shortEvent = convertBidTimeout(origEvent) + break + } + case EVENTS.BIDDER_ERROR: { + shortEvent = convertBidderError(origEvent) + break + } + case EVENTS.AUCTION_END: { + shortEvent = convertAuctionEnd(origEvent) + break + } + case EVENTS.BID_WON: { + shortEvent = convertBidWon(origEvent) + break + } + default: + return + } + + shortEvent.eventType = eventType + shortEvent.auctionId = origEvent.auctionId + shortEvent.timestamp = origEvent.timestamp || Date.now() + + sendEvent(shortEvent) +} + +function sendEvent(event) { + queue.push(event) + + if (event.eventType === EVENTS.AUCTION_END) { + sendEvents() + } +} + +advRedAnalytics.originEnableAnalytics = advRedAnalytics.enableAnalytics +advRedAnalytics.enableAnalytics = function (config) { + initOptions = config.options || {} + pwId = generateUUID() + flushInterval = setInterval(sendEvents, 1000) + + advRedAnalytics.originEnableAnalytics(config) +} + +advRedAnalytics.originDisableAnalytics = advRedAnalytics.disableAnalytics +advRedAnalytics.disableAnalytics = function () { + clearInterval(flushInterval) + sendEvents() + advRedAnalytics.originDisableAnalytics() +} + +adapterManager.registerAnalyticsAdapter({ + adapter: advRedAnalytics, + code: 'advRed' +}) + +advRedAnalytics.getOptions = function () { + return initOptions +} + +advRedAnalytics.sendEvents = sendEvents + +export default advRedAnalytics diff --git a/modules/advRedAnalyticsAdapter.md b/modules/advRedAnalyticsAdapter.md new file mode 100644 index 00000000000..59345dfd01e --- /dev/null +++ b/modules/advRedAnalyticsAdapter.md @@ -0,0 +1,30 @@ +# Overview +``` +Module Name: AdvRed Analytics Adapter +Module Type: Analytics Adapter +Maintainer: support@adv.red +``` + +### Usage + +The AdvRed analytics adapter can be used by all clients after approval. For more information, +please visit + +### Analytics Options +| Param enableAnalytics | Scope | Type | Description | Example | +|-----------------------|----------|--------|------------------------------------------------------|----------------------------------------| +| provider | Required | String | The name of this Adapter. | `'advRed'` | +| params | Required | Object | Details of module params. | | +| params.publisherId | Required | String | This is the Publisher ID value obtained from AdvRed. | `'123456'` | +| params.url | Optional | String | Custom URL of the endpoint to collect the events | `'https://pub1.api.adv.red/api/event'` | + +### Example Configuration + +```javascript +pbjs.enableAnalytics({ + provider: 'advRed', + options: { + publisherId: '123456' // change to the Publisher ID you received from AdvRed + } +}); +``` diff --git a/modules/advangelistsBidAdapter.js b/modules/advangelistsBidAdapter.js index 8e5be83f166..3a571831505 100755 --- a/modules/advangelistsBidAdapter.js +++ b/modules/advangelistsBidAdapter.js @@ -1,25 +1,23 @@ -import {deepAccess, generateUUID, isEmpty, isFn, parseSizesInput, parseUrl} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {find, includes} from '../src/polyfill.js'; +import { isEmpty } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { createRequestData, getBannerBidFloor, getBannerBidParam, getBannerSizes, getVideoBidFloor, getVideoBidParam, getVideoSizes, isBannerBidValid, isVideoBid, isVideoBidValid } from '../libraries/advangUtils/index.js'; const ADAPTER_VERSION = '1.0'; const BIDDER_CODE = 'advangelists'; - -export const VIDEO_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid=';// 0cf8d6d643e13d86a5b6374148a4afac'; -export const BANNER_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid=';// 0cf8d6d643e13d86a5b6374148a4afac'; -export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'skip', 'playerSize', 'context']; -export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; +export const VIDEO_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; +export const BANNER_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; +export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; let pubid = ''; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], - + aliases: ['saambaa'], isBidRequestValid(bidRequest) { - if (typeof bidRequest != 'undefined') { + if (typeof bidRequest !== 'undefined') { if (bidRequest.bidder !== BIDDER_CODE && typeof bidRequest.params === 'undefined') { return false; } if (bidRequest === '' || bidRequest.params.placement === '' || bidRequest.params.pubid === '') { return false; } return true; @@ -35,7 +33,7 @@ export const spec = { requests.push({ method: 'POST', url: VIDEO_ENDPOINT + pubid, - data: createVideoRequestData(bid, bidderRequest), + data: createRequestData(bid, bidderRequest, true, getVideoBidParam, getVideoSizes, getVideoBidFloor), bidRequest: bid }); }); @@ -45,16 +43,16 @@ export const spec = { requests.push({ method: 'POST', url: BANNER_ENDPOINT + pubid, - data: createBannerRequestData(bid, bidderRequest), + data: createRequestData(bid, bidderRequest, false, getBannerBidParam, getBannerSizes, getBannerBidFloor, BIDDER_CODE, ADAPTER_VERSION), bidRequest: bid }); }); return requests; }, - interpretResponse(serverResponse, {bidRequest}) { + interpretResponse(serverResponse, { bidRequest }) { let response = serverResponse.body; - if (response !== null && isEmpty(response) == false) { + if (response !== null && isEmpty(response) === false) { if (isVideoBid(bidRequest)) { let bidResponse = { requestId: response.id, @@ -63,11 +61,11 @@ export const spec = { height: response.seatbid[0].bid[0].h, ttl: response.seatbid[0].bid[0].ttl || 60, creativeId: response.seatbid[0].bid[0].crid, - meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, currency: response.cur, + meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, mediaType: VIDEO, netRevenue: true - } + }; if (response.seatbid[0].bid[0].adm) { bidResponse.vastXml = response.seatbid[0].bid[0].adm; @@ -93,298 +91,10 @@ export const spec = { meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, mediaType: BANNER, netRevenue: true - } + }; } } } }; -function isBannerBid(bid) { - return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); -} - -function isVideoBid(bid) { - return deepAccess(bid, 'mediaTypes.video'); -} - -function getBannerBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }) : {}; - return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); -} - -function getVideoBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'video', size: '*' }) : {}; - return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); -} - -function isVideoBidValid(bid) { - return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); -} - -function isBannerBidValid(bid) { - return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); -} - -function getVideoBidParam(bid, key) { - return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); -} - -function getBannerBidParam(bid, key) { - return deepAccess(bid, 'params.banner.' + key) || deepAccess(bid, 'params.' + key); -} - -function isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); -} - -function isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); -} - -function getDoNotTrack() { - return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; -} - -function findAndFillParam(o, key, value) { - try { - if (typeof value === 'function') { - o[key] = value(); - } else { - o[key] = value; - } - } catch (ex) {} -} - -function getOsVersion() { - let clientStrings = [ - { s: 'Android', r: /Android/ }, - { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, - { s: 'Mac OS X', r: /Mac OS X/ }, - { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, - { s: 'Linux', r: /(Linux|X11)/ }, - { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, - { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, - { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, - { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, - { s: 'Windows Vista', r: /Windows NT 6.0/ }, - { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, - { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, - { s: 'UNIX', r: /UNIX/ }, - { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } - ]; - let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); - return cs ? cs.s : 'unknown'; -} - -function getFirstSize(sizes) { - return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; -} - -function parseSizes(sizes) { - return parseSizesInput(sizes).map(size => { - let [ width, height ] = size.split('x'); - return { - w: parseInt(width, 10) || undefined, - h: parseInt(height, 10) || undefined - }; - }); -} - -function getVideoSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); -} - -function getBannerSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); -} - -function getTopWindowReferrer(bidderRequest) { - return bidderRequest?.refererInfo?.ref || ''; -} - -function getVideoTargetingParams(bid) { - const result = {}; - const excludeProps = ['playerSize', 'context', 'w', 'h']; - Object.keys(Object(bid.mediaTypes.video)) - .filter(key => !includes(excludeProps, key)) - .forEach(key => { - result[ key ] = bid.mediaTypes.video[ key ]; - }); - Object.keys(Object(bid.params.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.params.video[ key ]; - }); - return result; -} - -function createVideoRequestData(bid, bidderRequest) { - let topLocation = getTopWindowLocation(bidderRequest); - let topReferrer = getTopWindowReferrer(bidderRequest); - - let sizes = getVideoSizes(bid); - let firstSize = getFirstSize(sizes); - let bidfloor = (getVideoBidFloor(bid) == null || typeof getVideoBidFloor(bid) == 'undefined') ? 2 : getVideoBidFloor(bid); - let video = getVideoTargetingParams(bid); - const o = { - 'device': { - 'langauge': (global.navigator.language).split('-')[0], - 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), - 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, - 'js': 1, - 'os': getOsVersion() - }, - 'at': 2, - 'site': {}, - 'tmax': 3000, - 'cur': ['USD'], - 'id': bid.bidId, - 'imp': [], - 'regs': { - 'ext': { - } - }, - 'user': { - 'ext': { - } - } - }; - - o.site['page'] = topLocation.href; - o.site['domain'] = topLocation.hostname; - o.site['search'] = topLocation.search; - o.site['domain'] = topLocation.hostname; - o.site['ref'] = topReferrer; - o.site['mobile'] = isMobile() ? 1 : 0; - const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; - - o.device['dnt'] = getDoNotTrack() ? 1 : 0; - - findAndFillParam(o.site, 'name', function() { - return global.top.document.title; - }); - - findAndFillParam(o.device, 'h', function() { - return global.screen.height; - }); - findAndFillParam(o.device, 'w', function() { - return global.screen.width; - }); - - let placement = getVideoBidParam(bid, 'placement'); - - for (let j = 0; j < sizes.length; j++) { - o.imp.push({ - 'id': '' + j, - 'displaymanager': '' + BIDDER_CODE, - 'displaymanagerver': '' + ADAPTER_VERSION, - 'tagId': placement, - 'bidfloor': bidfloor, - 'bidfloorcur': 'USD', - 'secure': secure, - 'video': Object.assign({ - 'id': generateUUID(), - 'pos': 0, - 'w': firstSize.w, - 'h': firstSize.h, - 'mimes': DEFAULT_MIMES - }, video) - - }); - } - - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; - o.user.ext = {'consent': consentString}; - } - - return o; -} - -function getTopWindowLocation(bidderRequest) { - return parseUrl(bidderRequest?.refererInfo?.page, {decodeSearchAsString: true}); -} - -function createBannerRequestData(bid, bidderRequest) { - let topLocation = getTopWindowLocation(bidderRequest); - let topReferrer = getTopWindowReferrer(bidderRequest); - - let sizes = getBannerSizes(bid); - let bidfloor = (getBannerBidFloor(bid) == null || typeof getBannerBidFloor(bid) == 'undefined') ? 2 : getBannerBidFloor(bid); - - const o = { - 'device': { - 'langauge': (global.navigator.language).split('-')[0], - 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), - 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, - 'js': 1 - }, - 'at': 2, - 'site': {}, - 'tmax': 3000, - 'cur': ['USD'], - 'id': bid.bidId, - 'imp': [], - 'regs': { - 'ext': { - } - }, - 'user': { - 'ext': { - } - } - }; - - o.site['page'] = topLocation.href; - o.site['domain'] = topLocation.hostname; - o.site['search'] = topLocation.search; - o.site['domain'] = topLocation.hostname; - o.site['ref'] = topReferrer; - o.site['mobile'] = isMobile() ? 1 : 0; - const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; - - o.device['dnt'] = getDoNotTrack() ? 1 : 0; - - findAndFillParam(o.site, 'name', function() { - return global.top.document.title; - }); - - findAndFillParam(o.device, 'h', function() { - return global.screen.height; - }); - findAndFillParam(o.device, 'w', function() { - return global.screen.width; - }); - - let placement = getBannerBidParam(bid, 'placement'); - for (let j = 0; j < sizes.length; j++) { - let size = sizes[j]; - - o.imp.push({ - 'id': '' + j, - 'displaymanager': '' + BIDDER_CODE, - 'displaymanagerver': '' + ADAPTER_VERSION, - 'tagId': placement, - 'bidfloor': bidfloor, - 'bidfloorcur': 'USD', - 'secure': secure, - 'banner': { - 'id': generateUUID(), - 'pos': 0, - 'w': size['w'], - 'h': size['h'] - } - }); - } - - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; - o.user.ext = {'consent': consentString}; - } - - return o; -} - registerBidder(spec); diff --git a/modules/adverxoBidAdapter.js b/modules/adverxoBidAdapter.js new file mode 100644 index 00000000000..f293d7e01a6 --- /dev/null +++ b/modules/adverxoBidAdapter.js @@ -0,0 +1,356 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; +import {ortbConverter as OrtbConverter} from '../libraries/ortbConverter/converter.js'; +import {Renderer} from '../src/Renderer.js'; +import {deepAccess, deepSetValue} from '../src/utils.js'; +import {config} from '../src/config.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/auction.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const BIDDER_CODE = 'adverxo'; + +const ALIASES = [ + {code: 'adport', skipPbsAliasing: true}, + {code: 'bidsmind', skipPbsAliasing: true}, + {code: 'mobupps', skipPbsAliasing: true} +]; + +const AUCTION_URLS = { + adverxo: 'js.pbsadverxo.com', + adport: 'diclotrans.com', + bidsmind: 'egrevirda.com', + mobupps: 'traffhb.com' +}; + +const ENDPOINT_URL_AD_UNIT_PLACEHOLDER = '{AD_UNIT}'; +const ENDPOINT_URL_AUTH_PLACEHOLDER = '{AUTH}'; +const ENDPOINT_URL_HOST_PLACEHOLDER = '{HOST}'; + +const ENDPOINT_URL = `https://${ENDPOINT_URL_HOST_PLACEHOLDER}/pickpbs?id=${ENDPOINT_URL_AD_UNIT_PLACEHOLDER}&auth=${ENDPOINT_URL_AUTH_PLACEHOLDER}`; + +const ORTB_MTYPES = { + 1: BANNER, + 2: VIDEO, + 4: NATIVE +}; + +const USYNC_TYPES = { + IFRAME: 'iframe', + REDIRECT: 'image' +}; + +const DEFAULT_CURRENCY = 'USD'; + +const ortbConverter = OrtbConverter({ + context: { + netRevenue: true, + ttl: 60, + }, + request: function request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + utils.deepSetValue(request, 'device.ip', 'caller'); + utils.deepSetValue(request, 'ext.avx_add_vast_url', 1); + + const eids = deepAccess(bidderRequest, 'bids.0.userIdAsEids'); + + if (eids && eids.length) { + deepSetValue(request, 'user.ext.eids', eids); + } + + return request; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const floor = adverxoUtils.getBidFloor(bidRequest); + + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = DEFAULT_CURRENCY; + } + + return imp; + }, + bidResponse: function (buildBidResponse, bid, context) { + bid.adm = bid.adm.replaceAll(`\${AUCTION_PRICE}`, bid.price); + + if (FEATURES.NATIVE && ORTB_MTYPES[bid.mtype] === NATIVE) { + if (typeof bid?.adm === 'string') { + bid.adm = JSON.parse(bid.adm); + } + + if (bid?.adm?.native) { + bid.adm = bid.adm.native; + } + } + + const result = buildBidResponse(bid, context); + + if (FEATURES.VIDEO) { + if (bid?.ext?.avx_vast_url) { + result.vastUrl = bid.ext.avx_vast_url; + } + + if (bid?.ext?.avx_video_renderer_url) { + result.avxVideoRendererUrl = bid.ext.avx_video_renderer_url; + } + } + + return result; + } +}); + +const userSyncUtils = { + buildUsyncParams: function (gdprConsent, uspConsent, gppConsent) { + const params = []; + + if (gdprConsent) { + params.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + params.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + + if (config.getConfig('coppa') === true) { + params.push('coppa=1'); + } + + if (uspConsent) { + params.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + params.push('gpp=' + encodeURIComponent(gppConsent.gppString)); + params.push('gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(','))); + } + + return params.length ? params.join('&') : ''; + } +}; + +const videoUtils = { + createOutstreamVideoRenderer: function (bid) { + const renderer = Renderer.install({ + id: bid.bidId, + url: bid.avxVideoRendererUrl, + loaded: false, + adUnitCode: bid.adUnitCode + }); + + try { + renderer.setRender(this.outstreamRender.bind(this)); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; + }, + + outstreamRender: function (bid, doc) { + bid.renderer.push(() => { + const win = (doc) ? doc.defaultView : window; + + win.adxVideoRenderer.renderAd({ + targetId: bid.adUnitCode, + adResponse: {content: bid.vastXml} + }); + }); + } +}; + +const adverxoUtils = { + buildAuctionUrl: function (bidderCode, host, adUnitId, adUnitAuth) { + const auctionUrl = host || AUCTION_URLS[bidderCode]; + + return ENDPOINT_URL + .replace(ENDPOINT_URL_HOST_PLACEHOLDER, auctionUrl) + .replace(ENDPOINT_URL_AD_UNIT_PLACEHOLDER, adUnitId) + .replace(ENDPOINT_URL_AUTH_PLACEHOLDER, adUnitAuth); + }, + + groupBidRequestsByAdUnit: function (bidRequests) { + const groupedBidRequests = new Map(); + + bidRequests.forEach(bidRequest => { + const adUnit = { + host: bidRequest.params.host, + id: bidRequest.params.adUnitId, + auth: bidRequest.params.auth, + }; + + if (!groupedBidRequests.get(adUnit)) { + groupedBidRequests.set(adUnit, []); + } + + groupedBidRequests.get(adUnit).push(bidRequest); + }); + + return groupedBidRequests; + }, + + getBidFloor: function (bid) { + if (utils.isFn(bid.getFloor)) { + const floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + + if (utils.isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) { + return floor.floor; + } + } + + return null; + } +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + aliases: ALIASES, + + /** + * Determines whether 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. + */ + isBidRequestValid: function (bid) { + if (!utils.isPlainObject(bid.params) || !Object.keys(bid.params).length) { + utils.logWarn('Adverxo Bid Adapter: bid params must be provided.'); + return false; + } + + if (!bid.params.adUnitId || typeof bid.params.adUnitId !== 'number') { + utils.logWarn('Adverxo Bid Adapter: adUnitId bid param is required and must be a number'); + return false; + } + + if (!bid.params.auth || typeof bid.params.auth !== 'string') { + utils.logWarn('Adverxo Bid Adapter: auth bid param is required and must be a string'); + return false; + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest an array of bids + * @return {ServerRequest} Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const result = []; + + const bidRequestsByAdUnit = adverxoUtils.groupBidRequestsByAdUnit(validBidRequests); + + bidRequestsByAdUnit.forEach((adUnitBidRequests, adUnit) => { + const ortbRequest = ortbConverter.toORTB({ + bidRequests: adUnitBidRequests, + bidderRequest + }); + + result.push({ + method: 'POST', + url: adverxoUtils.buildAuctionUrl(bidderRequest.bidderCode, adUnit.host, adUnit.id, adUnit.auth), + data: ortbRequest, + bids: adUnitBidRequests + }); + }); + + return result; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest Adverxo bidRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + if (!serverResponse || !bidRequest) { + return []; + } + + const bids = ortbConverter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }).bids; + + return bids.map((bid) => { + const thisRequest = utils.getBidRequest(bid.requestId, [bidRequest]); + const context = utils.deepAccess(thisRequest, 'mediaTypes.video.context'); + + if (FEATURES.VIDEO && bid.mediaType === 'video' && context === 'outstream') { + bid.renderer = videoUtils.createOutstreamVideoRenderer(bid); + } + + return bid; + }); + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} responses List of server's responses. + * @param {*} gdprConsent + * @param {*} uspConsent + * @param {*} gppConsent + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { + if (!responses || responses.length === 0 || (!syncOptions.pixelEnabled && !syncOptions.iframeEnabled)) { + return []; + } + + const privacyParams = userSyncUtils.buildUsyncParams(gdprConsent, uspConsent, gppConsent); + const syncType = syncOptions.iframeEnabled ? USYNC_TYPES.IFRAME : USYNC_TYPES.REDIRECT; + + const result = []; + + for (const response of responses) { + const syncUrls = response.body?.ext?.avx_usync; + + if (!syncUrls || syncUrls.length === 0) { + continue; + } + + for (const url of syncUrls) { + let finalUrl = url; + + if (!finalUrl.includes('?')) { + finalUrl += '?'; + } else { + finalUrl += '&'; + } + + finalUrl += 'type=' + syncType; + + if (privacyParams.length !== 0) { + finalUrl += `&${privacyParams}`; + } + + result.push({ + type: syncType, + url: finalUrl + }); + } + } + + return result; + } +} + +registerBidder(spec); diff --git a/modules/adverxoBidAdapter.md b/modules/adverxoBidAdapter.md new file mode 100644 index 00000000000..ae6072d2738 --- /dev/null +++ b/modules/adverxoBidAdapter.md @@ -0,0 +1,101 @@ +# Overview + +``` +Module Name: Adverxo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: developer@adverxo.com +``` + +# Description + +Module that connects to Adverxo to fetch bids. +Banner, native and video formats are supported. + +# Bid Parameters + +| Name | Required? | Description | Example | Type | +|------------|-----------|-------------------------------------------------------------------|----------------------------------------------|-----------| +| `host` | No | Ad network host. | `prebidTest.adverxo.com` | `String` | +| `adUnitId` | Yes | Unique identifier for the ad unit in Adverxo platform. | `413` | `Integer` | +| `auth` | Yes | Authentication token provided by Adverxo platform for the AdUnit. | `'61336d75e414c77c367ce5c47c2599ce80a8x32b'` | `String` | + +# Test Parameters + +```javascript +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [ + [400, 300], + [320, 50] + ] + } + }, + bids: [{ + bidder: 'adverxo', + params: { + host: 'example.com', + adUnitId: 1, + auth: '61336e753414c77c367deac47c2595ce80a8032b' + } + }] + }, + { + code: 'native-ad-div', + mediaTypes: { + native: { + image: { + required: true, + sizes: [400, 300] + }, + title: { + required: true, + len: 75 + }, + body: { + required: false, + len: 200 + }, + cta: { + required: false, + len: 75 + }, + sponsoredBy: { + required: false + } + } + }, + bids: [{ + bidder: 'adverxo', + params: { + host: 'example.com', + adUnitId: 2, + auth: '9a640dfccc3381e71fxc29ffd4a72wabd29g9d86' + } + }] + }, + { + code: 'video-div', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream', + mimes: ['video/mp4'], + maxduration: 30, + skip: 1 + } + }, + bids: [{ + bidder: 'adverxo', + params: { + host: 'example.com', + adUnitId: 3, + auth: '1ax23d9621f21da28a2eab6f79bd5fbcf4d037c1' + } + }] + } +]; + +``` diff --git a/modules/adxcgAnalyticsAdapter.js b/modules/adxcgAnalyticsAdapter.js index 7ad95121209..7538e2962cc 100644 --- a/modules/adxcgAnalyticsAdapter.js +++ b/modules/adxcgAnalyticsAdapter.js @@ -3,7 +3,6 @@ import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { EVENTS } from '../src/constants.js'; -import {getGlobal} from '../src/prebidGlobal.js'; /** * Analytics adapter from adxcg.com @@ -123,7 +122,7 @@ function send (data) { ats: adxcgAnalyticsAdapter.context.auctionTimestamp, aav: adxcgAnalyticsVersion, iob: intersectionObserverAvailable(window) ? '1' : '0', - pbv: getGlobal().version, + pbv: '$prebid.version$', sz: window.screen.width + 'x' + window.screen.height } }); diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index dda88575ff5..a0e99572809 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,6 +1,5 @@ // jshint esversion: 6, es3: false, node: true import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { convertTypes } from '../libraries/transformParamsUtils/convertTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { @@ -17,7 +16,7 @@ const BIDDER_CODE = 'adxcg'; const SECURE_BID_URL = 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'; const DEFAULT_CURRENCY = 'EUR'; -const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; +const KNOWN_PARAMS = ['battr', 'deals']; const DEFAULT_TMAX = 500; /** @@ -88,14 +87,6 @@ export const spec = { if (bid.nurl) { triggerPixel(replaceAuctionPrice(bid.nurl, bid.originalCpm)) } - }, - transformBidParams: function (params) { - return convertTypes({ - 'cf': 'string', - 'cp': 'number', - 'ct': 'number', - 'adzoneid': 'string' - }, params); } }; @@ -131,7 +122,7 @@ const converter = ortbConverter({ }; } - imp.secure = Number(window.location.protocol === 'https:'); + imp.secure = bidRequest.ortb2Imp?.secure ?? 1; if (!imp.bidfloor && bidRequest.params.bidFloor) { imp.bidfloor = bidRequest.params.bidFloor; diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 146e1d3b24a..fce5d1ae000 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -255,7 +255,7 @@ function getFloor(bidRequest, size, mediaType) { size: [ size.width, size.height ] }); - if (!isNaN(bidFloors.floor) && (bidFloors.currency === CURRENCY)) { + if (!isNaN(bidFloors?.floor) && (bidFloors?.currency === CURRENCY)) { return bidFloors.floor; } } diff --git a/modules/agmaAnalyticsAdapter.js b/modules/agmaAnalyticsAdapter.js index e2e01fb4d03..cacdc5db976 100644 --- a/modules/agmaAnalyticsAdapter.js +++ b/modules/agmaAnalyticsAdapter.js @@ -3,6 +3,7 @@ import { generateUUID, logInfo, logError, + getWindowSelf, getPerformanceNow, isEmpty, isEmptyStr, @@ -13,24 +14,24 @@ import { EVENTS } from '../src/constants.js'; import adapterManager, { gdprDataHandler } from '../src/adapterManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { config } from '../src/config.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; const GVLID = 1122; const ModuleCode = 'agma'; const analyticsType = 'endpoint'; -const scriptVersion = '1.8.0'; +const scriptVersion = '1.9.0'; const batchDelayInMs = 1000; const agmaURL = 'https://pbc.agma-analytics.de/v1'; const pageViewId = generateUUID(); // Helper functions const getScreen = () => { - const w = window; - const d = document; - const e = d.documentElement; - const g = d.getElementsByTagName('body')[0]; - const x = w.innerWidth || e.clientWidth || g.clientWidth; - const y = w.innerHeight || e.clientHeight || g.clientHeight; - return { x, y }; + try { + const {width: x, height: y} = getViewportSize(); + return { x, y }; + } catch (e) { + return {x: 0, y: 0}; + } }; const getUserIDs = () => { @@ -40,32 +41,17 @@ const getUserIDs = () => { return []; }; -export const getOrtb2Data = (options) => { - let site = null; - let user = null; - - // check if data is provided via config - if (options.ortb2) { - if (options.ortb2.user) { - user = options.ortb2.user; - } - if (options.ortb2.site) { - site = options.ortb2.site; - } - if (site && user) { - return { site, user }; - } - } +export const getOrtb2Data = (options = {}) => { try { const configData = config.getConfig(); - // try to fallback to global config - if (configData.ortb2) { - site = site || configData.ortb2.site; - user = user || configData.ortb2.user; + const win = getWindowSelf(); + return { + site: win.agma?.ortb2?.site ?? options.ortb2?.site ?? configData.ortb2?.site, + user: win.agma?.ortb2?.user ?? options.ortb2?.user ?? configData.ortb2?.user, } - } catch (e) {} - - return { site, user }; + } catch (e) { + return {}; + } }; export const getTiming = () => { diff --git a/modules/aidemBidAdapter.js b/modules/aidemBidAdapter.js index c6a5cd96fb6..2f2c46942de 100644 --- a/modules/aidemBidAdapter.js +++ b/modules/aidemBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, deepSetValue, isBoolean, isNumber, isStr, logError, logInfo} from '../src/utils.js'; +import {deepAccess, deepClone, deepSetValue, getWinDimensions, isBoolean, isNumber, isStr, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; @@ -10,6 +10,7 @@ const BIDDER_CODE = 'aidem'; const BASE_URL = 'https://zero.aidemsrv.com'; const LOCAL_BASE_URL = 'http://127.0.0.1:8787'; +const GVLID = 1218 const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; const REQUIRED_VIDEO_PARAMS = [ 'mimes', 'protocols', 'context' ]; @@ -59,7 +60,7 @@ const converter = ortbConverter({ const request = buildRequest(imps, bidderRequest, context); deepSetValue(request, 'at', 1); setPrebidRequestEnvironment(request); - deepSetValue(request, 'regs', getRegs()); + deepSetValue(request, 'regs', getRegs(bidderRequest)); deepSetValue(request, 'site.publisher.id', bidderRequest.bids[0].params.publisherId); deepSetValue(request, 'site.id', bidderRequest.bids[0].params.siteId); return request; @@ -106,22 +107,22 @@ function recur(obj) { return result; } -function getRegs() { +function getRegs(bidderRequest) { let regs = {}; - const consentManagement = config.getConfig('consentManagement'); + const euConsentManagement = bidderRequest.gdprConsent; + const usConsentManagement = bidderRequest.uspConsent; const coppa = config.getConfig('coppa'); - if (consentManagement && !!(consentManagement.gdpr)) { - deepSetValue(regs, 'gdpr_applies', !!consentManagement.gdpr); + if (euConsentManagement && euConsentManagement.consentString) { + deepSetValue(regs, 'gdpr_applies', !!euConsentManagement.consentString); } else { deepSetValue(regs, 'gdpr_applies', false); } - if (consentManagement && deepAccess(consentManagement, 'usp.cmpApi') === 'static') { - deepSetValue(regs, 'usp_applies', !!deepAccess(consentManagement, 'usp')); - deepSetValue(regs, 'us_privacy', deepAccess(consentManagement, 'usp.consentData.getUSPData.uspString')); + if (usConsentManagement) { + deepSetValue(regs, 'usp_applies', true); + deepSetValue(regs, 'us_privacy', bidderRequest.uspConsent); } else { deepSetValue(regs, 'usp_applies', false); } - if (isBoolean(coppa)) { deepSetValue(regs, 'coppa_applies', !!coppa); } else { @@ -132,7 +133,7 @@ function getRegs() { } function setPrebidRequestEnvironment(payload) { - const __navigator = JSON.parse(JSON.stringify(recur(navigator))); + const __navigator = deepClone(recur(navigator)); delete __navigator.plugins; deepSetValue(payload, 'environment.ri', getRefererInfo()); deepSetValue(payload, 'environment.hl', window.history.length); @@ -143,8 +144,8 @@ function setPrebidRequestEnvironment(payload) { deepSetValue(payload, 'environment.inp.jp', window.JSON.parse.name === 'parse' && typeof window.JSON.parse.prototype === 'undefined'); deepSetValue(payload, 'environment.inp.ofe', window.Object.fromEntries.name === 'fromEntries' && typeof window.Object.fromEntries.prototype === 'undefined'); deepSetValue(payload, 'environment.inp.oa', window.Object.assign.name === 'assign' && typeof window.Object.assign.prototype === 'undefined'); - deepSetValue(payload, 'environment.wpar.innerWidth', window.innerWidth); - deepSetValue(payload, 'environment.wpar.innerHeight', window.innerHeight); + deepSetValue(payload, 'environment.wpar.innerWidth', getWinDimensions().innerWidth); + deepSetValue(payload, 'environment.wpar.innerHeight', getWinDimensions().innerHeight); } function hasValidMediaType(bidRequest) { @@ -232,6 +233,7 @@ function hasValidParameters(bidRequest) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: SUPPORTED_MEDIA_TYPES, isBidRequestValid: function(bidRequest) { logInfo('bid: ', bidRequest); diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index 079628c88fc..c547528a57e 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -17,7 +17,7 @@ import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'airgrid'; -const AG_TCF_ID = 782; +const MIQ_TCF_ID = 101; export const AG_AUDIENCE_IDS_KEY = 'edkt_matched_audience_ids'; export const storage = getStorageManager({ @@ -43,7 +43,7 @@ export function attachScriptTagToDOM(rtdConfig) { edktInitializor.apiKey = rtdConfig.params.apiKey; edktInitializor.invoked = true; const moduleSrc = getModuleUrl(rtdConfig.params.accountId); - loadExternalScript(moduleSrc, SUBMODULE_NAME); + loadExternalScript(moduleSrc, MODULE_TYPE_RTD, SUBMODULE_NAME); } } @@ -76,7 +76,7 @@ export function setAudiencesAsBidderOrtb2(bidConfig, rtdConfig, audiences) { const agUserData = [ { - id: String(AG_TCF_ID), + id: String(MIQ_TCF_ID), ext: { segtax: 540, }, @@ -129,7 +129,7 @@ export const airgridSubmodule = { name: SUBMODULE_NAME, init: init, getBidRequestData: passAudiencesToBidders, - gvlid: AG_TCF_ID + gvlid: MIQ_TCF_ID }; submodule(MODULE_NAME, airgridSubmodule); diff --git a/modules/ajaBidAdapter.js b/modules/ajaBidAdapter.js index e02ab920707..699dfd6fa04 100644 --- a/modules/ajaBidAdapter.js +++ b/modules/ajaBidAdapter.js @@ -55,11 +55,6 @@ export const spec = { for (let i = 0, len = validBidRequests.length; i < len; i++) { const bidRequest = validBidRequests[i]; - if ( - (bidRequest.mediaTypes?.native || bidRequest.mediaTypes?.video) && - bidRequest.mediaTypes?.banner) { - continue - } let queryString = ''; diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index 0bd53b2a91f..e5a647a90ef 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -5,802 +5,23 @@ * @module modules/akamaiDapRtdProvider * @requires module:modules/realTimeData */ -import {ajax} from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {submodule} from '../src/hook.js'; -import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/utils.js'; -import { loadExternalScript } from '../src/adloader.js'; -import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; -/** - * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule - */ - -const MODULE_NAME = 'realTimeData'; -const SUBMODULE_NAME = 'dap'; -const MODULE_CODE = 'akamaidap'; - -export const DAP_TOKEN = 'async_dap_token'; -export const DAP_MEMBERSHIP = 'async_dap_membership'; -export const DAP_ENCRYPTED_MEMBERSHIP = 'encrypted_dap_membership'; -export const DAP_SS_ID = 'dap_ss_id'; -export const DAP_DEFAULT_TOKEN_TTL = 3600; // in seconds -export const DAP_MAX_RETRY_TOKENIZE = 1; -export const DAP_CLIENT_ENTROPY = 'dap_client_entropy' - -export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); -let dapRetryTokenize = 0; - -/** - * Lazy merge objects. - * @param {String} target - * @param {String} source - */ -function mergeLazy(target, source) { - if (!isPlainObject(target)) { - target = {}; - } - if (!isPlainObject(source)) { - source = {}; - } - return mergeDeep(target, source); -} - -/** - * Add real-time data & merge segments. - * @param {Object} ortb2 destination object to merge RTD into - * @param {Object} rtd - */ -export function addRealTimeData(ortb2, rtd) { - logInfo('DEBUG(addRealTimeData) - ENTER'); - if (isPlainObject(rtd.ortb2)) { - logMessage('DEBUG(addRealTimeData): merging original: ', ortb2); - logMessage('DEBUG(addRealTimeData): merging in: ', rtd.ortb2); - mergeLazy(ortb2, rtd.ortb2); - } - logInfo('DEBUG(addRealTimeData) - EXIT'); -} - -/** - * Real-time data retrieval from Audigent - * @param {Object} bidConfig - * @param {function} onDone - * @param {Object} rtdConfig - * @param {Object} userConsent - */ -export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { - let entropyDict = JSON.parse(storage.getDataFromLocalStorage(DAP_CLIENT_ENTROPY)); - let loadScriptPromise = new Promise((resolve, reject) => { - if (rtdConfig && rtdConfig.params && rtdConfig.params.dapEntropyTimeout && Number.isInteger(rtdConfig.params.dapEntropyTimeout)) { - setTimeout(reject, rtdConfig.params.dapEntropyTimeout, Error('DapEntropy script could not be loaded')); - } - if (entropyDict && entropyDict.expires_at > Math.round(Date.now() / 1000.0)) { - logMessage('Using cached entropy'); - resolve(); - } else { - if (typeof window.dapCalculateEntropy === 'function') { - window.dapCalculateEntropy(resolve, reject); - } else { - if (rtdConfig && rtdConfig.params && dapUtils.isValidHttpsUrl(rtdConfig.params.dapEntropyUrl)) { - loadExternalScript(rtdConfig.params.dapEntropyUrl, MODULE_CODE, () => { dapUtils.dapGetEntropy(resolve, reject) }); - } else { - reject(Error('Please check if dapEntropyUrl is specified and is valid under config.params')); - } - } - } - }); - loadScriptPromise - .catch((error) => { - logError('Entropy could not be calculated due to: ', error.message); - }) - .finally(() => { - generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent); - }); -} - -export function generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { - logInfo('DEBUG(generateRealTimeData) - ENTER'); - logMessage(' - apiHostname: ' + rtdConfig.params.apiHostname); - logMessage(' - apiVersion: ' + rtdConfig.params.apiVersion); - dapRetryTokenize = 0; - var jsonData = null; - if (rtdConfig && isPlainObject(rtdConfig.params)) { - if (rtdConfig.params.segtax == 504) { - let encMembership = dapUtils.dapGetEncryptedMembershipFromLocalStorage(); - if (encMembership) { - jsonData = dapUtils.dapGetEncryptedRtdObj(encMembership, rtdConfig.params.segtax) - } - } else { - let membership = dapUtils.dapGetMembershipFromLocalStorage(); - if (membership) { - jsonData = dapUtils.dapGetRtdObj(membership, rtdConfig.params.segtax) - } - } - } - if (jsonData) { - if (jsonData.rtd) { - addRealTimeData(bidConfig.ortb2Fragments?.global, jsonData.rtd); - onDone(); - logInfo('DEBUG(generateRealTimeData) - 1'); - // Don't return - ensure the data is always fresh. - } - } - // Calling setTimeout to release the main thread so that the bid request could be sent. - setTimeout(dapUtils.callDapAPIs, 0, bidConfig, onDone, rtdConfig, userConsent); -} - -/** - * Module init - * @param {Object} provider - * @param {Object} userConsent - * @return {boolean} - */ -function init(provider, userConsent) { - if (dapUtils.checkConsent(userConsent) === false) { - return false; - } - return true; -} - -/** @type {RtdSubmodule} */ -export const akamaiDapRtdSubmodule = { - name: SUBMODULE_NAME, - getBidRequestData: getRealTimeData, - init: init -}; - -submodule(MODULE_NAME, akamaiDapRtdSubmodule); -export const dapUtils = { - - callDapAPIs: function(bidConfig, onDone, rtdConfig, userConsent) { - if (rtdConfig && isPlainObject(rtdConfig.params)) { - let config = { - api_hostname: rtdConfig.params.apiHostname, - api_version: rtdConfig.params.apiVersion, - domain: rtdConfig.params.domain, - segtax: rtdConfig.params.segtax, - identity: {type: rtdConfig.params.identityType} - }; - let refreshMembership = true; - let token = dapUtils.dapGetTokenFromLocalStorage(); - const ortb2 = bidConfig.ortb2Fragments.global; - logMessage('token is: ', token); - if (token !== null) { // If token is not null then check the membership in storage and add the RTD object - if (config.segtax == 504) { // Follow the encrypted membership path - dapUtils.dapRefreshEncryptedMembership(ortb2, config, token, onDone) // Get the encrypted membership from server - refreshMembership = false; - } else { - dapUtils.dapRefreshMembership(ortb2, config, token, onDone) // Get the membership from server - refreshMembership = false; - } - } - dapUtils.dapRefreshToken(ortb2, config, refreshMembership, onDone) // Refresh Token and membership in all the cases - } - }, - dapGetEntropy: function(resolve, reject) { - if (typeof window.dapCalculateEntropy === 'function') { - window.dapCalculateEntropy(resolve, reject); - } else { - reject(Error('window.dapCalculateEntropy function is not defined')) - } - }, - - dapGetTokenFromLocalStorage: function(ttl) { - let now = Math.round(Date.now() / 1000.0); // in seconds - let token = null; - let item = JSON.parse(storage.getDataFromLocalStorage(DAP_TOKEN)); - if (item) { - if (now < item.expires_at) { - token = item.token; - } - } - return token; - }, - - dapRefreshToken: function(ortb2, config, refreshMembership, onDone) { - dapUtils.dapLog('Token missing or expired, fetching a new one...'); - // Trigger a refresh - let now = Math.round(Date.now() / 1000.0); // in seconds - let item = {} - let configAsync = {...config}; - dapUtils.dapTokenize(configAsync, config.identity, onDone, - function(token, status, xhr, onDone) { - item.expires_at = now + DAP_DEFAULT_TOKEN_TTL; - let exp = dapUtils.dapExtractExpiryFromToken(token); - if (typeof exp == 'number') { - item.expires_at = exp - 10; - } - item.token = token; - storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(item)); - dapUtils.dapLog('Successfully updated and stored token; expires at ' + item.expires_at); - let dapSSID = xhr.getResponseHeader('Akamai-DAP-SS-ID'); - if (dapSSID) { - storage.setDataInLocalStorage(DAP_SS_ID, JSON.stringify(dapSSID)); - } - let deviceId100 = xhr.getResponseHeader('Akamai-DAP-100'); - if (deviceId100 != null) { - storage.setDataInLocalStorage('dap_deviceId100', deviceId100); - dapUtils.dapLog('Successfully stored DAP 100 Device ID: ' + deviceId100); - } - if (refreshMembership) { - if (config.segtax == 504) { - dapUtils.dapRefreshEncryptedMembership(ortb2, config, token, onDone); - } else { - dapUtils.dapRefreshMembership(ortb2, config, token, onDone); - } - } - }, - function(xhr, status, error, onDone) { - logError('ERROR(' + error + '): failed to retrieve token! ' + status); - onDone() - } - ); - }, - - dapGetMembershipFromLocalStorage: function() { - let now = Math.round(Date.now() / 1000.0); // in seconds - let membership = null; - let item = JSON.parse(storage.getDataFromLocalStorage(DAP_MEMBERSHIP)); - if (item) { - if (now < item.expires_at) { - membership = { - said: item.said, - cohorts: item.cohorts, - attributes: null - }; - } - } - return membership; - }, - - dapRefreshMembership: function(ortb2, config, token, onDone) { - let now = Math.round(Date.now() / 1000.0); // in seconds - let item = {} - let configAsync = {...config}; - dapUtils.dapMembership(configAsync, token, onDone, - function(membership, status, xhr, onDone) { - item.expires_at = now + DAP_DEFAULT_TOKEN_TTL; - let exp = dapUtils.dapExtractExpiryFromToken(membership.said) - if (typeof exp == 'number') { - item.expires_at = exp - 10; - } - item.said = membership.said; - item.cohorts = membership.cohorts; - storage.setDataInLocalStorage(DAP_MEMBERSHIP, JSON.stringify(item)); - dapUtils.dapLog('Successfully updated and stored membership:'); - dapUtils.dapLog(item); - - let data = dapUtils.dapGetRtdObj(item, config.segtax) - dapUtils.checkAndAddRealtimeData(ortb2, data, config.segtax); - onDone(); - }, - function(xhr, status, error, onDone) { - logError('ERROR(' + error + '): failed to retrieve membership! ' + status); - if (status == 403 && dapRetryTokenize < DAP_MAX_RETRY_TOKENIZE) { - dapRetryTokenize++; - dapUtils.dapRefreshToken(ortb2, config, true, onDone); - } else { - onDone(); - } - } - ); - }, - - dapGetEncryptedMembershipFromLocalStorage: function() { - let now = Math.round(Date.now() / 1000.0); // in seconds - let encMembership = null; - let item = JSON.parse(storage.getDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP)); - if (item) { - if (now < item.expires_at) { - encMembership = { - encryptedSegments: item.encryptedSegments - }; - } - } - return encMembership; - }, - - dapRefreshEncryptedMembership: function(ortb2, config, token, onDone) { - let now = Math.round(Date.now() / 1000.0); // in seconds - let item = {}; - let configAsync = {...config}; - dapUtils.dapEncryptedMembership(configAsync, token, onDone, - function(encToken, status, xhr, onDone) { - item.expires_at = now + DAP_DEFAULT_TOKEN_TTL; - let exp = dapUtils.dapExtractExpiryFromToken(encToken); - if (typeof exp == 'number') { - item.expires_at = exp - 10; - } - item.encryptedSegments = encToken; - storage.setDataInLocalStorage(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(item)); - dapUtils.dapLog('Successfully updated and stored encrypted membership:'); - dapUtils.dapLog(item); - - let encData = dapUtils.dapGetEncryptedRtdObj(item, config.segtax); - dapUtils.checkAndAddRealtimeData(ortb2, encData, config.segtax); - onDone(); - }, - function(xhr, status, error, onDone) { - logError('ERROR(' + error + '): failed to retrieve encrypted membership! ' + status); - if (status == 403 && dapRetryTokenize < DAP_MAX_RETRY_TOKENIZE) { - dapRetryTokenize++; - dapUtils.dapRefreshToken(ortb2, config, true, onDone); - } else { - onDone(); - } - } - ); - }, - - /** - * DESCRIPTION - * Extract expiry value from a token - */ - dapExtractExpiryFromToken: function(token) { - let exp = null; - if (token) { - const tokenArray = token.split('..'); - if (tokenArray && tokenArray.length > 0) { - let decode = atob(tokenArray[0]) - let header = JSON.parse(decode.replace(/"/g, '"')); - exp = header.exp; - } - } - return exp - }, - - /** - * DESCRIPTION - * - * Convert a DAP membership response to an OpenRTB2 segment object suitable - * for insertion into user.data.segment or site.data.segment and add it to the rtd obj. - */ - dapGetRtdObj: function(membership, segtax) { - let segment = { - name: 'dap.akamai.com', - ext: { - 'segtax': segtax - }, - segment: [] - }; - if (membership != null) { - for (const i of membership.cohorts) { - segment.segment.push({ id: i }); - } - } - let data = { - rtd: { - ortb2: { - user: { - data: [ - segment - ] - }, - site: { - ext: { - data: { - dapSAID: membership.said - } - } - } - } - } - }; - return data; - }, - - /** - * DESCRIPTION - * - * Convert a DAP membership response to an OpenRTB2 segment object suitable - * for insertion into user.data.segment or site.data.segment and add it to the rtd obj. - */ - dapGetEncryptedRtdObj: function(encToken, segtax) { - let segment = { - name: 'dap.akamai.com', - ext: { - 'segtax': segtax - }, - segment: [] - }; - if (encToken != null) { - segment.segment.push({ id: encToken.encryptedSegments }); - } - let encData = { - rtd: { - ortb2: { - user: { - data: [ - segment - ] - } - } - } - }; - return encData; - }, - - checkAndAddRealtimeData: function(ortb2, data, segtax) { - if (data.rtd) { - if (segtax == 504 && dapUtils.checkIfSegmentsAlreadyExist(ortb2, data.rtd, 504)) { - logMessage('DEBUG(handleInit): rtb Object already added'); - } else { - addRealTimeData(ortb2, data.rtd); - } - logInfo('DEBUG(checkAndAddRealtimeData) - 1'); - } - }, - - checkIfSegmentsAlreadyExist: function(ortb2, rtd, segtax) { - let segmentsExist = false - if (ortb2.user && ortb2.user.data && ortb2.user.data.length > 0) { - for (let i = 0; i < ortb2.user.data.length; i++) { - let element = ortb2.user.data[i] - if (element.ext && element.ext.segtax == segtax) { - segmentsExist = true - logMessage('DEBUG(checkIfSegmentsAlreadyExist): rtb Object already added: ', ortb2.user.data); - break; - } - } - } - return segmentsExist - }, - - dapLog: function(args) { - let css = ''; - css += 'display: inline-block;'; - css += 'color: #fff;'; - css += 'background: #F28B20;'; - css += 'padding: 1px 4px;'; - css += 'border-radius: 3px'; - - logInfo('%cDAP Client', css, args); - }, - - isValidHttpsUrl: function(urlString) { - let url; - try { - url = new URL(urlString); - } catch (_) { - return false; - } - return url.protocol === 'https:'; - }, - - checkConsent: function(userConsent) { - let consent = true; - - if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { - const gdpr = userConsent.gdpr; - const hasGdpr = (gdpr && typeof gdpr.gdprApplies === 'boolean' && gdpr.gdprApplies) ? 1 : 0; - const gdprConsentString = hasGdpr ? gdpr.consentString : ''; - if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { - logError('akamaiDapRtd submodule requires consent string to call API'); - consent = false; - } - } else if (userConsent && userConsent.usp) { - const usp = userConsent.usp; - consent = usp[1] !== 'N' && usp[2] !== 'Y'; - } - - return consent; - }, - - /******************************************************************************* - * - * V2 (And Beyond) API - * - ******************************************************************************/ - - /** - * SYNOPSIS - * - * dapTokenize( config, identity ); - * - * DESCRIPTION - * - * Tokenize an identity into a secure, privacy safe pseudonymiziation. - * - * PARAMETERS - * - * config: an array of system configuration parameters - * - * identity: an array of identity parameters passed to the tokenizing system - * - * EXAMPLE - * - * config = { - * api_hostname: "prebid.dap.akadns.net", // required - * domain: "prebid.org", // required - * api_version: "x1", // optional, default "x1" - * }; - * - * token = null; - * identity_email = { - * type: "email", - * identity: "obiwan@jedi.com" - * attributes: { cohorts: [ "100:1641013200", "101:1641013200", "102":3:1641013200" ] }, - * }; - * dap_x1_tokenize( config, identity_email, - * function( response, status, xhr ) { token = response; }, - * function( xhr, status, error ) { ; } // handle error - * - * token = null; - * identity_signature = { type: "signature:1.0.0" }; - * dap_x1_tokenize( config, identity_signature, - * function( response, status, xhr } { token = response; }, - * function( xhr, status, error ) { ; } // handle error - */ - dapTokenize: function(config, identity, onDone, onSuccess = null, onError = null) { - if (onError == null) { - onError = function(xhr, status, error, onDone) {}; - } - - if (config == null || typeof (config) == typeof (undefined)) { - onError(null, 'Invalid config object', 'ClientError', onDone); - return; - } - - if (typeof (config.domain) != 'string') { - onError(null, 'Invalid config.domain: must be a string', 'ClientError', onDone); - return; - } - - if (config.domain.length <= 0) { - onError(null, 'Invalid config.domain: must have non-zero length', 'ClientError', onDone); - return; - } - - if (!('api_version' in config) || (typeof (config.api_version) == 'string' && config.api_version.length == 0)) { - config.api_version = 'x1'; - } - - if (typeof (config.api_version) != 'string') { - onError(null, "Invalid api_version: must be a string like 'x1', etc.", 'ClientError', onDone); - return; - } - - if (!(('api_hostname') in config) || typeof (config.api_hostname) != 'string' || config.api_hostname.length == 0) { - onError(null, 'Invalid api_hostname: must be a non-empty string', 'ClientError', onDone); - return; - } - - if (identity == null || typeof (identity) == typeof (undefined)) { - onError(null, 'Invalid identity object', 'ClientError', onDone); - return; - } - - if (!('type' in identity) || typeof (identity.type) != 'string' || identity.type.length <= 0) { - onError(null, "Identity must contain a valid 'type' field", 'ClientError', onDone); - return; - } - - let apiParams = { - 'type': identity.type, - }; - - if (typeof (identity.identity) != typeof (undefined)) { - apiParams.identity = identity.identity; - } - if (typeof (identity.attributes) != typeof (undefined)) { - apiParams.attributes = identity.attributes; - } - - let entropyDict = JSON.parse(storage.getDataFromLocalStorage(DAP_CLIENT_ENTROPY)); - if (entropyDict && entropyDict.entropy) { - apiParams.entropy = entropyDict.entropy; - } - - let method; - let body; - let path; - switch (config.api_version) { - case 'x1': - case 'x1-dev': - method = 'POST'; - path = '/data-activation/' + config.api_version + '/domain/' + config.domain + '/identity/tokenize'; - body = JSON.stringify(apiParams); - break; - default: - onError(null, 'Invalid api_version: ' + config.api_version, 'ClientError', onDone); - return; - } - - let customHeaders = {'Content-Type': 'application/json'}; - let dapSSID = JSON.parse(storage.getDataFromLocalStorage(DAP_SS_ID)); - if (dapSSID) { - customHeaders['Akamai-DAP-SS-ID'] = dapSSID; - } - - let url = 'https://' + config.api_hostname + path; - let cb = { - success: (response, request) => { - let token = null; - switch (config.api_version) { - case 'x1': - case 'x1-dev': - token = request.getResponseHeader('Akamai-DAP-Token'); - break; - } - onSuccess(token, request.status, request, onDone); - }, - error: (request, error) => { - onError(request, request.statusText, error, onDone); - } - }; - - ajax(url, cb, body, { - method: method, - customHeaders: customHeaders - }); - }, - - /** - * SYNOPSIS - * - * dapMembership( config, token, onSuccess, onError ); - * - * DESCRIPTION - * - * Return the audience segment membership along with a new Secure Advertising - * ID for this token. - * - * PARAMETERS - * - * config: an array of system configuration parameters - * - * token: the token previously returned from the tokenize API - * - * EXAMPLE - * - * config = { - * api_hostname: 'api.dap.akadns.net', - * }; - * - * // token from dap_tokenize - * - * dapMembership( config, token, - * function( membership, status, xhr ) { - * // Run auction with membership.segments and membership.said - * }, - * function( xhr, status, error ) { - * // error - * } ); - * - */ - dapMembership: function(config, token, onDone, onSuccess = null, onError = null) { - if (onError == null) { - onError = function(xhr, status, error, onDone) {}; - } - - if (config == null || typeof (config) == typeof (undefined)) { - onError(null, 'Invalid config object', 'ClientError', onDone); - return; - } - - if (!('api_version' in config) || (typeof (config.api_version) == 'string' && config.api_version.length == 0)) { - config.api_version = 'x1'; - } - - if (typeof (config.api_version) != 'string') { - onError(null, "Invalid api_version: must be a string like 'x1', etc.", 'ClientError', onDone); - return; - } - - if (!(('api_hostname') in config) || typeof (config.api_hostname) != 'string' || config.api_hostname.length == 0) { - onError(null, 'Invalid api_hostname: must be a non-empty string', 'ClientError', onDone); - return; - } - - if (token == null || typeof (token) != 'string') { - onError(null, 'Invalid token: must be a non-null string', 'ClientError', onDone); - return; - } - let path = '/data-activation/' + - config.api_version + - '/token/' + token + - '/membership'; - - let url = 'https://' + config.api_hostname + path; - - let cb = { - success: (response, request) => { - onSuccess(JSON.parse(response), request.status, request, onDone); - }, - error: (error, request) => { - onError(request, request.status, error, onDone); - } - }; - - ajax(url, cb, undefined, { - method: 'GET', - customHeaders: {} - }); - }, - - /** - * SYNOPSIS - * - * dapEncryptedMembership( config, token, onSuccess, onError ); - * - * DESCRIPTION - * - * Return the audience segment membership along with a new Secure Advertising - * ID for this token in encrypted format. - * - * PARAMETERS - * - * config: an array of system configuration parameters - * - * token: the token previously returned from the tokenize API - * - * EXAMPLE - * - * config = { - * api_hostname: 'api.dap.akadns.net', - * }; - * - * // token from dap_tokenize - * - * dapEncryptedMembership( config, token, - * function( membership, status, xhr ) { - * // Run auction with membership.segments and membership.said after decryption - * }, - * function( xhr, status, error ) { - * // error - * } ); - * - */ - dapEncryptedMembership: function(config, token, onDone, onSuccess = null, onError = null) { - if (onError == null) { - onError = function(xhr, status, error, onDone) {}; - } - - if (config == null || typeof (config) == typeof (undefined)) { - onError(null, 'Invalid config object', 'ClientError', onDone); - return; - } - - if (!('api_version' in config) || (typeof (config.api_version) == 'string' && config.api_version.length == 0)) { - config.api_version = 'x1'; - } - - if (typeof (config.api_version) != 'string') { - onError(null, "Invalid api_version: must be a string like 'x1', etc.", 'ClientError', onDone); - return; - } - - if (!(('api_hostname') in config) || typeof (config.api_hostname) != 'string' || config.api_hostname.length == 0) { - onError(null, 'Invalid api_hostname: must be a non-empty string', 'ClientError', onDone); - return; - } - - if (token == null || typeof (token) != 'string') { - onError(null, 'Invalid token: must be a non-null string', 'ClientError', onDone); - return; - } - let path = '/data-activation/' + - config.api_version + - '/token/' + token + - '/membership/encrypt'; - - let url = 'https://' + config.api_hostname + path; - - let cb = { - success: (response, request) => { - let encToken = request.getResponseHeader('Akamai-DAP-Token'); - onSuccess(encToken, request.status, request, onDone); - }, - error: (error, request) => { - onError(request, request.status, error, onDone); - } - }; - ajax(url, cb, undefined, { - method: 'GET', - customHeaders: { - 'Content-Type': 'application/json', - 'Pragma': 'akamai-x-get-extracted-values' - } - }); - } -} +import { + createRtdProvider +} from './symitriDapRtdProvider.js'/* eslint prebid/validate-imports: "off" */ + +export const { + addRealTimeData, + getRealTimeData, + generateRealTimeData, + rtdSubmodule: akamaiDapRtdSubmodule, + storage, + dapUtils, + DAP_TOKEN, + DAP_MEMBERSHIP, + DAP_ENCRYPTED_MEMBERSHIP, + DAP_SS_ID, + DAP_DEFAULT_TOKEN_TTL, + DAP_MAX_RETRY_TOKENIZE, + DAP_CLIENT_ENTROPY +} = createRtdProvider('dap', 'akamaidap', 'Akamai'); diff --git a/modules/akceloBidAdapter.js b/modules/akceloBidAdapter.js new file mode 100644 index 00000000000..bfada1cc2eb --- /dev/null +++ b/modules/akceloBidAdapter.js @@ -0,0 +1,148 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { deepSetValue, getParameterByName, logError } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { ORTB_MTYPES } from '../libraries/ortbConverter/processors/mediaType.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @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 + */ + +const BIDDER_CODE = 'akcelo'; +const COOKIE_SYNC_ENDPOINT = 'akcelo'; + +const AUCTION_URL = 'https://s2s.sportslocalmedia.com/openrtb2/auction'; +const IFRAME_SYNC_URL = 'https://ads.sportslocalmedia.com/load-cookie.html'; + +const DEFAULT_TTL = 300; + +const akceloDemoIsOn = () => getParameterByName('akcelo_demo') === 'true'; + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: DEFAULT_TTL, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (bidRequest.params.siteId) { + deepSetValue(imp, 'ext.akcelo.siteId', bidRequest.params.siteId); + } else { + logError('Missing parameter : siteId'); + } + + if (bidRequest.params.adUnitId) { + deepSetValue(imp, 'ext.akcelo.adUnitId', bidRequest.params.adUnitId); + } else { + logError('Missing parameter : adUnitId'); + } + + if (akceloDemoIsOn()) { + deepSetValue(imp, 'ext.akcelo.test', 1); + } + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + deepSetValue(request, 'test', akceloDemoIsOn() ? 1 : 0); + + const siteId = bidderRequest.bids.map((bid) => bid.params.siteId).find(Boolean); + deepSetValue(request, 'site.publisher.ext.prebid.parentAccount', siteId); + + return request; + }, + bidResponse(buildBidResponse, bid, context) { + // In ORTB 2.5, bid responses do not specify their mediatype, which is something Prebid.js requires + context.mediaType = bid.mtype && ORTB_MTYPES[bid.mtype] + ? ORTB_MTYPES[bid.mtype] + : bid.ext?.prebid?.type; + + return buildBidResponse(bid, context); + }, +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + + /** + * Determines whether the given bid request is valid. + * + * @param {Bid} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid(bid) { + if (!bid?.params?.adUnitId) { + logError("Missing required parameter 'adUnitId'"); + return false; + } + if (!bid?.params?.siteId) { + logError("Missing required parameter 'siteId'"); + return false; + } + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({ bidRequests, bidderRequest }); + + return [{ method: 'POST', url: AUCTION_URL, data }]; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest The bid request sent to the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse(serverResponse, bidRequest) { + const { bids } = converter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }); + + return bids; + }, + + /** + * 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. + * @param {*} gdprConsent + * @param {*} uspConsent + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (syncOptions.iframeEnabled) { + let syncParams = `?endpoint=${COOKIE_SYNC_ENDPOINT}`; + if (gdprConsent) { + syncParams += `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + syncParams += `&gdpr_consent=${encodeURIComponent(gdprConsent.consentString || '')}`; + } + if (uspConsent) { + syncParams += `&us_privacy=${encodeURIComponent(uspConsent)}`; + } + + return [{ type: 'iframe', url: IFRAME_SYNC_URL + syncParams }]; + } + return []; + }, +}; + +registerBidder(spec); diff --git a/modules/akceloBidAdapter.md b/modules/akceloBidAdapter.md new file mode 100644 index 00000000000..02881ede2e1 --- /dev/null +++ b/modules/akceloBidAdapter.md @@ -0,0 +1,57 @@ +# Overview + +**Module Name**: Akcelo Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: tech@akcelo.io + +# Description + +A module that connects to the Akcelo network for bids + +## AdUnits configuration example + +```javascript +const adUnits = [ + { + code: 'div-123', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + video: { + context: "outstream", + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [1], + skip: 1, + api: [2], + minbitrate: 1000, + maxbitrate: 3000, + minduration: 3, + maxduration: 10, + startdelay: 2, + placement: 4, + linearity: 1 + }, + }, + bids: [ + { + bidder: 'akcelo', + params: { + siteId: 763, // required + adUnitId: 7965, // required + test: 1, // optional, use 0 to disable test creatives + }, + }, + ], + }, +]; + +pbjs.que.push(function () { + pbjs.addAdUnits(adUnits); +}); +``` diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index 3bd995cc112..f52c3ec7703 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -41,7 +41,8 @@ export const spec = { impMediaTypes: formatTypes, adUnitCode: bidRequest.adUnitCode, video: deepAccess(bidRequest, 'mediaTypes.video'), - banner: deepAccess(bidRequest, 'mediaTypes.banner') + banner: deepAccess(bidRequest, 'mediaTypes.banner'), + ext: bidRequest.ortb2Imp?.ext }) bidIds.push(bidRequest.bidId) }) @@ -81,6 +82,7 @@ export const spec = { }, at: ortb2?.at, bcat: ortb2?.bcat, + badv: ortb2?.badv, wseat: ortb2?.wseat } } diff --git a/modules/ampliffyBidAdapter.js b/modules/ampliffyBidAdapter.js index bcd28e5bcf1..e79b04ab4c4 100644 --- a/modules/ampliffyBidAdapter.js +++ b/modules/ampliffyBidAdapter.js @@ -278,7 +278,6 @@ function extractTrackingURL(htmlContent, ret) { const trackingUrlDiv = htmlContent.querySelectorAll('[bidder-tracking-url]')[0]; if (trackingUrlDiv) { const trackingUrl = trackingUrlDiv.getAttribute('bidder-tracking-url'); - // eslint-disable-next-line no-console logInfo(LOG_PREFIX + 'parseXML: trackingUrl: ', trackingUrl) ret.trackingUrl = trackingUrl; } @@ -304,10 +303,8 @@ export function parseXML(xml, bid) { ret.userSyncs = extractSyncs(htmlContent); } } catch (e) { - // eslint-disable-next-line no-console logError(LOG_PREFIX + 'Error parsing XML', e); } - // eslint-disable-next-line no-console logInfo(LOG_PREFIX + 'parseXML RET:', ret); return ret; @@ -341,7 +338,6 @@ export function isAllowedToBidUp(html, currentURL) { } } } catch (e) { - // eslint-disable-next-line no-console logError(LOG_PREFIX + 'isAllowedToBidUp', e); } return allowedToPush; @@ -399,7 +395,6 @@ function onBidWon(bid) { } } function onTimeOut() { - // eslint-disable-next-line no-console logInfo(LOG_PREFIX + 'TIMEOUT'); } diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index 6e14f65b0c8..9a3c61135a0 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -15,6 +15,7 @@ import { import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; import { fetch } from '../src/ajax.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const BIDDER_CODE = 'amx'; const storage = getStorageManager({ bidderCode: BIDDER_CODE }); @@ -142,7 +143,7 @@ function getFloor(bid) { size: '*', bidRequest: bid, }); - return floor.floor; + return floor?.floor; } catch (e) { logError('call to getFloor failed: ', e); return DEFAULT_MIN_FLOOR; @@ -161,8 +162,8 @@ function convertRequest(bid) { const au = bid.params != null && - typeof bid.params.adUnitId === 'string' && - bid.params.adUnitId !== '' + typeof bid.params.adUnitId === 'string' && + bid.params.adUnitId !== '' ? bid.params.adUnitId : bid.adUnitCode; @@ -298,6 +299,14 @@ function buildReferrerInfo(bidderRequest) { }; } +const alternateCodesAllowed = (bidderSettings, currentBidder) => + !!( + bidderSettings.amx ?? + bidderSettings[currentBidder] ?? + bidderSettings.standard ?? + {} + ).allowAlternateBidderCodes; + const isTrue = (boolValue) => boolValue === true || boolValue === 1 || boolValue === 'true'; @@ -323,8 +332,7 @@ export const spec = { : { bidderRequestsCount: 0, bidderWinsCount: 0, - bidRequestsCount: 0, - }; + bidRequestsCount: 0 }; const payload = { a: generateUUID(), @@ -418,9 +426,9 @@ export const spec = { const output = []; let hasFrame = false; - _each(serverResponses, function({ body: response }) { + _each(serverResponses, function ({ body: response }) { if (response != null && response.p != null && response.p.hreq) { - _each(response.p.hreq, function(syncPixel) { + _each(response.p.hreq, function (syncPixel) { const pixelType = syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image'; if (syncOptions.iframeEnabled || pixelType === 'image') { @@ -454,6 +462,11 @@ export const spec = { setUIDSafe(response.am); } + let { bidderSettings } = getGlobal(); + const currentBidder = config.getCurrentBidder(); + const allowAlternateBidderCodes = alternateCodesAllowed(bidderSettings ?? {}, currentBidder) || + alternateCodesAllowed(config.getConfig('bidderSettings') ?? {}, currentBidder); + return flatMap(Object.keys(response.r), (bidID) => { return flatMap(response.r[bidID], (siteBid) => siteBid.b.map((bid) => { @@ -466,8 +479,16 @@ export const spec = { const size = resolveSize(bid, request.data, bidID); const defaultExpiration = mediaType === BANNER ? 240 : 300; + const { + bc: bidderCode, + ds: demandSource, + dsp: dspCode, + } = bid.ext ?? {}; return { + ...(bidderCode != null && allowAlternateBidderCodes + ? { bidderCode } + : {}), requestId: bidID, cpm: bid.price, width: size[0], @@ -479,6 +500,8 @@ export const spec = { meta: { advertiserDomains: bid.adomain, mediaType, + ...(dspCode != null ? { networkId: dspCode } : {}), + ...(demandSource != null ? { demandSource } : {}), }, mediaType, ttl: typeof bid.exp === 'number' ? bid.exp : defaultExpiration, @@ -552,7 +575,7 @@ export const spec = { body: payload, keepalive: true, withCredentials: true, - method: 'POST' + method: 'POST', }).catch((_e) => { // do nothing; ignore errors }); diff --git a/modules/amxIdSystem.js b/modules/amxIdSystem.js index 184eff76c34..aafb4b99182 100644 --- a/modules/amxIdSystem.js +++ b/modules/amxIdSystem.js @@ -101,7 +101,7 @@ export const amxIdSubmodule = { return undefined; } - const consent = consentData || { gdprApplies: false, consentString: '' }; + const consent = consentData?.gdpr || { gdprApplies: false, consentString: '' }; const client = ajaxBuilder(AJAX_TIMEOUT); const usp = uspDataHandler.getConsentData(); const ref = getRefererInfo(); diff --git a/modules/anPspParamsConverter.js b/modules/anPspParamsConverter.js new file mode 100644 index 00000000000..27b90168476 --- /dev/null +++ b/modules/anPspParamsConverter.js @@ -0,0 +1,128 @@ +/* +- register a hook function on the makeBidRequests hook (after the main function ran) + +- this hook function will: +1. verify s2sconfig is defined and we (or our aliases) are included to the config +2. filter bidRequests that match to our bidderName or any registered aliases +3. for each request, read the bidderRequests.bids[].params to modify the keys/values + a. in particular change the keywords structure, apply underscore casing for keys, adjust use_payment_rule name, and convert certain values' types + b. will import some functions from the anKeywords library, but ideally should be kept separate to avoid including this code when it's not needed (strict client-side setups) and avoid the rest of the appnexus adapter's need for inclusion for those strictly server-side setups. +*/ + +// import { CONSTANTS } from '../src/cons tants.js'; +import {isArray, isPlainObject, isStr} from '../src/utils.js'; +import {getHook} from '../src/hook.js'; +import {config} from '../src/config.js'; +import {convertCamelToUnderscore, appnexusAliases} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import adapterManager from '../src/adapterManager.js'; + +// keywords: { 'genre': ['rock', 'pop'], 'pets': ['dog'] } goes to 'genre=rock,genre=pop,pets=dog' +function convertKeywordsToString(keywords) { + let result = ''; + Object.keys(keywords).forEach(key => { + // if 'text' or '' + if (isStr(keywords[key])) { + if (keywords[key] !== '') { + result += `${key}=${keywords[key]},` + } else { + result += `${key},`; + } + } else if (isArray(keywords[key])) { + if (keywords[key][0] === '') { + result += `${key},` + } else { + keywords[key].forEach(val => { + result += `${key}=${val},` + }); + } + } + }); + + // remove last trailing comma + result = result.substring(0, result.length - 1); + return result; +} + +function digForAppNexusBidder(s2sConfig) { + let result = false; + // check for plain setup + if (s2sConfig?.bidders?.includes('appnexus')) result = true; + + // registered aliases + const aliasList = appnexusAliases.map(aliasObj => (aliasObj.code)); + if (!result && s2sConfig?.bidders?.filter(s2sBidder => aliasList.includes(s2sBidder)).length > 0) result = true; + + // pbjs.aliasBidder + if (!result) { + result = !!(s2sConfig?.bidders?.find(bidder => (adapterManager.resolveAlias(bidder) === 'appnexus'))); + } + + return result; +} + +// need a separate check b/c we're checking a specific bidRequest to see if we modify it, not just that we have a server-side bidder somewhere in prebid.js +// function isThisOurBidderInDisguise(tarBidder, s2sConfig) { +// if (tarBidder === 'appnexus') return true; + +// if (isPlainObject(s2sConfig?.extPrebid?.aliases) && !!(Object.entries(s2sConfig?.extPrebid?.aliases).find((pair) => (pair[0] === tarBidder && pair[1] === 'appnexus')))) return true; + +// if (appnexusAliases.map(aliasObj => (aliasObj.code)).includes(tarBidder)) return true; + +// if (adapterManager.resolveAlias(tarBidder) === 'appnexus') return true; + +// return false; +// } + +export function convertAnParams(next, bidderRequests) { + // check s2sconfig + const s2sConfig = config.getConfig('s2sConfig'); + let proceed = false; + + if (isPlainObject(s2sConfig)) { + proceed = digForAppNexusBidder(s2sConfig); + } else if (isArray(s2sConfig)) { + s2sConfig.forEach(s2sCfg => { + proceed = digForAppNexusBidder(s2sCfg); + }); + } + + if (proceed) { + bidderRequests + .flatMap(br => br.bids) + .filter(bid => bid.src === 's2s' && adapterManager.resolveAlias(bid.bidder) === 'appnexus') + .forEach((bid) => { + transformBidParams(bid); + }); + } + + next(bidderRequests); +} + +function transformBidParams(bid) { + let params = bid.params; + if (params) { + params = convertTypes({ + 'member': 'string', + 'invCode': 'string', + 'placementId': 'number', + 'keywords': convertKeywordsToString, + 'publisherId': 'number' + }, params); + + Object.keys(params).forEach(paramKey => { + let convertedKey = convertCamelToUnderscore(paramKey); + if (convertedKey !== paramKey) { + params[convertedKey] = params[paramKey]; + delete params[paramKey]; + } + }); + + params.use_pmt_rule = (typeof params.use_payment_rule === 'boolean') ? params.use_payment_rule : false; + if (params.use_payment_rule) { + delete params.use_payment_rule; + } + } +} + +getHook('makeBidRequests').after(convertAnParams, 9); diff --git a/modules/anPspParamsConverter.md b/modules/anPspParamsConverter.md new file mode 100644 index 00000000000..f341b0a5976 --- /dev/null +++ b/modules/anPspParamsConverter.md @@ -0,0 +1,10 @@ +## Quick Summary + +This module is a temporary measure for publishers running Prebid.js 9.0+ and using the AppNexus PSP endpoint through their Prebid.js setup. Please ensure to include this module in your builds of Prebid.js 9.0+, otherwise requests to PSP may not complete successfully. + +## Module's purpose + +This module replicates certain functionality that was previously stored in the appnexusBidAdapter.js file within a function named transformBidParams. + +This transformBidParams was a standard function in all adapters, which helped to change/modify the params and their values to a format that matched the bidder's request structure on the server-side endpoint. In Prebid.js 9.0, this standard function was removed in all adapter files, so that the whole client-side file (eg appnexusBidAdapter.js) wouldn't have to be included in a prebid.js build file that was meant for server-side bidders. + diff --git a/modules/aniviewBidAdapter.js b/modules/aniviewBidAdapter.js index 84552638421..d7705521c7d 100644 --- a/modules/aniviewBidAdapter.js +++ b/modules/aniviewBidAdapter.js @@ -1,317 +1,331 @@ import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; -import { logError } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { + deepAccess, + mergeDeep, + isFn, + isStr, + isPlainObject, + getUniqueIdentifierStr +} from '../src/utils.js'; const BIDDER_CODE = 'aniview'; const GVLID = 780; const TTL = 600; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_PLAYER_DOMAIN = 'player.aniview.com'; +const SSP_ENDPOINT = 'https://rtb.aniview.com/sspRTB2'; +const RENDERER_FILENAME = 'prebidRenderer.js'; -function avRenderer(bid) { - bid.renderer.push(function() { - let eventCallback = bid && bid.renderer && bid.renderer.handleVideoEvent ? bid.renderer.handleVideoEvent : null; - window.aniviewRenderer.renderAd({ - id: bid.adUnitCode + '_' + bid.adId, - debug: window.location.href.indexOf('pbjsDebug') >= 0, - placement: bid.adUnitCode, - width: bid.width, - height: bid.height, - vastUrl: bid.vastUrl, - vastXml: bid.vastXml, - config: bid.params[0].rendererConfig, - eventsCallback: eventCallback, - bid: bid - }); - }); -} +const converter = ortbConverter({ + context: { + netRevenue: true, // required + ttl: TTL, // required + currency: DEFAULT_CURRENCY, + }, -function newRenderer(bidRequest) { - let playerDomain = 'player.aniview.com'; - const config = {}; + imp(buildImp, bidRequest, context) { + const { mediaType } = context; + const imp = buildImp(bidRequest, context); + const { width, height } = getSize(mediaType, bidRequest); + const floor = getFloor(bidRequest, { width, height }, mediaType); - if (bidRequest && bidRequest.bidRequest && bidRequest.bidRequest.params) { - const params = bidRequest.bidRequest.params + imp.tagid = deepAccess(bidRequest, 'params.AV_CHANNELID'); - if (params.playerDomain) { - playerDomain = params.playerDomain; + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = DEFAULT_CURRENCY; } - if (params.AV_PUBLISHERID) { - config.AV_PUBLISHERID = params.AV_PUBLISHERID; + if (isBannerType(mediaType)) { + mergeDeep(imp.banner, { w: width, h: height }); } - if (params.AV_CHANNELID) { - config.AV_CHANNELID = params.AV_CHANNELID; - } - } + return imp; + }, - const renderer = Renderer.install({ - url: 'https://' + playerDomain + '/script/6.1/prebidRenderer.js', - config: config, - loaded: false, - }); + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const replacements = context.bidRequests[0]?.params?.replacements; - try { - renderer.setRender(avRenderer); - } catch (err) { - } + mergeDeep(request, { + ext: { + [BIDDER_CODE]: { + pbjs: 1, + pbv: '$prebid.version$', + } + } + }); - return renderer; -} + if (isPlainObject(replacements)) { + mergeDeep(request, { ext: { [BIDDER_CODE]: { replacements } } }); + } -function isBidRequestValid(bid) { - if (!bid.params || !bid.params.AV_PUBLISHERID || !bid.params.AV_CHANNELID) { return false; } + return request; + }, - return true; -} -let irc = 0; -function buildRequests(validBidRequests, bidderRequest) { - let bidRequests = []; - - for (let i = 0; i < validBidRequests.length; i++) { - let bidRequest = validBidRequests[i]; - var sizes = [[640, 480]]; - - if (bidRequest.mediaTypes && bidRequest.mediaTypes.video && bidRequest.mediaTypes.video.playerSize) { - sizes = bidRequest.mediaTypes.video.playerSize; - } else { - if (bidRequest.sizes) { - sizes = bidRequest.sizes; - } - } - if (sizes.length === 2 && typeof sizes[0] === 'number') { - sizes = [[sizes[0], sizes[1]]]; + bidResponse(buildBidResponse, bid, context) { + const { bidRequest, mediaType } = context; + const { width, height } = getSize(mediaType, bidRequest); + + if (!bid.w || !bid.h) { + bid.w = width; + bid.h = height; } - for (let j = 0; j < sizes.length; j++) { - let size = sizes[j]; - let playerWidth; - let playerHeight; + bid.crid ??= getUniqueIdentifierStr(); + bid.adid ??= getUniqueIdentifierStr(); + bid.bidid ??= getUniqueIdentifierStr(); - if (size && size.length == 2) { - playerWidth = size[0]; - playerHeight = size[1]; - } else { - playerWidth = 640; - playerHeight = 480; - } + const prebidBid = buildBidResponse(bid, context); - let s2sParams = {}; + if (!bid.adm || prebidBid.cpm <= 0) { + return prebidBid; + } - for (var attrname in bidRequest.params) { - if (bidRequest.params.hasOwnProperty(attrname) && attrname.indexOf('AV_') == 0) { - s2sParams[attrname] = bidRequest.params[attrname]; - } - }; + mergeDeep(prebidBid, { meta: { advertiserDomains: bid.adomain || [] } }); - if (s2sParams.AV_APPPKGNAME && !s2sParams.AV_URL) { s2sParams.AV_URL = s2sParams.AV_APPPKGNAME; } - if (!s2sParams.AV_IDFA && !s2sParams.AV_URL) { - // TODO: does it make sense to fall back to window.location here? - s2sParams.AV_URL = bidderRequest?.refererInfo?.page || window.location.href; - } - if (s2sParams.AV_IDFA && !s2sParams.AV_AID) { s2sParams.AV_AID = s2sParams.AV_IDFA; } - if (s2sParams.AV_AID && !s2sParams.AV_IDFA) { s2sParams.AV_IDFA = s2sParams.AV_AID; } - - s2sParams.cb = Math.floor(Math.random() * 999999999); - s2sParams.AV_WIDTH = playerWidth; - s2sParams.AV_HEIGHT = playerHeight; - s2sParams.bidWidth = playerWidth; - s2sParams.bidHeight = playerHeight; - s2sParams.bidId = bidRequest.bidId; - s2sParams.pbjs = 1; - s2sParams.tgt = 10; - s2sParams.s2s = '1'; - s2sParams.irc = irc; - irc++; - s2sParams.wpm = 1; - - if (bidderRequest && bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies) { - s2sParams.AV_GDPR = 1; - s2sParams.AV_CONSENT = bidderRequest.gdprConsent.consentString; + if (bid.ext?.aniview) { + prebidBid.meta.aniview = bid.ext.aniview + + if (prebidBid.meta.aniview.tag) { + try { + prebidBid.meta.aniview.tag = JSON.parse(bid.ext.aniview.tag) + } catch { + // Ignore the error } } - if (bidderRequest && bidderRequest.uspConsent) { - s2sParams.AV_CCPA = bidderRequest.uspConsent; + } + + if (isVideoType(mediaType)) { + if (bidRequest.mediaTypes.video.context === 'outstream') { + prebidBid.renderer = createRenderer(bidRequest); + } + } else if (isBannerType(mediaType)) { + if (bid.adm?.trim().startsWith(' { + Object.keys(bidRequest.mediaTypes).forEach((mediaType) => { + const endpoint = bidRequest.params.dev?.endpoint || SSP_ENDPOINT; - bidRequests.push({ - method: 'GET', - url: servingUrl, - data: s2sParams, - bidRequest + requests.push({ + method: 'POST', + url: endpoint, + bids: [bidRequest], + options: { withCredentials: true }, + data: converter.toORTB({ + bidderRequest, + bidRequests: [bidRequest], + context: { mediaType }, + }), + }); }); + }); + + return requests; + }, + + interpretResponse(serverResponse, bidderRequest) { + const { body } = serverResponse; + const bids = body?.seatbid?.flatMap(seatbid => seatbid?.bid || []) || []; + + if (!bidderRequest.data || bids.length <= 0) { + return []; } - } - return bidRequests; -} -function getCpmData(xml) { - let ret = {cpm: 0, currency: 'USD'}; - if (xml) { - let ext = xml.getElementsByTagName('Extensions'); - if (ext && ext.length > 0) { - ext = ext[0].getElementsByTagName('Extension'); - if (ext && ext.length > 0) { - for (var i = 0; i < ext.length; i++) { - if (ext[i].getAttribute('type') == 'ANIVIEW') { - let price = ext[i].getElementsByTagName('Cpm'); - if (price && price.length == 1) { - ret.cpm = price[0].textContent; - } - break; - } + return converter.fromORTB({ response: body, request: bidderRequest.data }).bids.map((prebidBid, index) => { + const bid = bids[index]; + const replacements = { + auctionPrice: prebidBid.cpm, + auctionId: prebidBid.requestId, + auctionBidId: bid.bidid, + auctionImpId: bid.impid, + auctionSeatId: prebidBid.seatBidId, + auctionAdId: bid.adid, + }; + + const bidAdmWithReplacedMacros = replaceMacros(bid.adm, replacements); + + if (isVideoType(prebidBid.mediaType)) { + prebidBid.vastXml = bidAdmWithReplacedMacros; + + if (bid?.nurl) { + prebidBid.vastUrl = replaceMacros(bid.nurl, replacements); } + } else { + prebidBid.ad = bidAdmWithReplacedMacros; } + + return prebidBid; + }); + }, + + getUserSyncs(syncOptions, serverResponses) { + if (!serverResponses?.[0]?.body || serverResponses.error) { + return []; } - } - return ret; + + try { + const syncs = serverResponses[0].body.ext?.[BIDDER_CODE]?.sync; + + if (syncs) { + return getValidSyncs(syncs, syncOptions); + } + } catch (error) {} + + return []; + }, +}; + +function isVideoType(mediaType) { + return mediaType === VIDEO; } -function buildBanner(xmlStr, bidRequest, bidResponse) { - var rendererData = JSON.stringify({ - id: bidRequest.adUnitCode, - debug: window.location.href.indexOf('pbjsDebug') >= 0, - placement: bidRequest.bidRequest.adUnitCode, - width: bidResponse.width, - height: bidResponse.height, - vastXml: xmlStr, - bid: bidResponse, - config: bidRequest.bidRequest.params.rendererConfig - }); - var playerDomain = bidRequest.bidRequest.params.playerDomain || 'player.aniview.com'; - var ad = ''; - ad += '' - return ad; + +function isBannerType(mediaType) { + return mediaType === BANNER; } -function interpretResponse(serverResponse, bidRequest) { - let bidResponses = []; - if (serverResponse && serverResponse.body) { - if (serverResponse.error) { - return bidResponses; - } else { - try { - let bidResponse = {}; - if (bidRequest && bidRequest.data && bidRequest.data.bidId && bidRequest.data.bidId !== '') { - let mediaType = VIDEO; - if (bidRequest.bidRequest && bidRequest.bidRequest.mediaTypes && !bidRequest.bidRequest.mediaTypes[VIDEO]) { - mediaType = BANNER; - } - let xmlStr = serverResponse.body; - let xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); - if (xml && xml.getElementsByTagName('parsererror').length == 0) { - let cpmData = getCpmData(xml); - if (cpmData.cpm > 0) { - bidResponse.requestId = bidRequest.data.bidId; - bidResponse.ad = ''; - bidResponse.cpm = cpmData.cpm; - bidResponse.width = bidRequest.data.AV_WIDTH; - bidResponse.height = bidRequest.data.AV_HEIGHT; - bidResponse.ttl = TTL; - bidResponse.creativeId = xml.getElementsByTagName('Ad') && xml.getElementsByTagName('Ad')[0] && xml.getElementsByTagName('Ad')[0].getAttribute('id') ? xml.getElementsByTagName('Ad')[0].getAttribute('id') : 'creativeId'; - bidResponse.currency = cpmData.currency; - bidResponse.netRevenue = true; - bidResponse.mediaType = mediaType; - if (mediaType === VIDEO) { - try { - var blob = new Blob([xmlStr], { - type: 'application/xml' - }); - bidResponse.vastUrl = window.URL.createObjectURL(blob); - } catch (ex) { - logError('Aniview Debug create vastXml error:\n\n' + ex); - } - bidResponse.vastXml = xmlStr; - if (bidRequest.bidRequest && bidRequest.bidRequest.mediaTypes && bidRequest.bidRequest.mediaTypes.video && bidRequest.bidRequest.mediaTypes.video.context === 'outstream') { - bidResponse.renderer = newRenderer(bidRequest); - } - } else { - bidResponse.ad = buildBanner(xmlStr, bidRequest, bidResponse); - } - bidResponse.meta = { - advertiserDomains: [] - }; - - bidResponses.push(bidResponse); - } - } else {} - } else {} - } catch (e) {} - } - } else {} - return bidResponses; +function getValidSyncs(syncs, options) { + return syncs + .filter(sync => isSyncValid(sync, options)) + .map(sync => processSync(sync)) || []; } -function getSyncData(xml, options) { - let ret = []; - if (xml) { - let ext = xml.getElementsByTagName('Extensions'); - if (ext && ext.length > 0) { - ext = ext[0].getElementsByTagName('Extension'); - if (ext && ext.length > 0) { - for (var i = 0; i < ext.length; i++) { - if (ext[i].getAttribute('type') == 'ANIVIEW') { - let syncs = ext[i].getElementsByTagName('AdServingSync'); - if (syncs && syncs.length == 1) { - try { - let data = JSON.parse(syncs[0].textContent); - if (data && data.trackers && data.trackers.length) { - data = data.trackers; - for (var j = 0; j < data.length; j++) { - if (typeof data[j] === 'object' && - typeof data[j].url === 'string' && - (data[j].e === 'inventory' || data[j].e === 'sync') - ) { - if (data[j].t == 1 && options.pixelEnabled) { - ret.push({url: data[j].url, type: 'image'}); - } else { - if (data[j].t == 3 && options.iframeEnabled) { - ret.push({url: data[j].url, type: 'iframe'}); - } - } - } - } - } - } catch (e) {} - } - break; - } - } - } - } +function isSyncValid(sync, options) { + return isPlainObject(sync) && + isStr(sync.url) && + (sync.e === 'inventory' || sync.e === 'sync') && + ((sync.t === 1 && options?.pixelEnabled) || (sync.t === 3 && options?.iframeEnabled)); +} + +function processSync(sync) { + return { url: sync.url, type: sync.t === 1 ? 'image' : 'iframe' }; +} + +function getSize(mediaType, bidRequest) { + const { mediaTypes, sizes } = bidRequest; + const videoSizes = mediaTypes?.video?.playerSize; + const bannerSizes = mediaTypes?.banner?.sizes; + + let size = [640, 480]; + + if (isVideoType(mediaType) && videoSizes?.length > 0) { + size = videoSizes[0]; + } else if (isBannerType(mediaType) && bannerSizes?.length > 0) { + size = bannerSizes[0]; + } else if (sizes?.length > 0) { + size = sizes[0]; } - return ret; + + return { + width: size[0], + height: size[1], + }; } -function getUserSyncs(syncOptions, serverResponses) { - if (serverResponses && serverResponses[0] && serverResponses[0].body) { - if (serverResponses.error) { - return []; - } else { - try { - let xmlStr = serverResponses[0].body; - let xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); - if (xml && xml.getElementsByTagName('parsererror').length == 0) { - let syncData = getSyncData(xml, syncOptions); - return syncData; - } - } catch (e) {} +// https://docs.prebid.org/dev-docs/modules/floors.html#example-getfloor-scenarios +function getFloor(bidRequest, size, mediaType) { + if (!isFn(bidRequest?.getFloor)) { + return null; + } + + try { + const bidFloor = bidRequest.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType, // or '*' for all media types + size: [size.width, size.height], // or '*' for all sizes + }); + + if (isPlainObject(bidFloor) && !isNaN(bidFloor.floor) && bidFloor.currency === DEFAULT_CURRENCY) { + return bidFloor.floor; } + } catch {} + + return null; +} + +function replaceMacros(str, replacements) { + if (!replacements || !isStr(str)) { + return str; } + + return str + .replaceAll(`\${AUCTION_PRICE}`, replacements.auctionPrice || '') + .replaceAll(`\${AUCTION_ID}`, replacements.auctionId || '') + .replaceAll(`\${AUCTION_BID_ID}`, replacements.auctionBidId || '') + .replaceAll(`\${AUCTION_IMP_ID}`, replacements.auctionImpId || '') + .replaceAll(`\${AUCTION_SEAT_ID}`, replacements.auctionSeatId || '') + .replaceAll(`\${AUCTION_AD_ID}`, replacements.auctionAdId || ''); } -export const spec = { - code: BIDDER_CODE, - gvlid: GVLID, - aliases: ['avantisvideo', 'selectmediavideo', 'vidcrunch', 'openwebvideo', 'didnavideo', 'ottadvisors', 'pgammedia'], - supportedMediaTypes: [VIDEO, BANNER], - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs -}; +function createRenderer(bidRequest) { + const config = {}; + const { params = {} } = bidRequest; + const playerDomain = params.playerDomain || DEFAULT_PLAYER_DOMAIN; + + if (params.AV_PUBLISHERID) { + config.AV_PUBLISHERID = params.AV_PUBLISHERID; + } + + if (params.AV_CHANNELID) { + config.AV_CHANNELID = params.AV_CHANNELID; + } + + const renderer = Renderer.install({ + url: `https://${playerDomain}/script/6.1/${RENDERER_FILENAME}`, + config, + loaded: false, + }); + + try { + renderer.setRender(avRenderer); + } catch (error) {} + + return renderer; +} + +function avRenderer(bid) { + bid.renderer.push(function() { + const eventsCallback = bid?.renderer?.handleVideoEvent ?? null; + const { ad, adId, adUnitCode, vastUrl, vastXml, width, height, params = [] } = bid; + + window.aniviewRenderer.renderAd({ + id: adUnitCode + '_' + adId, + debug: window.location.href.indexOf('pbjsDebug') >= 0, + placement: adUnitCode, + config: params[0]?.rendererConfig, + width, + height, + vastUrl, + vastXml: vastXml || ad, + eventsCallback, + bid, + }); + }); +} registerBidder(spec); diff --git a/modules/aniviewBidAdapter.md b/modules/aniviewBidAdapter.md index 63c91ca009a..b067166b080 100644 --- a/modules/aniviewBidAdapter.md +++ b/modules/aniviewBidAdapter.md @@ -1,16 +1,16 @@ # Overview ``` -Module Name: ANIVIEW Bidder Adapter +Module Name: Aniview Bidder Adapter Module Type: Bidder Adapter Maintainer: support@aniview.com ``` # Description -Connects to ANIVIEW Ad server for bids. +Connects to Aniview Ad server for bids. -ANIVIEW bid adapter supports Banner and Video currently. +Aniview bid adapter supports Banner and Video currently. For more information about [Aniview](http://www.aniview.com), please contact [support@aniview.com](support@aniview.com). @@ -34,5 +34,3 @@ var videoAdUnit = [ }] }]; ``` - -``` diff --git a/modules/anonymisedRtdProvider.js b/modules/anonymisedRtdProvider.js index 48ac649f002..98cf81edb2a 100644 --- a/modules/anonymisedRtdProvider.js +++ b/modules/anonymisedRtdProvider.js @@ -7,12 +7,17 @@ */ import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import {isPlainObject, mergeDeep, logMessage, logError} from '../src/utils.js'; +import {isPlainObject, mergeDeep, logMessage, logWarn, logError} from '../src/utils.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; - +import {loadExternalScript} from '../src/adloader.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ export function createRtdProvider(moduleName) { const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = moduleName; + const GVLID = 1116; + const MARKETING_TAG_URL = 'https://static.anonymised.io/light/loader.js'; const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); /** @@ -39,17 +44,43 @@ export function createRtdProvider(moduleName) { return null; } } + /** + * Load the Anonymised Marketing Tag script + * @param {Object} config + */ + function tryLoadMarketingTag(config) { + const clientId = config?.params?.tagConfig?.clientId; + if (typeof clientId !== 'string' || !clientId.trim()) { + logWarn(`${SUBMODULE_NAME}RtdProvider: clientId missing or invalid; Marketing Tag not loaded.`); + return; + } + logMessage(`${SUBMODULE_NAME}RtdProvider: Loading Marketing Tag`); + // Check if the script is already loaded + if (document.querySelector(`script[src*="${config.params.tagUrl ?? MARKETING_TAG_URL}"]`)) { + logMessage(`${SUBMODULE_NAME}RtdProvider: Marketing Tag already loaded`); + return; + } + const tagConfig = config.params?.tagConfig ? {...config.params.tagConfig, idw_client_id: config.params.tagConfig.clientId} : {}; + delete tagConfig.clientId; + + const tagUrl = config.params.tagUrl ? config.params.tagUrl : `${MARKETING_TAG_URL}?ref=prebid`; + + loadExternalScript(tagUrl, MODULE_TYPE_RTD, SUBMODULE_NAME, () => { + logMessage(`${SUBMODULE_NAME}RtdProvider: Marketing Tag loaded successfully`); + }, document, tagConfig); + } + /** * Real-time data retrieval from Anonymised * @param {Object} reqBidsConfigObj * @param {function} onDone - * @param {Object} rtdConfig + * @param {Object} config * @param {Object} userConsent */ - function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent) { - if (rtdConfig && isPlainObject(rtdConfig.params)) { - const cohortStorageKey = rtdConfig.params.cohortStorageKey; - const bidders = rtdConfig.params.bidders; + function getRealTimeData(reqBidsConfigObj, onDone, config, userConsent) { + if (config && isPlainObject(config.params)) { + const cohortStorageKey = config.params.cohortStorageKey; + const bidders = config.params.bidders; if (cohortStorageKey !== 'cohort_ids') { logError(`${SUBMODULE_NAME}RtdProvider: 'cohortStorageKey' should be 'cohort_ids'`) @@ -67,7 +98,7 @@ export function createRtdProvider(moduleName) { const udSegment = { name: 'anonymised.io', ext: { - segtax: rtdConfig.params.segtax + segtax: config.params.segtax }, segment: segments.map(x => ({id: x})) } @@ -90,22 +121,23 @@ export function createRtdProvider(moduleName) { } addRealTimeData(reqBidsConfigObj.ortb2Fragments?.global, data.rtd); - onDone(); } } } /** * Module init - * @param {Object} provider + * @param {Object} config * @param {Object} userConsent * @return {boolean} */ - function init(provider, userConsent) { + function init(config, userConsent) { + tryLoadMarketingTag(config); return true; } /** @type {RtdSubmodule} */ const rtdSubmodule = { name: SUBMODULE_NAME, + gvlid: GVLID, getBidRequestData: getRealTimeData, init: init }; diff --git a/modules/anonymisedRtdProvider.md b/modules/anonymisedRtdProvider.md index 2ff2597690b..0541eeae746 100644 --- a/modules/anonymisedRtdProvider.md +++ b/modules/anonymisedRtdProvider.md @@ -8,7 +8,7 @@ Anonymised’s Real-time Data Provider automatically obtains segment IDs from th - Build the anonymisedRtd module into the Prebid.js package with: ```bash - gulp build --modules=anonymisedRtdProvider,... + gulp build --modules=rtdModule,anonymisedRtdProvider,... ``` - Use `setConfig` to instruct Prebid.js to initilaize the anonymisedRtdProvider module, as specified below. @@ -24,8 +24,12 @@ Anonymised’s Real-time Data Provider automatically obtains segment IDs from th waitForIt: true, params: { cohortStorageKey: "cohort_ids", - bidders: ["smartadserver", "appnexus"], - segtax: 1000 + bidders: ["appnexus", "onetag", "pubmatic", "smartadserver", ...], + segtax: 1000, + tagConfig: { + clientId: 'testId' + //The rest of the Anonymised Marketing Tag parameters goes here + } } } ] @@ -36,14 +40,19 @@ Anonymised’s Real-time Data Provider automatically obtains segment IDs from th ### Config Syntax details | Name |Type | Description | Notes | | :------------ | :------------ | :------------ |:------------ | -| name | String | Anonymised Rtd module name | 'anonymised' always| -| waitForIt | Boolean | Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false | -| params.cohortStorageKey | String | the `localStorage` key, under which Anonymised Marketing Tag stores the segment IDs | 'cohort_ids' always | -| params.bidders | Array | Bidders with which to share segment information | Optional | -| params.segtax | Integer | The taxonomy for Anonymised | '1000' always | - -Please note that anonymisedRtdProvider should be integrated into the publisher website along with the [Anonymised Marketing Tag](https://support.anonymised.io/integrate/marketing-tag). -Please reach out to Anonymised [representative](mailto:support@anonymised.io) if you have any questions or need further help to integrate Prebid, anonymisedRtdProvider, and Anonymised Marketing Tag +| name | `String` | Anonymised Rtd module name | 'anonymised' always| +| waitForIt | `Boolean` | Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false | +| params.cohortStorageKey | `String` | the `localStorage` key, under which Anonymised Marketing Tag stores the segment IDs | 'cohort_ids' always | +| params.bidders | `Array` | Bidders with which to share segment information | Optional | +| params.segtax | `Integer` | The taxonomy for Anonymised | '1000' always | +| params.tagConfig | `Object` | Configuration for the Anonymised Marketing Tag | Optional. Defaults to `{}`. | +| params.tagUrl | `String` | The URL of the Anonymised Marketing Tag script | Optional. Defaults to `https://static.anonymised.io/light/loader.js`. | + +The `anonymisedRtdProvider` must be integrated into the publisher's website along with the [Anonymised Marketing Tag](https://support.anonymised.io/integrate/marketing-tag?t=LPukVCXzSIcRoal5jggyeg). One way to install the Marketing Tag is through `anonymisedRtdProvider` by specifying the required [parameters](https://support.anonymised.io/integrate/optional-anonymised-tag-parameters?t=LPukVCXzSIcRoal5jggyeg) in the `tagConfig` object. + +The `tagConfig.clientId` parameter is mandatory for the Marketing Tag to initialize. If `tagConfig` is undefined or empty or `tagConfig.clientId` is undefined, the `anonymisedRtdProvider` will not initialize the Marketing Tag. The publisher's `clientId` is [provided by Anonymised](https://support.anonymised.io/integrate/install-the-anonymised-tag-natively#InstalltheAnonymisedtagnatively-Instructions?t=LPukVCXzSIcRoal5jggyeg). + +For any questions or assistance with integrating Prebid, `anonymisedRtdProvider`, or the Anonymised Marketing Tag, please contact an [Anonymised representative](mailto:support@anonymised.io). ### Testing To view an example of available segments returned by Anonymised: diff --git a/modules/anyclipBidAdapter.js b/modules/anyclipBidAdapter.js index cb9103899a4..8a5906ebc93 100644 --- a/modules/anyclipBidAdapter.js +++ b/modules/anyclipBidAdapter.js @@ -1,213 +1,54 @@ +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; -import {deepAccess, isArray, isFn, logError, logInfo} from '../src/utils.js'; -import {config} from '../src/config.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').BidderSpec} BidderSpec - */ +import { + buildRequests, + getUserSyncs, + interpretResponse, +} from '../libraries/xeUtils/bidderUtils.js'; +import {deepAccess, getBidIdParameter, isArray, logError} from '../src/utils.js'; const BIDDER_CODE = 'anyclip'; -const ENDPOINT_URL = 'https://prebid.anyclip.com'; -const DEFAULT_CURRENCY = 'USD'; -const NET_REVENUE = false; +const ENDPOINT = 'https://prebid.anyclip.com'; + +function isBidRequestValid(bid) { + if (bid && typeof bid.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } -/* - * Get the bid floor value from the bidRequest object, either using the getFloor function or by accessing the 'params.floor' property. - * If the bid floor cannot be determined, return 0 as a fallback value. - */ -function getBidFloor(bidRequest) { - if (!isFn(bidRequest.getFloor)) { - return deepAccess(bidRequest, 'params.floor', 0); + if (!getBidIdParameter('publisherId', bid.params) || !getBidIdParameter('supplyTagId', bid.params)) { + logError('PublisherId or supplyTagId is not present in bidder params'); + return false; } - try { - const bidFloor = bidRequest.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; + if (deepAccess(bid, 'mediaTypes.video') && !isArray(deepAccess(bid, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; } + + return true; } -/** @type {BidderSpec} */ export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], - - /** - * @param {object} bid - * @return {boolean} - */ - isBidRequestValid: (bid = {}) => { - const bidder = deepAccess(bid, 'bidder'); - const params = deepAccess(bid, 'params', {}); - const mediaTypes = deepAccess(bid, 'mediaTypes', {}); - const banner = deepAccess(mediaTypes, BANNER, {}); - - const isValidBidder = (bidder === BIDDER_CODE); - const isValidSize = (Boolean(banner.sizes) && isArray(mediaTypes[BANNER].sizes) && mediaTypes[BANNER].sizes.length > 0); - const hasSizes = mediaTypes[BANNER] ? isValidSize : false; - const hasRequiredBidParams = Boolean(params.publisherId && params.supplyTagId); - - const isValid = isValidBidder && hasSizes && hasRequiredBidParams; - if (!isValid) { - logError(`Invalid bid request: isValidBidder: ${isValidBidder}, hasSizes: ${hasSizes}, hasRequiredBidParams: ${hasRequiredBidParams}`); - } - return isValid; - }, - - /** - * @param {BidRequest[]} validBidRequests - * @param {*} bidderRequest - * @return {ServerRequest} - */ + aliases: ['anyclip'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, buildRequests: (validBidRequests, bidderRequest) => { - const bidRequest = validBidRequests[0]; - - let refererInfo; - if (bidderRequest && bidderRequest.refererInfo) { - refererInfo = bidderRequest.refererInfo; - } - - const timeout = bidderRequest.timeout; - const timeoutAdjustment = timeout - ((20 / 100) * timeout); // timeout adjustment - 20% - - if (isPubTagAvailable()) { - // Options - const options = { - publisherId: bidRequest.params.publisherId, - supplyTagId: bidRequest.params.supplyTagId, - url: refererInfo.page, - domain: refererInfo.domain, - prebidTimeout: timeoutAdjustment, - gpid: bidRequest.adUnitCode, - ext: { - transactionId: bidRequest.transactionId - }, - sizes: bidRequest.sizes.map((size) => { - return {width: size[0], height: size[1]} - }) - } - // Floor - const floor = parseFloat(getBidFloor(bidRequest)); - if (!isNaN(floor)) { - options.ext.floor = floor; - } - // Supply Chain (Schain) - if (bidRequest?.schain) { - options.schain = bidRequest.schain - } - // GDPR & Consent (EU) - if (bidderRequest?.gdprConsent) { - options.gdpr = (bidderRequest.gdprConsent.gdprApplies ? 1 : 0); - options.consent = bidderRequest.gdprConsent.consentString; - } - // GPP - if (bidderRequest?.gppConsent?.gppString) { - options.gpp = { - gppVersion: bidderRequest.gppConsent.gppVersion, - sectionList: bidderRequest.gppConsent.sectionList, - applicableSections: bidderRequest.gppConsent.applicableSections, - gppString: bidderRequest.gppConsent.gppString - } - } - // CCPA (US Privacy) - if (bidderRequest?.uspConsent) { - options.usPrivacy = bidderRequest.uspConsent; - } - // COPPA - if (config.getConfig('coppa') === true) { - options.coppa = 1; - } - // Eids - if (bidRequest?.userIdAsEids) { - const eids = bidRequest.userIdAsEids; - if (eids && eids.length) { - options.eids = eids; - } - } - - // Request bids - const requestBidsPromise = window._anyclip.pubTag.requestBids(options); - if (requestBidsPromise !== undefined) { - requestBidsPromise - .then(() => { - logInfo('PubTag requestBids done'); - }) - .catch((err) => { - logError('PubTag requestBids error', err); - }); - } - - // Request - const payload = { - tmax: timeoutAdjustment - } - - return { - method: 'GET', - url: ENDPOINT_URL, - data: payload, - bidRequest - } - } + const builtRequests = buildRequests(validBidRequests, bidderRequest, ENDPOINT) + const requests = JSON.parse(builtRequests.data) + const updatedRequests = requests.map(req => ({ + ...req, + env: { + publisherId: validBidRequests[0].params.publisherId, + supplyTagId: validBidRequests[0].params.supplyTagId, + floor: req.floor + }, + })) + return {...builtRequests, data: JSON.stringify(updatedRequests)} }, - - /** - * @param {*} serverResponse - * @param {ServerRequest} bidRequest - * @return {Bid[]} - */ - interpretResponse: (serverResponse, { bidRequest }) => { - const bids = []; - - if (bidRequest && isPubTagAvailable()) { - const bidResponse = window._anyclip.pubTag.getBids(bidRequest.transactionId); - if (bidResponse) { - const { adServer } = bidResponse; - if (adServer) { - bids.push({ - requestId: bidRequest.bidId, - creativeId: adServer.bid.creativeId, - cpm: bidResponse.cpm, - width: adServer.bid.width, - height: adServer.bid.height, - currency: adServer.bid.currency || DEFAULT_CURRENCY, - netRevenue: NET_REVENUE, - ttl: adServer.bid.ttl, - ad: adServer.bid.ad, - meta: adServer.bid.meta - }); - } - } - } - - return bids; - }, - - /** - * @param {Bid} bid - */ - onBidWon: (bid) => { - if (isPubTagAvailable()) { - window._anyclip.pubTag.bidWon(bid); - } - } -} - -/** - * @return {boolean} - */ -const isPubTagAvailable = () => { - return !!(window._anyclip && window._anyclip.pubTag); + interpretResponse, + getUserSyncs } registerBidder(spec); diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index dadbdb72e95..83119052f3a 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -1,7 +1,7 @@ -import { deepAccess, isPlainObject, isArray, replaceAuctionPrice, isFn, logError } from '../src/utils.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'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; import {parseDomain} from '../src/refererDetection.js'; const BIDDER_CODE = 'apacdex'; const ENDPOINT = 'https://useast.quantumdex.io/auction/pbjs' @@ -85,7 +85,7 @@ export const spec = { bidReq.bidFloor = bidFloor; } - bids.push(JSON.parse(JSON.stringify(bidReq))); + bids.push(deepClone(bidReq)); }); const payload = {}; diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 551bf5401e2..2074f1745f2 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -15,7 +15,8 @@ import { logError, logInfo, logMessage, - logWarn + logWarn, + mergeDeep } from '../src/utils.js'; import {Renderer} from '../src/Renderer.js'; import {config} from '../src/config.js'; @@ -25,16 +26,15 @@ import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {getStorageManager} from '../src/storageManager.js'; import {bidderSettings} from '../src/bidderSettings.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js'; import { convertKeywordStringToANMap, getANKewyordParamFromMaps, - getANKeywordParam, - transformBidderParamKeywords + getANKeywordParam } from '../libraries/appnexusUtils/anKeywords.js'; -import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertCamelToUnderscore, fill, appnexusAliases} from '../libraries/appnexusUtils/anUtils.js'; import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; import {chunk} from '../libraries/chunk/chunk.js'; @@ -104,25 +104,33 @@ const VIEWABILITY_URL_START = /\/\/cdn\.adnxs\.com\/v|\/\/cdn\.adnxs\-simple\.co const VIEWABILITY_FILE_NAME = 'trk.js'; const GVLID = 32; const storage = getStorageManager({bidderCode: BIDDER_CODE}); +// ORTB2 device types according to the OpenRTB specification +const ORTB2_DEVICE_TYPE = { + MOBILE_TABLET: 1, + PERSONAL_COMPUTER: 2, + CONNECTED_TV: 3, + PHONE: 4, + TABLET: 5, + CONNECTED_DEVICE: 6, + SET_TOP_BOX: 7, + OOH_DEVICE: 8 +}; +// Map of ORTB2 device types to AppNexus device types +const ORTB2_DEVICE_TYPE_MAP = new Map([ + [ORTB2_DEVICE_TYPE.MOBILE_TABLET, 'Mobile/Tablet - General'], + [ORTB2_DEVICE_TYPE.PERSONAL_COMPUTER, 'Personal Computer'], + [ORTB2_DEVICE_TYPE.CONNECTED_TV, 'Connected TV'], + [ORTB2_DEVICE_TYPE.PHONE, 'Phone'], + [ORTB2_DEVICE_TYPE.TABLET, 'Tablet'], + [ORTB2_DEVICE_TYPE.CONNECTED_DEVICE, 'Connected Device'], + [ORTB2_DEVICE_TYPE.SET_TOP_BOX, 'Set Top Box'], + [ORTB2_DEVICE_TYPE.OOH_DEVICE, 'OOH Device'], +]); export const spec = { code: BIDDER_CODE, gvlid: GVLID, - aliases: [ - { code: 'appnexusAst', gvlid: 32 }, - { code: 'emxdigital', gvlid: 183 }, - { code: 'emetriq', gvlid: 213 }, - { code: 'pagescience', gvlid: 32 }, - { code: 'gourmetads', gvlid: 32 }, - { code: 'matomy', gvlid: 32 }, - { code: 'featureforward', gvlid: 32 }, - { code: 'oftmedia', gvlid: 32 }, - { code: 'adasta', gvlid: 32 }, - { code: 'beintoo', gvlid: 618 }, - { code: 'projectagora', gvlid: 1032 }, - { code: 'uol', gvlid: 32 }, - { code: 'adzymic', gvlid: 32 }, - ], + aliases: appnexusAliases, supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** @@ -262,6 +270,12 @@ export const spec = { payload.app = appIdObj; } + // if present, convert and merge device object from ortb2 into `payload.device` + if (bidderRequest?.ortb2?.device) { + payload.device = payload.device || {}; + mergeDeep(payload.device, convertORTB2DeviceDataToAppNexusDeviceObject(bidderRequest.ortb2.device)); + } + // grab the ortb2 keyword data (if it exists) and convert from the comma list string format to object format let ortb2 = deepClone(bidderRequest && bidderRequest.ortb2); @@ -340,18 +354,23 @@ export const spec = { if (bidRequests[0].userId) { let eids = []; - bidRequests[0].userIdAsEids.forEach(eid => { - if (!eid || !eid.uids || eid.uids.length < 1) { return; } - eid.uids.forEach(uid => { - let tmp = {'source': eid.source, 'id': uid.id}; - if (eid.source == 'adserver.org') { - tmp.rti_partner = 'TDID'; - } else if (eid.source == 'uidapi.com') { - tmp.rti_partner = 'UID2'; - } - eids.push(tmp); + const processEids = (uids) => { + uids.forEach(eid => { + if (!eid || !eid.uids || eid.uids.length < 1) { return; } + eid.uids.forEach(uid => { + let tmp = {'source': eid.source, 'id': uid.id}; + if (eid.source == 'adserver.org') { + tmp.rti_partner = 'TDID'; + } else if (eid.source == 'uidapi.com') { + tmp.rti_partner = 'UID2'; + } + eids.push(tmp); + }); }); - }); + } + if (bidRequests[0].userIdAsEids) { + processEids(bidRequests[0].userIdAsEids); + } if (eids.length) { payload.eids = eids; } @@ -438,62 +457,21 @@ export const spec = { }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { - function checkGppStatus(gppConsent) { - // user sync suppression for adapters is handled in activity controls and not needed in adapters - return true; - } - - if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent) && checkGppStatus(gppConsent)) { + if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent)) { return [{ type: 'iframe', url: 'https://acdn.adnxs.com/dmp/async_usersync.html' }]; } - }, - transformBidParams: function (params, isOpenRtb, adUnit, bidRequests) { - let conversionFn = transformBidderParamKeywords; - if (isOpenRtb === true) { - let s2sEndpointUrl = null; - let s2sConfig = config.getConfig('s2sConfig'); - - if (isPlainObject(s2sConfig)) { - s2sEndpointUrl = deepAccess(s2sConfig, 'endpoint.p1Consent'); - } else if (isArray(s2sConfig)) { - s2sConfig.forEach(s2sCfg => { - if (includes(s2sCfg.bidders, adUnit.bids[0].bidder)) { - s2sEndpointUrl = deepAccess(s2sCfg, 'endpoint.p1Consent'); - } - }); - } - - if (s2sEndpointUrl && s2sEndpointUrl.match('/openrtb2/prebid')) { - conversionFn = convertKeywordsToString; - } + 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 + })); } - - params = convertTypes({ - 'member': 'string', - 'invCode': 'string', - 'placementId': 'number', - 'keywords': conversionFn, - 'publisherId': 'number' - }, params); - - if (isOpenRtb) { - Object.keys(params).forEach(paramKey => { - let convertedKey = convertCamelToUnderscore(paramKey); - if (convertedKey !== paramKey) { - params[convertedKey] = params[paramKey]; - delete params[paramKey]; - } - }); - - params.use_pmt_rule = (typeof params.use_payment_rule === 'boolean') ? params.use_payment_rule : false; - if (params.use_payment_rule) { delete params.use_payment_rule; } - } - - return params; } }; @@ -721,19 +699,124 @@ function newBid(serverBid, rtbBid, bidderRequest) { javascriptTrackers: jsTrackers }; if (nativeAd.main_img) { - bid['native'].image = { + bid[NATIVE].image = { url: nativeAd.main_img.url, height: nativeAd.main_img.height, width: nativeAd.main_img.width, }; } if (nativeAd.icon) { - bid['native'].icon = { + bid[NATIVE].icon = { url: nativeAd.icon.url, height: nativeAd.icon.height, width: nativeAd.icon.width, }; } + + // Custom fields + bid[NATIVE].ext = { + video: nativeAd.video, + customImage1: nativeAd.image1 && { + url: nativeAd.image1.url, + height: nativeAd.image1.height, + width: nativeAd.image1.width, + }, + customImage2: nativeAd.image2 && { + url: nativeAd.image2.url, + height: nativeAd.image2.height, + width: nativeAd.image2.width, + }, + customImage3: nativeAd.image3 && { + url: nativeAd.image3.url, + height: nativeAd.image3.height, + width: nativeAd.image3.width, + }, + customImage4: nativeAd.image4 && { + url: nativeAd.image4.url, + height: nativeAd.image4.height, + width: nativeAd.image4.width, + }, + customImage5: nativeAd.image5 && { + url: nativeAd.image5.url, + height: nativeAd.image5.height, + width: nativeAd.image5.width, + }, + customIcon1: nativeAd.icon1 && { + url: nativeAd.icon1.url, + height: nativeAd.icon1.height, + width: nativeAd.icon1.width, + }, + customIcon2: nativeAd.icon2 && { + url: nativeAd.icon2.url, + height: nativeAd.icon2.height, + width: nativeAd.icon2.width, + }, + customIcon3: nativeAd.icon3 && { + url: nativeAd.icon3.url, + height: nativeAd.icon3.height, + width: nativeAd.icon3.width, + }, + customIcon4: nativeAd.icon4 && { + url: nativeAd.icon4.url, + height: nativeAd.icon4.height, + width: nativeAd.icon4.width, + }, + customIcon5: nativeAd.icon5 && { + url: nativeAd.icon5.url, + height: nativeAd.icon5.height, + width: nativeAd.icon5.width, + }, + customSocialIcon1: nativeAd.socialicon1 && { + url: nativeAd.socialicon1.url, + height: nativeAd.socialicon1.height, + width: nativeAd.socialicon1.width, + }, + customSocialIcon2: nativeAd.socialicon2 && { + url: nativeAd.socialicon2.url, + height: nativeAd.socialicon2.height, + width: nativeAd.socialicon2.width, + }, + customSocialIcon3: nativeAd.socialicon3 && { + url: nativeAd.socialicon3.url, + height: nativeAd.socialicon3.height, + width: nativeAd.socialicon3.width, + }, + customSocialIcon4: nativeAd.socialicon4 && { + url: nativeAd.socialicon4.url, + height: nativeAd.socialicon4.height, + width: nativeAd.socialicon4.width, + }, + customSocialIcon5: nativeAd.socialicon5 && { + url: nativeAd.socialicon5.url, + height: nativeAd.socialicon5.height, + width: nativeAd.socialicon5.width, + }, + customTitle1: nativeAd.title1, + customTitle2: nativeAd.title2, + customTitle3: nativeAd.title3, + customTitle4: nativeAd.title4, + customTitle5: nativeAd.title5, + customBody1: nativeAd.body1, + customBody2: nativeAd.body2, + customBody3: nativeAd.body3, + customBody4: nativeAd.body4, + customBody5: nativeAd.body5, + customCta1: nativeAd.ctatext1, + customCta2: nativeAd.ctatext2, + customCta3: nativeAd.ctatext3, + customCta4: nativeAd.ctatext4, + customCta5: nativeAd.ctatext5, + customDisplayUrl1: nativeAd.displayurl1, + customDisplayUrl2: nativeAd.displayurl2, + customDisplayUrl3: nativeAd.displayurl3, + customDisplayUrl4: nativeAd.displayurl4, + customDisplayUrl5: nativeAd.displayurl5, + customSocialUrl1: nativeAd.socialurl1, + customSocialUrl2: nativeAd.socialurl2, + customSocialUrl3: nativeAd.socialurl3, + customSocialUrl4: nativeAd.socialurl4, + customSocialUrl5: nativeAd.socialurl5 + }; } else { Object.assign(bid, { width: rtbBid.rtb.banner.width, @@ -778,7 +861,7 @@ function bidToTag(bid) { // page.html?ast_override_div=divId:creativeId,divId2:creativeId2 const overrides = getParameterByName('ast_override_div'); if (isStr(overrides) && overrides !== '') { - const adUnitOverride = overrides.split(',').find((pair) => pair.startsWith(`${bid.adUnitCode}:`)); + const adUnitOverride = decodeURIComponent(overrides).split(',').find((pair) => pair.startsWith(`${bid.adUnitCode}:`)); if (adUnitOverride) { const forceCreativeId = adUnitOverride.split(':')[1]; if (forceCreativeId) { @@ -1011,7 +1094,7 @@ function getContextFromPlacement(ortbPlacement) { } function getContextFromStartDelay(ortbStartDelay) { - if (!ortbStartDelay) { + if (typeof ortbStartDelay === 'undefined') { return; } @@ -1256,31 +1339,29 @@ function getBidFloor(bid) { return null; } -// keywords: { 'genre': ['rock', 'pop'], 'pets': ['dog'] } goes to 'genre=rock,genre=pop,pets=dog' -function convertKeywordsToString(keywords) { - let result = ''; - Object.keys(keywords).forEach(key => { - // if 'text' or '' - if (isStr(keywords[key])) { - if (keywords[key] !== '') { - result += `${key}=${keywords[key]},` - } else { - result += `${key},`; - } - } else if (isArray(keywords[key])) { - if (keywords[key][0] === '') { - result += `${key},` - } else { - keywords[key].forEach(val => { - result += `${key}=${val},` - }); - } - } - }); +// Convert device data to a format that AppNexus expects +function convertORTB2DeviceDataToAppNexusDeviceObject(ortb2DeviceData) { + const _device = { + useragent: ortb2DeviceData.ua, + devicetype: ORTB2_DEVICE_TYPE_MAP.get(ortb2DeviceData.devicetype), + make: ortb2DeviceData.make, + model: ortb2DeviceData.model, + os: ortb2DeviceData.os, + os_version: ortb2DeviceData.osv, + w: ortb2DeviceData.w, + h: ortb2DeviceData.h, + ppi: ortb2DeviceData.ppi, + pxratio: ortb2DeviceData.pxratio, + }; - // remove last trailing comma - result = result.substring(0, result.length - 1); - return result; + // filter out any empty values and return the object + return Object.keys(_device) + .reduce((r, key) => { + if (_device[key]) { + r[key] = _device[key]; + } + return r; + }, {}); } registerBidder(spec); diff --git a/modules/appushBidAdapter.js b/modules/appushBidAdapter.js index 97772b65e45..ec742120582 100644 --- a/modules/appushBidAdapter.js +++ b/modules/appushBidAdapter.js @@ -57,6 +57,7 @@ function getPlacementReqData(bid) { placement.protocols = mediaTypes[VIDEO].protocols; placement.startdelay = mediaTypes[VIDEO].startdelay; placement.placement = mediaTypes[VIDEO].placement; + placement.plcmt = mediaTypes[VIDEO].plcmt; placement.skip = mediaTypes[VIDEO].skip; placement.skipafter = mediaTypes[VIDEO].skipafter; placement.minbitrate = mediaTypes[VIDEO].minbitrate; @@ -84,7 +85,7 @@ function getBidFloor(bid) { mediaType: '*', size: '*', }); - return bidFloor.floor; + return bidFloor?.floor; } catch (err) { logError(err); return 0; diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index 2856fb02087..37e2bde44c1 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -292,7 +292,6 @@ function getConsentStringFromPrebid(gdprConsentConfig) { return null; } - let isIab = config.getConfig('consentManagement.cmpApi') != 'static'; let vendorConsents = ( gdprConsentConfig.vendorData.vendorConsents || (gdprConsentConfig.vendorData.vendor || {}).consents || @@ -300,7 +299,7 @@ function getConsentStringFromPrebid(gdprConsentConfig) { ); let isConsentGiven = !!vendorConsents[CONSTANTS.GVLID.toString(10)]; - return isIab && isConsentGiven ? consentString : null; + return isConsentGiven ? consentString : null; } function getIabConsentString(bidderRequest) { diff --git a/modules/arcspanRtdProvider.js b/modules/arcspanRtdProvider.js index 8ccf3d160b9..3a9e9b175d8 100644 --- a/modules/arcspanRtdProvider.js +++ b/modules/arcspanRtdProvider.js @@ -1,6 +1,7 @@ import { submodule } from '../src/hook.js'; import { mergeDeep } from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -28,7 +29,7 @@ function init(config, userConsent) { } else { scriptUrl = 'https://silo' + config.params.silo + '.p7cloud.net/as.js'; } - loadExternalScript(scriptUrl, SUBMODULE_NAME); + loadExternalScript(scriptUrl, MODULE_TYPE_RTD, SUBMODULE_NAME); } return true; } diff --git a/modules/asoBidAdapter.js b/modules/asoBidAdapter.js index a4a6c78566e..388eee86fe2 100644 --- a/modules/asoBidAdapter.js +++ b/modules/asoBidAdapter.js @@ -17,7 +17,9 @@ export const spec = { supportedMediaTypes: [BANNER, VIDEO, NATIVE], aliases: [ {code: 'bcmint'}, - {code: 'bidgency'} + {code: 'bidgency'}, + {code: 'kuantyx'}, + {code: 'cordless'} ], isBidRequestValid: bid => { @@ -106,7 +108,7 @@ const converter = ortbConverter({ const imp = buildImp(bidRequest, context); imp.tagid = bidRequest.adUnitCode; - imp.secure = Number(window.location.protocol === 'https:'); + imp.secure = bidRequest.ortb2Imp?.secure ?? 1; return imp; }, diff --git a/modules/asteriobidAnalyticsAdapter.js b/modules/asteriobidAnalyticsAdapter.js index d5b6c0b4cf7..615293e2641 100644 --- a/modules/asteriobidAnalyticsAdapter.js +++ b/modules/asteriobidAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { generateUUID, getParameterByName, logError, logInfo, parseUrl } from '../src/utils.js' +import { generateUUID, getParameterByName, logError, logInfo, parseUrl, deepClone, hasNonSerializableProperty } from '../src/utils.js' import { ajaxBuilder } from '../src/ajax.js' import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js' import adapterManager from '../src/adapterManager.js' @@ -192,10 +192,10 @@ function handleEvent(eventType, eventArgs) { return } - try { - eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {} - } catch (e) { - // keep eventArgs as is + if (eventArgs) { + eventArgs = hasNonSerializableProperty(eventArgs) ? eventArgs : deepClone(eventArgs) + } else { + eventArgs = {} } const pmEvent = {} diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index d94e68b7e55..eaeead6a249 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -325,7 +325,7 @@ atsAnalyticsAdapter.getUserAgent = function () { atsAnalyticsAdapter.setSamplingCookie = function (samplRate) { const now = new Date(); - now.setTime(now.getTime() + 86400000); + now.setTime(now.getTime() + 604800000); storage.setCookie('_lr_sampling_rate', samplRate, now.toUTCString()); } diff --git a/modules/audiencerunBidAdapter.js b/modules/audiencerunBidAdapter.js index 92a4343b3ed..df3bbda6a53 100644 --- a/modules/audiencerunBidAdapter.js +++ b/modules/audiencerunBidAdapter.js @@ -1,3 +1,7 @@ +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; import { _each, deepAccess, @@ -8,9 +12,6 @@ import { logError, triggerPixel, } from '../src/utils.js'; -import {config} from '../src/config.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -47,7 +48,7 @@ function getBidFloor(bid) { mediaType: BANNER, size: '*', }); - return bidFloor.floor; + return bidFloor?.floor; } catch (_) { return 0; } @@ -136,7 +137,7 @@ export const spec = { referer: deepAccess(bidderRequest, 'refererInfo.topmostLocation'), // TODO: please do not send internal data structures over the network refererInfo: deepAccess(bidderRequest, 'refererInfo.legacy'), - currencyCode: config.getConfig('currency.adServerCurrency'), + currencyCode: getCurrencyFromBidderRequest(bidderRequest), timeout: config.getConfig('bidderTimeout'), bids, }; diff --git a/modules/automatadAnalyticsAdapter.js b/modules/automatadAnalyticsAdapter.js index 523e8d558ca..435041cf630 100644 --- a/modules/automatadAnalyticsAdapter.js +++ b/modules/automatadAnalyticsAdapter.js @@ -18,6 +18,8 @@ var isLoggingEnabled; var queuePointer = 0; var retryCount = 0; var timer = null const prettyLog = (level, text, isGroup = false, cb = () => {}) => { if (self.isLoggingEnabled === undefined) { + // TODO FIX THIS RULES VIOLATION + // eslint-disable-next-line no-restricted-properties if (window.localStorage.getItem('__aggLoggingEnabled')) { self.isLoggingEnabled = true } else { @@ -301,7 +303,7 @@ atmtdAdapter.enableAnalytics = function (configuration) { logMessage(`Automatad Analytics Adapter enabled with sdk config`, window.__atmtdSDKConfig) - // eslint-disable-next-line + atmtdAdapter.originEnableAnalytics(configuration) }; diff --git a/modules/axisBidAdapter.js b/modules/axisBidAdapter.js index 8d7f2dd04fd..c2ad40b2b94 100644 --- a/modules/axisBidAdapter.js +++ b/modules/axisBidAdapter.js @@ -1,189 +1,53 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - +import { deepAccess } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; +import { + isBidRequestValid, + buildRequestsBase, + interpretResponse, + buildPlacementProcessingFunction, +} from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'axis'; const AD_URL = 'https://prebid.axis-marketplace.com/pbjs'; const SYNC_URL = 'https://cs.axis-marketplace.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { integration, token } = params; - const bidfloor = getBidFloor(bid); +const addPlacementType = (bid, bidderRequest, placement) => { + placement.integration = bid.params.integration; + placement.token = bid.params.token; +}; - const placement = { - integration, - token, - bidId, - schain, - bidfloor - }; +const addCustomFieldsToPlacement = (bid, bidderRequest, placement) => { + const { mediaTypes } = bid; - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; + if (placement.adFormat === BANNER) { placement.pos = mediaTypes[BANNER].pos; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; + } else if (placement.adFormat === VIDEO) { placement.pos = mediaTypes[VIDEO].pos; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; placement.context = mediaTypes[VIDEO].context; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; } +}; - return placement; -} +const placementProcessingFunction = buildPlacementProcessingFunction({ addPlacementType, addCustomFieldsToPlacement }); -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + const request = buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest, placementProcessingFunction }); - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (e) { - logError(e); - return 0; - } -} + request.data.iabCat = deepAccess(bidderRequest, 'ortb2.site.cat'); + + return request; +}; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && params.integration && params.token); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + isBidRequestValid: isBidRequestValid(['integration', 'token'], 'every'), + buildRequests, + interpretResponse, - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - iabCat: deepAccess(bidderRequest, 'ortb2.site.cat'), - coppa: deepAccess(bidderRequest, 'ortb2.regs.coppa') ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout || 3000, - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; if (gdprConsent && gdprConsent.consentString) { @@ -197,6 +61,11 @@ export const spec = { syncUrl += `&ccpa=${uspConsent.consentString}`; } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + syncUrl += '&gpp=' + gppConsent.gppString; + syncUrl += '&gpp_sid=' + gppConsent.applicableSections.join(','); + } + const coppa = config.getConfig('coppa') ? 1 : 0; syncUrl += `&coppa=${coppa}`; @@ -205,6 +74,6 @@ export const spec = { url: syncUrl }]; } -}; +} registerBidder(spec); diff --git a/modules/axonixBidAdapter.js b/modules/axonixBidAdapter.js index 87c3aff444a..2eefb617636 100644 --- a/modules/axonixBidAdapter.js +++ b/modules/axonixBidAdapter.js @@ -21,7 +21,7 @@ function getBidFloor(bidRequest) { }); } - return floorInfo.floor || 0; + return floorInfo?.floor || 0; } function getPageUrl(bidRequest, bidderRequest) { @@ -120,7 +120,7 @@ export const spec = { prebidVersion: '$prebid.version$', screenHeight: screen.height, screenWidth: screen.width, - tmax: config.getConfig('bidderTimeout'), + tmax: bidderRequest.timeout, ua: navigator.userAgent, }; diff --git a/modules/azerionedgeRtdProvider.js b/modules/azerionedgeRtdProvider.js index a162ce074aa..7e8a32e6f93 100644 --- a/modules/azerionedgeRtdProvider.js +++ b/modules/azerionedgeRtdProvider.js @@ -19,6 +19,8 @@ const REAL_TIME_MODULE = 'realTimeData'; const SUBREAL_TIME_MODULE = 'azerionedge'; export const STORAGE_KEY = 'ht-pa-v1-a'; +const IMPROVEDIGITAL_GVLID = '253'; + export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBREAL_TIME_MODULE, @@ -42,14 +44,21 @@ function getScriptURL(config) { * Attach script tag to DOM * * @param {Object} config + * @param {Object} userConsent * * @return {void} */ -export function attachScript(config) { +export function attachScript(config, userConsent) { const script = getScriptURL(config); - loadExternalScript(script, SUBREAL_TIME_MODULE, () => { + loadExternalScript(script, MODULE_TYPE_RTD, SUBREAL_TIME_MODULE, () => { if (typeof window.azerionPublisherAudiences === 'function') { - window.azerionPublisherAudiences(config.params?.process || {}); + const publisherConfig = config.params?.process || {}; + window.azerionPublisherAudiences({ + ...publisherConfig, + gdprApplies: userConsent?.gdpr?.gdprApplies, + gdprConsent: userConsent?.gdpr?.consentString, + uspConsent: userConsent?.usp, + }); } }); } @@ -106,7 +115,7 @@ export function setAudiencesToBidders(reqBidsConfigObj, config, audiences) { * @return {boolean} */ function init(config, userConsent) { - attachScript(config); + attachScript(config, userConsent); return true; } @@ -138,6 +147,7 @@ export const azerionedgeSubmodule = { name: SUBREAL_TIME_MODULE, init: init, getBidRequestData: getBidRequestData, + gvlid: IMPROVEDIGITAL_GVLID, }; submodule(REAL_TIME_MODULE, azerionedgeSubmodule); diff --git a/modules/azerionedgeRtdProvider.md b/modules/azerionedgeRtdProvider.md index 6658907c480..e1bdf792647 100644 --- a/modules/azerionedgeRtdProvider.md +++ b/modules/azerionedgeRtdProvider.md @@ -79,23 +79,6 @@ provided to the module when the user gives the relevant permissions on the publi As Prebid.js utilizes TCF vendor consent for the RTD module to load, the module needs to be labeled within the Vendor Exceptions. -### Instructions - -If the Prebid GDPR enforcement is enabled, the module should be labeled -as exception, as shown below: - -```js -[ - { - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: ["azerionedge"] - }, - ... -] -``` - ## Testing To view an example: diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 658fc30b43b..d05769de010 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -4,18 +4,16 @@ import { deepSetValue, getUniqueIdentifierStr, isArray, - isFn, logWarn, - parseSizesInput, - parseUrl, formatQS } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {find, includes} from '../src/polyfill.js'; +import {find} from '../src/polyfill.js'; +import { getFirstSize, getOsVersion, getVideoSizes, getBannerSizes, isConnectedTV, getDoNotTrack, isMobile, isBannerBid, isVideoBid, getBannerBidFloor, getVideoBidFloor, getVideoTargetingParams, getTopWindowLocation } from '../libraries/advangUtils/index.js'; -const ADAPTER_VERSION = '1.20'; +const ADAPTER_VERSION = '1.21'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; const CURRENCY = 'USD'; @@ -26,7 +24,7 @@ export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/l export const SYNC_IFRAME_ENDPOINT = 'https://sync.bfmio.com/sync_iframe'; export const SYNC_IMAGE_ENDPOINT = 'https://sync.bfmio.com/syncb'; -export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'skip', 'skipmin', 'skipafter']; +export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'plcmt', 'skip', 'skipmin', 'skipafter']; export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; export const SUPPORTED_USER_IDS = [ @@ -222,69 +220,6 @@ function createRenderer(bidRequest) { return renderer; } -function getFirstSize(sizes) { - return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; -} - -function parseSizes(sizes) { - return parseSizesInput(sizes).map(size => { - let [ width, height ] = size.split('x'); - return { - w: parseInt(width, 10) || undefined, - h: parseInt(height, 10) || undefined - }; - }); -} - -function getVideoSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); -} - -function getBannerSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); -} - -function getOsVersion() { - let clientStrings = [ - { s: 'Android', r: /Android/ }, - { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, - { s: 'Mac OS X', r: /Mac OS X/ }, - { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, - { s: 'Linux', r: /(Linux|X11)/ }, - { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, - { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, - { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, - { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, - { s: 'Windows Vista', r: /Windows NT 6.0/ }, - { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, - { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, - { s: 'UNIX', r: /UNIX/ }, - { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } - ]; - let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); - return cs ? cs.s : 'unknown'; -} - -function isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); -} - -function isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); -} - -function getDoNotTrack() { - return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; -} - -function isVideoBid(bid) { - return deepAccess(bid, 'mediaTypes.video'); -} - -function isBannerBid(bid) { - return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); -} - function getVideoBidParam(bid, key) { return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); } @@ -298,16 +233,6 @@ function getPlayerBidParam(bid, key, defaultValue) { return param === undefined ? defaultValue : param; } -function getBannerBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: CURRENCY, mediaType: 'banner', size: '*' }) : {}; - return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); -} - -function getVideoBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: CURRENCY, mediaType: 'video', size: '*' }) : {}; - return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); -} - function isVideoBidValid(bid) { return isVideoBid(bid) && getVideoBidParam(bid, 'appId') && getVideoBidParam(bid, 'bidfloor'); } @@ -316,10 +241,6 @@ function isBannerBidValid(bid) { return isBannerBid(bid) && getBannerBidParam(bid, 'appId') && getBannerBidParam(bid, 'bidfloor'); } -function getTopWindowLocation(bidderRequest) { - return parseUrl(bidderRequest?.refererInfo?.page, { decodeSearchAsString: true }); -} - function getEids(bid) { return SUPPORTED_USER_IDS .map(getUserId(bid)) @@ -347,26 +268,10 @@ function formatEid(id, source, rtiPartner, atype) { }; } -function getVideoTargetingParams(bid) { - const result = {}; - const excludeProps = ['playerSize', 'context', 'w', 'h']; - Object.keys(Object(bid.mediaTypes.video)) - .filter(key => !includes(excludeProps, key)) - .forEach(key => { - result[ key ] = bid.mediaTypes.video[ key ]; - }); - Object.keys(Object(bid.params.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.params.video[ key ]; - }); - return result; -} - function createVideoRequestData(bid, bidderRequest) { let sizes = getVideoSizes(bid); let firstSize = getFirstSize(sizes); - let video = getVideoTargetingParams(bid); + let video = getVideoTargetingParams(bid, VIDEO_TARGETING); let appId = getVideoBidParam(bid, 'appId'); let bidfloor = getVideoBidFloor(bid); let tagid = getVideoBidParam(bid, 'tagid'); diff --git a/modules/beopBidAdapter.js b/modules/beopBidAdapter.js index 0b2a965448b..e58cf0f1708 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -1,28 +1,33 @@ +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { getAllOrtbKeywords } from '../libraries/keywords/keywords.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getRefererInfo } from '../src/refererDetection.js'; import { buildUrl, - deepAccess, getBidIdParameter, + deepAccess, generateUUID, getBidIdParameter, getValue, isArray, + isPlainObject, logInfo, logWarn, triggerPixel } from '../src/utils.js'; -import {getRefererInfo} from '../src/refererDetection.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; -import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import { getStorageManager } from '../src/storageManager.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync */ const BIDDER_CODE = 'beop'; -const ENDPOINT_URL = 'https://hb.beop.io/bid'; +const ENDPOINT_URL = 'https://hb.collectiveaudience.co/bid'; +const COOKIE_NAME = 'beopid'; const TCF_VENDOR_ID = 666; const validIdRegExp = /^[0-9a-fA-F]{24}$/ +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, @@ -52,7 +57,7 @@ export const spec = { * @return ServerRequest Info describing the request to the BeOp's server */ buildRequests: function(validBidRequests, bidderRequest) { - const slots = validBidRequests.map(beOpRequestSlotsMaker); + const slots = validBidRequests.map((bid) => beOpRequestSlotsMaker(bid, bidderRequest)); const firstPartyData = bidderRequest.ortb2 || {}; const psegs = firstPartyData.user?.ext?.permutive || firstPartyData.user?.ext?.data?.permutive || []; const userBpSegs = firstPartyData.user?.ext?.bpsegs || firstPartyData.user?.ext?.data?.bpsegs || []; @@ -63,22 +68,36 @@ export const spec = { const kwdsFromRequest = firstSlot.kwds; let keywords = getAllOrtbKeywords(bidderRequest.ortb2, kwdsFromRequest); + let beopid = ''; + if (storage.cookiesAreEnabled) { + beopid = storage.getCookie(COOKIE_NAME, undefined); + if (!beopid) { + beopid = generateUUID(); + let expirationDate = new Date(); + expirationDate.setTime(expirationDate.getTime() + 86400 * 183 * 1000); + storage.setCookie(COOKIE_NAME, beopid, expirationDate.toUTCString()); + } + } else { + storage.setCookie(COOKIE_NAME, '', 0); + } + const payloadObject = { at: new Date().toString(), nid: firstSlot.nid, nptnid: firstSlot.nptnid, pid: firstSlot.pid, - psegs: psegs, - bpsegs: (userBpSegs.concat(siteBpSegs)).map(item => item.toString()), + bpsegs: (userBpSegs.concat(siteBpSegs, psegs)).map(item => item.toString()), url: pageUrl, lang: (window.navigator.language || window.navigator.languages[0]), kwds: keywords, dbg: false, + fg: beopid, slts: slots, is_amp: deepAccess(bidderRequest, 'referrerInfo.isAmp'), gdpr_applies: gdpr ? gdpr.gdprApplies : false, tc_string: (gdpr && gdpr.gdprApplies) ? gdpr.consentString : null, eids: firstSlot.eids, + pv: '$prebid.version$' }; const payloadString = JSON.stringify(payloadObject); @@ -104,7 +123,7 @@ export const spec = { logWarn(BIDDER_CODE + ': timed out request'); triggerPixel(buildUrl({ protocol: 'https', - hostname: 't.beop.io', + hostname: 't.collectiveaudience.co', pathname: '/bid', search: trackingParams })); @@ -118,19 +137,47 @@ export const spec = { logInfo(BIDDER_CODE + ': won request'); triggerPixel(buildUrl({ protocol: 'https', - hostname: 't.beop.io', + hostname: 't.collectiveaudience.co', pathname: '/bid', search: trackingParams })); }, - onSetTargeting: function(bid) {} + + /** + * User syncs. + * + * @param {*} syncOptions Publisher prebid configuration. + * @param {*} serverResponses A successful response from the server. + * @return {UserSync[]} An array of syncs that should be executed. + */ + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + + if (serverResponses.length > 0) { + const body = serverResponses[0].body; + + if (syncOptions.iframeEnabled && Array.isArray(body.sync_frames)) { + body.sync_frames.forEach(url => { + syncs.push({ type: 'iframe', url }); + }); + } + + if (syncOptions.pixelEnabled && Array.isArray(body.sync_pixels)) { + body.sync_pixels.forEach(url => { + syncs.push({ type: 'image', url }); + }); + } + } + + return syncs; + } } function buildTrackingParams(data, info, value) { let params = Array.isArray(data.params) ? data.params[0] : data.params; const pageUrl = getPageUrl(null, window); return { - pid: params.accountId === undefined ? data.ad.match(/account: \“([a-f\d]{24})\“/)[1] : params.accountId, + pid: params.accountId ?? (data.ad?.match(/account: \“([a-f\d]{24})\“/)?.[1] ?? ''), nid: params.networkId, nptnid: params.networkPartnerId, bid: data.bidId || data.requestId, @@ -138,17 +185,18 @@ function buildTrackingParams(data, info, value) { se_ca: 'bid', se_ac: info, se_va: value, - url: pageUrl + url: pageUrl, + pv: '$prebid.version$' }; } -function beOpRequestSlotsMaker(bid) { +function beOpRequestSlotsMaker(bid, bidderRequest) { const bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); - const publisherCurrency = config.getConfig('currency.adServerCurrency') || getValue(bid.params, 'currency') || 'EUR'; + const publisherCurrency = getCurrencyFromBidderRequest(bidderRequest) || getValue(bid.params, 'currency') || 'EUR'; let floor; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({currency: publisherCurrency, mediaType: 'banner', size: [1, 1]}); - if (typeof floorInfo === 'object' && floorInfo.currency === publisherCurrency && !isNaN(parseFloat(floorInfo.floor))) { + if (isPlainObject(floorInfo) && floorInfo.currency === publisherCurrency && !isNaN(parseFloat(floorInfo.floor))) { floor = parseFloat(floorInfo.floor); } } diff --git a/modules/beopBidAdapter.md b/modules/beopBidAdapter.md index 53d2542b69c..dd723af10d5 100644 --- a/modules/beopBidAdapter.md +++ b/modules/beopBidAdapter.md @@ -2,7 +2,7 @@ **Module Name** : BeOp Bidder Adapter **Module Type** : Bidder Adapter -**Maintainer** : tech@beop.io +**Maintainer** : tech@collectiveaudience.co # Description diff --git a/modules/beyondmediaBidAdapter.js b/modules/beyondmediaBidAdapter.js index bbcd972470c..077717c36f4 100644 --- a/modules/beyondmediaBidAdapter.js +++ b/modules/beyondmediaBidAdapter.js @@ -1,202 +1,19 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'beyondmedia'; const AD_URL = 'https://backend.andbeyond.media/pbjs'; const SYNC_URL = 'https://cookies.andbeyond.media'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - placement.placementId = placementId; - placement.type = 'publisher'; - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && params.placementId); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } - - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/bidResponseFilter/index.js b/modules/bidResponseFilter/index.js new file mode 100644 index 00000000000..5b138965983 --- /dev/null +++ b/modules/bidResponseFilter/index.js @@ -0,0 +1,55 @@ +import { auctionManager } from '../../src/auctionManager.js'; +import { config } from '../../src/config.js'; +import { getHook } from '../../src/hook.js'; + +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'; + +let moduleConfig; +let enabled = false; + +function init() { + config.getConfig(MODULE_NAME, (cfg) => { + moduleConfig = cfg[MODULE_NAME]; + if (enabled && !moduleConfig) { + reset(); + } else if (!enabled && moduleConfig) { + enabled = true; + getHook('addBidResponse').before(addBidResponseHook); + } + }) +} + +export function reset() { + enabled = false; + getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); +} + +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 catConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.cat || {})}; + const advConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.adv || {})}; + const attrConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.attr || {})}; + + const { primaryCatId, secondaryCatIds = [], advertiserDomains = [], attr: metaAttr } = bid.meta || {}; + + // checking if bid fulfills ortb2 fields rules + if ((catConfig.enforce && bcat.some(category => [primaryCatId, ...secondaryCatIds].includes(category))) || + (catConfig.blockUnknown && !primaryCatId)) { + reject(BID_CATEGORY_REJECTION_REASON); + } else if ((advConfig.enforce && badv.some(domain => advertiserDomains.includes(domain))) || + (advConfig.blockUnknown && !advertiserDomains.length)) { + reject(BID_ADV_DOMAINS_REJECTION_REASON); + } else if ((attrConfig.enforce && battr.includes(metaAttr)) || + (attrConfig.blockUnknown && !metaAttr)) { + reject(BID_ATTR_REJECTION_REASON); + } else { + return next(adUnitCode, bid, reject); + } +} + +init(); diff --git a/modules/bidViewability.js b/modules/bidViewability.js index c1aab83b7e6..3bfcd93174a 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -4,11 +4,12 @@ import {config} from '../src/config.js'; import * as events from '../src/events.js'; -import { EVENTS } from '../src/constants.js'; +import {EVENTS} from '../src/constants.js'; import {isFn, logWarn, triggerPixel} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import adapterManager, {gdprDataHandler, uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; +import adapterManager, {gppDataHandler, uspDataHandler} from '../src/adapterManager.js'; import {find} from '../src/polyfill.js'; +import {gdprParams} from '../libraries/dfpUtils/dfpUtils.js'; const MODULE_NAME = 'bidViewability'; const CONFIG_ENABLED = 'enabled'; @@ -32,14 +33,7 @@ export let getMatchingWinningBidForGPTSlot = (globalModuleConfig, slot) => { export let fireViewabilityPixels = (globalModuleConfig, bid) => { if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && bid.hasOwnProperty(BID_VURL_ARRAY)) { - let queryParams = {}; - - const gdprConsent = gdprDataHandler.getConsentData(); - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } - if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; } - if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } - } + let queryParams = gdprParams(); const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } @@ -67,7 +61,6 @@ export let logWinningBidNotFound = (slot) => { export let impressionViewableHandler = (globalModuleConfig, slot, event) => { let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot); - let respectiveDeferredAdUnit = getGlobal().adUnits.find(adUnit => adUnit.deferBilling && respectiveBid.adUnitCode === adUnit.code); if (respectiveBid === null) { logWinningBidNotFound(slot); @@ -77,8 +70,8 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => { // trigger respective bidder's onBidViewable handler adapterManager.callBidViewableBidder(respectiveBid.adapterCode || respectiveBid.bidder, respectiveBid); - if (respectiveDeferredAdUnit) { - adapterManager.callBidBillableBidder(respectiveBid); + if (respectiveBid.deferBilling) { + adapterManager.triggerBilling(respectiveBid); } // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels diff --git a/modules/biddoBidAdapter.js b/modules/biddoBidAdapter.js index cf39c572629..6bfa0ac6ef8 100644 --- a/modules/biddoBidAdapter.js +++ b/modules/biddoBidAdapter.js @@ -1,5 +1,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +import { buildBannerRequests, interpretBannerResponse } from '../libraries/biddoInvamiaUtils/index.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -12,86 +13,15 @@ const ENDPOINT_URL = 'https://ad.adopx.net/delivery/impress'; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], - /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bidRequest The bid request params to validate. - * @return boolean True if this is a valid bid request, and false otherwise. - */ - isBidRequestValid: function(bidRequest) { + isBidRequestValid: function (bidRequest) { return !!bidRequest.params.zoneId; }, - /** - * Make a server request from the list of BidRequests. - * - * @param {Array} validBidRequests an array of bid requests - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: function(validBidRequests) { - let serverRequests = []; - - validBidRequests.forEach(bidRequest => { - const sizes = bidRequest.mediaTypes.banner.sizes; - - sizes.forEach(([width, height]) => { - bidRequest.params.requestedSizes = [width, height]; - - const payload = { - ctype: 'div', - pzoneid: bidRequest.params.zoneId, - width, - height, - }; - - const payloadString = Object.keys(payload).map(k => k + '=' + encodeURIComponent(payload[k])).join('&'); - - serverRequests.push({ - method: 'GET', - url: ENDPOINT_URL, - data: payloadString, - bidderRequest: bidRequest, - }); - }); - }); - - return serverRequests; + buildRequests: function (validBidRequests) { + return validBidRequests.flatMap((bidRequest) => buildBannerRequests(bidRequest, ENDPOINT_URL)); }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @param {BidRequest} bidderRequest A matched bid request for this response. - * @return Array An array of bids which were nested inside the server. - */ - interpretResponse: function(serverResponse, {bidderRequest}) { - const response = serverResponse.body; - const bidResponses = []; - - if (response && response.template && response.template.html) { - const {bidId} = bidderRequest; - const [width, height] = bidderRequest.params.requestedSizes; - - const bidResponse = { - requestId: bidId, - cpm: response.hb.cpm, - creativeId: response.banner.hash, - currency: 'USD', - netRevenue: response.hb.netRevenue, - ttl: 600, - ad: response.template.html, - mediaType: 'banner', - meta: { - advertiserDomains: response.hb.adomains || [], - }, - width, - height, - }; - - bidResponses.push(bidResponse); - } - - return bidResponses; + interpretResponse: function (serverResponse, { bidderRequest }) { + return interpretBannerResponse(serverResponse, bidderRequest); }, -} +}; registerBidder(spec); diff --git a/modules/bidmaticBidAdapter.js b/modules/bidmaticBidAdapter.js new file mode 100644 index 00000000000..6c88a3f1932 --- /dev/null +++ b/modules/bidmaticBidAdapter.js @@ -0,0 +1,144 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { replaceAuctionPrice, isNumber, deepAccess, isFn } from '../src/utils.js'; + +const HOST = 'https://adapter.bidmatic.io'; +const BIDDER_CODE = 'bidmatic'; +const DEFAULT_CURRENCY = 'USD'; +export const SYNC_URL = `${HOST}/sync.html`; +export const END_POINT = `${HOST}/ortb-client`; + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 290, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const floorInfo = isFn(bidRequest.getFloor) ? bidRequest.getFloor({ + currency: context.currency || 'USD', + size: '*', + mediaType: '*' + }) : { + floor: imp.bidfloor || deepAccess(bidRequest, 'params.bidfloor') || 0, + currency: DEFAULT_CURRENCY + }; + + if (floorInfo) { + imp.bidfloor = floorInfo.floor; + imp.bidfloorcur = floorInfo.currency; + } + imp.tagid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || bidRequest.adUnitCode; + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + if (!request.cur) { + request.cur = [DEFAULT_CURRENCY]; + } + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + + let resMediaType; + const reqMediaTypes = Object.keys(bidRequest.mediaTypes); + if (reqMediaTypes.length === 1) { + resMediaType = reqMediaTypes[0]; + } else { + if (bid.adm.search(/^(<\?xml| syncMade === 0) + .map(([sourceId]) => { + processedSources[sourceId] = 1 + + let url = `${SYNC_URL}?aid=${sourceId}` + if (gdprConsent && gdprConsent.gdprApplies) { + url += `&gdpr=${+(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` + } + if (uspConsent) { + url += `&usp=${uspConsent}`; + } + if (gppConsent) { + url += `&gpp=${gppConsent.gppString}&gpp_sid=${gppConsent.applicableSections?.toString()}` + } + return { + type: 'iframe', + url + }; + }) + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + gvlid: 1134, + isBidRequestValid: function (bid) { + return isNumber(deepAccess(bid, 'params.source')) + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + return createUserSyncs(PROCESSED_SOURCES, syncOptions, gdprConsent, uspConsent, gppConsent); + }, + buildRequests: function (validBidRequests, bidderRequest) { + const requestsBySource = validBidRequests.reduce((acc, bidRequest) => { + acc[bidRequest.params.source] = acc[bidRequest.params.source] || []; + acc[bidRequest.params.source].push(bidRequest); + return acc; + }, {}); + + return Object.entries(requestsBySource).map(([source, bidRequests]) => { + if (!PROCESSED_SOURCES[source]) { + PROCESSED_SOURCES[source] = 0; + } + const data = converter.toORTB({ bidRequests, bidderRequest }); + const url = new URL(END_POINT); + url.searchParams.append('source', source); + return { + method: 'POST', + url: url.toString(), + data: data, + options: { + withCredentials: true, + } + }; + }); + }, + + interpretResponse: function (serverResponse, bidRequest) { + if (!serverResponse || !serverResponse.body) return []; + const parsedSeatbid = serverResponse.body.seatbid.map(seatbidItem => { + const parsedBid = seatbidItem.bid.map((bidItem) => ({ + ...bidItem, + adm: replaceAuctionPrice(bidItem.adm, bidItem.price), + nurl: replaceAuctionPrice(bidItem.nurl, bidItem.price) + })); + return { ...seatbidItem, bid: parsedBid }; + }); + const responseBody = { ...serverResponse.body, seatbid: parsedSeatbid }; + return converter.fromORTB({ + response: responseBody, + request: bidRequest.data, + }).bids; + }, + +}; +registerBidder(spec); diff --git a/modules/bidmaticBidAdapter.md b/modules/bidmaticBidAdapter.md new file mode 100644 index 00000000000..242d7d9e77b --- /dev/null +++ b/modules/bidmaticBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +``` +Module Name: Bidmatic Bid Adapter +Module Type: Bidder Adapter +Maintainer: mg@bidmatic.io +``` + +# Description + +Adds access to Bidmatic SSP oRTB service. + +# Sample Ad Unit: For Publishers +``` +var adUnits = [{ + code: 'bg-test-rectangle', + sizes: [[300, 250]], + bids: [{ + bidder: 'bidmatic', + params: { + source: 886409, + bidfloor: 0.1 + } + }] +}] +``` + + +# Testing +```gulp test-only --file=./test/spec/modules/bidmaticBidAdapter_spec.js``` +```gulp test-coverage --file=./test/spec/modules/bidmaticBidAdapter_spec.js``` +```gulp view-coverage``` diff --git a/modules/bidtheatreBidAdapter.js b/modules/bidtheatreBidAdapter.js new file mode 100644 index 00000000000..8fb3dc2fd3b --- /dev/null +++ b/modules/bidtheatreBidAdapter.js @@ -0,0 +1,117 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepSetValue, logError, replaceAuctionPrice } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const GVLID = 30; +export const BIDDER_CODE = 'bidtheatre'; +export const ENDPOINT_URL = 'https://prebidjs-bids.bidtheatre.net/prebidjsbid'; +const METHOD = 'POST'; +const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; +export const DEFAULT_CURRENCY = 'USD'; +const BIDTHEATRE_COOKIE_NAME = '__kuid'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 120, + currency: DEFAULT_CURRENCY + } +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + gvlid: GVLID, + isBidRequestValid: function (bidRequest) { + const isValid = bidRequest && + bidRequest.params && + typeof bidRequest.params.publisherId === 'string' && + bidRequest.params.publisherId.trim().length === 36 + + if (!isValid) { + logError('Bidtheatre Header Bidding Publisher ID not provided or in incorrect format'); + } + + return isValid; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const seenUrls = new Set(); + const syncs = []; + + if (syncOptions.pixelEnabled && serverResponses) { + serverResponses.forEach(response => { + if (response.body && response.body.seatbid) { + response.body.seatbid.forEach(seatbid => { + if (seatbid.bid) { + seatbid.bid.forEach(bid => { + const urls = bid.ext && bid.ext.usersync_urls; + if (Array.isArray(urls)) { + urls.forEach(url => { + if (!seenUrls.has(url)) { + seenUrls.add(url); + syncs.push({ + type: 'image', + url: url + }); + } + }); + } + }); + } + }); + } + }); + } + return syncs; + }, + buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({bidRequests, bidderRequest}); + + const cookieValue = storage.getCookie(BIDTHEATRE_COOKIE_NAME); + if (cookieValue) { + deepSetValue(data, 'user.buyeruid', cookieValue); + } + + data.imp.forEach((impObj, index) => { + let publisherId = bidRequests[index].params.publisherId; + + if (publisherId) { + deepSetValue(impObj, 'ext.bidder.publisherId', publisherId); + } + }); + + return [{ + method: METHOD, + url: ENDPOINT_URL, + data + }] + }, + interpretResponse(response, request) { + if (!response || !response.body || !response.body.seatbid) { + return []; + } + + const macroReplacedSeatbid = response.body.seatbid.map(seatbidItem => { + const macroReplacedBid = seatbidItem.bid.map((bidItem) => ({ + ...bidItem, + adm: replaceAuctionPrice(bidItem.adm, bidItem.price), + nurl: replaceAuctionPrice(bidItem.nurl, bidItem.price) + })); + return { ...seatbidItem, bid: macroReplacedBid }; + }); + + const macroReplacedResponseBody = { ...response.body, seatbid: macroReplacedSeatbid }; + const bids = converter.fromORTB({response: macroReplacedResponseBody, request: request.data}).bids; + return bids; + }, + onTimeout: function(timeoutData) {}, + onBidWon: function(bid) {}, + onSetTargeting: function(bid) {}, + // onBidderError: function({ error, bidderRequest }) {}, + onAdRenderSucceeded: function(bid) {} +} + +registerBidder(spec); diff --git a/modules/bidtheatreBidAdapter.md b/modules/bidtheatreBidAdapter.md new file mode 100644 index 00000000000..7f9301596aa --- /dev/null +++ b/modules/bidtheatreBidAdapter.md @@ -0,0 +1,111 @@ +# Overview + +``` +Module Name : Bidtheatre Bidder Adapter +Module Type : Bidder Adapter +Maintainer : operations@bidtheatre.com +``` + +# Description + +Module that connects to Bidtheatre's demand sources + +About us: https://www.bidtheatre.com + +The Bidtheatre Bidding adapter requires manual set up before use. Please contact us at [operations@bidtheatre.com](mailto:operations@bidtheatre.com) if you would like to access Bidtheatre demand. + +# Bid params +| Name | Scope | Description | Example | +|:--------------| :------- |:---------------------------------------|:-------------------------------------| +| `publisherId` | required | Manually set up publisher ID | `73b20b3a-12a0-4869-b54e-8d42b55786ee`| + +In addition to the required bid param above, Bidtheatre will also enforce the following requirements +- All ad slots on a page must belong to the same publisher ID +- The publisher must provide either a client IP and/or explicit geo data in the request + +# Test Parameters + +## Banner + +```javascript +var displayAdUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[980,240]] + } + }, + bids: [ + { + bidder: 'bidtheatre', + params: { + publisherId: '73b20b3a-12a0-4869-b54e-8d42b55786ee' + } + } + ] + } +]; +``` + +## Video + +```javascript +var videoAdUnits = [ + { + code: 'test-video', + mediaTypes: { + video: { + playerSize: [[1280, 720]] + } + }, + bids: [ + { + bidder: 'bidtheatre', + params: { + publisherId: '73b20b3a-12a0-4869-b54e-8d42b55786ee' + } + } + ] + } +]; +``` + +## Multiformat + +```javascript +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[980,240]] + } + }, + bids: [ + { + bidder: 'bidtheatre', + params: { + publisherId: '73b20b3a-12a0-4869-b54e-8d42b55786ee' + } + } + ] + }, + { + code: 'test-video', + mediaTypes: { + video: { + playerSize: [[1280, 720]] + } + }, + bids: [ + { + bidder: 'bidtheatre', + params: { + publisherId: '73b20b3a-12a0-4869-b54e-8d42b55786ee' + } + } + ] + } +]; +``` diff --git a/modules/bidwatchAnalyticsAdapter.js b/modules/bidwatchAnalyticsAdapter.js index ffbd125eeab..e385b02fe5f 100644 --- a/modules/bidwatchAnalyticsAdapter.js +++ b/modules/bidwatchAnalyticsAdapter.js @@ -3,6 +3,7 @@ import adapterManager from '../src/adapterManager.js'; import { EVENTS } from '../src/constants.js'; import { ajax } from '../src/ajax.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import { deepClone } from '../src/utils.js'; const analyticsType = 'endpoint'; const url = 'URL_TO_SERVER_ENDPOINT'; @@ -113,7 +114,7 @@ function addTimeout(args) { let stringArgs = JSON.parse(dereferenceWithoutRenderer(args)); argsDereferenced = stringArgs; argsDereferenced.forEach((attr) => { - argsCleaned.push(filterAttributes(JSON.parse(JSON.stringify(attr)), false)); + argsCleaned.push(filterAttributes(deepClone(attr), false)); }); if (auctionEnd[eventType] == undefined) { auctionEnd[eventType] = [] } auctionEnd[eventType].push(argsCleaned); diff --git a/modules/big-richmediaBidAdapter.js b/modules/big-richmediaBidAdapter.js index ecb1724c2a1..858dad2ffde 100644 --- a/modules/big-richmediaBidAdapter.js +++ b/modules/big-richmediaBidAdapter.js @@ -112,11 +112,6 @@ export const spec = { return baseAdapter.getUserSyncs(syncOptions, responses, gdprConsent); }, - transformBidParams: function (params, isOpenRtb) { - if (!baseAdapter.transformBidParams) { return params; } - return baseAdapter.transformBidParams(params, isOpenRtb); - }, - /** * Add element selector to javascript tracker to improve native viewability * @param {Bid} bid diff --git a/modules/bitmediaBidAdapter.js b/modules/bitmediaBidAdapter.js new file mode 100644 index 00000000000..c07c3b4b228 --- /dev/null +++ b/modules/bitmediaBidAdapter.js @@ -0,0 +1,265 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import { + generateUUID, + isEmpty, + isFn, + isPlainObject, + logError, + logInfo, + triggerPixel +} from '../src/utils.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; + +const BIDDER_CODE = 'bitmedia'; +export const ENDPOINT_URL = 'https://cdn.bmcdn7.com/prebid/'; +const AVAILABLE_SIZES = [ + [320, 100], [125, 125], [250, 250], [728, 90], [468, 60], + [300, 100], [300, 250], [120, 240], [320, 1200], [200, 200], + [160, 600], [336, 280], [120, 600], [300, 600], [180, 150], + [320, 50], [468, 90], [970, 90], [250, 100], +]; + +const SIZE_SET = new Set(AVAILABLE_SIZES.map(([w, h]) => `${w}x${h}`)); +const DEFAULT_TTL = 30; // seconds +const DEFAULT_CURRENCY = 'USD'; +const ALLOWED_CURRENCIES = [ + 'USD' +]; +const DEFAULT_NET_REVENUE = true; +const PREBID_VERSION = '$prebid.version$'; +const ADAPTER_VERSION = '1.0'; +export const STORAGE = getStorageManager({bidderCode: BIDDER_CODE}); +const USER_FINGERPRINT_KEY = 'bitmedia_fid'; + +const _handleOnBidWon = (endpoint) => { + logInfo(BIDDER_CODE, `____handle bid won____`, endpoint); + triggerPixel(endpoint); +} + +const _getFidFromBitmediaFid = (bitmediaFid) => { + try { + const decoded = atob(bitmediaFid); + const parsed = JSON.parse(decoded); + logInfo(BIDDER_CODE, 'Parsed bitmedia_fid', parsed); + return parsed.fid || null; + } catch (e) { + logError(BIDDER_CODE, 'Failed to parse bitmedia_fid', e); + return null; + } +} + +const _getBidFloor = (bid, size) => { + logInfo(BIDDER_CODE, '[Bid Floor] Retrieving bid floor for bid:', bid, size); + if (isFn(bid.getFloor)) { + let floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: BANNER, + size: size || '*' + }); + + logInfo(BIDDER_CODE, '[Bid Floor] Floor data received:', floor); + + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) { + logInfo(BIDDER_CODE, '[Bid Floor] Valid floor found:', floor); + return floor.floor; + } + } + logInfo(BIDDER_CODE, '[Bid Floor] Returning null for bid floor.'); + return null; +} + +const CONVERTER = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_TTL, + currency: DEFAULT_CURRENCY, // Only one currency available + mediaType: BANNER, + }, + + imp(buildImp, bidRequest) { + logInfo(BIDDER_CODE, 'Building imp object for bidRequest', bidRequest); + const sizes = bidRequest.sizes; + + const validSizes = sizes.filter(([w, h]) => SIZE_SET.has(`${w}x${h}`)); + + const imps = validSizes.map(size => { + const imp = { + id: bidRequest.bidId, + tagid: bidRequest.params.adUnitID, + banner: { + w: size[0], + h: size[1], + } + }; + + const floor = _getBidFloor(bidRequest, size); + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = DEFAULT_CURRENCY; + } + + return imp; + }); + logInfo(BIDDER_CODE, 'Result imp objects for bidRequest', imps); + // Should hasOwnProperty id. + return {id: bidRequest.bidId, imps}; + }, + + request(buildRequest, imps, bidderRequest, context) { + logInfo(BIDDER_CODE, 'Building request with imps and bidderRequest', imps, bidderRequest); + + const requestImps = imps[0].imps;// Unpacking: each imp has the same id, but different banner size + + const reqData = { + ...bidderRequest.ortb2, + id: generateUUID(), + imp: requestImps, + cur: [context.currency], + tmax: bidderRequest.timeout, + ext: { + ...bidderRequest.ortb2.ext, + adapter_version: ADAPTER_VERSION, + prebid_version: PREBID_VERSION + } + }; + + let userId = null; + let bitmediaFid = null; + if (STORAGE.hasLocalStorage()) { + bitmediaFid = STORAGE.getDataFromLocalStorage(USER_FINGERPRINT_KEY); + logInfo(BIDDER_CODE, 'Fingerprint in localstorage', bitmediaFid); + } + + if (!bitmediaFid && STORAGE.cookiesAreEnabled()) { + bitmediaFid = STORAGE.getCookie(USER_FINGERPRINT_KEY); + logInfo(BIDDER_CODE, 'Fingerprint in cookies', bitmediaFid); + } + + if (bitmediaFid) { + userId = _getFidFromBitmediaFid(bitmediaFid); + } + + if (userId) { + reqData.user = reqData.user || {}; + reqData.user.id = userId; + } + + return reqData; + }, + + bidResponse(buildBidResponse, bid, context) { + logInfo(BIDDER_CODE, 'Processing bid response in converter', bid); + const bidResponse = { + requestId: bid.impid, + cpm: bid.price, + currency: bid.cur || context.currency, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: bid.exp || context.ttl, + creativeId: bid.crid, + netRevenue: context.netRevenue, + meta: { + advertiserDomains: bid.adomain || [], + }, + nurl: bid.nurl || bid.burl, + mediaType: context.mediaType, + }; + + return bidResponse; + }, +}); + +const isBidRequestValid = (bid) => { + logInfo(BIDDER_CODE, 'Validating bid request', bid); + const {banner} = bid.mediaTypes || {}; + const {adUnitID, currency} = bid.params || {}; + + if (!banner || !Array.isArray(banner.sizes)) { + logError(BIDDER_CODE, 'Invalid bid: missing or malformed banner sizes', banner); + return false; + } + + const hasValidSize = banner.sizes.some(([w, h]) => SIZE_SET.has(`${w}x${h}`)); + if (!hasValidSize) { + logError(BIDDER_CODE, 'Invalid bid: no valid sizes found', banner.sizes); + return false; + } + + if (typeof adUnitID !== 'string') { + logError(BIDDER_CODE, 'Invalid bid: malformed adUnitId', adUnitID); + return false; + } + + const isCurrencyValid = ALLOWED_CURRENCIES.includes(currency); + if (!isCurrencyValid) { + logError(BIDDER_CODE, `Invalid currency: ${currency}. Allowed currencies are ${ALLOWED_CURRENCIES.join(', ')}`); + return false; + } + + logInfo(BIDDER_CODE, 'Bid request is valid', bid); + return true; +}; + +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + logInfo(BIDDER_CODE, 'Building OpenRTB request', {validBidRequests, bidderRequest}); + const requests = validBidRequests.map(bidRequest => { + const data = CONVERTER.toORTB({ + bidRequests: [bidRequest], + bidderRequest, + }); + + logInfo(BIDDER_CODE, 'Result OpenRTB request data for bidRequest', data); + + const adUnitId = bidRequest.params.adUnitID; + + return { + method: 'POST', + url: `${ENDPOINT_URL}${adUnitId}`, + data: data, + bids: [bidRequest], + options: { + withCredentials: false, + crossOrigin: true + }, + }; + }); + return requests; +}; + +const interpretResponse = (serverResponse, bidRequest) => { + logInfo(BIDDER_CODE, 'Interpreting server response', {serverResponse, bidRequest}); + + if (isEmpty(serverResponse.body)) { + logInfo(BIDDER_CODE, 'Empty response'); + return []; + } + + const bids = CONVERTER.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }).bids; + + logInfo(BIDDER_CODE, `${bids.length} bids successfully interpreted`, bids); + + return bids; +}; + +const onBidWon = (bid) => { + const cpm = bid.adserverTargeting?.hb_pb || ''; + logInfo(BIDDER_CODE, `-----Bid won-----`, {bid, cpm: cpm}); + _handleOnBidWon(bid.nurl); +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidWon, +}; + +registerBidder(spec); diff --git a/modules/bitmediaBidAdapter.md b/modules/bitmediaBidAdapter.md new file mode 100644 index 00000000000..dd02b4785c0 --- /dev/null +++ b/modules/bitmediaBidAdapter.md @@ -0,0 +1,184 @@ +# Bitmedia Bid Adapter + +## Overview + +``` +Module Name: Bitmedia Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@bitmedia.io +``` + +## Description + +The Bitmedia Bid Adapter allows you to integrate BitmediaIO for banner advertising. + +### Key Points: +- Supported Media Type: **Banner** +- Bids are only provided in **USD**. +- Access to **local storage** is optional. + +Before using this adapter, simply [create a publisher account](https://bitmedia.io/become-a-publisher) on our platform to obtain your `adUnitID`. + +More about us: [https://bitmedia.io](https://bitmedia.io) + +--- + +## Test Parameters + +### Example +```javascript +var adUnits = [ + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bids: [{ + bidder: 'bitmedia', + params: { + adUnitID: 'exampleAdUnitID', + currency: 'USD', + }, + }], + }, +]; +``` + +--- + +## Testing Instructions + +The HTML file below can be used to test the integration of the Bitmedia Bid Adapter. + +### Simple Test HTML + +```html + + + + + + + + + + + + + + + + + + +

Ad Serverless Test Page

+ +
+
+
+ + +``` \ No newline at end of file diff --git a/modules/bizzclickBidAdapter.js b/modules/blastoBidAdapter.js similarity index 90% rename from modules/bizzclickBidAdapter.js rename to modules/blastoBidAdapter.js index d2eba3f0f81..0e97c294049 100644 --- a/modules/bizzclickBidAdapter.js +++ b/modules/blastoBidAdapter.js @@ -3,11 +3,11 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -const BIDDER_CODE = 'bizzclick'; +const BIDDER_CODE = 'blasto'; const SOURCE_ID_MACRO = '[sourceid]'; const ACCOUNT_ID_MACRO = '[accountid]'; const HOST_MACRO = '[host]'; -const URL = `https://${HOST_MACRO}.bizzclick.com/bid?rtb_seat_id=${SOURCE_ID_MACRO}&secret_key=${ACCOUNT_ID_MACRO}&integration_type=prebidjs`; +const URL = `https://${HOST_MACRO}.blasto.ai/bid?rtb_seat_id=${SOURCE_ID_MACRO}&secret_key=${ACCOUNT_ID_MACRO}&integration_type=prebidjs`; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_HOST = 'us-e-node1'; @@ -53,7 +53,7 @@ export const spec = { buildRequests: (validBidRequests, bidderRequest) => { if (validBidRequests && validBidRequests.length === 0) return []; const { sourceId, accountId } = validBidRequests[0].params; - const host = validBidRequests[0].params.host || 'USE'; + const host = validBidRequests[0].params.host; const endpointURL = URL.replace(HOST_MACRO, host || DEFAULT_HOST) .replace(ACCOUNT_ID_MACRO, accountId) .replace(SOURCE_ID_MACRO, sourceId); diff --git a/modules/bizzclickBidAdapter.md b/modules/blastoBidAdapter.md similarity index 88% rename from modules/bizzclickBidAdapter.md rename to modules/blastoBidAdapter.md index ad342f34e07..60ebad14764 100644 --- a/modules/bizzclickBidAdapter.md +++ b/modules/blastoBidAdapter.md @@ -1,14 +1,14 @@ # Overview ``` -Module Name: BizzClick SSP Bidder Adapter +Module Name: Blasto SSP Bidder Adapter Module Type: Bidder Adapter -Maintainer: support@bizzclick.com +Maintainer: support@blasto.ai ``` # Description -Module that connects to BizzClick SSP demand sources +Module that connects to Blasto SSP demand sources # Test Parameters @@ -26,7 +26,7 @@ const adUnits = [ }, bids: [ { - bidder: "bizzclick", + bidder: "blasto", params: { placementId: "hash", accountId: "accountId", @@ -68,7 +68,7 @@ const adUnits = [ }, bids: [ { - bidder: "bizzclick", + bidder: "blasto", params: { placementId: "hash", accountId: "accountId", @@ -96,7 +96,7 @@ const adUnits = [ }, bids: [ { - bidder: "bizzclick", + bidder: "blasto", params: { placementId: "hash", accountId: "accountId", diff --git a/modules/bliinkBidAdapter.js b/modules/bliinkBidAdapter.js index 37c99878d68..b66923fd476 100644 --- a/modules/bliinkBidAdapter.js +++ b/modules/bliinkBidAdapter.js @@ -1,8 +1,6 @@ -// eslint-disable-next-line prebid/validate-imports -// eslint-disable-next-line prebid/validate-imports import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' -import { _each, deepAccess, deepSetValue, getWindowSelf, getWindowTop } from '../src/utils.js' +import { _each, canAccessWindowTop, deepAccess, deepSetValue, getDomLoadingDuration, getWindowSelf, getWindowTop } from '../src/utils.js' export const BIDDER_CODE = 'bliink' export const GVL_ID = 658 export const BLIINK_ENDPOINT_ENGINE = 'https://engine.bliink.io/prebid' @@ -123,35 +121,6 @@ export function getKeywords() { return []; } -function canAccessTopWindow() { - try { - if (getWindowTop().location.href) { - return true; - } - } catch (error) { - return false; - } -} - -/** - * domLoading feature is computed on window.top if reachable. - */ -export function getDomLoadingDuration() { - let domLoadingDuration = -1; - let performance; - - performance = (canAccessTopWindow()) ? getWindowTop().performance : getWindowSelf().performance; - - if (performance && performance.timing && performance.timing.navigationStart > 0) { - const val = performance.timing.domLoading - performance.timing.navigationStart; - if (val > 0) { - domLoadingDuration = val; - } - } - - return domLoadingDuration; -} - /** * @param bidResponse * @return {({cpm, netRevenue: boolean, requestId, width: number, currency, ttl: number, creativeId, height: number}&{mediaType: string, vastXml})|null} @@ -210,7 +179,8 @@ export const isBidRequestValid = (bid) => { */ export const buildRequests = (validBidRequests, bidderRequest) => { if (!validBidRequests || !bidderRequest || !bidderRequest.bids) return null - const domLoadingDuration = getDomLoadingDuration().toString(); + const w = (canAccessWindowTop()) ? getWindowTop() : getWindowSelf(); + const domLoadingDuration = getDomLoadingDuration(w).toString(); const tags = bidderRequest.bids.map((bid) => { let bidFloor; const sizes = bid.sizes.map((size) => ({ w: size[0], h: size[1] })); diff --git a/modules/blueBidAdapter.js b/modules/blueBidAdapter.js new file mode 100644 index 00000000000..ba8d0a9e8fe --- /dev/null +++ b/modules/blueBidAdapter.js @@ -0,0 +1,122 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { deepSetValue, isFn, isPlainObject } from '../src/utils.js'; + +const BIDDER_CODE = 'blue'; +const ENDPOINT_URL = 'https://bidder-us-east-1.getblue.io/engine/?src=prebid'; +const GVLID = 620; // GVLID for your bidder +const COOKIE_NAME = 'ckid'; // Cookie name for identifying users +const CURRENCY = 'USD'; // Currency used in bid floors + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +const converter = ortbConverter({ + context: { + netRevenue: true, // Default netRevenue setting + ttl: 100, // Default time-to-live for bid responses + }, + imp, + request, +}); + +function request(buildRequest, imps, bidderRequest, context) { + let request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'site.publisher.id', context.publisherId); + return request; +} + +function imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const floor = getBidFloor(bidRequest); + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = CURRENCY; + } + return imp; +} + +function getBidFloor(bid) { + if (isFn(bid.getFloor)) { + let floor = bid.getFloor({ + currency: CURRENCY, + mediaType: BANNER, + size: '*', + }); + if ( + isPlainObject(floor) && + !isNaN(floor.floor) && + floor.currency === CURRENCY + ) { + return floor.floor; + } + } + return null; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], // Supported ad types + + // Validate bid request + isBidRequestValid: function (bid) { + return !!bid.params.placementId && !!bid.params.publisherId; + }, + + // Build OpenRTB requests using `ortbConverter` + buildRequests: function (validBidRequests, bidderRequest) { + const context = { + publisherId: validBidRequests.find( + (bidRequest) => bidRequest.params?.publisherId + )?.params.publisherId, + }; + + const ortbRequest = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest, + context, + }); + + // Add GVLID and cookie ID to the request + ortbRequest.ext = ortbRequest.ext || {}; + deepSetValue(ortbRequest, 'ext.gvlid', GVLID); + + // Include user cookie if available + const ckid = storage.getDataFromLocalStorage('blueID') || storage.getCookie(COOKIE_NAME) || null; + if (ckid) { + deepSetValue(ortbRequest, 'user.ext.buyerid', ckid); + } + + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(ortbRequest), + options: { + contentType: 'text/plain', + }, + }; + }, + + // Interpret OpenRTB responses using `ortbConverter` + interpretResponse: function (serverResponse, request) { + const ortbResponse = serverResponse.body; + + // Parse the OpenRTB response into Prebid bid responses + const prebidResponses = converter.fromORTB({ + response: ortbResponse, + request: request.data, + }).bids; + + // Example: Modify bid responses if needed + prebidResponses.forEach((bid) => { + bid.meta = bid.meta || {}; + bid.meta.adapterVersion = '1.0.0'; + }); + + return prebidResponses; + }, +}; + +registerBidder(spec); diff --git a/modules/blueBidAdapter.md b/modules/blueBidAdapter.md new file mode 100644 index 00000000000..4f446b1ff4b --- /dev/null +++ b/modules/blueBidAdapter.md @@ -0,0 +1,28 @@ +# Overview + +Module Name: Blue Bidder Adapter +Module Type: Bidder Adapter +Maintainer: celsooliveira@getblue.io + +# Description + +Module that connects to Blue's demand sources. + +# Test Parameters +``` + var adUnits = [ + { + code: 'banner-ad-div', + sizes: [[300, 250], [728, 90]], + bids: [ + { + bidder: 'blue', + params: { + publisherId: "xpto", + placementId: "xpto", + } + } + ] + } + ]; +``` diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js deleted file mode 100644 index d4bde9b3f2c..00000000000 --- a/modules/bluebillywigBidAdapter.js +++ /dev/null @@ -1,374 +0,0 @@ -import {deepAccess, deepClone, deepSetValue, logError, logWarn} from '../src/utils.js'; -import {find} from '../src/polyfill.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {Renderer} from '../src/Renderer.js'; - -const DEV_MODE = window.location.search.match(/bbpbs_debug=true/); - -// Blue Billywig Constants -const BB_CONSTANTS = { - BIDDER_CODE: 'bluebillywig', - AUCTION_URL: '$$URL_STARTpbs.bluebillywig.com/openrtb2/auction?pub=$$PUBLICATION', - SYNC_URL: '$$URL_STARTpbs.bluebillywig.com/static/cookie-sync.html?pub=$$PUBLICATION', - RENDERER_URL: 'https://$$PUBLICATION.bbvms.com/r/$$RENDERER.js', - DEFAULT_TIMEOUT: 5000, - DEFAULT_TTL: 300, - DEFAULT_WIDTH: 768, - DEFAULT_HEIGHT: 432, - DEFAULT_NET_REVENUE: true, - VIDEO_PARAMS: ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', - 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', - 'api', 'companiontype', 'ext'] -}; - -// Aliasing -const getConfig = config.getConfig; - -// Helper Functions -const BB_HELPERS = { - addSiteAppDevice: function(request, pageUrl) { - if (typeof getConfig('app') === 'object') request.app = getConfig('app'); - else { - request.site = {}; - if (typeof getConfig('site') === 'object') request.site = getConfig('site'); - if (pageUrl) request.site.page = pageUrl; - } - - if (typeof getConfig('device') === 'object') request.device = getConfig('device'); - if (!request.device) request.device = {}; - if (!request.device.w) request.device.w = window.innerWidth; - if (!request.device.h) request.device.h = window.innerHeight; - }, - addSchain: function(request, validBidRequests) { - const schain = deepAccess(validBidRequests, '0.schain'); - if (schain) request.source.ext = { schain: schain }; - }, - addCurrency: function(request) { - const adServerCur = getConfig('currency.adServerCurrency'); - if (adServerCur && typeof adServerCur === 'string') request.cur = [adServerCur]; - else if (Array.isArray(adServerCur) && adServerCur.length) request.cur = [adServerCur[0]]; - }, - addUserIds: function(request, validBidRequests) { - const eids = deepAccess(validBidRequests, '0.userIdAsEids'); - - if (eids != null && eids.length) { - deepSetValue(request, 'user.ext.eids', eids); - } - }, - substituteUrl: function (url, publication, renderer) { - return url.replace('$$URL_START', (DEV_MODE) ? 'https://dev.' : 'https://').replace('$$PUBLICATION', publication).replace('$$RENDERER', renderer); - }, - getAuctionUrl: function(publication) { - return BB_HELPERS.substituteUrl(BB_CONSTANTS.AUCTION_URL, publication); - }, - getSyncUrl: function(publication) { - return BB_HELPERS.substituteUrl(BB_CONSTANTS.SYNC_URL, publication); - }, - getRendererUrl: function(publication, renderer) { - return BB_HELPERS.substituteUrl(BB_CONSTANTS.RENDERER_URL, publication, renderer); - }, - transformVideoParams: function(videoParams, videoParamsExt) { - videoParams = deepClone(videoParams); - - let playerSize = videoParams.playerSize || [BB_CONSTANTS.DEFAULT_WIDTH, BB_CONSTANTS.DEFAULT_HEIGHT]; - if (Array.isArray(playerSize[0])) playerSize = playerSize[0]; - - videoParams.w = playerSize[0]; - videoParams.h = playerSize[1]; - videoParams.placement = 3; - - if (videoParamsExt) videoParams = Object.assign(videoParams, videoParamsExt); - - const videoParamsProperties = Object.keys(videoParams); - - videoParamsProperties.forEach(property => { - if (BB_CONSTANTS.VIDEO_PARAMS.indexOf(property) === -1) delete videoParams[property]; - }); - - return videoParams; - }, - transformRTBToPrebidProps: function(bid, serverResponse) { - const bidObject = { - cpm: bid.price, - currency: serverResponse.cur, - netRevenue: BB_CONSTANTS.DEFAULT_NET_REVENUE, - bidId: bid.impid, - requestId: bid.impid, - creativeId: bid.crid, - mediaType: VIDEO, - width: bid.w || BB_CONSTANTS.DEFAULT_WIDTH, - height: bid.h || BB_CONSTANTS.DEFAULT_HEIGHT, - ttl: BB_CONSTANTS.DEFAULT_TTL - }; - - const extPrebidTargeting = deepAccess(bid, 'ext.prebid.targeting'); - const extPrebidCache = deepAccess(bid, 'ext.prebid.cache'); - - if (extPrebidCache && typeof extPrebidCache.vastXml === 'object' && extPrebidCache.vastXml.cacheId && extPrebidCache.vastXml.url) { - bidObject.videoCacheKey = extPrebidCache.vastXml.cacheId; - bidObject.vastUrl = extPrebidCache.vastXml.url; - } else if (extPrebidTargeting && extPrebidTargeting.hb_uuid && extPrebidTargeting.hb_cache_host && extPrebidTargeting.hb_cache_path) { - bidObject.videoCacheKey = extPrebidTargeting.hb_uuid; - bidObject.vastUrl = `https://${extPrebidTargeting.hb_cache_host}${extPrebidTargeting.hb_cache_path}?uuid=${extPrebidTargeting.hb_uuid}`; - } - if (bid.adm) { - bidObject.ad = bid.adm; - bidObject.vastXml = bid.adm; - } - if (!bidObject.vastUrl && bid.nurl && !bid.adm) { // ad markup is on win notice url, and adm is ommited according to OpenRTB 2.5 - bidObject.vastUrl = bid.nurl; - } - bidObject.meta = bid.meta || {}; - if (bid.adomain) { bidObject.meta.advertiserDomains = bid.adomain; } - return bidObject; - }, -}; - -// Renderer Functions -const BB_RENDERER = { - bootstrapPlayer: function(bid) { - const config = { - code: bid.adUnitCode, - }; - - if (bid.vastXml) config.vastXml = bid.vastXml; - else if (bid.vastUrl) config.vastUrl = bid.vastUrl; - - if (!bid.vastXml && !bid.vastUrl) { - logWarn(`${BB_CONSTANTS.BIDDER_CODE}: No vastXml or vastUrl on bid, bailing...`); - return; - } - - if (!(window.bluebillywig && window.bluebillywig.renderers)) { - logWarn(`${BB_CONSTANTS.BIDDER_CODE}: renderer code failed to initialize...`); - return; - } - - const rendererId = BB_RENDERER.getRendererId(bid.publicationName, bid.rendererCode); - const ele = document.getElementById(bid.adUnitCode); // NB convention - const renderer = find(window.bluebillywig.renderers, r => r._id === rendererId); - - if (renderer) renderer.bootstrap(config, ele, bid.rendererSettings || {}); - else logWarn(`${BB_CONSTANTS.BIDDER_CODE}: Couldn't find a renderer with ${rendererId}`); - }, - newRenderer: function(rendererUrl, adUnitCode) { - const renderer = Renderer.install({ - url: rendererUrl, - loaded: false, - adUnitCode - }); - - try { - renderer.setRender(BB_RENDERER.outstreamRender); - } catch (err) { - logWarn(`${BB_CONSTANTS.BIDDER_CODE}: Error tying to setRender on renderer`, err); - } - - return renderer; - }, - outstreamRender: function(bid) { - bid.renderer.push(function() { BB_RENDERER.bootstrapPlayer(bid) }); - }, - getRendererId: function(pub, renderer) { - return `${pub}-${renderer}`; // NB convention! - } -}; - -// Spec Functions -// These functions are used to construct the core spec for the adapter -export const spec = { - code: BB_CONSTANTS.BIDDER_CODE, - supportedMediaTypes: [VIDEO], - syncStore: { bidders: [], }, - isBidRequestValid(bid) { - const publicationNameRegex = /^\w+\.?\w+$/; - const rendererRegex = /^[\w+_]+$/; - - if (!bid.params) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: no params set on bid. Rejecting bid: `, bid); - return false; - } - - if (!bid.params.hasOwnProperty('publicationName') || typeof bid.params.publicationName !== 'string') { - logError(`${BB_CONSTANTS.BIDDER_CODE}: no publicationName specified in bid params, or it's not a string. Rejecting bid: `, bid); - return false; - } else if (!publicationNameRegex.test(bid.params.publicationName)) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: publicationName must be in format 'publication' or 'publication.environment'. Rejecting bid: `, bid); - return false; - } - - if ((!bid.params.hasOwnProperty('rendererCode') || typeof bid.params.rendererCode !== 'string')) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: no rendererCode was specified in bid params. Rejecting bid: `, bid); - return false; - } else if (!rendererRegex.test(bid.params.rendererCode)) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: rendererCode must be alphanumeric, including underscores. Rejecting bid: `, bid); - return false; - } - - if (!bid.params.accountId) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: no accountId specified in bid params. Rejecting bid: `, bid); - return false; - } - - if (bid.params.hasOwnProperty('connections')) { - if (!Array.isArray(bid.params.connections)) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: connections is not of type array. Rejecting bid: `, bid); - return false; - } else { - for (let i = 0; i < bid.params.connections.length; i++) { - if (!bid.params.hasOwnProperty(bid.params.connections[i])) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: connection specified in params.connections, but not configured in params. Rejecting bid: `, bid); - return false; - } - } - } - } else { - logError(`${BB_CONSTANTS.BIDDER_CODE}: no connections specified in bid. Rejecting bid: `, bid); - return false; - } - - if (bid.params.hasOwnProperty('video') && (bid.params.video === null || typeof bid.params.video !== 'object')) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: params.video must be of type object. Rejecting bid: `, bid); - return false; - } - - if (bid.params.hasOwnProperty('rendererSettings') && (bid.params.rendererSettings === null || typeof bid.params.rendererSettings !== 'object')) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: params.rendererSettings must be of type object. Rejecting bid: `, bid); - return false; - } - - if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { - if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { - logError(`${BB_CONSTANTS.BIDDER_CODE}: no context specified in bid. Rejecting bid: `, bid); - return false; - } - - if (bid.mediaTypes[VIDEO].context !== 'outstream') { - logError(`${BB_CONSTANTS.BIDDER_CODE}: video.context is invalid, must be "outstream". Rejecting bid: `, bid); - return false; - } - } else { - logError(`${BB_CONSTANTS.BIDDER_CODE}: mediaTypes or mediaTypes.video is not specified. Rejecting bid: `, bid); - return false; - } - - return true; - }, - buildRequests(validBidRequests, bidderRequest) { - const imps = []; - - validBidRequests.forEach(validBidRequest => { - if (!this.syncStore.publicationName) this.syncStore.publicationName = validBidRequest.params.publicationName; - if (!this.syncStore.accountId) this.syncStore.accountId = validBidRequest.params.accountId; - - const ext = validBidRequest.params.connections.reduce((extBuilder, connection) => { - extBuilder[connection] = validBidRequest.params[connection]; - - if (this.syncStore.bidders.indexOf(connection) === -1) this.syncStore.bidders.push(connection); - - return extBuilder; - }, {}); - - const videoParams = BB_HELPERS.transformVideoParams(deepAccess(validBidRequest, 'mediaTypes.video'), deepAccess(validBidRequest, 'params.video')); - imps.push({ id: validBidRequest.bidId, ext, secure: window.location.protocol === 'https' ? 1 : 0, video: videoParams }); - }); - - const request = { - id: bidderRequest.bidderRequestId, - source: {tid: bidderRequest.ortb2?.source?.tid}, - tmax: BB_CONSTANTS.DEFAULT_TIMEOUT, - imp: imps, - test: DEV_MODE ? 1 : 0, - ext: { - prebid: { - targeting: { includewinners: true, includebidderkeys: false } - } - } - }; - - // handle privacy settings for GDPR/CCPA/COPPA - if (bidderRequest.gdprConsent) { - let gdprApplies = 0; - if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - deepSetValue(request, 'regs.ext.gdpr', gdprApplies); - deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - } - - if (bidderRequest.uspConsent) { - deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); - this.syncStore.uspConsent = bidderRequest.uspConsent; - } - - if (getConfig('coppa') == true) deepSetValue(request, 'regs.coppa', 1); - - // Enrich the request with any external data we may have - BB_HELPERS.addSiteAppDevice(request, bidderRequest.refererInfo && bidderRequest.refererInfo.page); - BB_HELPERS.addSchain(request, validBidRequests); - BB_HELPERS.addCurrency(request); - BB_HELPERS.addUserIds(request, validBidRequests); - - return { - method: 'POST', - url: BB_HELPERS.getAuctionUrl(validBidRequests[0].params.publicationName), - data: JSON.stringify(request), - bidderRequest: bidderRequest - }; - }, - interpretResponse(serverResponse, request) { - serverResponse = serverResponse.body || {}; - - if (!serverResponse.hasOwnProperty('seatbid') || !Array.isArray(serverResponse.seatbid)) { - return []; - } - - const bids = []; - - serverResponse.seatbid.forEach(seatbid => { - if (!seatbid.bid || !Array.isArray(seatbid.bid)) return; - seatbid.bid.forEach(bid => { - bid = BB_HELPERS.transformRTBToPrebidProps(bid, serverResponse); - - const bidParams = find(request.bidderRequest.bids, bidderRequestBid => bidderRequestBid.bidId === bid.bidId).params; - bid.publicationName = bidParams.publicationName; - bid.rendererCode = bidParams.rendererCode; - bid.accountId = bidParams.accountId; - bid.rendererSettings = bidParams.rendererSettings; - - const rendererUrl = BB_HELPERS.getRendererUrl(bid.publicationName, bid.rendererCode); - bid.renderer = BB_RENDERER.newRenderer(rendererUrl, bid.adUnitCode); - - bids.push(bid); - }); - }); - - return bids; - }, - getUserSyncs(syncOptions, serverResponses, gdpr) { - if (!syncOptions.iframeEnabled) return []; - - const queryString = []; - - if (gdpr.gdprApplies) queryString.push(`gdpr=${gdpr.gdprApplies ? 1 : 0}`); - if (gdpr.gdprApplies && gdpr.consentString) queryString.push(`gdpr_consent=${gdpr.consentString}`); - - if (this.syncStore.uspConsent) queryString.push(`usp_consent=${this.syncStore.uspConsent}`); - - queryString.push(`accountId=${this.syncStore.accountId}`); - queryString.push(`bidders=${btoa(JSON.stringify(this.syncStore.bidders))}`); - queryString.push(`cb=${Date.now()}-${Math.random().toString().replace('.', '')}`); - - if (DEV_MODE) queryString.push('bbpbs_debug=true'); - - // NB syncUrl by default starts with ?pub=$$PUBLICATION - const syncUrl = `${BB_HELPERS.getSyncUrl(this.syncStore.publicationName)}&${queryString.join('&')}`; - - return [{ - type: 'iframe', - url: syncUrl - }]; - } -}; - -registerBidder(spec); diff --git a/modules/bluebillywigBidAdapter.md b/modules/bluebillywigBidAdapter.md deleted file mode 100644 index 7879697baf5..00000000000 --- a/modules/bluebillywigBidAdapter.md +++ /dev/null @@ -1,38 +0,0 @@ -# Overview - -``` -Module Name: Blue Billywig Adapter -Module Type: Bidder Adapter -Maintainer: dev+prebid@bluebillywig.com -``` - -# Description - -Prebid Blue Billywig Bidder Adapter - -# Test Parameters - -``` - const adUnits = [{ - code: 'ad-unit', - sizes: [[[768,432],[640,480],[640,360]]], - mediaTypes: { - video: { - playerSize: [768, 432], - context: 'outstream', - mimes: ['video/mp4'], - protocols: [ 2,3,5,6] - } - }, - bids: [{ - bidder: 'bluebillywig', - params: { - publicationName: "bbprebid", - rendererCode: "renderer", - accountId: 642, - connections: [ 'bluebillywig' ], - bluebillywig: {} - } - }] - }]; -``` diff --git a/modules/bmsBidAdapter.js b/modules/bmsBidAdapter.js new file mode 100644 index 00000000000..36c48db919e --- /dev/null +++ b/modules/bmsBidAdapter.js @@ -0,0 +1,160 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { + replaceAuctionPrice, + isFn, + isPlainObject, + deepSetValue, + isEmpty, + triggerPixel, +} from '../src/utils.js'; +const BIDDER_CODE = 'bms'; +const ENDPOINT_URL = + 'https://api.prebid.int.us-east-1.bluems.com/v1/bid?exchangeId=prebid'; +const GVLID = 1105; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_BID_TTL = 1200; + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +function getBidFloor(bid) { + if (isFn(bid.getFloor)) { + let floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: BANNER, + size: '*', + }); + if ( + isPlainObject(floor) && + !isNaN(floor.floor) && + floor.currency === DEFAULT_CURRENCY + ) { + return floor.floor; + } + } + return null; +} + +const converter = ortbConverter({ + context: { + netRevenue: true, // Default net revenue configuration + ttl: 100, // Default time-to-live for bid responses + }, + imp, + request, +}); + +function request(buildRequest, imps, bidderRequest, context) { + let request = buildRequest(imps, bidderRequest, context); + + // Add publisher ID + deepSetValue(request, 'site.publisher.id', context.publisherId); + return request; +} + +function imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + const floor = getBidFloor(bidRequest); + imp.tagid = bidRequest.params.placementId; + + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = DEFAULT_CURRENCY; + } + + return imp; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + + // Validate bid request + isBidRequestValid: function (bid) { + return !!bid.params.placementId && !!bid.params.publisherId; + }, + + // Build OpenRTB requests using `ortbConverter` + buildRequests: function (validBidRequests, bidderRequest) { + const context = { + publisherId: validBidRequests.find( + (bidRequest) => bidRequest.params?.publisherId + )?.params.publisherId, + }; + + const ortbRequest = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest, + context, + }); + + // Add extensions to the request + ortbRequest.ext = ortbRequest.ext || {}; + deepSetValue(ortbRequest, 'ext.gvlid', GVLID); + + return [ + { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(ortbRequest), + options: { + contentType: 'text/plain', + withCredentials: true, + }, + }, + ]; + }, + + interpretResponse: (serverResponse) => { + if (!serverResponse || isEmpty(serverResponse.body)) return []; + + let bids = []; + serverResponse.body.seatbid.forEach((response) => { + response.bid.forEach((bid) => { + const mediaType = bid.ext?.mediaType || 'banner'; + bids.push({ + ad: replaceAuctionPrice(bid.adm, bid.price), + adapterCode: BIDDER_CODE, + cpm: bid.price, + creativeId: bid.ext.bms.adId, + currency: serverResponse.body.cur || 'USD', + deferBilling: false, + deferRendering: false, + width: bid.w, + height: bid.h, + mediaType, + netRevenue: true, + originalCpm: bid.price, + originalCurrency: serverResponse.body.cur || 'USD', + requestId: bid.impid, + seatBidId: bid.id, + ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL, + nurl: bid.nurl || null, + burl: bid.burl || null, + meta: { + advertiserDomains: bid.adomain || [], + networkId: bid.ext?.networkId || 1105, + networkName: bid.ext?.networkName || 'BMS', + } + }); + }); + }); + return bids; + }, + + onBidWon: function (bid) { + const { burl, nurl } = bid || {}; + if (nurl) { + triggerPixel(nurl); + } + + if (burl) { + triggerPixel(burl); + } + }, +}; + +registerBidder(spec); diff --git a/modules/bmsBidAdapter.md b/modules/bmsBidAdapter.md new file mode 100644 index 00000000000..f6a04c5402a --- /dev/null +++ b/modules/bmsBidAdapter.md @@ -0,0 +1,48 @@ +# Overview + +Module Name: bms Bidder Adapter +Module Type: Bidder Adapter +Maintainer: celsooliveira@getbms.io + +# Description + +Module that connects to bms's demand sources. + +# Test Parameters + +``` + var adUnits = [ + { + code: "test-div", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + floors: { + currency: "USD", + schema: { + delimiter: "|", + fields: ["mediaType", "size"], + }, + values: { + "banner|300x250": 1.1, + "banner|300x600": 1.35, + "banner|*": 2, + }, + }, + bids: [ + { + bidder: "bms", + params: { + placementId: 13144370, + publisherId: 13144370, + }, + }, + ], + }, + ]; +``` diff --git a/modules/boldwinBidAdapter.js b/modules/boldwinBidAdapter.js index c7def383b5e..1cf3bf889b7 100644 --- a/modules/boldwinBidAdapter.js +++ b/modules/boldwinBidAdapter.js @@ -1,175 +1,38 @@ -import { isFn, deepAccess, logMessage } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { + isBidRequestValid, + buildRequestsBase, + interpretResponse, + getUserSyncs, + buildPlacementProcessingFunction, +} from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'boldwin'; const AD_URL = 'https://ssp.videowalldirect.com/pbjs'; const SYNC_URL = 'https://sync.videowalldirect.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; +const addCustomFieldsToPlacement = (bid, bidderRequest, placement) => { + if (placement.adFormat === VIDEO) { + placement.wPlayer = placement.playerSize?.[0]?.[0]; + placement.hPlayer = placement.playerSize?.[0]?.[1]; } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl); - case NATIVE: - return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); - default: - return false; - } -} +}; -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } +const placementProcessingFunction = buildPlacementProcessingFunction({ addCustomFieldsToPlacement }); - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + return buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest, placementProcessingFunction }); +}; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && (bid.params.placementId || bid.params.endpointId)); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let winTop = window; - let location; - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page); - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - let placements = []; - let request = { - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - 'secure': 1, - 'host': location.host, - 'page': location.pathname, - 'placements': placements - }; - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent; - } - - // Add GPP consent - if (bidderRequest.gppConsent) { - request.gpp = bidderRequest.gppConsent.gppString; - request.gpp_sid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - request.gpp = bidderRequest.ortb2.regs.gpp; - request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - } - const len = validBidRequests.length; - - for (let i = 0; i < len; i++) { - let bid = validBidRequests[i]; - const { mediaTypes, params } = bid; - const placement = {}; - let sizes; - if (mediaTypes) { - if (mediaTypes[BANNER] && mediaTypes[BANNER].sizes) { - placement.adFormat = BANNER; - sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize) { - placement.adFormat = VIDEO; - sizes = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else { - placement.adFormat = NATIVE; - placement.native = mediaTypes[NATIVE]; - } - } - - const { placementId, endpointId } = params; - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - placements.push({ - ...placement, - bidId: bid.bidId, - sizes: sizes || [], - wPlayer: sizes ? sizes[0] : 0, - hPlayer: sizes ? sizes[1] : 0, - schain: bid.schain || {}, - bidFloor: getBidFloor(bid), - }); - } - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: () => { - return [{ - type: 'image', - url: SYNC_URL - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests, + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/brainxBidAdapter.js b/modules/brainxBidAdapter.js new file mode 100644 index 00000000000..69832987fb7 --- /dev/null +++ b/modules/brainxBidAdapter.js @@ -0,0 +1,117 @@ +import { deepAccess, generateUUID, isArray, logWarn } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +// import { config } from 'src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +// import { config } from '../src/config.js'; + +const BIDDER_CODE = 'brainx'; +const METHOD = 'POST'; +const TTL = 200; +const NET_REV = true; +let ENDPOINT = 'https://dsp.brainx.tech/bid' +// let ENDPOINT = 'http://adx-engine-gray.tec-do.cn/bid' + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: NET_REV, // or false if your adapter should set bidResponse.netRevenue = false + ttl: TTL // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + } +}); + +export const spec = { + code: BIDDER_CODE, + // gvlid: IAB_GVL_ID, + // aliases: [ + // { code: "myalias", gvlid: IAB_GVL_ID_IF_DIFFERENT } + // ], + isBidRequestValid: function (bid) { + if (!(hasBanner(bid) || hasVideo(bid))) { + logWarn('Invalid bid request - missing required mediaTypes'); + return false; + } + if (!(bid && bid.params)) { + logWarn('Invalid bid request - missing required bid data'); + return false; + } + + if (!(bid.params.pubId)) { + logWarn('Invalid bid request - missing required field pubId'); + return false; + } + return true; + }, + buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({ bidRequests, bidderRequest }) + ENDPOINT = String(deepAccess(bidRequests[0], 'params.endpoint')) ? deepAccess(bidRequests[0], 'params.endpoint') : ENDPOINT + data.user = { + buyeruid: generateUUID() + } + return { + method: METHOD, + url: `${ENDPOINT}?token=${String(deepAccess(bidRequests[0], 'params.pubId'))}`, + data + } + }, + interpretResponse(response, request) { + let bids = []; + if (response.body && response.body.seatbid && isArray(response.body.seatbid)) { + response.body.seatbid.forEach(function (bidder) { + if (isArray(bidder.bid)) { + bidder.bid.map((bid) => { + let serverBody = response.body; + // bidRequest = request.originalBidRequest, + let mediaType = BANNER; + let currency = serverBody.cur || 'USD' + + const cpm = (parseFloat(bid.price) || 0).toFixed(2); + const categories = deepAccess(bid, 'cat', []); + + const bidRes = { + ad: bid.adm, + width: bid.w, + height: bid.h, + requestId: bid.impid, + cpm: cpm, + currency: currency, + mediaType: mediaType, + ttl: TTL, + creativeId: bid.crid || bid.id, + netRevenue: NET_REV, + nurl: bid.nurl, + lurl: bid.lurl, + meta: { + mediaType: mediaType, + primaryCatId: categories[0], + secondaryCatIds: categories.slice(1), + } + }; + if (bid.adomain && isArray(bid.adomain) && bid.adomain.length > 0) { + bidRes.meta.advertiserDomains = bid.adomain; + bidRes.meta.clickUrl = bid.adomain[0]; + } + bids.push(bidRes); + }) + } + }); + } + + return bids; + }, + // getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { }, + // onTimeout: function (timeoutData) { }, + // onBidWon: function (bid) { }, + // onSetTargeting: function (bid) { }, + // onBidderError: function ({ error, bidderRequest }) { }, + // onAdRenderSucceeded: function (bid) { }, + supportedMediaTypes: [BANNER] +} +function hasBanner(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.banner'); +} +function hasVideo(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.video'); +} + +registerBidder(spec); diff --git a/modules/brainxBidAdapter.md b/modules/brainxBidAdapter.md new file mode 100644 index 00000000000..a734147321b --- /dev/null +++ b/modules/brainxBidAdapter.md @@ -0,0 +1,56 @@ +# brianx Bidder Adapter + +## Overview + +``` +Module Name: brianx Bidder Adapter +Module Type: Bidder Adapter +Maintainer: brainx.official@tec-do.com +``` + +## Description + +Module that connects to brianx's demand sources + +## Bid Parameters + +| Name | Scope | Type | Description | Example | +| ---------- | ------------ | ------ | ------------------------------------ | -------------------------------------- | +| `pubId` | required | String | The Pub Id provided by Brainx Ads. | `F7B53DBC-85C1-4685-9A06-9CF4B6261FA3` | +| `endpoint` | optional | String | The endpoint provided by Brainx Url. | `https://dsp.brainx.tech/bid` | + +## Example + +### Banner Ads + +```javascript +var adUnits = [{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [ + [320, 250], + [320, 480] + ] + } + }, + bids: [{ + bidder: 'brianx', + params: { + pubId: 'F7B53DBC-85C1-4685-9A06-9CF4B6261FA3', + endpoint: 'https://dsp.brainx.tech/bid' + } + }] +}]; +``` + +* For video ads, enable prebid cache. + +```javascript +pbjs.setConfig({ + ortb2: { + ortbVersion: '2.5' + }, + debug: false // or true +}); +``` diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index b463a5480f8..7502a579745 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -10,6 +10,7 @@ import { deepAccess, deepSetValue, logError, mergeDeep, generateUUID } from '../ import { loadExternalScript } from '../src/adloader.js'; import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -140,7 +141,7 @@ function initializeBrandmetrics(scriptId) { const file = scriptId + '.js' const url = path + file - loadExternalScript(url, MODULE_CODE) + loadExternalScript(url, MODULE_TYPE_RTD, MODULE_CODE) } } diff --git a/modules/braveBidAdapter.js b/modules/braveBidAdapter.js index 4c5448482db..23fdfd43d7f 100644 --- a/modules/braveBidAdapter.js +++ b/modules/braveBidAdapter.js @@ -1,8 +1,8 @@ -import {isEmpty, isStr, parseUrl, triggerPixel} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import { isStr, triggerPixel } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { parseNative } from '../libraries/braveUtils/index.js'; +import { buildRequests, interpretResponse } from '../libraries/braveUtils/buildAndInterpret.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -11,162 +11,17 @@ import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; const BIDDER_CODE = 'brave'; const DEFAULT_CUR = 'USD'; -const ENDPOINT_URL = `https://point.bravegroup.tv/?t=2&partner=hash`; - -const NATIVE_ASSETS_IDS = { 1: 'title', 2: 'icon', 3: 'image', 4: 'body', 5: 'sponsoredBy', 6: 'cta' }; -const NATIVE_ASSETS = { - title: { id: 1, name: 'title' }, - icon: { id: 2, type: 1, name: 'img' }, - image: { id: 3, type: 3, name: 'img' }, - body: { id: 4, type: 2, name: 'data' }, - sponsoredBy: { id: 5, type: 1, name: 'data' }, - cta: { id: 6, type: 12, name: 'data' } -}; +const ENDPOINT_URL = `https://point.braveglobal.tv/?t=2&partner=hash`; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - /** - * Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: (bid) => { - return !!(bid.params.placementId && bid.params.placementId.toString().length === 32); - }, - - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - if (validBidRequests.length === 0 || !bidderRequest) return []; - - const endpointURL = ENDPOINT_URL.replace('hash', validBidRequests[0].params.placementId); - - let imp = validBidRequests.map(br => { - let impObject = { - id: br.bidId, - secure: 1 - }; - - if (br.mediaTypes.banner) { - impObject.banner = createBannerRequest(br); - } else if (br.mediaTypes.video) { - impObject.video = createVideoRequest(br); - } else if (br.mediaTypes.native) { - impObject.native = { - // TODO: `id` is not part of the ORTB native spec, is this intentional? - id: br.bidId, - ver: '1.2', - request: createNativeRequest(br) - }; - } - return impObject; - }); - - // TODO: do these values make sense? - let page = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - let r = bidderRequest.refererInfo.ref; - - let data = { - id: bidderRequest.bidderRequestId, - cur: [ DEFAULT_CUR ], - device: { - w: screen.width, - h: screen.height, - language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', - ua: navigator.userAgent, - }, - site: { - domain: parseUrl(page).hostname, - page: page, - }, - tmax: bidderRequest.timeout, - imp - }; - - if (r) { - data.site.ref = r; - } - - if (bidderRequest.gdprConsent) { - data['regs'] = {'ext': {'gdpr': bidderRequest.gdprConsent.gdprApplies ? 1 : 0}}; - data['user'] = {'ext': {'consent': bidderRequest.gdprConsent.consentString ? bidderRequest.gdprConsent.consentString : ''}}; - } - - if (bidderRequest.uspConsent !== undefined) { - if (!data['regs'])data['regs'] = {'ext': {}}; - data['regs']['ext']['us_privacy'] = bidderRequest.uspConsent; - } - - if (config.getConfig('coppa') === true) { - if (!data['regs'])data['regs'] = {'coppa': 1}; - else data['regs']['coppa'] = 1; - } - - if (validBidRequests[0].schain) { - data['source'] = {'ext': {'schain': validBidRequests[0].schain}}; - } - - return { - method: 'POST', - url: endpointURL, - data: data - }; - }, - - /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: (serverResponse) => { - if (!serverResponse || isEmpty(serverResponse.body)) return []; - - let bids = []; - serverResponse.body.seatbid.forEach(response => { - response.bid.forEach(bid => { - let mediaType = bid.ext && bid.ext.mediaType ? bid.ext.mediaType : 'banner'; - - let bidObj = { - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - ttl: 1200, - currency: DEFAULT_CUR, - netRevenue: true, - creativeId: bid.crid, - dealId: bid.dealid || null, - mediaType: mediaType - }; - - switch (mediaType) { - case 'video': - bidObj.vastUrl = bid.adm; - break; - case 'native': - bidObj.native = parseNative(bid.adm); - break; - default: - bidObj.ad = bid.adm; - } + isBidRequestValid: (bid) => !!(bid.params.placementId && bid.params.placementId.toString().length === 32), - bids.push(bidObj); - }); - }); + buildRequests: (validBidRequests, bidderRequest) => buildRequests(validBidRequests, bidderRequest, ENDPOINT_URL, DEFAULT_CUR), - return bids; - }, + interpretResponse: (serverResponse) => interpretResponse(serverResponse, DEFAULT_CUR, parseNative), onBidWon: (bid) => { if (isStr(bid.nurl) && bid.nurl !== '') { @@ -175,90 +30,4 @@ export const spec = { } }; -const parseNative = adm => { - let bid = { - clickUrl: adm.native.link && adm.native.link.url, - impressionTrackers: adm.native.imptrackers || [], - clickTrackers: (adm.native.link && adm.native.link.clicktrackers) || [], - jstracker: adm.native.jstracker || [] - }; - adm.native.assets.forEach(asset => { - let kind = NATIVE_ASSETS_IDS[asset.id]; - let content = kind && asset[NATIVE_ASSETS[kind].name]; - if (content) { - bid[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; - } - }); - - return bid; -} - -const createNativeRequest = br => { - let impObject = { - ver: '1.2', - assets: [] - }; - - let keys = Object.keys(br.mediaTypes.native); - - for (let key of keys) { - const props = NATIVE_ASSETS[key]; - if (props) { - const asset = { - required: br.mediaTypes.native[key].required ? 1 : 0, - id: props.id, - [props.name]: {} - }; - - if (props.type) asset[props.name]['type'] = props.type; - if (br.mediaTypes.native[key].len) asset[props.name]['len'] = br.mediaTypes.native[key].len; - if (br.mediaTypes.native[key].sizes && br.mediaTypes.native[key].sizes[0]) { - asset[props.name]['w'] = br.mediaTypes.native[key].sizes[0]; - asset[props.name]['h'] = br.mediaTypes.native[key].sizes[1]; - } - - impObject.assets.push(asset); - } - } - - return impObject; -} - -const createBannerRequest = br => { - let size = []; - - if (br.mediaTypes.banner.sizes && Array.isArray(br.mediaTypes.banner.sizes)) { - if (Array.isArray(br.mediaTypes.banner.sizes[0])) { size = br.mediaTypes.banner.sizes[0]; } else { size = br.mediaTypes.banner.sizes; } - } else size = [300, 250]; - - return { id: br.transactionId, w: size[0], h: size[1] }; -}; - -const createVideoRequest = br => { - // TODO: `id` is not part of imp.video in ORTB; is this intentional? - let videoObj = {id: br.bidId}; - let supportParamsList = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'skip', 'minbitrate', 'maxbitrate', 'api', 'linearity']; - - for (let param of supportParamsList) { - if (br.mediaTypes.video[param] !== undefined) { - videoObj[param] = br.mediaTypes.video[param]; - } - } - - if (br.mediaTypes.video.playerSize && Array.isArray(br.mediaTypes.video.playerSize)) { - if (Array.isArray(br.mediaTypes.video.playerSize[0])) { - videoObj.w = br.mediaTypes.video.playerSize[0][0]; - videoObj.h = br.mediaTypes.video.playerSize[0][1]; - } else { - videoObj.w = br.mediaTypes.video.playerSize[0]; - videoObj.h = br.mediaTypes.video.playerSize[1]; - } - } else { - videoObj.w = 640; - videoObj.h = 480; - } - - return videoObj; -} - registerBidder(spec); diff --git a/modules/bridBidAdapter.js b/modules/bridBidAdapter.js index e784ea517ac..c9840ad57f8 100644 --- a/modules/bridBidAdapter.js +++ b/modules/bridBidAdapter.js @@ -1,27 +1,17 @@ -import {createTrackPixelHtml, _each, deepAccess, getDefinedParams, parseGPTSingleSizeArrayToRtbSize} from '../src/utils.js'; +import {_each, deepAccess, getDefinedParams, parseGPTSingleSizeArrayToRtbSize} from '../src/utils.js'; import {VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getRefererInfo} from '../src/refererDetection.js'; +import {getAd, getSiteObj, getSyncResponse} from '../libraries/targetVideoUtils/bidderUtils.js' +import {GVLID, SOURCE, TIME_TO_LIVE, VIDEO_ENDPOINT_URL, VIDEO_PARAMS} from '../libraries/targetVideoUtils/constants.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest */ - -const SOURCE = 'pbjs'; -const BIDDER_CODE = 'brid'; -const ENDPOINT_URL = 'https://pbs.prebrid.tv/openrtb2/auction'; -const GVLID = 934; -const TIME_TO_LIVE = 300; -const VIDEO_PARAMS = [ - 'api', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', - 'playbackmethod', 'protocols', 'startdelay' -]; - export const spec = { - code: BIDDER_CODE, + code: 'brid', gvlid: GVLID, supportedMediaTypes: [VIDEO], @@ -110,14 +100,16 @@ export const spec = { }; if (bidRequests[0].schain) { - postBody.schain = bidRequests[0].schain; + postBody.source = { + ext: { schain: bidRequests[0].schain } + }; } const params = bid.params; requests.push({ method: 'POST', - url: ENDPOINT_URL, + url: VIDEO_ENDPOINT_URL, data: JSON.stringify(postBody), options: { withCredentials: true @@ -138,92 +130,59 @@ export const spec = { */ interpretResponse: function(serverResponse, bidRequest) { const response = serverResponse.body; - const bidResponses = []; - - _each(response.seatbid, (resp) => { - _each(resp.bid, (bid) => { - const requestId = bidRequest.bidId; - const params = bidRequest.params; - - const {ad, adUrl, vastUrl, vastXml} = getAd(bid); - - const bidResponse = { - requestId, - params, - cpm: bid.price, - width: bid.w, - height: bid.h, - creativeId: bid.adid, - currency: response.cur, - netRevenue: false, - ttl: TIME_TO_LIVE, - meta: { - advertiserDomains: bid.adomain || [] - } - }; + let highestBid = null; + + if (response && response.seatbid && response.seatbid.length && response.seatbid[0].bid && response.seatbid[0].bid.length) { + _each(response.seatbid, (resp) => { + _each(resp.bid, (bid) => { + const requestId = bidRequest.bidId; + const params = bidRequest.params; + + const {ad, adUrl, vastUrl, vastXml} = getAd(bid); + + const bidResponse = { + requestId, + params, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.adid, + currency: response.cur, + netRevenue: false, + ttl: TIME_TO_LIVE, + meta: { + advertiserDomains: bid.adomain || [] + } + }; - if (vastUrl || vastXml) { - bidResponse.mediaType = VIDEO; - if (vastUrl) bidResponse.vastUrl = vastUrl; - if (vastXml) bidResponse.vastXml = vastXml; - } else { - bidResponse.ad = ad; - bidResponse.adUrl = adUrl; - }; + if (vastUrl || vastXml) { + bidResponse.mediaType = VIDEO; + if (vastUrl) bidResponse.vastUrl = vastUrl; + if (vastXml) bidResponse.vastXml = vastXml; + } else { + bidResponse.ad = ad; + bidResponse.adUrl = adUrl; + }; - bidResponses.push(bidResponse); + if (!highestBid || highestBid.cpm < bidResponse.cpm) { + highestBid = bidResponse; + } + }); }); - }); + } - return bidResponses; + return highestBid ? [highestBid] : []; }, -} - -/** - * Helper function to get ad - * - * @param {object} bid The bid. - * @return {object} ad object. - */ -function getAd(bid) { - let ad, adUrl, vastXml, vastUrl; - - switch (deepAccess(bid, 'ext.prebid.type')) { - case VIDEO: - if (bid.adm.substr(0, 4) === 'http') { - vastUrl = bid.adm; - } else { - vastXml = bid.adm; - }; - break; - default: - if (bid.adm && bid.nurl) { - ad = bid.adm; - ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); - } else if (bid.adm) { - ad = bid.adm; - } else if (bid.nurl) { - adUrl = bid.nurl; - }; + /** + * Determine the user sync type (either 'iframe' or 'image') based on syncOptions. + * Construct the sync URL by appending required query parameters such as gdpr, ccpa, and coppa consents. + * Return an array containing an object with the sync type and the constructed URL. + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + return getSyncResponse(syncOptions, gdprConsent, uspConsent, gppConsent, 'brid'); } - return {ad, adUrl, vastXml, vastUrl}; -} - -/** - * Helper function to get site object - * - * @return {object} siteObj. - */ -function getSiteObj() { - const refInfo = (getRefererInfo && getRefererInfo()) || {}; - - return { - page: refInfo.page, - ref: refInfo.ref, - domain: refInfo.domain - }; } registerBidder(spec); diff --git a/modules/bridgeuppBidAdapter.js b/modules/bridgeuppBidAdapter.js new file mode 100644 index 00000000000..96ffb88cef7 --- /dev/null +++ b/modules/bridgeuppBidAdapter.js @@ -0,0 +1,289 @@ +import {deepSetValue, isFn, 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 {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').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid + */ + +export const ADAPTER_VERSION = 1.0; +export const BIDDER_CODE = 'sonarads'; +export const GVLID = 1300; +export const DEFAULT_CUR = 'USD'; +// export const SERVER_PATH_US1_BID = 'http://localhost:8000/analyze_request/bids'; +// export const SERVER_PATH_US1_EVENTS = 'http://localhost:8000/analyze_request/events'; +// export const SERVER_PATH_US1_SYNC = 'http://localhost:8000/analyze_request/sync'; +export const SERVER_PATH_US1_BID = 'https://prebidjs-bids-us1.sonar-ads.com/analyze_request/bids'; +export const SERVER_PATH_US1_EVENTS = 'https://prebidjs-events-us1.sonar-ads.com/events'; +export const SERVER_PATH_US1_SYNC = 'https://prebidjs-sync-us1.sonar-ads.com/sync'; + +/** + * Bridgeupp : Report events for analytics and debuging. + */ +function reportEvents(eventType, eventData) { + if (!eventData || spec?.reportEventsEnabled !== true) { + return; + } + + const payload = JSON.stringify({ + domain: location.hostname, + prebidVersion: '$prebid.version$', + eventType: eventType, + eventPayload: eventData + }); + + fetch(`${SERVER_PATH_US1_EVENTS}`, { + body: payload, + keepalive: true, + credentials: 'include', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }).catch((_e) => { + // ignore errors for now + }); +} + +/** + * Bridgeupp : Defines the core oRTB converter inherited from converter library and all customization functions. + */ +const CONVERTER = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: DEFAULT_CUR + }, + imp, + request, + bidResponse, + response +}); + +/** + * Bridgeupp : Builds an impression object for oRTB requests based on the bid request. + * + * @param {function} buildImp - Function to build the imp object. + * @param {Object} bidRequest - The request containing bid details. + * @param {Object} context - Context for the impression. + * @returns {Object} The constructed impression object. + */ +function imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + const params = bidRequest.params; + + imp.tagid = bidRequest.adUnitCode; + let floorInfo = {}; + + if (isFn(bidRequest.getFloor)) { + floorInfo = bidRequest.getFloor(); + } + + // if floor price module is not set reading from bidRequest.params or default + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0.001; + imp.bidfloorcur = DEFAULT_CUR; + } + + imp.secure = bidRequest.ortb2Imp?.secure ?? 1; + deepSetValue(imp, 'ext', { + ...imp.ext, + params: bidRequest.params, + bidder: { + siteId: params?.siteId, + }, + floorInfo: floorInfo + }); + + return imp; +} + +/** + * Bridgeupp: Constructs the request object. + * + * @param {function} buildRequest - Function to build the request. + * @param {Array} imps - Array of impression objects. + * @param {Object} bidderRequest - Object containing bidder request information. + * @param {Object} context - Additional context. + * @returns {Object} The complete oRTB request object. + */ +function request(buildRequest, imps, bidderRequest, context) { + let request = buildRequest(imps, bidderRequest, context); + const siteId = context.bidRequests[0]?.params?.siteId; + + deepSetValue(request, 'auctionStart', bidderRequest.auctionStart); + deepSetValue(request, 'ext.prebid.channel', { + name: 'pbjs_bridgeupp', + pbjsversion: '$prebid.version$', + adapterversion: ADAPTER_VERSION, + siteId: siteId + }); + + return request; +} + +function bidResponse(buildBidResponse, bid, context) { + return buildBidResponse(bid, context); +} + +/** + * Bridgeupp bid response + * + * @param {function} buildResponse - Function to build the response. + * @param {Array} bidResponses - List of bid responses. + * @param {Object} ortbResponse - Original oRTB response data. + * @param {Object} context - Additional context. + * @returns {Object} Prebid.js compatible bid response. + */ +function response(buildResponse, bidResponses, ortbResponse, context) { + return buildResponse(bidResponses, ortbResponse, context); +} + +export const spec = { + reportEventsEnabled: false, + code: BIDDER_CODE, + aliases: ['bridgeupp'], + gvlid: GVLID, + supportedMediaTypes: [BANNER], + + /** + * Bridgeupp : Determines whether the given bid request is valid. + * + * @param {Object} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: (bid) => { + if (!bid || bid.bidder !== BIDDER_CODE || !bid.params.siteId) { + logWarn('Bridgeupp - bid is not valid, reach out to support@bridgeupp.com'); + return false; + } + return true; + }, + + /** + * Bridgeupp: Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @param {Object} bidderRequest - Additional request details. + * @return {ServerRequest} Info describing the request to the server. + */ + buildRequests: (validBidRequests, bidderRequest) => { + const data = CONVERTER.toORTB({ bidderRequest: bidderRequest, bidRequests: validBidRequests, }); + + if (data) { + return { + method: 'POST', + url: SERVER_PATH_US1_BID, + data: data, + options: { + contentType: 'application/json', + crossOrigin: true, + withCredentials: true + } + }; + } + }, + + /** + * Bridgeupp: Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {ServerRequest} bidRequest - Original bid request. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + if (typeof serverResponse?.body == 'undefined') { + return []; + } + + // reportEventsEnabled is returned from the server default false + spec.reportEventsEnabled = serverResponse.headers.get('reportEventsEnabled') > 0 + + const interpretedResponse = CONVERTER.fromORTB({ response: serverResponse.body, request: bidRequest.data }); + return interpretedResponse.bids || []; + }, + + /** + * Bridgeupp : User sync options based on consent, support only iframe for now. + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + logWarn('Bridgeupp - Bidder ConnectAd: No User-Matching allowed'); + return []; + } + + let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SERVER_PATH_US1_SYNC + '?'; + + syncUrl = gdprConsent ? tryAppendQueryString(syncUrl, 'gdpr', gdprConsent.gdprApplies ? 1 : 0) : syncUrl; + syncUrl = gdprConsent?.consentString ? tryAppendQueryString(syncUrl, 'gdpr_consent', gdprConsent.consentString) : syncUrl; + syncUrl = uspConsent ? tryAppendQueryString(syncUrl, 'us_privacy', uspConsent) : syncUrl; + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + syncUrl = tryAppendQueryString(syncUrl, 'gpp', gppConsent.gppString); + syncUrl = tryAppendQueryString(syncUrl, 'gpp_sid', gppConsent.applicableSections.join(',')); + } + + if ((syncUrl.slice(-1) === '&') || (syncUrl.slice(-1) === '?')) { + syncUrl = syncUrl.slice(0, -1); + } + + return [{ + type: pixelType, + url: syncUrl + }]; + }, + + /** + * Bridgeupp: Callback to report timeout event. + * + * @param {TimedOutBid[]} timeoutData - Array of timeout details. + */ + onTimeout: (timeoutData) => { + reportEvents('onTimeout', timeoutData); + }, + + /** + * Bridgeupp: Callback to report targeting event. + * + * @param {Bid} bid - The bid object + */ + onSetTargeting: (bid) => { + reportEvents('onSetTargeting', bid); + }, + + /** + * Bridgeupp: Callback to report successful ad render event. + * + * @param {Bid} bid - The bid that successfully rendered. + */ + onAdRenderSucceeded: (bid) => { + reportEvents('onAdRenderSucceeded', bid); + }, + + /** + * Bridgeupp: Callback to report bidder error event. + * + * @param {Object} errorData - Details about the error. + */ + onBidderError: (errorData) => { + reportEvents('onBidderError', errorData); + }, + + /** + * Bridgeupp: Callback to report bid won event. + * + * @param {Bid} bid - The bid that won the auction. + */ + onBidWon: (bid) => { + reportEvents('onBidWon', bid); + } + +}; + +registerBidder(spec); diff --git a/modules/bridgeuppBidAdapter.md b/modules/bridgeuppBidAdapter.md new file mode 100644 index 00000000000..88d447b308a --- /dev/null +++ b/modules/bridgeuppBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +Module Name: Bridgeupp Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@bridgeupp.com + +# Description + +Module that connects to Bridgeupp's demand sources. + +# Bid Params + +| Name | Scope | Description | Example | Type | +|---------------|----------|----------------------|----------|----------| +| `siteId` | required | Unique site id | `'1234'` | `string` | +| `bidfloor` | optional | Minimum price in USD | `'1.50'` | `float` | + +# Test Parameters + +## Banner +``` + var adUnits = [{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 336]] + } + }, + + bids: [{ + bidder: 'sonarads', + params: { + siteId: 'site-id-example-132', // siteId provided by Bridgeupp + bidfloor: 0.01 + } + }] + + }]; +``` diff --git a/modules/brightMountainMediaBidAdapter.js b/modules/brightMountainMediaBidAdapter.js index 6db06744c24..5e5b062889d 100644 --- a/modules/brightMountainMediaBidAdapter.js +++ b/modules/brightMountainMediaBidAdapter.js @@ -1,4 +1,4 @@ -import { generateUUID, deepAccess, logWarn, deepSetValue } from '../src/utils.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'; import { config } from '../src/config.js'; @@ -31,7 +31,7 @@ export const spec = { site: buildSite(bidderRequest), device: buildDevice(), cur: [CURRENCY], - tmax: 1000, + tmax: Math.min(1000, bidderRequest.timeout), regs: buildRegs(bidderRequest), user: {}, source: {}, @@ -224,7 +224,7 @@ function getFloor(bid, size) { size: size, }); - if (typeof floorInfo === 'object' && floorInfo.currency === 'USD') { + if (isPlainObject(floorInfo) && floorInfo.currency === 'USD') { return parseFloat(floorInfo.floor); } } diff --git a/modules/brightcomBidAdapter.js b/modules/brightcomBidAdapter.js deleted file mode 100644 index 1fa1dac4e95..00000000000 --- a/modules/brightcomBidAdapter.js +++ /dev/null @@ -1,303 +0,0 @@ -import { - _each, - isArray, - getWindowTop, - getUniqueIdentifierStr, - deepSetValue, - logError, - logWarn, - createTrackPixelHtml, - getWindowSelf, - isFn, - isPlainObject, - getBidIdParameter -} from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; - -const BIDDER_CODE = 'brightcom'; -const URL = 'https://brightcombid.marphezis.com/hb'; - -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER], - gvlid: 883, - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs -}; - -function buildRequests(bidReqs, bidderRequest) { - try { - let referrer = ''; - if (bidderRequest && bidderRequest.refererInfo) { - referrer = bidderRequest.refererInfo.page; - } - const brightcomImps = []; - const publisherId = getBidIdParameter('publisherId', bidReqs[0].params); - _each(bidReqs, function (bid) { - let bidSizes = (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) || bid.sizes; - bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]); - bidSizes = bidSizes.filter(size => isArray(size)); - const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})); - - const element = document.getElementById(bid.adUnitCode); - const minSize = _getMinSize(processedSizes); - const viewabilityAmount = _isViewabilityMeasurable(element) - ? _getViewability(element, getWindowTop(), minSize) - : 'na'; - const viewabilityAmountRounded = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); - - const imp = { - id: bid.bidId, - banner: { - format: processedSizes, - ext: { - viewability: viewabilityAmountRounded - } - }, - tagid: String(bid.adUnitCode) - }; - const bidFloor = _getBidFloor(bid); - if (bidFloor) { - imp.bidfloor = bidFloor; - } - brightcomImps.push(imp); - }); - const brightcomBidReq = { - id: getUniqueIdentifierStr(), - imp: brightcomImps, - site: { - domain: bidderRequest?.refererInfo?.domain || '', - page: referrer, - publisher: { - id: publisherId - } - }, - device: { - devicetype: _getDeviceType(), - w: screen.width, - h: screen.height - }, - tmax: bidderRequest?.timeout - }; - - if (bidderRequest && bidderRequest.gdprConsent) { - deepSetValue(brightcomBidReq, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); - deepSetValue(brightcomBidReq, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - } - - if (bidderRequest && bidderRequest.uspConsent) { - deepSetValue(brightcomBidReq, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (config.getConfig('coppa') === true) { - deepSetValue(brightcomBidReq, 'regs.coppa', 1); - } - - if (bidReqs[0] && bidReqs[0].schain) { - deepSetValue(brightcomBidReq, 'source.ext.schain', bidReqs[0].schain) - } - - if (bidReqs[0] && bidReqs[0].userIdAsEids) { - deepSetValue(brightcomBidReq, 'user.ext.eids', bidReqs[0].userIdAsEids || []) - } - - if (bidReqs[0] && bidReqs[0].userId) { - deepSetValue(brightcomBidReq, 'user.ext.ids', bidReqs[0].userId || []) - } - - return { - method: 'POST', - url: URL, - data: JSON.stringify(brightcomBidReq), - }; - } catch (e) { - logError(e, {bidReqs, bidderRequest}); - } -} - -function isBidRequestValid(bid) { - if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { - return false; - } - - if (typeof bid.params.publisherId === 'undefined') { - return false; - } - - return true; -} - -function interpretResponse(serverResponse) { - if (!serverResponse.body || typeof serverResponse.body != 'object') { - logWarn('Brightcom server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); - return []; - } - const {body: {id, seatbid}} = serverResponse; - try { - const brightcomBidResponses = []; - if (id && - seatbid && - seatbid.length > 0 && - seatbid[0].bid && - seatbid[0].bid.length > 0) { - seatbid[0].bid.map(brightcomBid => { - brightcomBidResponses.push({ - requestId: brightcomBid.impid, - cpm: parseFloat(brightcomBid.price), - width: parseInt(brightcomBid.w), - height: parseInt(brightcomBid.h), - creativeId: brightcomBid.crid || brightcomBid.id, - currency: 'USD', - netRevenue: true, - mediaType: BANNER, - ad: _getAdMarkup(brightcomBid), - ttl: 60, - meta: { - advertiserDomains: brightcomBid && brightcomBid.adomain ? brightcomBid.adomain : [] - } - }); - }); - } - return brightcomBidResponses; - } catch (e) { - logError(e, {id, seatbid}); - } -} - -// Don't do user sync for now -function getUserSyncs(syncOptions, responses, gdprConsent) { - return []; -} - -function _isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); -} - -function _isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); -} - -function _getDeviceType() { - return _isMobile() ? 1 : _isConnectedTV() ? 3 : 2; -} - -function _getAdMarkup(bid) { - let adm = bid.adm; - if ('nurl' in bid) { - adm += createTrackPixelHtml(bid.nurl); - } - return adm; -} - -function _isViewabilityMeasurable(element) { - return !_isIframe() && element !== null; -} - -function _getViewability(element, topWin, {w, h} = {}) { - return getWindowTop().document.visibilityState === 'visible' - ? _getPercentInView(element, topWin, {w, h}) - : 0; -} - -function _isIframe() { - try { - return getWindowSelf() !== getWindowTop(); - } catch (e) { - return true; - } -} - -function _getMinSize(sizes) { - return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); -} - -function _getBoundingBox(element, {w, h} = {}) { - let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); - - if ((width === 0 || height === 0) && w && h) { - width = w; - height = h; - right = left + w; - bottom = top + h; - } - - return {width, height, left, top, right, bottom}; -} - -function _getIntersectionOfRects(rects) { - const bbox = { - left: rects[0].left, - right: rects[0].right, - top: rects[0].top, - bottom: rects[0].bottom - }; - - for (let i = 1; i < rects.length; ++i) { - bbox.left = Math.max(bbox.left, rects[i].left); - bbox.right = Math.min(bbox.right, rects[i].right); - - if (bbox.left >= bbox.right) { - return null; - } - - bbox.top = Math.max(bbox.top, rects[i].top); - bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); - - if (bbox.top >= bbox.bottom) { - return null; - } - } - - bbox.width = bbox.right - bbox.left; - bbox.height = bbox.bottom - bbox.top; - - return bbox; -} - -function _getPercentInView(element, topWin, {w, h} = {}) { - const elementBoundingBox = _getBoundingBox(element, {w, h}); - - // Obtain the intersection of the element and the viewport - const elementInViewBoundingBox = _getIntersectionOfRects([{ - left: 0, - top: 0, - right: topWin.innerWidth, - bottom: topWin.innerHeight - }, elementBoundingBox]); - - let elementInViewArea, elementTotalArea; - - if (elementInViewBoundingBox !== null) { - // Some or all of the element is in view - elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; - elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; - - return ((elementInViewArea / elementTotalArea) * 100); - } - - // No overlap between element and the viewport; therefore, the element - // lies completely out of view - return 0; -} - -function _getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return bid.params.bidFloor ? bid.params.bidFloor : null; - } - - let floor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { - return floor.floor; - } - return null; -} - -registerBidder(spec); diff --git a/modules/brightcomBidAdapter.md b/modules/brightcomBidAdapter.md deleted file mode 100644 index 9f9aa0e5dd7..00000000000 --- a/modules/brightcomBidAdapter.md +++ /dev/null @@ -1,46 +0,0 @@ -# Overview - -``` -Module Name: Brightcom Bid Adapter -Module Type: Bidder Adapter -Maintainer: vladislavy@brightcom.com -``` - -# Description - -Brightcom's adapter integration to the Prebid library. - -# Test Parameters - -``` -var adUnits = [ - { - code: 'test-leaderboard', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - bids: [{ - bidder: 'brightcom', - params: { - publisherId: 2141020, - bidFloor: 0.01 - } - }] - }, { - code: 'test-banner', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - }, - bids: [{ - bidder: 'brightcom', - params: { - publisherId: 2141020 - } - }] - } -] -``` diff --git a/modules/brightcomSSPBidAdapter.js b/modules/brightcomSSPBidAdapter.js deleted file mode 100644 index 4750881da40..00000000000 --- a/modules/brightcomSSPBidAdapter.js +++ /dev/null @@ -1,321 +0,0 @@ -import { - isArray, - getWindowTop, - getUniqueIdentifierStr, - deepSetValue, - logError, - logWarn, - createTrackPixelHtml, - getWindowSelf, - isFn, - isPlainObject, getBidIdParameter, -} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {ajax} from '../src/ajax.js'; - -const BIDDER_CODE = 'bcmssp'; -const URL = 'https://rt.marphezis.com/hb'; -const TRACK_EVENT_URL = 'https://rt.marphezis.com/prebid' - -export const spec = { - code: BIDDER_CODE, - gvlid: 883, - supportedMediaTypes: [BANNER], - isBidRequestValid, - buildRequests, - interpretResponse, - onBidderError, - onTimeout, - onBidWon, - getUserSyncs, -}; - -function buildRequests(bidReqs, bidderRequest) { - try { - const impressions = bidReqs.map(bid => { - let bidSizes = bid?.mediaTypes?.banner?.sizes || bid.sizes; - bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]); - bidSizes = bidSizes.filter(size => isArray(size)); - const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})); - - const element = document.getElementById(bid.adUnitCode); - const minSize = _getMinSize(processedSizes); - const viewabilityAmount = _isViewabilityMeasurable(element) ? _getViewability(element, getWindowTop(), minSize) : 'na'; - const viewabilityAmountRounded = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); - - const imp = { - id: bid.bidId, - banner: { - format: processedSizes, - ext: { - viewability: viewabilityAmountRounded - } - }, - tagid: String(bid.adUnitCode) - }; - - const bidFloor = _getBidFloor(bid); - - if (bidFloor) { - imp.bidfloor = bidFloor; - } - - return imp; - }) - - const referrer = bidderRequest?.refererInfo?.page || ''; - const publisherId = getBidIdParameter('publisherId', bidReqs[0].params); - - const payload = { - id: getUniqueIdentifierStr(), - imp: impressions, - site: { - domain: bidderRequest?.refererInfo?.domain || '', - page: referrer, - publisher: { - id: publisherId - } - }, - device: { - devicetype: _getDeviceType(), - w: screen.width, - h: screen.height - }, - tmax: bidderRequest?.timeout - }; - - if (bidderRequest?.gdprConsent) { - deepSetValue(payload, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); - deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - } - - if (bidderRequest?.uspConsent) { - deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (config.getConfig('coppa') === true) { - deepSetValue(payload, 'regs.coppa', 1); - } - - if (bidReqs?.[0]?.schain) { - deepSetValue(payload, 'source.ext.schain', bidReqs[0].schain) - } - - if (bidReqs?.[0]?.userIdAsEids) { - deepSetValue(payload, 'user.ext.eids', bidReqs[0].userIdAsEids || []) - } - - if (bidReqs?.[0].userId) { - deepSetValue(payload, 'user.ext.ids', bidReqs[0].userId || []) - } - - return { - method: 'POST', - url: URL, - data: JSON.stringify(payload), - }; - } catch (e) { - logError(e, {bidReqs, bidderRequest}); - } -} - -function isBidRequestValid(bid) { - if (bid.bidder !== BIDDER_CODE || !bid.params || !bid.params.publisherId) { - return false; - } - - return true; -} - -function interpretResponse(serverResponse) { - let response = []; - if (!serverResponse.body || typeof serverResponse.body != 'object') { - logWarn('Brightcom server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); - return response; - } - - const {body: {id, seatbid}} = serverResponse; - - try { - if (id && seatbid && seatbid.length > 0 && seatbid[0].bid && seatbid[0].bid.length > 0) { - response = seatbid[0].bid.map(bid => { - return { - requestId: bid.impid, - cpm: parseFloat(bid.price), - width: parseInt(bid.w), - height: parseInt(bid.h), - creativeId: bid.crid || bid.id, - currency: 'USD', - netRevenue: true, - mediaType: BANNER, - ad: _getAdMarkup(bid), - ttl: 60, - meta: { - advertiserDomains: bid?.adomain || [] - } - }; - }); - } - } catch (e) { - logError(e, {id, seatbid}); - } - - return response; -} - -// Don't do user sync for now -function getUserSyncs(syncOptions, responses, gdprConsent) { - return []; -} - -function onTimeout(timeoutData) { - if (timeoutData === null) { - return; - } - - _trackEvent('timeout', timeoutData); -} - -function onBidderError(errorData) { - if (errorData === null || !errorData.bidderRequest) { - return; - } - - _trackEvent('error', errorData.bidderRequest) -} - -function onBidWon(bid) { - if (bid === null) { - return; - } - - _trackEvent('bidwon', bid) -} - -function _trackEvent(endpoint, data) { - ajax(`${TRACK_EVENT_URL}/${endpoint}`, null, JSON.stringify(data), { - method: 'POST', - withCredentials: false - }); -} - -function _isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); -} - -function _isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); -} - -function _getDeviceType() { - return _isMobile() ? 1 : _isConnectedTV() ? 3 : 2; -} - -function _getAdMarkup(bid) { - let adm = bid.adm; - if ('nurl' in bid) { - adm += createTrackPixelHtml(bid.nurl); - } - return adm; -} - -function _isViewabilityMeasurable(element) { - return !_isIframe() && element !== null; -} - -function _getViewability(element, topWin, {w, h} = {}) { - return getWindowTop().document.visibilityState === 'visible' ? _getPercentInView(element, topWin, {w, h}) : 0; -} - -function _isIframe() { - try { - return getWindowSelf() !== getWindowTop(); - } catch (e) { - return true; - } -} - -function _getMinSize(sizes) { - return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); -} - -function _getBoundingBox(element, {w, h} = {}) { - let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); - - if ((width === 0 || height === 0) && w && h) { - width = w; - height = h; - right = left + w; - bottom = top + h; - } - - return {width, height, left, top, right, bottom}; -} - -function _getIntersectionOfRects(rects) { - const bbox = { - left: rects[0].left, right: rects[0].right, top: rects[0].top, bottom: rects[0].bottom - }; - - for (let i = 1; i < rects.length; ++i) { - bbox.left = Math.max(bbox.left, rects[i].left); - bbox.right = Math.min(bbox.right, rects[i].right); - - if (bbox.left >= bbox.right) { - return null; - } - - bbox.top = Math.max(bbox.top, rects[i].top); - bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); - - if (bbox.top >= bbox.bottom) { - return null; - } - } - - bbox.width = bbox.right - bbox.left; - bbox.height = bbox.bottom - bbox.top; - - return bbox; -} - -function _getPercentInView(element, topWin, {w, h} = {}) { - const elementBoundingBox = _getBoundingBox(element, {w, h}); - - // Obtain the intersection of the element and the viewport - const elementInViewBoundingBox = _getIntersectionOfRects([{ - left: 0, top: 0, right: topWin.innerWidth, bottom: topWin.innerHeight - }, elementBoundingBox]); - - let elementInViewArea, elementTotalArea; - - if (elementInViewBoundingBox !== null) { - // Some or all of the element is in view - elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; - elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; - - return ((elementInViewArea / elementTotalArea) * 100); - } - - // No overlap between element and the viewport; therefore, the element - // lies completely out of view - return 0; -} - -function _getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return bid.params.bidFloor ? bid.params.bidFloor : null; - } - - let floor = bid.getFloor({ - currency: 'USD', mediaType: '*', size: '*' - }); - if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { - return floor.floor; - } - return null; -} - -registerBidder(spec); diff --git a/modules/brightcomSSPBidAdapter.md b/modules/brightcomSSPBidAdapter.md deleted file mode 100644 index 8d0e4ec70dc..00000000000 --- a/modules/brightcomSSPBidAdapter.md +++ /dev/null @@ -1,46 +0,0 @@ -# Overview - -``` -Module Name: Brightcom SSP Bid Adapter -Module Type: Bidder Adapter -Maintainer: alexandruc@brightcom.com -``` - -# Description - -Brightcom's adapter integration to the Prebid library. - -# Test Parameters - -``` -var adUnits = [ - { - code: 'test-leaderboard', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - bids: [{ - bidder: 'bcmssp', - params: { - publisherId: 2141020, - bidFloor: 0.01 - } - }] - }, { - code: 'test-banner', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - }, - bids: [{ - bidder: 'bcmssp', - params: { - publisherId: 2141020 - } - }] - } -] -``` diff --git a/modules/britepoolIdSystem.js b/modules/britepoolIdSystem.js deleted file mode 100644 index dcc365faaac..00000000000 --- a/modules/britepoolIdSystem.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * This module adds BritePoolId to the User ID module - * The {@link module:modules/userId} module is required - * @module modules/britepoolIdSystem - * @requires module:modules/userId - */ - -import { isEmpty, triggerPixel, logError } from '../src/utils.js'; -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js'; -const PIXEL = 'https://px.britepool.com/new?partner_id=t'; - -/** - * @typedef {import('../modules/userId/index.js').Submodule} Submodule - * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig - * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData - * @typedef {import('../modules/userId/index.js').SubmoduleParams} SubmoduleParams - */ - -/** @type {Submodule} */ -export const britepoolIdSubmodule = { - /** - * Used to link submodule with config - * @type {string} - */ - name: 'britepoolId', - /** - * Decode the stored id value for passing to bid requests - * @function - * @param {string} value - * @returns {{britepoolid:string}} - */ - decode(value) { - return (value && typeof value['primaryBPID'] === 'string') ? { 'britepoolid': value['primaryBPID'] } : null; - }, - /** - * Performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [submoduleConfig] - * @param {ConsentData|undefined} consentData - * @returns {function} - */ - getId(submoduleConfig, consentData) { - const submoduleConfigParams = (submoduleConfig && submoduleConfig.params) || {}; - const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams(submoduleConfigParams, consentData); - let getterResponse = null; - if (typeof getter === 'function') { - getterResponse = getter(params); - // First let's rule out that the response is not a function - if (typeof getterResponse !== 'function') { - // Optimization to return value from getter - return { - id: britepoolIdSubmodule.normalizeValue(getterResponse) - }; - } - } - if (isEmpty(params)) { - triggerPixel(PIXEL); - } - // Return for async operation - return { - callback: function(callback) { - if (errors.length > 0) { - errors.forEach(error => logError(error)); - callback(); - return; - } - if (getterResponse) { - // Resolve the getter function response - try { - getterResponse(function(response) { - callback(britepoolIdSubmodule.normalizeValue(response)); - }); - } catch (error) { - if (error !== '') logError(error); - callback(); - } - } else { - ajax(url, { - success: response => { - const responseObj = britepoolIdSubmodule.normalizeValue(response); - callback(responseObj ? { primaryBPID: responseObj.primaryBPID } : null); - }, - error: error => { - if (error !== '') logError(error); - callback(); - } - }, JSON.stringify(params), { customHeaders: headers, contentType: 'application/json', method: 'POST', withCredentials: true }); - } - } - } - }, - /** - * Helper method to create params for our API call - * @param {SubmoduleParams} [submoduleConfigParams] - * @param {ConsentData|undefined} consentData - * @returns {object} Object with parsed out params - */ - createParams(submoduleConfigParams, consentData) { - const hasGdprData = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies; - const gdprConsentString = hasGdprData ? consentData.consentString : undefined; - let errors = []; - const headers = {}; - const dynamicVars = typeof britepool_pubparams !== 'undefined' ? britepool_pubparams : {}; // eslint-disable-line camelcase, no-undef - let params = Object.assign({}, submoduleConfigParams, dynamicVars); - if (params.getter) { - // Custom getter will not require other params - if (typeof params.getter !== 'function') { - errors.push(`userIdTargeting - britepoolId submodule requires getter to be a function`); - return { errors }; - } - } else { - if (params.api_key) { - // Add x-api-key into the header - headers['x-api-key'] = params.api_key; - } - } - const url = params.url || `https://api.britepool.com/v1/britepool/id${gdprConsentString ? '?gdprString=' + encodeURIComponent(gdprConsentString) : ''}`; - const getter = params.getter; - delete params.api_key; - delete params.url; - delete params.getter; - return { - params, - headers, - url, - getter, - errors - }; - }, - /** - * Helper method to normalize a JSON value - */ - normalizeValue(value) { - let valueObj = null; - if (typeof value === 'object') { - valueObj = value; - } else if (typeof value === 'string') { - try { - valueObj = JSON.parse(value); - } catch (error) { - logError(error); - } - } - return valueObj; - }, - eids: { - 'britepoolid': { - source: 'britepool.com', - atype: 3 - }, - } -}; - -submodule('userId', britepoolIdSubmodule); diff --git a/modules/britepoolIdSystem.md b/modules/britepoolIdSystem.md deleted file mode 100644 index 72edbe2324b..00000000000 --- a/modules/britepoolIdSystem.md +++ /dev/null @@ -1,42 +0,0 @@ -## BritePool User ID Submodule - -BritePool User ID Module. For assistance setting up your module please contact us at [prebid@britepool.com](prebid@britepool.com). - -### Prebid Params - -Individual params may be set for the BritePool User ID Submodule. -``` -pbjs.setConfig({ - userSync: { - userIds: [{ - name: 'britepoolId', - storage: { - name: 'britepoolid', - type: 'cookie', - expires: 30 - }, - params: { - url: 'https://sandbox-api.britepool.com/v1/britepool/id', // optional - api_key: '3fdbe297-3690-4f5c-9e11-ee9186a6d77c', // provided by britepool - hash: '31c5543c1734d25c7206f5fd591525d0295bec6fe84ff82f946a34fe970a1e66', // example hash identifier (sha256) - ssid: '221aa074-57fc-453b-81f0-6c74f628cd5c' // example identifier - } - }] - } -}); -``` -## Parameter Descriptions for the `usersync` Configuration Section -The below parameters apply only to the BritePool User ID Module integration. - -| Param under usersync.userIds[] | Scope | Type | Description | Example | -| --- | --- | --- | --- | --- | -| name | Required | String | ID value for the BritePool module - `"britepoolId"` | `"britepoolId"` | -| params | Required | Object | Details for BritePool initialization. | | -| params.api_key | Required | String |BritePool API Key provided by BritePool | "3fdbe297-3690-4f5c-9e11-ee9186a6d77c" | -| params.url | Optional | String |BritePool API url | "https://sandbox-api.britepool.com/v1/britepool/id" | -| params.identifier | Required | String | Where identifier in the params object is the key name. At least one identifier is required. Available Identifiers `aaid` `dtid` `idfa` `ilid` `luid` `mmid` `msid` `mwid` `rida` `ssid` `hash` | `params.ssid` `params.aaid` | -| storage | Required | Object | The publisher must specify the local storage in which to store the results of the call to get the user ID. This can be either cookie or HTML5 storage. | | -| storage.type | Required | String | This is where the results of the user ID will be stored. The recommended method is `localStorage` by specifying `html5`. | `"html5"` | -| storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. | `"britepoolid"` | -| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. | `365` | -| value | Optional | Object | Used only if the page has a separate mechanism for storing the BritePool ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"primaryBPID": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | diff --git a/modules/browsiAnalyticsAdapter.js b/modules/browsiAnalyticsAdapter.js new file mode 100644 index 00000000000..6bf0ba8da5f --- /dev/null +++ b/modules/browsiAnalyticsAdapter.js @@ -0,0 +1,151 @@ +import { logMessage, isGptPubadsDefined, timestamp } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import { EVENTS } from '../src/constants.js'; +import { getGlobal } from '../src/prebidGlobal.js'; + +const analyticsType = 'endpoint'; +const PROVIDER_NAME = 'browsi'; +const GVLID = 329; +const EVENT_SERVER_URL = `https://events.browsiprod.com/events/v2`; + +/** @type {null|Object} */ +let _staticData = null; +/** @type {string} */ +let VERSION = getGlobal().version; +/** @type {string} */ +let URL = encodeURIComponent(window.location.href); + +const { AUCTION_END, BROWSI_INIT, BROWSI_DATA } = EVENTS; + +export function getStaticData() { + return _staticData; +} + +export function setStaticData(data) { + _staticData = { + pvid: data.pvid, + device: data.d, + geo: data.g, + aid: data.aid, + es: data.es, + pk: data.pk, + sk: data.sk, + t: data.t, + }; +} + +function getTimeOffset(ts) { + if (!ts) return undefined; + return timestamp() - ts; +} + +function getAdUnitPathByCode(code) { + const slots = isGptPubadsDefined() && window.googletag.pubads().getSlots(); + if (!slots || !slots.length) return null; + const match = slots.find(slot => slot.getSlotElementId() === code); + return match?.getAdUnitPath(); +} + +function getAdUnitsData(args) { + const shouldSampleRtm = !!_staticData?.es; + return args.adUnits?.map(adUnit => { + let rtm; + const pbd = adUnit.bids + .filter(({ ortb2Imp }) => { + const brwData = ortb2Imp?.ext?.data?.browsi; + if (brwData && !rtm) rtm = brwData; + return !!brwData; + }) + .map(bid => bid.bidder); + return { + plid: adUnit.code, + au: getAdUnitPathByCode(adUnit.code), + pbd, + dpc: rtm ? Object.keys(rtm).length : 0, + ...(shouldSampleRtm && rtm ? { rtm } : {}) + } + }); +} + +function handleAuctionEnd(args) { + const event = { + et: 'auction_data_sent', + to: getTimeOffset(_staticData?.t), + pvid: _staticData?.pvid, + pk: _staticData?.pk, + sk: _staticData?.sk, + geo: _staticData?.geo, + dp: _staticData?.device, + aid: _staticData?.aid, + pbv: VERSION, + url: URL, + aucid: args.auctionId, + ad_units: getAdUnitsData(args) + } + sendEvent(event, 'rtd_demand'); +} + +function handleBrowsiData(args) { + if (args.moduleName !== 'browsi') return; + setStaticData(args); +} + +function handleModuleInit(args) { + if (args.moduleName !== 'browsi') return; + const event = { + et: 'rtd_init', + to: getTimeOffset(args.t), + pvid: args.pvid, + pk: args.pk, + sk: args.sk, + pbv: VERSION, + url: URL, + ...(args.rsn ? { rsn: args.rsn } : {}), + } + sendEvent(event, 'rtd_supply'); +} + +function sendEvent(event, topic) { + try { + const pvid = event.pvid || _staticData?.pvid || ''; + const body = JSON.stringify([event]); + ajax(`${EVENT_SERVER_URL}/${topic}?p=${pvid}`, () => { }, body, { + contentType: 'application/json', + method: 'POST' + }); + } catch (err) { logMessage('Browsi Analytics error') } +} + +let browsiAnalytics = Object.assign(adapter({ url: EVENT_SERVER_URL, analyticsType }), { + track({ eventType, args }) { + switch (eventType) { + case BROWSI_INIT: + handleModuleInit(args); + break; + case BROWSI_DATA: + handleBrowsiData(args); + break; + case AUCTION_END: + handleAuctionEnd(args); + break; + default: + break; + } + } +}); + +browsiAnalytics.originEnableAnalytics = browsiAnalytics.enableAnalytics; + +browsiAnalytics.enableAnalytics = function (config) { + browsiAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: browsiAnalytics, + code: PROVIDER_NAME, + gvlid: GVLID +}); + +export default browsiAnalytics; diff --git a/modules/browsiAnalyticsAdapter.md b/modules/browsiAnalyticsAdapter.md new file mode 100644 index 00000000000..3cb14ebb0bc --- /dev/null +++ b/modules/browsiAnalyticsAdapter.md @@ -0,0 +1,19 @@ +# Overview + +Module Name: Browsi Analytics Adapter +Module Type: Analytics Adapter +Maintainer: support@browsi.com + +# Description + +Analytics adapter for Browsi. + +# Settings + +```js + pbjs.enableAnalytics({ + provider: 'browsi', + options: {} + }); +``` + diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index ad4019ed03c..30aaf6bd85e 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -1,5 +1,5 @@ /** - * This module adds browsi provider to the eal time data module + * This module adds browsi provider to the real time data module * The {@link module:modules/realTimeData} module is required * The module will fetch predictions from browsi server * The module will place browsi bootstrap script on page @@ -13,18 +13,30 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName + * @property {?string} splitKey */ -import {deepClone, deepSetValue, isFn, isGptPubadsDefined, isNumber, logError, logInfo, generateUUID} from '../src/utils.js'; -import {submodule} from '../src/hook.js'; -import {ajaxBuilder} from '../src/ajax.js'; -import {loadExternalScript} from '../src/adloader.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {find, includes} from '../src/polyfill.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import { deepClone, deepSetValue, isFn, isNumber, logError, logInfo, generateUUID, timestamp } from '../src/utils.js'; +import { submodule } from '../src/hook.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { includes } from '../src/polyfill.js'; +import { getGlobal } from '../src/prebidGlobal.js'; import * as events from '../src/events.js'; -import {EVENTS} from '../src/constants.js'; -import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +import { EVENTS } from '../src/constants.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { + getUUID, + toUrlParams, + getTargetingKeys, + getTargetingValues, + getPredictorData, + generateRandomString, + setKeyValue, + getMacroId, + getSlotByCode +} from '../libraries/browsiUtils/browsiUtils.js'; /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -32,36 +44,31 @@ import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'browsi'; -const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); +const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME }); +const RANDOM = Math.floor(Math.random() * 10) + 1; +const API_KEY = generateRandomString(); +let PVID = getUUID(); /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ let _browsiData = null; -/** @type {string} */ -const DEF_KEYNAME = 'browsiViewability'; /** @type {null | function} */ let _dataReadyCallback = null; /** @type {null|Object} */ let _ic = {}; +/** @type {null|number} */ +let TIMESTAMP = null; -/** - * add browsi script to page - * @param {Object} data - */ -export function addBrowsiTag(data) { - let script = loadExternalScript(data.u, 'browsi'); - script.async = true; - script.setAttribute('data-sitekey', _moduleParams.siteKey); - script.setAttribute('data-pubkey', _moduleParams.pubKey); - script.setAttribute('prebidbpt', 'true'); - script.setAttribute('id', 'browsi-tag'); - script.setAttribute('src', data.u); - script.prebidData = deepClone(data); - if (_moduleParams.keyName) { - script.prebidData.kn = _moduleParams.keyName; - } - return script; +export function setTimestamp() { + TIMESTAMP = timestamp(); +} + +export function initAnalytics() { + getGlobal().enableAnalytics({ + provider: 'browsi', + options: {} + }) } export function sendPageviewEvent(eventType) { @@ -76,33 +83,47 @@ export function sendPageviewEvent(eventType) { } } +function sendModuleInitEvent(rsn) { + events.emit(EVENTS.BROWSI_INIT, { + moduleName: MODULE_NAME, + sk: _moduleParams.siteKey, + pk: _moduleParams.pubKey, + t: TIMESTAMP, + pvid: PVID, + ...(rsn || {}) + }); +} + +function sendBrowsiDataEvent(data) { + events.emit(EVENTS.BROWSI_DATA, { + moduleName: MODULE_NAME, + pvid: PVID || data.pvid, + d: data.d, + g: data.g, + aid: data.aid, + es: data.es, + sk: _moduleParams.siteKey, + pk: _moduleParams.pubKey, + t: TIMESTAMP + }); +} + /** * collect required data from page * send data to browsi server to get predictions */ export function collectData() { - const win = window.top; - const doc = win.document; - let browsiData = null; - try { - browsiData = storage.getDataFromLocalStorage('__brtd'); - } catch (e) { - logError('unable to parse __brtd'); - } + const predictorData = getPredictorData(storage, _moduleParams, TIMESTAMP, PVID); + getPredictionsFromServer(`//${_moduleParams.url}/prebid/v2?${toUrlParams(predictorData)}`); +} - let predictorData = { - ...{ - sk: _moduleParams.siteKey, - pk: _moduleParams.pubKey, - sw: (win.screen && win.screen.width) || -1, - sh: (win.screen && win.screen.height) || -1, - url: `${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`, - }, - ...(browsiData ? {us: browsiData} : {us: '{}'}), - ...(document.referrer ? {r: document.referrer} : {}), - ...(document.title ? {at: document.title} : {}) - }; - getPredictionsFromServer(`//${_moduleParams.url}/prebid?${toUrlParams(predictorData)}`); +export function setBrowsiData(data) { + _browsiData = data; + if (!PVID) { PVID = data.pvid; } + if (isFn(_dataReadyCallback)) { + _dataReadyCallback(); + _dataReadyCallback = null; + } } /** @@ -119,36 +140,82 @@ function waitForData(callback) { } } -export function setData(data) { - _browsiData = data; - if (isFn(_dataReadyCallback)) { - _dataReadyCallback(); - _dataReadyCallback = null; +/** + * add browsi script to page + * @param {Object} data + */ +export function addBrowsiTag(data) { + let script = loadExternalScript(data.u, MODULE_TYPE_RTD, 'browsi'); + script.async = true; + script.setAttribute('data-sitekey', _moduleParams.siteKey); + script.setAttribute('data-pubkey', _moduleParams.pubKey); + script.setAttribute('prebidbpt', 'true'); + script.setAttribute('id', 'browsi-tag'); + script.setAttribute('src', data.u); + script.prebidData = deepClone(typeof data === 'string' ? Object(data) : data) + script.brwRandom = RANDOM; + Object.assign(script.prebidData, { pvid: PVID || data.pvid, t: TIMESTAMP, apik: API_KEY }); + if (_moduleParams.keyName) { + script.prebidData.kn = _moduleParams.keyName; + } + return script; +} + +/** + * add browsitag to window object + * @param {Object} data + */ +function setWindowBrowsiTag(data) { + if (data.eap) { + window.browsitag = window.browsitag || {}; + window.browsitag.cmd = window.browsitag.cmd || []; + window.browsitag.data = window.browsitag.data || {}; + window.browsitag.data.get = getBrowsiTagServerData; + window.dispatchEvent(new CustomEvent('browsiData', { detail: { isPartialData: true } })); } } -function getRTD(auc) { +function getBrowsiTagServerData(identifier) { + const uc = identifier || 'placeholder'; + const rtd = getServerData([uc]); + return Object.assign(rtd[uc], { isPartialData: true }); +} + +function getOnPageData(auc) { + try { + const dataMap = {}; + auc.forEach(uc => { + dataMap[uc] = window[API_KEY]?.get(uc); + }); + return dataMap; + } catch (e) { + return {}; + } +} + +function getServerData(auc) { logInfo(`Browsi RTD provider is fetching data for ${auc}`); try { - const _bp = (_browsiData && _browsiData.p) || {}; + const _pg = (_browsiData && _browsiData.pg) || {}; + const _plc = (_browsiData && _browsiData.plc) || {}; return auc.reduce((rp, uc) => { _ic[uc] = _ic[uc] || 0; const _c = _ic[uc]; if (!uc) { return rp } + rp[uc] = {}; + Object.assign(rp[uc], _pg); const adSlot = getSlotByCode(uc); const identifier = adSlot ? getMacroId(_browsiData['pmd'], adSlot) : uc; - const _pd = _bp[identifier]; + const _pd = _plc[identifier]; if (!_pd) { return rp } - if (_pd.ps) { - if (!isIdMatchingAdUnit(adSlot, _pd.w)) { - return rp; - } - rp[uc] = getKVObject(getCurrentData(_pd.ps, _c)); - } + Object.entries(_pd).forEach(([key, value]) => { + const kv = getKVObject(key, getCurrentData(value, _c)); + Object.assign(rp[uc], kv); + }); return rp; }, {}); } catch (e) { @@ -159,113 +226,42 @@ function getRTD(auc) { /** * get prediction * return -1 if prediction not found - * @param {object} predictionObject + * @param {object} prediction * @param {number} _c * @return {number} */ -export function getCurrentData(predictionObject, _c) { - if (!predictionObject || !isNumber(_c)) { +export function getCurrentData(prediction, _c) { + if (!prediction || !isNumber(_c)) { return -1; } - if (isNumber(predictionObject[_c])) { - return predictionObject[_c]; + if (isNumber(prediction)) { + return prediction; } - if (Object.keys(predictionObject).length > 1) { + if (isNumber(prediction[_c])) { + return prediction[_c]; + } + if (Object.keys(prediction).length > 1) { while (_c > 0) { _c--; - if (isNumber(predictionObject[_c])) { - return predictionObject[_c]; + if (isNumber(prediction[_c])) { + return prediction[_c]; } } } return -1; } -/** - * get all slots on page - * @return {Object[]} slot GoogleTag slots - */ -function getAllSlots() { - return isGptPubadsDefined() && window.googletag.pubads().getSlots(); -} /** * get prediction and return valid object for key value set + * @param {string} k * @param {number} p * @return {Object} key:value */ -function getKVObject(p) { - const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); - let prObject = {}; - prObject[getKey()] = prValue.toString(); - return prObject; -} - -function getKey() { - return ((_moduleParams['keyName'] || (_browsiData && _browsiData['kn']) || DEF_KEYNAME).toString()) -} -/** - * check if placement id matches one of given ad units - * @param {Object} slot google slot - * @param {string[]} whitelist ad units - * @return {boolean} - */ -export function isIdMatchingAdUnit(slot, whitelist) { - if (!whitelist || !whitelist.length || !slot) { - return true; - } - const slotAdUnits = slot.getAdUnitPath(); - return whitelist.indexOf(slotAdUnits) !== -1; -} - -/** - * get GPT slot by placement id - * @param {string} code placement id - * @return {?Object} - */ -function getSlotByCode(code) { - const slots = getAllSlots(); - if (!slots || !slots.length) { - return null; - } - return find(slots, s => s.getSlotElementId() === code || s.getAdUnitPath() === code) || null; -} - -/** - * generate id according to macro script - * @param {Object} macro replacement macro - * @param {Object} slot google slot - * @return {?Object} - */ -export function getMacroId(macro, slot) { - if (macro) { - try { - const macroResult = evaluate(macro, slot.getSlotElementId(), slot.getAdUnitPath(), (match, p1) => { - return (p1 && slot.getTargeting(p1).join('_')) || 'NA'; - }); - return macroResult; - } catch (e) { - logError(`failed to evaluate: ${macro}`); - } - } - return slot.getSlotElementId(); +function getKVObject(k, p) { + if (p < 0) return {}; + return { [k]: p }; } -function evaluate(macro, divId, adUnit, replacer) { - let macroResult = macro.p - .replace(/['"]+/g, '') - .replace(//g, divId); - - if (adUnit) { - macroResult = macroResult.replace(//g, adUnit); - } - if (replacer) { - macroResult = macroResult.replace(//g, replacer); - } - if (macro.s) { - macroResult = macroResult.substring(macro.s.s, macro.s.e); - } - return macroResult; -} /** * XMLHttpRequest to get data form browsi server * @param {string} url server url with query params @@ -280,41 +276,40 @@ function getPredictionsFromServer(url) { try { const data = JSON.parse(response); if (data) { - setData({p: data.p, kn: data.kn, pmd: data.pmd, bet: data.bet}); + setBrowsiData(data); + setWindowBrowsiTag(data); + sendBrowsiDataEvent(data); } else { - setData({}); + setBrowsiData({}); } sendPageviewEvent(data.bet); addBrowsiTag(data); } catch (err) { logError('unable to parse data'); - setData({}) + setBrowsiData({}) } } else if (req.status === 204) { // unrecognized site key - setData({}); + setBrowsiData({}); } }, error: function () { - setData({}); + setBrowsiData({}); logError('unable to get prediction data'); } } ); } -/** - * serialize object and return query params string - * @param {Object} data - * @return {string} - */ -function toUrlParams(data) { - return Object.keys(data) - .map(key => key + '=' + encodeURIComponent(data[key])) - .join('&'); +function mergeAdUnitData(rtdData = {}, onPageData = {}) { + const mergedData = {}; + Object.keys({ ...rtdData, ...onPageData }).forEach((key) => { + mergedData[key] = { ...onPageData[key], ...rtdData[key] }; + }); + return mergedData; } -function setBidRequestsData(bidObj, callback) { +function getAdUnitCodes(bidObj) { let adUnitCodes = bidObj.adUnitCodes; let adUnits = bidObj.adUnits || getGlobal().adUnits || []; if (adUnitCodes) { @@ -322,39 +317,90 @@ function setBidRequestsData(bidObj, callback) { } else { adUnitCodes = adUnits.map(au => au.code); } - waitForData(() => { - const data = getRTD(adUnitCodes); - if (data) { + return { adUnitCodes, adUnits }; +} + +function isOnPageDataApiReady() { + return !!(window[API_KEY]?.get); +} + +function onDataReady() { + return new Promise((resolve) => { + waitForData(() => { + if (isOnPageDataApiReady()) { + return resolve(); + } + const interval = setInterval(() => { + if (isOnPageDataApiReady()) { + clearInterval(interval); + return resolve(); + } + }, 250); + }); + }); +} + +function onTimeout(config, timeout) { + return new Promise((resolve) => { + if (!config.waitForIt || !timeout) { + return resolve(); + } + setTimeout(() => { + return resolve(); + }, timeout * 0.7); + }); +} + +function setBidRequestsData(bidObj, callback, config, userConsent, timeout) { + Promise.race([onDataReady(), onTimeout(config, timeout)]) + .then(() => { + const pr = _browsiData && _browsiData.pr; + if (!pr || !pr.length) return; + const { adUnitCodes, adUnits } = getAdUnitCodes(bidObj); + const onPageData = getOnPageData(adUnitCodes); + const rtdData = getServerData(adUnitCodes); + const data = mergeAdUnitData(rtdData, onPageData); adUnits.forEach(adUnit => { - const adUnitCode = adUnit.code; - if (data[adUnitCode]) { - deepSetValue(adUnit, 'ortb2Imp.ext.data.browsi', {[getKey()]: data[adUnitCode][getKey()]}); + const validBidders = adUnit.bids.filter(bid => pr.includes(bid.bidder)); + if (validBidders.length) { + const adUnitData = data[adUnit.code]; + if (adUnitData) { + validBidders.forEach(bid => { + deepSetValue(bid, 'ortb2Imp.ext.data.browsi', adUnitData); + }); + } } }); - } - callback(); - }) + callback(); + }); } -/** @type {RtdSubmodule} */ -export const browsiSubmodule = { - /** - * used to link submodule with realTimeData - * @type {string} - */ - name: MODULE_NAME, - /** - * get data and send back to realTimeData module - * @function - * @param {string[]} adUnitsCodes - */ - getTargetingData: getTargetingData, - init: init, - getBidRequestData: setBidRequestsData -}; +function getGptTargeting(uc) { + try { + const sg = !!(_browsiData && _browsiData.sg); + if (!sg) return {}; + + const rtd = getServerData(uc); + const { viewabilityKey, scrollKey, revenueKey } = getTargetingKeys(_moduleParams['keyName']); + + return Object.fromEntries( + Object.entries(rtd).map(([key, value]) => { + const { viewabilityValue, scrollValue, revenueValue } = getTargetingValues(value); + const result = { + ...(viewabilityValue ? { [viewabilityKey]: viewabilityValue } : {}), + ...(scrollValue ? { [scrollKey]: scrollValue } : {}), + ...(revenueValue ? { [revenueKey]: revenueValue } : {}), + } + return [key, result]; + }) + ); + } catch (e) { + return {}; + } +} function getTargetingData(uc, c, us, a) { - const targetingData = getRTD(uc); + const targetingData = getGptTargeting(uc); const auctionId = a.auctionId; const sendAdRequestEvent = (_browsiData && _browsiData['bet'] === 'AD_REQUEST'); uc.forEach(auc => { @@ -378,14 +424,38 @@ function getTargetingData(uc, c, us, a) { 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(); collectData(); + setKeyValue(_moduleParams.splitKey, RANDOM); } else { logError('missing params for Browsi provider'); + sendModuleInitEvent('missing params'); } return true; } +/** @type {RtdSubmodule} */ +export const browsiSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: MODULE_NAME, + /** + * get data and send back to realTimeData module + * @function + * @param {string[]} adUnitsCodes + */ + getTargetingData: getTargetingData, + init: init, + getBidRequestData: setBidRequestsData +}; + function registerSubModule() { submodule('realTimeData', browsiSubmodule); } diff --git a/modules/byDataAnalyticsAdapter.js b/modules/byDataAnalyticsAdapter.js index 508a5593f58..6e3135f5969 100644 --- a/modules/byDataAnalyticsAdapter.js +++ b/modules/byDataAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { deepClone, logInfo, logError } from '../src/utils.js'; +import { deepClone, logInfo, logError, getWinDimensions } from '../src/utils.js'; import Base64 from 'crypto-js/enc-base64'; import hmacSHA512 from 'crypto-js/hmac-sha512'; import enc from 'crypto-js/enc-utf8'; @@ -9,6 +9,7 @@ import {getStorageManager} from '../src/storageManager.js'; import { auctionManager } from '../src/auctionManager.js'; import { ajax } from '../src/ajax.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; const versionCode = '4.4.1' const secretKey = 'bydata@123456' @@ -261,7 +262,9 @@ ascAdapter.getVisitorData = function (data = {}) { return signedToken; } function detectWidth() { - return window.screen.width || (window.innerWidth && document.documentElement.clientWidth) ? Math.min(window.innerWidth, document.documentElement.clientWidth) : window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth; + const {width: viewportWidth} = getViewportSize(); + const windowDimensions = getWinDimensions(); + return windowDimensions.screen.width || (windowDimensions.innerWidth && windowDimensions.document.documentElement.clientWidth) ? Math.min(windowDimensions.innerWidth, windowDimensions.document.documentElement.clientWidth) : viewportWidth; } function giveDeviceTypeOnScreenSize() { var _dWidth = detectWidth(); diff --git a/modules/c1xBidAdapter.js b/modules/c1xBidAdapter.js index 79ba8cf499d..d1b51dcb27d 100644 --- a/modules/c1xBidAdapter.js +++ b/modules/c1xBidAdapter.js @@ -185,7 +185,7 @@ function getBidFloor(bidRequest) { } let floor = - floorInfo.floor || + floorInfo?.floor || bidRequest.params.bidfloor || bidRequest.params.floorPriceMap || 0; diff --git a/modules/cadentApertureMXBidAdapter.js b/modules/cadentApertureMXBidAdapter.js index e73564dacdb..fa441a4f4fa 100644 --- a/modules/cadentApertureMXBidAdapter.js +++ b/modules/cadentApertureMXBidAdapter.js @@ -230,11 +230,6 @@ export const spec = { return false; } - if (bid.bidder !== BIDDER_CODE) { - logWarn(BIDDER_CODE + ': Must use "cadent_aperture_mx" as bidder code.'); - return false; - } - if (!bid.params.tagid || !isStr(bid.params.tagid)) { logWarn(BIDDER_CODE + ': Missing tagid param or tagid present and not type String.'); return false; @@ -282,12 +277,13 @@ export const spec = { }; // adding gpid support - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.adserver.adslot'); - if (!gpid) { - gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); - } + let gpid = + deepAccess(bid, 'ortb2Imp.ext.gpid') || + deepAccess(bid, 'ortb2Imp.ext.data.adserver.adslot') || + deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { - data.ext = {gpid: gpid.toString()}; + data.ext = { gpid: gpid.toString() }; } let typeSpecifics = isVideo ? { video: cadentAdapter.buildVideo(bid) } : { banner: cadentAdapter.buildBanner(bid) }; let bidfloorObj = bidfloor > 0 ? { bidfloor, bidfloorcur: DEFAULT_CUR } : {}; diff --git a/modules/carodaBidAdapter.js b/modules/carodaBidAdapter.js index cb7b5fbe7c5..3060501ba8d 100644 --- a/modules/carodaBidAdapter.js +++ b/modules/carodaBidAdapter.js @@ -1,16 +1,19 @@ // jshint esversion: 6, es3: false, node: true 'use strict' +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { deepAccess, deepSetValue, + getWinDimensions, logError, mergeDeep, - parseSizesInput + sizeTupleToRtbSize, + sizesToSizeTuples } from '../src/utils.js'; -import { config } from '../src/config.js'; const { getConfig } = config; @@ -44,7 +47,7 @@ export const spec = { getFirstWithKey(validBidRequests, 'params.priceType') || 'net'; const test = getFirstWithKey(validBidRequests, 'params.test'); - const currency = getConfig('currency.adServerCurrency'); + const currency = getCurrencyFromBidderRequest(bidderRequest); const eids = getFirstWithKey(validBidRequests, 'userIdAsEids'); const schain = getFirstWithKey(validBidRequests, 'schain'); const request = { @@ -167,8 +170,10 @@ function getORTBCommon (bidderRequest) { } } const device = getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; + const { innerWidth, innerHeight } = getWinDimensions(); + + device.w = device.w || innerWidth; + device.h = device.h || innerHeight; device.ua = device.ua || navigator.userAgent; return { app, @@ -183,8 +188,8 @@ function getImps (validBidRequests, common) { const floorInfo = bid.getFloor ? bid.getFloor({ currency: common.currency || 'EUR' }) : {}; - const bidfloor = floorInfo.floor; - const bidfloorcur = floorInfo.currency; + const bidfloor = floorInfo?.floor; + const bidfloorcur = floorInfo?.currency; const { ctok, placementId } = bid.params; const imp = { bid_id: bid.bidId, @@ -195,13 +200,7 @@ function getImps (validBidRequests, common) { }; const bannerParams = deepAccess(bid, 'mediaTypes.banner'); if (bannerParams && bannerParams.sizes) { - const sizes = parseSizesInput(bannerParams.sizes); - const format = sizes.map(size => { - const [width, height] = size.split('x'); - const w = parseInt(width, 10); - const h = parseInt(height, 10); - return { w, h }; - }); + const format = sizesToSizeTuples(bannerParams.sizes).map(sizeTupleToRtbSize); imp.banner = { format }; diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js index b1fcb29e3d0..0a305a651cb 100644 --- a/modules/ccxBidAdapter.js +++ b/modules/ccxBidAdapter.js @@ -65,7 +65,7 @@ function _validateSizes (sizeObj, type) { return true } -function _buildBid (bid) { +function _buildBid (bid, bidderRequest) { let placement = {} placement.id = bid.bidId placement.secure = 1 @@ -105,6 +105,10 @@ function _buildBid (bid) { placement.ext = {'pid': bid.params.placementId} + if (bidderRequest.paapi?.enabled) { + placement.ext.ae = bid?.ortb2Imp?.ext?.ae + } + return placement } @@ -197,7 +201,7 @@ export const spec = { } _each(validBidRequests, function (bid) { - requestBody.imp.push(_buildBid(bid)) + requestBody.imp.push(_buildBid(bid, bidderRequest)) }) // Return the server request return { diff --git a/modules/ceeIdSystem.js b/modules/ceeIdSystem.js new file mode 100644 index 00000000000..0a4ce73172e --- /dev/null +++ b/modules/ceeIdSystem.js @@ -0,0 +1,128 @@ +/** + * This module adds ceeId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/ceeId + * @requires module:modules/userId + */ + +import { logError } from '../src/utils.js' +import { ajax } from '../src/ajax.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { submodule } from '../src/hook.js'; +import { domainOverrideToRootDomain } from '../libraries/domainOverrideToRootDomain/index.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const MODULE_NAME = 'ceeId'; +export const storage = getStorageManager({ moduleName: MODULE_NAME, moduleType: MODULE_TYPE_UID }); + +/** + * Reads the ID token from local storage or cookies. + * @returns {string|undefined} The ID token, or undefined if not found. + */ +export const readId = tokenName => storage.getDataFromLocalStorage(tokenName) || storage.getCookie(tokenName); + +/** + * performs fetch to obtain id and return a value + * @function fetchCeeIdToken + * @param {Object} requestData The data to be sent in the request body. + * @returns {Promise} A promise that resolves to the fetched token. + */ +export function fetchCeeIdToken(requestData) { + return new Promise((resolve, reject) => { + ajax( + 'https://ceeid.eu/api/token/generate', + { + success: (response) => { + try { + const responseJson = JSON.parse(response); + const newCeeIdToken = responseJson.value; + if (newCeeIdToken) { + resolve(newCeeIdToken); + } else { + logError(`${MODULE_NAME}: No token in response`); + reject(new Error('No token in response')); + } + } catch (error) { + logError(`${MODULE_NAME}: Server error while fetching ID`, error); + reject(error); + } + }, + error: (error) => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + reject(error); + } + }, + JSON.stringify(requestData), + { + method: 'POST', + contentType: 'application/json' + } + ); + }); +} + +/** @type {Submodule} */ +export const ceeIdSubmodule = { + name: MODULE_NAME, + gvlid: 676, + + /** + * decode the stored id value for passing to bid requests + * @function decode + * @param {string} value + * @returns {(Object)} + */ + decode(value) { + return typeof value === 'string' ? { 'ceeId': value } : undefined; + }, + + /** + * performs action to obtain id and return a value + * @function getId + * @returns {(IdResponse|undefined)} + */ + getId(config, consentData) { + const { params = {}, storage = {} } = config; + const { name: storedCeeIdToken } = storage; + const { publisherId, type, value, cookieName } = params; + const { consentString } = consentData; + const ceeIdToken = readId(storedCeeIdToken) || readId(cookieName); + + if (ceeIdToken) { + return { id: ceeIdToken }; + } + + if (cookieName) return; + + if (publisherId && type && value && consentString) { + const requestData = { + publisherId, + type, + value, + properties: { + consent: consentString + }, + }; + const resp = function (callback) { + fetchCeeIdToken(requestData) + .then((id) => callback(id)); + }; + + return { callback: resp }; + } + }, + domainOverride: domainOverrideToRootDomain(storage, MODULE_NAME), + eids: { + 'ceeId': { + source: 'ceeid.eu', + atype: 1 + }, + }, +}; + +submodule('userId', ceeIdSubmodule); diff --git a/modules/ceeIdSystem.md b/modules/ceeIdSystem.md new file mode 100644 index 00000000000..54823aa2f68 --- /dev/null +++ b/modules/ceeIdSystem.md @@ -0,0 +1,55 @@ +# Overview + +Module Name: ceeIdSystem +Module Type: UserID Module +Maintainer: pawel.grudzien@grupawp.pl + +# Description + +User identification system for WPM + +### Prebid Params example + +You can configure this submodule in your `userSync.userIds[]` configuration. We have two implementation methods depending on the publisher's needs. The first method we suggest for publishers is to provide appropriate data that will allow you to query the endpoint to retrieve the ceeId token. To query the endpoint correctly, you will need the publisher's ID in the params.publisheId field. In addition, the HEM type, i.e. how the user's email was encoded, we consider two methods: base64 encoding and hex encoding. The value of HEM should be passed in the params.value field. + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'ceeId', + storage: { + type: 'cookie', + name: 'ceeIdToken', + expires: 7, + refreshInSeconds: 360 + }, + params: { + publisherId: '123', // Publisher ID + type: 'email', // use 'email' if HEM was encoded by base64 or use 'hex' if it was encoded by hex + value: 'exampleHEMValue', // HEM value + } + }] + } +}); +``` + +The second way is to use a token from a cookie or local storage previously prepared by the publisher. The only thing needed in this approach is to enter the name of the cookie/local storage that the module should use in the params.cookieName field. + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'ceeId', + storage: { + type: 'cookie', + name: 'ceeIdToken', + expires: 7, + refreshInSeconds: 360 + }, + params: { + cookieName: 'name' // Your custom name of token to read from cookies or local storage + } + }] + } +}); +``` \ No newline at end of file diff --git a/modules/chtnwBidAdapter.js b/modules/chtnwBidAdapter.js index 5f77cec018a..97843a7074c 100644 --- a/modules/chtnwBidAdapter.js +++ b/modules/chtnwBidAdapter.js @@ -2,6 +2,7 @@ import { generateUUID, getDNT, _each, + getWinDimensions, } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -33,8 +34,9 @@ export const spec = { storage.setCookie(COOKIE_NAME, chtnwId); } const device = getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; + const { innerWidth, innerHeight } = getWinDimensions(); + device.w = device.w || innerWidth; + device.h = device.h || innerHeight; device.ua = device.ua || navigator.userAgent; device.dnt = getDNT() ? 1 : 0; device.language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; diff --git a/modules/cleanioRtdProvider.js b/modules/cleanioRtdProvider.js index c3f3dd51a61..35751210878 100644 --- a/modules/cleanioRtdProvider.js +++ b/modules/cleanioRtdProvider.js @@ -11,6 +11,7 @@ import { loadExternalScript } from '../src/adloader.js'; import { logError, generateUUID, insertElement } from '../src/utils.js'; import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -43,6 +44,7 @@ let preloadStatus = 0; * @param {string} scriptURL The script URL to preload */ function pageInitStepPreloadScript(scriptURL) { + // TODO: this bypasses adLoader const linkElement = document.createElement('link'); linkElement.rel = 'preload'; linkElement.as = 'script'; @@ -57,7 +59,7 @@ function pageInitStepPreloadScript(scriptURL) { * @param {string} scriptURL The script URL to add to the page for protection */ function pageInitStepProtectPage(scriptURL) { - loadExternalScript(scriptURL, 'clean.io'); + loadExternalScript(scriptURL, MODULE_TYPE_RTD, 'clean.io'); } /** diff --git a/modules/cleanmedianetBidAdapter.js b/modules/cleanmedianetBidAdapter.js index 601a237baa8..01178d63872 100644 --- a/modules/cleanmedianetBidAdapter.js +++ b/modules/cleanmedianetBidAdapter.js @@ -157,7 +157,7 @@ export const spec = { maxduration: bidRequest.mediaTypes.video.maxduration, api: bidRequest.mediaTypes.video.api, skip: bidRequest.mediaTypes.video.skip || bidRequest.params.video.skip, - placement: bidRequest.mediaTypes.video.placement || bidRequest.params.video.placement, + plcmt: bidRequest.mediaTypes.video.plcmt || bidRequest.params.video.plcmt, minduration: bidRequest.mediaTypes.video.minduration || bidRequest.params.video.minduration, playbackmethod: bidRequest.mediaTypes.video.playbackmethod || bidRequest.params.video.playbackmethod, startdelay: bidRequest.mediaTypes.video.startdelay || bidRequest.params.video.startdelay diff --git a/modules/codefuelBidAdapter.js b/modules/codefuelBidAdapter.js index a4accee3ce0..9f85b2b82cb 100644 --- a/modules/codefuelBidAdapter.js +++ b/modules/codefuelBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, isArray} from '../src/utils.js'; +import {isArray, setOnAny} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; @@ -148,15 +148,6 @@ function getDeviceType() { return 2; // 'desktop' } -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; - } - } -} - function flatten(arr) { return [].concat(...arr); } diff --git a/modules/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js index 3b90529b6cc..c626d1f56aa 100644 --- a/modules/cointrafficBidAdapter.js +++ b/modules/cointrafficBidAdapter.js @@ -2,6 +2,7 @@ import { parseSizesInput, logError, isEmpty } from '../src/utils.js'; 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'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -45,7 +46,7 @@ export const spec = { const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); const currency = config.getConfig(`currency.bidderCurrencyDefault.${BIDDER_CODE}`) || - config.getConfig('currency.adServerCurrency') || + getCurrencyFromBidderRequest(bidderRequest) || DEFAULT_CURRENCY; if (ALLOWED_CURRENCIES.indexOf(currency) === -1) { diff --git a/modules/colombiaBidAdapter.js b/modules/colombiaBidAdapter.js new file mode 100644 index 00000000000..0d25ca6cb60 --- /dev/null +++ b/modules/colombiaBidAdapter.js @@ -0,0 +1,107 @@ +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'colombia'; +const ENDPOINT_URL = 'https://ade.clmbtech.com/cde/prebid.htm'; +const HOST_NAME = document.location.protocol + '//' + window.location.host; + +export const spec = { + code: BIDDER_CODE, + aliases: ['clmb'], + supportedMediaTypes: [BANNER], + isBidRequestValid: function(bid) { + return !!(bid.params.placementId); + }, + buildRequests: function(validBidRequests, bidderRequest) { + if (validBidRequests.length === 0) { + return []; + } + let payloadArr = [] + let ctr = 1; + validBidRequests = validBidRequests.map(bidRequest => { + const params = bidRequest.params; + const sizes = utils.parseSizesInput(bidRequest.sizes)[0]; + const width = sizes.split('x')[0]; + const height = sizes.split('x')[1]; + const placementId = params.placementId; + const cb = Math.floor(Math.random() * 99999999999); + const bidId = bidRequest.bidId; + const referrer = (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : ''; + let mediaTypes = {} + let payload = { + v: 'hb1', + p: placementId, + pos: '~' + ctr, + w: width, + h: height, + cb: cb, + r: referrer, + uid: bidId, + t: 'i', + d: HOST_NAME, + fpc: params.fpc, + _u: window.location.href, + mediaTypes: Object.assign({}, mediaTypes, bidRequest.mediaTypes) + }; + if (params.keywords) payload.keywords = params.keywords; + if (params.category) payload.cat = params.category; + if (params.pageType) payload.pgt = params.pageType; + if (params.incognito) payload.ic = params.incognito; + if (params.dsmi) payload.smi = params.dsmi; + if (params.optout) payload.out = params.optout; + if (bidRequest && bidRequest.hasOwnProperty('ortb2Imp') && bidRequest.ortb2Imp.hasOwnProperty('ext')) { + payload.ext = bidRequest.ortb2Imp.ext; + if (bidRequest.ortb2Imp.ext.hasOwnProperty('gpid')) payload.pubAdCode = bidRequest.ortb2Imp.ext.gpid.split('#')[0]; + } + payloadArr.push(payload); + ctr++; + }); + return [{ + method: 'POST', + url: ENDPOINT_URL, + data: payloadArr, + }] + }, + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + const res = serverResponse.body || serverResponse; + if (!res || res.length === 0) { + return bidResponses; + } + try { + res.forEach(response => { + const crid = response.creativeId || 0; + const width = response.width || 0; + const height = response.height || 0; + let cpm = response.cpm || 0; + if (cpm <= 0) { + return bidResponses; + } + if (width !== 0 && height !== 0 && cpm !== 0 && crid !== 0) { + const dealId = response.dealid || ''; + const currency = response.currency || 'USD'; + const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue; + const bidResponse = { + requestId: response.requestId, + cpm: cpm.toFixed(2), + width: response.width, + height: response.height, + creativeId: crid, + dealId: dealId, + currency: currency, + netRevenue: netRevenue, + ttl: config.getConfig('_bidderTimeout') || 300, + referrer: bidRequest.data.r, + ad: response.ad + }; + bidResponses.push(bidResponse); + } + }); + } catch (error) { + utils.logError(error); + } + return bidResponses; + } +} +registerBidder(spec); diff --git a/modules/colombiaBidAdapter.md b/modules/colombiaBidAdapter.md new file mode 100644 index 00000000000..c6ef5e6b749 --- /dev/null +++ b/modules/colombiaBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: COLOMBIA Bidder Adapter +Module Type: Bidder Adapter +Maintainer: colombiaonline@timesinteret.in +``` + +# Description + +Connect to COLOMBIA for bids. + +COLOMBIA adapter requires setup and approval from the COLOMBIA team. Please reach out to your account team or colombiaonline@timesinteret.in for more information. + +# Test Parameters +``` + var adUnits = [{ + code: 'test-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250],[728,90],[320,50]] + } + }, + bids: [{ + bidder: 'colombia', + params: { + placementId: '540799' + } + }] + }]; +``` \ No newline at end of file diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index 5fe78ff932d..2abe9cb94a8 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -147,12 +147,11 @@ export const spec = { if (bid.schain) { placement.schain = bid.schain; } - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + let gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { placement.gpid = gpid; } if (bid.userId) { - getUserId(placement.eids, bid.userId.britepoolid, 'britepool.com'); getUserId(placement.eids, bid.userId.idl_env, 'identityLink'); getUserId(placement.eids, bid.userId.id5id, 'id5-sync.com'); getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); @@ -173,7 +172,7 @@ export const spec = { placement.mimes = mediaTypes[VIDEO].mimes; placement.protocols = mediaTypes[VIDEO].protocols; placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; + placement.placement = mediaTypes[VIDEO].plcmt; placement.skip = mediaTypes[VIDEO].skip; placement.skipafter = mediaTypes[VIDEO].skipafter; placement.minbitrate = mediaTypes[VIDEO].minbitrate; diff --git a/modules/compassBidAdapter.js b/modules/compassBidAdapter.js index addcdfebb27..1a4cc3faeaf 100644 --- a/modules/compassBidAdapter.js +++ b/modules/compassBidAdapter.js @@ -1,212 +1,21 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'compass'; +const GVLID = 883; const AD_URL = 'https://sa-lb.deliverimp.com/pbjs'; const SYNC_URL = 'https://sa-cs.deliverimp.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 87ac96f2131..67ebd88e4e4 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -5,10 +5,12 @@ import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'conceptx'; const ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; // const LOG_PREFIX = 'ConceptX: '; +const GVLID = 1340; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], + gvlid: GVLID, isBidRequestValid: function (bid) { return !!(bid.bidId && bid.params.site && bid.params.adunit); }, @@ -55,6 +57,9 @@ export const spec = { return bidResponses } const firstSeat = firstBid.ads[0] + if (!firstSeat) { + return bidResponses + } const bidResponse = { requestId: firstSeat.requestId, cpm: firstSeat.cpm, diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index bd738a39bba..69b05ccfdba 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -1,7 +1,8 @@ import { logWarn, logMessage, debugTurnedOn, generateUUID, deepAccess } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; -import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { hasPurpose1Consent } from '../src/utils/gdpr.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -128,7 +129,8 @@ export const spec = { meta: { advertiserDomains: bid && bid.adomain ? bid.adomain : [] }, creativeId: bid.creativeId, netRevenue: bid.netRevenue, - currency: bid.currency + currency: bid.currency, + ...(bid.dealid && { dealId: bid.dealid }), }; }); @@ -264,7 +266,7 @@ function getUserId(id, source, uidExt, atype) { function getOffset(el) { if (el) { - const rect = el.getBoundingClientRect(); + const rect = getBoundingClientRect(el); return { left: rect.left + window.scrollX, top: rect.top + window.scrollY diff --git a/modules/condorxBidAdapter.js b/modules/condorxBidAdapter.js new file mode 100644 index 00000000000..45aff3ffb72 --- /dev/null +++ b/modules/condorxBidAdapter.js @@ -0,0 +1,183 @@ +import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import { createTrackPixelHtml, inIframe } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +const BIDDER_CODE = 'condorx'; +const API_URL = 'https://api.condorx.io/cxb/get.json'; +const REQUEST_METHOD = 'GET'; +const MAX_SIZE_DEVIATION = 0.05; +const SUPPORTED_AD_SIZES = [ + [100, 100], [200, 200], [300, 250], [400, 200], [300, 200], [600, 600], [236, 202], [1080, 1920], [300, 374] +]; + +function getBidRequestUrl(bidRequest, bidderRequest) { + if (bidRequest.params.url && bidRequest.params.url !== 'current url') { + return bidRequest.params.url; + } + return getBidderRequestUrl(bidderRequest); +} + +function getBidderRequestUrl(bidderRequest) { + if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { + return bidderRequest.refererInfo.page; + } + const pageUrl = inIframe() && document.referrer ? document.referrer : window.location.href; + return encodeURIComponent(pageUrl); +} + +function getTileImageUrl(tile) { + return tile.imageUrl.indexOf('http') === -1 ? 'https:' + tile.imageUrl : tile.imageUrl; +} + +function collectImpressionTrackers(tile, response) { + const trackers = [response.widgetViewPixel]; + if (!tile.trackers) return trackers; + const impressionTrackers = tile.trackers.impressionPixels || []; + const viewTrackers = tile.trackers.viewPixels || []; + return [...impressionTrackers, ...viewTrackers, ...trackers]; +} + +function parseNativeAdResponse(tile, response) { + return { + title: tile.title, + body: '', + image: { + url: getTileImageUrl(tile), + width: response.imageWidth, + height: response.imageHeight + }, + privacyLink: '', + clickUrl: tile.clickUrl, + displayUrl: tile.url, + cta: '', + sponsoredBy: tile.displayName, + impressionTrackers: collectImpressionTrackers(tile, response), + }; +} + +function parseBannerAdResponse(tile, response) { + if (tile.tag) { + return tile.tag; + } + let style = ''; + try { + const config = JSON.parse(response.widget.config); + const css = config.css || ''; + style = css ? `` : ''; + } catch (e) { + style = ''; + } + const title = tile.title && tile.title.trim() ? `` : ''; + const displayName = tile.displayName && title ? `` : ''; + const trackers = collectImpressionTrackers(tile, response) + .map((url) => createTrackPixelHtml(url)) + .join(''); + return `${style}`; +} + +function getAdSize(bidRequest) { + if (bidRequest.sizes && bidRequest.sizes.length > 0) { + return bidRequest.sizes[0]; + } else if (bidRequest.nativeParams && bidRequest.nativeParams.image && bidRequest.nativeParams.image.sizes) { + return bidRequest.nativeParams.image.sizes; + } + return [-1, -1]; +} + +function isValidAdSize([width, height]) { + if (!width || !height) { + return false; + } + return SUPPORTED_AD_SIZES.some(([supportedWidth, supportedHeight]) => { + if (supportedWidth === width && supportedHeight === height) { + return true; + } + const supportedRatio = supportedWidth / supportedHeight; + const ratioDeviation = supportedRatio / width * height; + return Math.abs(ratioDeviation - 1) <= MAX_SIZE_DEVIATION && (supportedWidth > width || (width - supportedWidth) / width <= MAX_SIZE_DEVIATION); + }); +} + +export const bidderSpec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE], + + isBidRequestValid: function (bidRequest) { + return bidRequest && + bidRequest.params && + bidRequest.params.hasOwnProperty('widget') && + bidRequest.params.hasOwnProperty('website') && + !isNaN(bidRequest.params.widget) && + !isNaN(bidRequest.params.website) && + isValidAdSize(getAdSize(bidRequest)); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + if (!validBidRequests) { + return []; + } + return validBidRequests.map(bidRequest => { + if (bidRequest.params) { + const mediaType = bidRequest.hasOwnProperty('nativeParams') ? 1 : 2; + const [imageWidth, imageHeight] = getAdSize(bidRequest); + const widgetId = bidRequest.params.widget; + const websiteId = bidRequest.params.website; + const pageUrl = getBidRequestUrl(bidRequest, bidderRequest); + let subid; + try { + let url + try { + url = new URL(pageUrl); + } catch (e) { + url = new URL(getBidderRequestUrl(bidderRequest)) + } + subid = url.hostname; + } catch (e) { + subid = widgetId; + } + const bidId = bidRequest.bidId; + let apiUrl = `${API_URL}?w=${websiteId}&wg=${widgetId}&u=${pageUrl}&s=${subid}&p=0&ireqid=${bidId}&prebid=${mediaType}&imgw=${imageWidth}&imgh=${imageHeight}`; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprApplies && bidderRequest.consentString) { + apiUrl += `&g=1&gc=${bidderRequest.consentString}`; + } + return { + url: apiUrl, + method: REQUEST_METHOD, + data: '' + }; + } + }).filter(Boolean); + }, + + interpretResponse: function (serverResponse, bidRequest) { + if (!serverResponse.body || !serverResponse.body.tiles || !serverResponse.body.tiles.length) { + return []; + } + const response = serverResponse.body; + const isNative = response.pbtypeId === 1; + return response.tiles.map(tile => { + let bid = { + requestId: response.ireqId, + width: response.imageWidth, + height: response.imageHeight, + creativeId: tile.postId, + cpm: tile.pecpm || (tile.ecpm / 100), + currency: 'USD', + netRevenue: !!tile.pecpm, + ttl: 360, + meta: { advertiserDomains: tile.domain ? [tile.domain] : [] }, + }; + if (isNative) { + bid.native = parseNativeAdResponse(tile, response); + } else { + bid.ad = parseBannerAdResponse(tile, response); + } + return bid; + }); + } +}; + +registerBidder(bidderSpec); diff --git a/modules/condorxBidAdapter.md b/modules/condorxBidAdapter.md new file mode 100644 index 00000000000..2df8be7220a --- /dev/null +++ b/modules/condorxBidAdapter.md @@ -0,0 +1,64 @@ +# Overview + +``` +Module Name: CondorX's Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@condorx.io +``` + +# Description + +Module that connects to CondorX bidder to fetch bids. + +# Test Parameters +``` + var adUnits = [{ + code: 'condorx-container-id', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + bidder: "condorx", + params: { + widget: 'widget id by CondorX', + website: 'website id by CondorX', + url:'current url' + } + }] + }, + { + code: 'condorx-container-id', + mediaTypes: { + native: { + image: { + required: true, + sizes: [236, 202] + }, + title: { + required: true, + len: 100 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + body: { + required: true + } + } + }, + bids: [{ + bidder: "condorx", + params: { + widget: 'widget id by CondorX', + website: 'website id by CondorX', + url:'current url' + } + }] + } + }]; +``` diff --git a/modules/confiantRtdProvider.js b/modules/confiantRtdProvider.js index 4c5475421bb..7aee63472c9 100644 --- a/modules/confiantRtdProvider.js +++ b/modules/confiantRtdProvider.js @@ -13,6 +13,7 @@ import { logError, generateUUID } from '../src/utils.js'; import { loadExternalScript } from '../src/adloader.js'; import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; /** * Injects the Confiant Inc. configuration script into the page, based on proprtyId provided @@ -21,7 +22,8 @@ import { EVENTS } from '../src/constants.js'; function injectConfigScript(propertyId) { const scriptSrc = `https://cdn.confiant-integrations.net/${propertyId}/gpt_and_prebid/config.js`; - loadExternalScript(scriptSrc, 'confiant', () => {}); + loadExternalScript(scriptSrc, MODULE_TYPE_RTD, 'confiant', () => { + }); } /** diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js index 0b840db6c26..15b74e1f814 100644 --- a/modules/connatixBidAdapter.js +++ b/modules/connatixBidAdapter.js @@ -1,23 +1,46 @@ +import adapterManager from '../src/adapterManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { percentInView } from '../libraries/percentInView/percentInView.js'; + +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; import { deepAccess, - isFn, - logError, + deepSetValue, + formatQS, + getWindowTop, isArray, - formatQS + isFn, + isNumber, + isStr, + logError } from '../src/utils.js'; import { + ADPOD, BANNER, + VIDEO, } from '../src/mediaTypes.js'; const BIDDER_CODE = 'connatix'; + const AD_URL = 'https://capi.connatix.com/rtb/hba'; const DEFAULT_MAX_TTL = '3600'; const DEFAULT_CURRENCY = 'USD'; +const CNX_IDS_LOCAL_STORAGE_COOKIES_KEY = 'cnx_user_ids'; +const CNX_IDS_EXPIRY = 24 * 30 * 60 * 60 * 1000; // 30 days +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); +const ALL_PROVIDERS_RESOLVED_EVENT = 'cnx_all_identity_providers_resolved'; +const IDENTITY_PROVIDER_COLLECTION_UPDATED_EVENT = 'cnx_identity_provider_collection_updated'; +let cnxIdsValues; + +const EVENTS_URL = 'https://capi.connatix.com/tr/am'; + +let context = {}; /* * Get the bid floor value from the bid object, either using the getFloor function or by accessing the 'params.bidfloor' property. @@ -34,17 +57,169 @@ export function getBidFloor(bid) { mediaType: '*', size: '*', }); - return bidFloor.floor; + return bidFloor?.floor; } catch (err) { logError(err); return 0; } } +export function validateBanner(mediaTypes) { + if (!mediaTypes[BANNER]) { + return true; + } + + const banner = deepAccess(mediaTypes, BANNER, {}); + return (Boolean(banner.sizes) && isArray(mediaTypes[BANNER].sizes) && mediaTypes[BANNER].sizes.length > 0); +} + +export function validateVideo(mediaTypes) { + const video = mediaTypes[VIDEO]; + if (!video) { + return true; + } + + return video.context !== ADPOD; +} + +export function _getMinSize(sizes) { + if (!sizes || sizes.length === 0) return undefined; + return sizes.reduce((minSize, currentSize) => { + const minArea = minSize.w * minSize.h; + const currentArea = currentSize.w * currentSize.h; + return currentArea < minArea ? currentSize : minSize; + }); +} + +export function _canSelectViewabilityContainer() { + try { + window.top.document.querySelector('#viewability-container'); + return true; + } catch (e) { + return false; + } +} + +export function _isViewabilityMeasurable(element) { + if (!element) return false; + return _canSelectViewabilityContainer(element); +} + +export function _getViewability(element, topWin, { w, h } = {}) { + return topWin.document.visibilityState === 'visible' + ? percentInView(element, { w, h }) + : 0; +} + +export function detectViewability(bid) { + const { params, adUnitCode } = bid; + + const viewabilityContainerIdentifier = params.viewabilityContainerIdentifier; + + let element = null; + let bidParamSizes = null; + let minSize = []; + + if (isStr(viewabilityContainerIdentifier)) { + try { + element = document.querySelector(viewabilityContainerIdentifier) || window.top.document.querySelector(viewabilityContainerIdentifier); + if (element) { + bidParamSizes = [element.offsetWidth, element.offsetHeight]; + minSize = _getMinSize(bidParamSizes) + } + } catch (e) { + logError(`Error while trying to find viewability container element: ${viewabilityContainerIdentifier}`); + } + } + + if (!element) { + // Get the sizes from the mediaTypes object if it exists, otherwise use the sizes array from the bid object + bidParamSizes = bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes ? bid.mediaTypes.banner.sizes : bid.sizes; + bidParamSizes = typeof bidParamSizes === 'undefined' && bid.mediaType && bid.mediaType.video && bid.mediaType.video.playerSize ? bid.mediaType.video.playerSize : bidParamSizes; + bidParamSizes = typeof bidParamSizes === 'undefined' && bid.mediaType && bid.mediaType.video && isNumber(bid.mediaType.video.w) && isNumber(bid.mediaType.h) ? [bid.mediaType.video.w, bid.mediaType.video.h] : bidParamSizes; + minSize = _getMinSize(bidParamSizes ?? []) + element = document.getElementById(adUnitCode); + } + + if (_isViewabilityMeasurable(element)) { + const minSizeObj = { + w: minSize[0], + h: minSize[1] + } + return Math.round(_getViewability(element, getWindowTop(), minSizeObj)) + } + + return null; +} + +export function _getBidRequests(validBidRequests) { + return validBidRequests.map(bid => { + const { + bidId, + mediaTypes, + params, + sizes, + } = bid; + const { placementId, viewabilityContainerIdentifier } = params; + let detectedViewabilityPercentage = detectViewability(bid); + if (isNumber(detectedViewabilityPercentage)) { + detectedViewabilityPercentage = detectedViewabilityPercentage / 100; + } + return { + bidId, + mediaTypes, + sizes, + placementId, + floor: getBidFloor(bid), + hasViewabilityContainerId: Boolean(viewabilityContainerIdentifier), + declaredViewabilityPercentage: bid.params.viewabilityPercentage ?? null, + detectedViewabilityPercentage, + }; + }); +} + +/** + * Get ids from Prebid User ID Modules and add them to the payload + */ +function _handleEids(payload, validBidRequests) { + let bidUserIdAsEids = deepAccess(validBidRequests, '0.userIdAsEids'); + if (isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) { + deepSetValue(payload, 'userIdList', bidUserIdAsEids); + } +} + +export function hasQueryParams(url) { + try { + const urlObject = new URL(url); + return !!urlObject.search; + } catch (e) { + return false; + } +} + +export function saveOnAllStorages(name, value, expirationTimeMs) { + const date = new Date(); + date.setTime(date.getTime() + expirationTimeMs); + const expires = `expires=${date.toUTCString()}`; + storage.setCookie(name, JSON.stringify(value), expires); + storage.setDataInLocalStorage(name, JSON.stringify(value)); + cnxIdsValues = value; +} + +export function readFromAllStorages(name) { + const fromCookie = storage.getCookie(name); + const fromLocalStorage = storage.getDataFromLocalStorage(name); + + const parsedCookie = fromCookie ? JSON.parse(fromCookie) : undefined; + const parsedLocalStorage = fromLocalStorage ? JSON.parse(fromLocalStorage) : undefined; + + return parsedCookie || parsedLocalStorage || undefined; +} + export const spec = { code: BIDDER_CODE, gvlid: 143, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /* * Validate the bid request. @@ -55,19 +230,24 @@ export const spec = { const bidId = deepAccess(bid, 'bidId'); const mediaTypes = deepAccess(bid, 'mediaTypes', {}); const params = deepAccess(bid, 'params', {}); - const bidder = deepAccess(bid, 'bidder'); - - const banner = deepAccess(mediaTypes, BANNER, {}); const hasBidId = Boolean(bidId); - const isValidBidder = (bidder === BIDDER_CODE); - const isValidSize = (Boolean(banner.sizes) && isArray(mediaTypes[BANNER].sizes) && mediaTypes[BANNER].sizes.length > 0); - const hasSizes = mediaTypes[BANNER] ? isValidSize : false; + const hasMediaTypes = Boolean(mediaTypes) && (Boolean(mediaTypes[BANNER]) || Boolean(mediaTypes[VIDEO])); + const isValidBanner = validateBanner(mediaTypes); + const isValidVideo = validateVideo(mediaTypes); + const isValidViewability = typeof params.viewabilityPercentage === 'undefined' || (isNumber(params.viewabilityPercentage) && params.viewabilityPercentage >= 0 && params.viewabilityPercentage <= 1); const hasRequiredBidParams = Boolean(params.placementId); - const isValid = isValidBidder && hasBidId && hasSizes && hasRequiredBidParams; + const isValid = hasBidId && hasMediaTypes && isValidBanner && isValidVideo && hasRequiredBidParams && isValidViewability; if (!isValid) { - logError(`Invalid bid request: isValidBidder: ${isValidBidder} hasBidId: ${hasBidId}, hasSizes: ${hasSizes}, hasRequiredBidParams: ${hasRequiredBidParams}`); + logError( + `Invalid bid request: + hasBidId: ${hasBidId}, + hasMediaTypes: ${hasMediaTypes}, + isValidBanner: ${isValidBanner}, + isValidVideo: ${isValidVideo}, + hasRequiredBidParams: ${hasRequiredBidParams}` + ); } return isValid; }, @@ -78,21 +258,13 @@ export const spec = { * Return an object containing the request method, url, and the constructed payload. */ buildRequests: (validBidRequests = [], bidderRequest = {}) => { - const bidRequests = validBidRequests.map(bid => { - const { - bidId, - mediaTypes, - params, - sizes, - } = bid; - return { - bidId, - mediaTypes, - sizes, - placementId: params.placementId, - floor: getBidFloor(bid), - }; - }); + const bidRequests = _getBidRequests(validBidRequests); + let userIds; + try { + userIds = readFromAllStorages(CNX_IDS_LOCAL_STORAGE_COOKIES_KEY) || cnxIdsValues; + } catch (error) { + userIds = cnxIdsValues; + } const requestPayload = { ortb2: bidderRequest.ortb2, @@ -100,13 +272,18 @@ export const spec = { uspConsent: bidderRequest.uspConsent, gppConsent: bidderRequest.gppConsent, refererInfo: bidderRequest.refererInfo, + identityProviderData: userIds, bidRequests, }; + _handleEids(requestPayload, validBidRequests); + + context = requestPayload; + return { method: 'POST', url: AD_URL, - data: requestPayload + data: context }; }, @@ -129,12 +306,13 @@ export const spec = { cpm: bidResponse.Cpm, ttl: bidResponse.Ttl || DEFAULT_MAX_TTL, currency: 'USD', - mediaType: BANNER, + mediaType: bidResponse.VastXml ? VIDEO : BANNER, netRevenue: true, width: bidResponse.Width, height: bidResponse.Height, creativeId: bidResponse.CreativeId, ad: bidResponse.Ad, + vastXml: bidResponse.VastXml, referrer: referrer, })); }, @@ -171,15 +349,82 @@ export const spec = { params['us_privacy'] = encodeURIComponent(uspConsent); } + window.addEventListener('message', function handler(event) { + if (!event.data || event.origin !== 'https://cds.connatix.com' || !event.data.cnx) { + return; + } + + const { message, data } = event.data.cnx; + + if (message === ALL_PROVIDERS_RESOLVED_EVENT) { + this.removeEventListener('message', handler); + event.stopImmediatePropagation(); + } + + if (message === ALL_PROVIDERS_RESOLVED_EVENT || message === IDENTITY_PROVIDER_COLLECTION_UPDATED_EVENT) { + if (data) { + saveOnAllStorages(CNX_IDS_LOCAL_STORAGE_COOKIES_KEY, data, CNX_IDS_EXPIRY); + } + } + }, true) + const syncUrl = serverResponses[0].body.UserSyncEndpoint; const queryParams = Object.keys(params).length > 0 ? formatQS(params) : ''; - const url = queryParams ? `${syncUrl}?${queryParams}` : syncUrl; + let url = syncUrl; + if (queryParams) { + url += hasQueryParams(syncUrl) ? `&${queryParams}` : `?${queryParams}`; + } + return [{ type: 'iframe', url }]; - } + }, + + isConnatix: (aliasName) => { + if (!aliasName) { + return false; + } + + const originalBidderName = adapterManager.aliasRegistry[aliasName] || aliasName; + return originalBidderName === BIDDER_CODE; + }, + + /** + * Register bidder specific code, which will execute if the server response time is greater than auction timeout + */ + onTimeout: (timeoutData) => { + const connatixBidRequestTimeout = timeoutData.find(bidderRequest => spec.isConnatix(bidderRequest.bidder)); + + // Log only it is a timeout for Connatix + // Otherwise it is not relevant for us + if (!connatixBidRequestTimeout) { + return; + } + const requestTimeout = connatixBidRequestTimeout.timeout; + const timeout = isNumber(requestTimeout) ? requestTimeout : config.getConfig('bidderTimeout'); + spec.triggerEvent({type: 'Timeout', timeout, context}); + }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + */ + onBidWon(bidWinData) { + if (bidWinData == null) { + return; + } + const {bidder, cpm, requestId, bidId, adUnitCode, timeToRespond, auctionId} = bidWinData; + + spec.triggerEvent({type: 'BidWon', bestBidBidder: bidder, bestBidPrice: cpm, requestId, bidId, adUnitCode, timeToRespond, auctionId, context}); + }, + + triggerEvent(data) { + ajax(EVENTS_URL, null, JSON.stringify(data), { + method: 'POST', + withCredentials: false + }); + }, }; registerBidder(spec); diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index 2ebc68baa84..58c19895b81 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -11,7 +11,6 @@ import {includes} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js'; -import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; /** @@ -219,16 +218,16 @@ export const connectIdSubmodule = { } } - const uspString = uspDataHandler.getConsentData() || ''; + const uspString = consentData.usp || ''; const data = { v: '1', '1p': includes([1, '1', true], params['1p']) ? '1' : '0', - gdpr: connectIdSubmodule.isEUConsentRequired(consentData) ? '1' : '0', - gdpr_consent: connectIdSubmodule.isEUConsentRequired(consentData) ? consentData.consentString : '', + gdpr: connectIdSubmodule.isEUConsentRequired(consentData?.gdpr) ? '1' : '0', + gdpr_consent: connectIdSubmodule.isEUConsentRequired(consentData?.gdpr) ? consentData.gdpr.consentString : '', us_privacy: uspString }; - const gppConsent = gppDataHandler.getConsentData(); + const gppConsent = consentData.gpp; if (gppConsent) { data.gpp = `${gppConsent.gppString ? gppConsent.gppString : ''}`; if (Array.isArray(gppConsent.applicableSections)) { @@ -313,12 +312,16 @@ export const connectIdSubmodule = { /** * Utility function that returns a boolean flag indicating if the user - * has opeted out via the Yahoo easy-opt-out mechanism. + * has opted out via the Yahoo easy-opt-out mechanism. * @returns {Boolean} */ userHasOptedOut() { try { - return localStorage.getItem(OVERRIDE_OPT_OUT_KEY) === '1'; + if (storage.localStorageIsEnabled()) { + return storage.getDataFromLocalStorage(OVERRIDE_OPT_OUT_KEY) === '1'; + } else { + return true; + } } catch { return false; } diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index b40ef30f6bc..982bff22585 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -1,9 +1,9 @@ -import { deepSetValue, logWarn } from '../src/utils.js'; +import { deepAccess, deepSetValue, mergeDeep, logWarn, generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import {config} from '../src/config.js'; import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; + const BIDDER_CODE = 'connectad'; const BIDDER_CODE_ALIAS = 'connectadrealtime'; const ENDPOINT_URL = 'https://i.connectad.io/api/v2'; @@ -31,25 +31,39 @@ export const spec = { return ret; } + const sellerDefinedAudience = deepAccess(bidderRequest, 'ortb2.user.data', config.getAnyConfig('ortb2.user.data')); + const sellerDefinedContext = deepAccess(bidderRequest, 'ortb2.site.content.data', config.getAnyConfig('ortb2.site.content.data')); + const data = Object.assign({ placements: [], time: Date.now(), - user: {}, - // TODO: does the fallback to window.location make sense? - url: bidderRequest.refererInfo?.page || window.location.href, + url: bidderRequest.refererInfo?.page, referrer: bidderRequest.refererInfo?.ref, - // TODO: please do not send internal data structures over the network - referrer_info: bidderRequest.refererInfo?.legacy, screensize: getScreenSize(), dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, language: navigator.language, ua: navigator.userAgent, - pversion: '$prebid.version$' + pversion: '$prebid.version$', + cur: 'USD', + user: {}, + regs: {}, + source: {}, + site: {}, + sda: sellerDefinedAudience, + sdc: sellerDefinedContext, + }); + + const ortb2Params = bidderRequest?.ortb2 || {}; + ['site', 'user', 'device', 'bcat', 'badv', 'regs'].forEach(entry => { + const ortb2Param = ortb2Params[entry]; + if (ortb2Param) { + mergeDeep(data, { [entry]: ortb2Param }); + } }); // coppa compliance if (config.getConfig('coppa') === true) { - deepSetValue(data, 'user.coppa', 1); + deepSetValue(data, 'regs.coppa', 1); } // adding schain object @@ -72,24 +86,49 @@ export const spec = { deepSetValue(data, 'user.ext.us_privacy', bidderRequest.uspConsent); } + // GPP Support + if (bidderRequest?.gppConsent?.gppString) { + deepSetValue(data, 'regs.gpp', bidderRequest.gppConsent.gppString); + deepSetValue(data, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); + } else if (bidderRequest?.ortb2?.regs?.gpp) { + deepSetValue(data, 'regs.gpp', bidderRequest.ortb2.regs.gpp); + deepSetValue(data, 'regs.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); + } + + // DSA Support + if (bidderRequest?.ortb2?.regs?.ext?.dsa) { + deepSetValue(data, 'regs.ext.dsa', bidderRequest.ortb2.regs.ext.dsa); + } + // EIDS Support if (validBidRequests[0].userIdAsEids) { deepSetValue(data, 'user.ext.eids', validBidRequests[0].userIdAsEids); } + const tid = deepAccess(bidderRequest, 'ortb2.source.tid') + if (tid) { + deepSetValue(data, 'source.tid', tid) + } + data.tmax = bidderRequest.timeout; + validBidRequests.map(bid => { const placement = Object.assign({ - // TODO: fix transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - id: bid.transactionId, + id: generateUUID(), divName: bid.bidId, + tagId: bid.adUnitCode, pisze: bid.mediaTypes.banner.sizes[0] || bid.sizes[0], sizes: bid.mediaTypes.banner.sizes, - adTypes: getSize(bid.mediaTypes.banner.sizes || bid.sizes), bidfloor: getBidFloor(bid), siteId: bid.params.siteId, - networkId: bid.params.networkId + networkId: bid.params.networkId, + tid: bid.ortb2Imp?.ext?.tid }); + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + placement.gpid = gpid; + } + if (placement.networkId && placement.siteId) { data.placements.push(placement); } @@ -127,12 +166,22 @@ export const spec = { bid.width = decision.width; bid.height = decision.height; bid.dealid = decision.dealid || null; - bid.meta = { advertiserDomains: decision && decision.adomain ? decision.adomain : [] }; + bid.meta = { + advertiserDomains: decision && decision.adomain ? decision.adomain : [] + }; bid.ad = retrieveAd(decision); bid.currency = 'USD'; bid.creativeId = decision.adId; bid.ttl = 360; bid.netRevenue = true; + + if (decision.dsa) { + bid.meta = Object.assign({}, bid.meta, { dsa: decision.dsa }) + } + if (decision.category) { + bid.meta = Object.assign({}, bid.meta, { primaryCatId: decision.category }) + } + bidResponses.push(bid); } } @@ -141,15 +190,15 @@ export const spec = { return bidResponses; }, - transformBidParams: function (params, isOpenRtb) { - return convertTypes({ - 'siteId': 'number', - 'networkId': 'number' - }, params); - }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { + let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncEndpoint; - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { - let syncEndpoint = 'https://cdn.connectad.io/connectmyusers.php?'; + if (pixelType == 'iframe') { + syncEndpoint = 'https://sync.connectad.io/iFrameSyncer?'; + } else { + syncEndpoint = 'https://sync.connectad.io/ImageSyncer?'; + } if (gdprConsent) { syncEndpoint = tryAppendQueryString(syncEndpoint, 'gdpr', (gdprConsent.gdprApplies ? 1 : 0)); @@ -163,73 +212,26 @@ export const spec = { syncEndpoint = tryAppendQueryString(syncEndpoint, 'us_privacy', uspConsent); } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + syncEndpoint = tryAppendQueryString(syncEndpoint, 'gpp', gppConsent.gppString); + syncEndpoint = tryAppendQueryString(syncEndpoint, 'gpp_sid', gppConsent?.applicableSections?.join(',')); + } + if (config.getConfig('coppa') === true) { syncEndpoint = tryAppendQueryString(syncEndpoint, 'coppa', 1); } - if (syncOptions.iframeEnabled) { + if (syncOptions.iframeEnabled || syncOptions.pixelEnabled) { return [{ - type: 'iframe', + type: pixelType, url: syncEndpoint }]; } else { - logWarn('Bidder ConnectAd: Please activate iFrame Sync'); + logWarn('Bidder ConnectAd: No User-Matching allowed'); } } }; -const sizeMap = [ - null, - '120x90', - '200x200', - '468x60', - '728x90', - '300x250', - '160x600', - '120x600', - '300x100', - '180x150', - '336x280', - '240x400', - '234x60', - '88x31', - '120x60', - '120x240', - '125x125', - '220x250', - '250x250', - '250x90', - '0x0', - '200x90', - '300x50', - '320x50', - '320x480', - '185x185', - '620x45', - '300x125', - '800x250', - '980x120', - '980x150', - '320x150', - '300x300', - '200x600', - '320x500', - '320x320' -]; - -sizeMap[77] = '970x90'; -sizeMap[123] = '970x250'; -sizeMap[43] = '300x600'; -sizeMap[286] = '970x66'; -sizeMap[3230] = '970x280'; -sizeMap[429] = '486x60'; -sizeMap[374] = '700x500'; -sizeMap[934] = '300x1050'; -sizeMap[1578] = '320x100'; -sizeMap[331] = '320x250'; -sizeMap[3301] = '320x267'; -sizeMap[2730] = '728x250'; - function getBidFloor(bidRequest) { let floorInfo = {}; @@ -241,22 +243,11 @@ function getBidFloor(bidRequest) { }); } - let floor = floorInfo.floor || bidRequest.params.bidfloor || bidRequest.params.floorprice || 0; + let floor = floorInfo?.floor || bidRequest.params.bidfloor || bidRequest.params.floorprice || 0; return floor; } -function getSize(sizes) { - const result = []; - sizes.forEach(function(size) { - const index = sizeMap.indexOf(size[0] + 'x' + size[1]); - if (index >= 0) { - result.push(index); - } - }); - return result; -} - function retrieveAd(decision) { return decision.contents && decision.contents[0] && decision.contents[0].body; } diff --git a/modules/consentManagement.js b/modules/consentManagement.js deleted file mode 100644 index 346b241fc1f..00000000000 --- a/modules/consentManagement.js +++ /dev/null @@ -1,328 +0,0 @@ -/** - * This module adds GDPR consentManagement support to prebid.js. It interacts with - * supported CMPs (Consent Management Platforms) to grab the user's consent information - * and make it available for any GDPR supported adapters to read/pass this information to - * their system. - */ -import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; -import {config} from '../src/config.js'; -import {gdprDataHandler} from '../src/adapterManager.js'; -import {includes} from '../src/polyfill.js'; -import {timedAuctionHook} from '../src/utils/perfMetrics.js'; -import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; -import {enrichFPD} from '../src/fpd/enrichment.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import {cmpClient} from '../libraries/cmp/cmpClient.js'; - -const DEFAULT_CMP = 'iab'; -const DEFAULT_CONSENT_TIMEOUT = 10000; -const CMP_VERSION = 2; - -export let userCMP; -export let consentTimeout; -export let gdprScope; -export let staticConsentData; -let actionTimeout; - -let consentData; -let addedConsentHook = false; - -// add new CMPs here, with their dedicated lookup function -const cmpCallMap = { - 'iab': lookupIabConsent, - 'static': lookupStaticConsentData -}; - -/** - * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP - */ -function lookupStaticConsentData({onSuccess, onError}) { - processCmpData(staticConsentData, {onSuccess, onError}) -} - -/** - * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. - * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function - * based on the appropriate result. - * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP - * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) - */ -function lookupIabConsent({onSuccess, onError, onEvent}) { - function cmpResponseCallback(tcfData, success) { - logInfo('Received a response from CMP', tcfData); - if (success) { - onEvent(tcfData); - if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { - processCmpData(tcfData, {onSuccess, onError}); - } - } else { - onError('CMP unable to register callback function. Please check CMP setup.'); - } - } - - const cmp = cmpClient({ - apiName: '__tcfapi', - apiVersion: CMP_VERSION, - apiArgs: ['command', 'version', 'callback', 'parameter'], - }); - - if (!cmp) { - return onError('TCF2 CMP not found.'); - } - if (cmp.isDirect) { - logInfo('Detected CMP API is directly accessible, calling it now...'); - } else { - logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); - } - - cmp({ - command: 'addEventListener', - callback: cmpResponseCallback - }) -} - -/** - * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. - * - * @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra - * error arguments that will be undefined if there's no error. - */ -function loadConsentData(cb) { - let isDone = false; - let timer = null; - let onTimeout, provisionalConsent; - let cmpLoaded = false; - - function resetTimeout(timeout) { - if (timer != null) { - clearTimeout(timer); - } - if (!isDone && timeout != null) { - if (timeout === 0) { - onTimeout() - } else { - timer = setTimeout(onTimeout, timeout); - } - } - } - - function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) { - resetTimeout(null); - isDone = true; - gdprDataHandler.setConsentData(consentData); - if (typeof cb === 'function') { - cb(shouldCancelAuction, errMsg, ...extraArgs); - } - } - - if (!includes(Object.keys(cmpCallMap), userCMP)) { - done(null, false, `CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); - return; - } - - const callbacks = { - onSuccess: (data) => done(data, false), - onError: function (msg, ...extraArgs) { - done(null, true, msg, ...extraArgs); - }, - onEvent: function (consentData) { - provisionalConsent = consentData; - if (cmpLoaded) return; - cmpLoaded = true; - if (actionTimeout != null) { - resetTimeout(actionTimeout); - } - } - } - - onTimeout = () => { - const continueToAuction = (data) => { - done(data, false, `${cmpLoaded ? 'Timeout waiting for user action on CMP' : 'CMP did not load'}, continuing auction...`); - } - processCmpData(provisionalConsent, { - onSuccess: continueToAuction, - onError: () => continueToAuction(storeConsentData(undefined)), - }) - } - - cmpCallMap[userCMP](callbacks); - if (!(actionTimeout != null && cmpLoaded)) { - resetTimeout(consentTimeout); - } -} - -/** - * Like `loadConsentData`, but cache and re-use previously loaded data. - * @param cb - */ -function loadIfMissing(cb) { - if (consentData) { - logInfo('User consent information already known. Pulling internally stored information...'); - // eslint-disable-next-line standard/no-callback-literal - cb(false); - } else { - loadConsentData(cb); - } -} - -/** - * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the - * user's encoded consent string from the supported CMP. Once obtained, the module will store this - * data as part of a gdprConsent object which gets transferred to adapterManager's gdprDataHandler object. - * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. - * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. - * @param {function} fn required; The next function in the chain, used by hook.js - */ -export const requestBidsHook = timedAuctionHook('gdpr', function requestBidsHook(fn, reqBidsConfigObj) { - loadIfMissing(function (shouldCancelAuction, errMsg, ...extraArgs) { - if (errMsg) { - let log = logWarn; - if (shouldCancelAuction) { - log = logError; - errMsg = `${errMsg} Canceling auction as per consentManagement config.`; - } - log(errMsg, ...extraArgs); - } - - if (shouldCancelAuction) { - fn.stopTiming(); - if (typeof reqBidsConfigObj.bidsBackHandler === 'function') { - reqBidsConfigObj.bidsBackHandler(); - } else { - logError('Error executing bidsBackHandler'); - } - } else { - fn.call(this, reqBidsConfigObj); - } - }); -}); - -/** - * This function checks the consent data provided by CMP to ensure it's in an expected state. - * If it's bad, we call `onError` - * If it's good, then we store the value and call `onSuccess` - */ -function processCmpData(consentObject, {onSuccess, onError}) { - function checkData() { - // if CMP does not respond with a gdprApplies boolean, use defaultGdprScope (gdprScope) - const gdprApplies = consentObject && typeof consentObject.gdprApplies === 'boolean' ? consentObject.gdprApplies : gdprScope; - const tcString = consentObject && consentObject.tcString; - return !!( - (typeof gdprApplies !== 'boolean') || - (gdprApplies === true && (!tcString || !isStr(tcString))) - ); - } - - if (checkData()) { - onError(`CMP returned unexpected value during lookup process.`, consentObject); - } else { - onSuccess(storeConsentData(consentObject)); - } -} - -/** - * Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction - * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) - */ -function storeConsentData(cmpConsentObject) { - consentData = { - consentString: (cmpConsentObject) ? cmpConsentObject.tcString : undefined, - vendorData: (cmpConsentObject) || undefined, - gdprApplies: cmpConsentObject && typeof cmpConsentObject.gdprApplies === 'boolean' ? cmpConsentObject.gdprApplies : gdprScope - }; - if (cmpConsentObject && cmpConsentObject.addtlConsent && isStr(cmpConsentObject.addtlConsent)) { - consentData.addtlConsent = cmpConsentObject.addtlConsent; - } - consentData.apiVersion = CMP_VERSION; - return consentData; -} - -/** - * Simply resets the module's consentData variable back to undefined, mainly for testing purposes - */ -export function resetConsentData() { - consentData = undefined; - userCMP = undefined; - consentTimeout = undefined; - gdprDataHandler.reset(); -} - -/** - * A configuration function that initializes some module variables, as well as add a hook into the requestBids function - * @param {{cmp:string, timeout:number, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int)) - */ -export function setConsentConfig(config) { - // if `config.gdpr`, `config.usp` or `config.gpp` exist, assume new config format. - // else for backward compatability, just use `config` - config = config && (config.gdpr || config.usp || config.gpp ? config.gdpr : config); - if (!config || typeof config !== 'object') { - logWarn('consentManagement (gdpr) config not defined, exiting consent manager'); - return; - } - if (isStr(config.cmpApi)) { - userCMP = config.cmpApi; - } else { - userCMP = DEFAULT_CMP; - logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`); - } - - if (isNumber(config.timeout)) { - consentTimeout = config.timeout; - } else { - consentTimeout = DEFAULT_CONSENT_TIMEOUT; - logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); - } - - actionTimeout = isNumber(config.actionTimeout) ? config.actionTimeout : null; - - // if true, then gdprApplies should be set to true - gdprScope = config.defaultGdprScope === true; - - logInfo('consentManagement module has been activated...'); - - if (userCMP === 'static') { - if (isPlainObject(config.consentData)) { - staticConsentData = config.consentData; - if (staticConsentData?.getTCData != null) { - // accept static config with or without `getTCData` - see https://github.com/prebid/Prebid.js/issues/9581 - staticConsentData = staticConsentData.getTCData; - } - consentTimeout = 0; - } else { - logError(`consentManagement config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); - } - } - if (!addedConsentHook) { - getGlobal().requestBids.before(requestBidsHook, 50); - } - addedConsentHook = true; - gdprDataHandler.enable(); - loadConsentData(); // immediately look up consent data to make it available without requiring an auction -} -config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); - -export function enrichFPDHook(next, fpd) { - return next(fpd.then(ortb2 => { - const consent = gdprDataHandler.getConsentData(); - if (consent) { - if (typeof consent.gdprApplies === 'boolean') { - deepSetValue(ortb2, 'regs.ext.gdpr', consent.gdprApplies ? 1 : 0); - } - deepSetValue(ortb2, 'user.ext.consent', consent.consentString); - } - return ortb2; - })); -} - -enrichFPD.before(enrichFPDHook); - -export function setOrtbAdditionalConsent(ortbRequest, bidderRequest) { - // this is not a standardized name for addtlConsent, so keep this as an ORTB library processor rather than an FPD enrichment - const addtl = bidderRequest.gdprConsent?.addtlConsent; - if (addtl && typeof addtl === 'string') { - deepSetValue(ortbRequest, 'user.ext.ConsentedProvidersSettings.consented_providers', addtl); - } -} - -registerOrtbProcessor({type: REQUEST, name: 'gdprAddtlConsent', fn: setOrtbAdditionalConsent}) diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index a7bbca62205..cf916e58b13 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -4,42 +4,15 @@ * and make it available for any GPP supported adapters to read/pass this information to * their system and for various other features/modules in Prebid.js. */ -import {deepSetValue, isEmpty, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {deepSetValue, isEmpty, isPlainObject, isStr, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import {gppDataHandler} from '../src/adapterManager.js'; -import {timedAuctionHook} from '../src/utils/perfMetrics.js'; import {enrichFPD} from '../src/fpd/enrichment.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import {cmpClient, MODE_CALLBACK, MODE_MIXED, MODE_RETURN} from '../libraries/cmp/cmpClient.js'; -import {GreedyPromise} from '../src/utils/promise.js'; -import {buildActivityParams} from '../src/activities/params.js'; +import {cmpClient, MODE_CALLBACK} from '../libraries/cmp/cmpClient.js'; +import {PbPromise, defer} from '../src/utils/promise.js'; +import {configParser} from '../libraries/consentManagement/cmUtils.js'; -const DEFAULT_CMP = 'iab'; -const DEFAULT_CONSENT_TIMEOUT = 10000; - -export let userCMP; -export let consentTimeout; -let staticConsentData; - -let consentData; -let addedConsentHook = false; - -function pipeCallbacks(fn, {onSuccess, onError}) { - new GreedyPromise((resolve) => resolve(fn())).then(onSuccess, (err) => { - if (err instanceof GPPError) { - onError(err.message, ...err.args); - } else { - onError(`GPP error:`, err); - } - }); -} - -function lookupStaticConsentData(callbacks) { - return pipeCallbacks(() => processCmpData(staticConsentData), callbacks); -} - -const GPP_10 = '1.0'; -const GPP_11 = '1.1'; +export let consentConfig = {}; class GPPError { constructor(message, arg) { @@ -49,104 +22,22 @@ class GPPError { } export class GPPClient { - static CLIENTS = {}; - - static register(apiVersion, defaultVersion = false) { - this.apiVersion = apiVersion; - this.CLIENTS[apiVersion] = this; - if (defaultVersion) { - this.CLIENTS.default = this; - } - } - + apiVersion = '1.1'; static INST; - /** - * Ping the CMP to set up an appropriate client for it, and initialize it. - * - * @param mkCmp - * @returns {Promise<[GPPClient,Promise<{}>]>} a promise to two objects: - * - a GPPClient that talks the best GPP dialect we know for the CMP's version; - * - a promise to GPP data. - */ - static init(mkCmp = cmpClient) { - let inst = this.INST; - if (!inst) { - let err; - const reset = () => err && (this.INST = null); - inst = this.INST = this.ping(mkCmp).catch(e => { - err = true; - reset(); - throw e; + static get(mkCmp = cmpClient) { + if (this.INST == null) { + const cmp = mkCmp({ + apiName: '__gpp', + apiArgs: ['command', 'callback', 'parameter'], // do not pass version - not clear what it's for (or what we should use), + mode: MODE_CALLBACK }); - reset(); - } - return inst.then(([client, pingData]) => [ - client, - client.initialized ? client.refresh() : client.init(pingData) - ]); - } - - /** - * Ping the CMP to determine its version and set up a client appropriate for it. - * - * @param mkCmp - * @returns {Promise<[GPPClient, {}]>} a promise to two objects: - * - a GPPClient that talks the best GPP dialect we know for the CMP's version; - * - the result from pinging the CMP. - */ - static ping(mkCmp = cmpClient) { - const cmpOptions = { - apiName: '__gpp', - apiArgs: ['command', 'callback', 'parameter'], // do not pass version - not clear what it's for (or what we should use) - }; - - // in 1.0, 'ping' should return pingData but ignore callback; - // in 1.1 it should not return anything but run the callback - // the following looks for either - but once the version is known, produce a client that knows whether the - // rest of the interactions should pick return values or pass callbacks - - const probe = mkCmp({...cmpOptions, mode: MODE_RETURN}); - return new GreedyPromise((resolve, reject) => { - if (probe == null) { - reject(new GPPError('GPP CMP not found')); - return; + if (cmp == null) { + throw new GPPError('GPP CMP not found'); } - let done = false; // some CMPs do both return value and callbacks - avoid repeating log messages - const pong = (result, success) => { - if (done) return; - if (success != null && !success) { - reject(result); - return; - } - if (result == null) return; - done = true; - const cmpVersion = result?.gppVersion; - const Client = this.getClient(cmpVersion); - if (cmpVersion !== Client.apiVersion) { - logWarn(`Unrecognized GPP CMP version: ${cmpVersion}. Continuing using GPP API version ${Client}...`); - } else { - logInfo(`Using GPP version ${cmpVersion}`); - } - const mode = Client.apiVersion === GPP_10 ? MODE_MIXED : MODE_CALLBACK; - const client = new Client( - cmpVersion, - mkCmp({...cmpOptions, mode}) - ); - resolve([client, result]); - }; - - probe({ - command: 'ping', - callback: pong - }).then((res) => pong(res, true), reject); - }).finally(() => { - probe && probe.close(); - }); - } - - static getClient(cmpVersion) { - return this.CLIENTS.hasOwnProperty(cmpVersion) ? this.CLIENTS[cmpVersion] : this.CLIENTS.default; + this.INST = new this(cmp); + } + return this.INST; } #resolve; @@ -155,11 +46,9 @@ export class GPPClient { initialized = false; - constructor(cmpVersion, cmp) { - this.apiVersion = this.constructor.apiVersion; - this.cmpVersion = cmp; + constructor(cmp) { this.cmp = cmp; - [this.#resolve, this.#reject] = [0, 1].map(slot => (result) => { + [this.#resolve, this.#reject] = ['resolve', 'reject'].map(slot => (result) => { while (this.#pending.length) { this.#pending.pop()[slot](result); } @@ -176,6 +65,9 @@ export class GPPClient { init(pingData) { const ready = this.updateWhenReady(pingData); if (!this.initialized) { + if (pingData.gppVersion !== this.apiVersion) { + logWarn(`Unrecognized GPP CMP version: ${pingData.apiVersion}. Continuing using GPP API version ${this.apiVersion}...`); + } this.initialized = true; this.cmp({ command: 'addEventListener', @@ -184,9 +76,18 @@ export class GPPClient { this.#reject(new GPPError('Received error response from CMP', event)); } else if (event?.pingData?.cmpStatus === 'error') { this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event)); - } else if (this.isCMPReady(event?.pingData || {}) && this.events.includes(event?.eventName)) { + } else if (this.isCMPReady(event?.pingData || {}) && ['sectionChange', 'signalStatus'].includes(event?.eventName)) { this.#resolve(this.updateConsent(event.pingData)); } + // NOTE: according to https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md, + // > [signalStatus] Event is called whenever the display status of the CMP changes (e.g. the CMP shows the consent layer). + // + // however, from real world testing, at least some CMPs only trigger 'cmpDisplayStatus' + // other CMPs may do something else yet; here we just look for 'signalStatus: not ready' on any event + // to decide if consent data is likely to change + if (gppDataHandler.getConsentData() != null && event?.pingData != null && !this.isCMPReady(event.pingData)) { + gppDataHandler.setConsentData(null); + } } }); } @@ -194,7 +95,7 @@ export class GPPClient { } refresh() { - return this.cmp({command: 'ping'}).then(this.updateWhenReady.bind(this)); + return this.cmp({command: 'ping'}).then(this.init.bind(this)); } /** @@ -204,14 +105,14 @@ export class GPPClient { * @returns {Promise<{}>} a promise to GPP consent data */ updateConsent(pingData) { - return this.getGPPData(pingData).then((data) => { - if (data == null || isEmpty(data)) { - throw new GPPError('Received empty response from CMP', data); + return new PbPromise(resolve => { + if (pingData == null || isEmpty(pingData)) { + throw new GPPError('Received empty response from CMP', pingData); } - return processCmpData(data); - }).then((data) => { - logInfo('Retrieved GPP consent from CMP:', data); - return data; + const consentData = parseConsentData(pingData); + logInfo('Retrieved GPP consent from CMP:', consentData); + gppDataHandler.setConsentData(consentData); + resolve(consentData); }); } @@ -221,9 +122,9 @@ export class GPPClient { * @returns {Promise<{}>} */ nextUpdate() { - return new GreedyPromise((resolve, reject) => { - this.#pending.push([resolve, reject]); - }); + const def = defer(); + this.#pending.push(def); + return def.promise; } /** @@ -236,275 +137,68 @@ export class GPPClient { updateWhenReady(pingData) { return this.isCMPReady(pingData) ? this.updateConsent(pingData) : this.nextUpdate(); } -} - -// eslint-disable-next-line no-unused-vars -class GPP10Client extends GPPClient { - static { - super.register(GPP_10); - } - - events = ['sectionChange', 'cmpStatus']; - - isCMPReady(pingData) { - return pingData.cmpStatus === 'loaded'; - } - - getGPPData(pingData) { - const parsedSections = GreedyPromise.all( - (pingData.supportedAPIs || pingData.apiSupport || []).map((api) => this.cmp({ - command: 'getSection', - parameter: api - }).catch(err => { - logWarn(`Could not retrieve GPP section '${api}'`, err); - }).then((section) => [api, section])) - ).then(sections => { - // parse single section object into [core, gpc] to uniformize with 1.1 parsedSections - return Object.fromEntries( - sections.filter(([_, val]) => val != null) - .map(([api, section]) => { - const subsections = [ - Object.fromEntries(Object.entries(section).filter(([k]) => k !== 'Gpc')) - ]; - if (section.Gpc != null) { - subsections.push({ - SubsectionType: 1, - Gpc: section.Gpc - }); - } - return [api, subsections]; - }) - ); - }); - return GreedyPromise.all([ - this.cmp({command: 'getGPPData'}), - parsedSections - ]).then(([gppData, parsedSections]) => Object.assign({}, gppData, {parsedSections})); - } -} - -// eslint-disable-next-line no-unused-vars -class GPP11Client extends GPPClient { - static { - super.register(GPP_11, true); - } - - events = ['sectionChange', 'signalStatus']; isCMPReady(pingData) { return pingData.signalStatus === 'ready'; } - - getGPPData(pingData) { - return GreedyPromise.resolve(pingData); - } } -/** - * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. - * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function - * based on the appropriate result. - * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP - * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) - */ -export function lookupIabConsent({onSuccess, onError}, mkCmp = cmpClient) { - pipeCallbacks(() => GPPClient.init(mkCmp).then(([client, gppDataPm]) => gppDataPm), {onSuccess, onError}); +function lookupIabConsent() { + return new PbPromise((resolve) => resolve(GPPClient.get().refresh())) } // add new CMPs here, with their dedicated lookup function const cmpCallMap = { 'iab': lookupIabConsent, - 'static': lookupStaticConsentData }; -/** - * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. - * - * @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra - * error arguments that will be undefined if there's no error. - */ -function loadConsentData(cb) { - let isDone = false; - let timer = null; - - function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) { - if (timer != null) { - clearTimeout(timer); - } - isDone = true; - gppDataHandler.setConsentData(consentData); - if (typeof cb === 'function') { - cb(shouldCancelAuction, errMsg, ...extraArgs); - } - } - - if (!cmpCallMap.hasOwnProperty(userCMP)) { - done(null, false, `GPP CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); - return; - } - - const callbacks = { - onSuccess: (data) => done(data, false), - onError: function (msg, ...extraArgs) { - done(null, true, msg, ...extraArgs); - } - }; - cmpCallMap[userCMP](callbacks); - - if (!isDone) { - const onTimeout = () => { - const continueToAuction = (data) => { - done(data, false, 'GPP CMP did not load, continuing auction...'); - }; - pipeCallbacks(() => processCmpData(consentData), { - onSuccess: continueToAuction, - onError: () => continueToAuction(storeConsentData()) - }); - }; - if (consentTimeout === 0) { - onTimeout(); - } else { - timer = setTimeout(onTimeout, consentTimeout); - } - } -} - -/** - * Like `loadConsentData`, but cache and re-use previously loaded data. - * @param cb - */ -function loadIfMissing(cb) { - if (consentData) { - logInfo('User consent information already known. Pulling internally stored information...'); - // eslint-disable-next-line standard/no-callback-literal - cb(false); - } else { - loadConsentData(cb); - } -} - -/** - * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the - * user's encoded consent string from the supported CMP. Once obtained, the module will store this - * data as part of a gppConsent object which gets transferred to adapterManager's gppDataHandler object. - * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. - * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. - * @param {function} fn required; The next function in the chain, used by hook.js - */ -export const requestBidsHook = timedAuctionHook('gpp', function requestBidsHook(fn, reqBidsConfigObj) { - loadIfMissing(function (shouldCancelAuction, errMsg, ...extraArgs) { - if (errMsg) { - let log = logWarn; - if (shouldCancelAuction) { - log = logError; - errMsg = `${errMsg} Canceling auction as per consentManagement config.`; - } - log(errMsg, ...extraArgs); - } - - if (shouldCancelAuction) { - fn.stopTiming(); - if (typeof reqBidsConfigObj.bidsBackHandler === 'function') { - reqBidsConfigObj.bidsBackHandler(); - } else { - logError('Error executing bidsBackHandler'); - } - } else { - fn.call(this, reqBidsConfigObj); - } - }); -}); - -function processCmpData(consentData) { +function parseConsentData(cmpData) { if ( - (consentData?.applicableSections != null && !Array.isArray(consentData.applicableSections)) || - (consentData?.gppString != null && !isStr(consentData.gppString)) || - (consentData?.parsedSections != null && !isPlainObject(consentData.parsedSections)) + (cmpData?.applicableSections != null && !Array.isArray(cmpData.applicableSections)) || + (cmpData?.gppString != null && !isStr(cmpData.gppString)) || + (cmpData?.parsedSections != null && !isPlainObject(cmpData.parsedSections)) ) { - throw new GPPError('CMP returned unexpected value during lookup process.', consentData); + throw new GPPError('CMP returned unexpected value during lookup process.', cmpData); } ['usnatv1', 'uscav1'].forEach(section => { - if (consentData?.parsedSections?.[section]) { - logWarn(`Received invalid section from cmp: '${section}'. Some functionality may not work as expected`, consentData) + if (cmpData?.parsedSections?.[section]) { + logWarn(`Received invalid section from cmp: '${section}'. Some functionality may not work as expected`, cmpData); } - }) - return storeConsentData(consentData); + }); + return toConsentData(cmpData); } -/** - * Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction - * @param {{}} gppData the result of calling a CMP's `getGPPData` (or equivalent) - * @param {{}} sectionData map from GPP section name to the result of calling a CMP's `getSection` (or equivalent) - */ -export function storeConsentData(gppData = {}) { - consentData = { +export function toConsentData(gppData = {}) { + return { gppString: gppData?.gppString, applicableSections: gppData?.applicableSections || [], parsedSections: gppData?.parsedSections || {}, gppData: gppData }; - gppDataHandler.setConsentData(gppData); - return consentData; } /** * Simply resets the module's consentData variable back to undefined, mainly for testing purposes */ export function resetConsentData() { - consentData = undefined; - userCMP = undefined; - consentTimeout = undefined; + consentConfig = {}; gppDataHandler.reset(); GPPClient.INST = null; } -/** - * A configuration function that initializes some module variables, as well as add a hook into the requestBids function - * @param {{cmp:string, timeout:number, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int)) - */ -export function setConsentConfig(config) { - config = config && config.gpp; - if (!config || typeof config !== 'object') { - logWarn('consentManagement.gpp config not defined, exiting consent manager module'); - return; - } - - if (isStr(config.cmpApi)) { - userCMP = config.cmpApi; - } else { - userCMP = DEFAULT_CMP; - logInfo(`consentManagement.gpp config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`); - } - - if (isNumber(config.timeout)) { - consentTimeout = config.timeout; - } else { - consentTimeout = DEFAULT_CONSENT_TIMEOUT; - logInfo(`consentManagement.gpp config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); - } - - if (userCMP === 'static') { - if (isPlainObject(config.consentData)) { - staticConsentData = config.consentData; - consentTimeout = 0; - } else { - logError(`consentManagement.gpp config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); - } - } - - logInfo('consentManagement.gpp module has been activated...'); +const parseConfig = configParser({ + namespace: 'gpp', + displayName: 'GPP', + consentDataHandler: gppDataHandler, + parseConsentData, + getNullConsent: () => toConsentData(null), + cmpHandlers: cmpCallMap +}); - if (!addedConsentHook) { - getGlobal().requestBids.before(requestBidsHook, 50); - buildActivityParams.before((next, params) => { - return next(Object.assign({gppConsent: gppDataHandler.getConsentData()}, params)); - }); - } - addedConsentHook = true; - gppDataHandler.enable(); - loadConsentData(); // immediately look up consent data to make it available without requiring an auction +export function setConsentConfig(config) { + consentConfig = parseConfig(config); + return consentConfig.loadConsentData?.()?.catch?.(() => null); } - config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); export function enrichFPDHook(next, fpd) { diff --git a/modules/consentManagementTcf.js b/modules/consentManagementTcf.js new file mode 100644 index 00000000000..6e8ed7b6cab --- /dev/null +++ b/modules/consentManagementTcf.js @@ -0,0 +1,164 @@ +/** + * This module adds GDPR consentManagement support to prebid.js. It interacts with + * supported CMPs (Consent Management Platforms) to grab the user's consent information + * and make it available for any GDPR supported adapters to read/pass this information to + * their system. + */ +import {deepSetValue, isStr, logInfo} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {gdprDataHandler} from '../src/adapterManager.js'; +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'; + +export let consentConfig = {}; +export let gdprScope; +let dsaPlatform; +const CMP_VERSION = 2; + +// add new CMPs here, with their dedicated lookup function +const cmpCallMap = { + 'iab': lookupIabConsent, +}; + +/** + * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. + */ +function lookupIabConsent(setProvisionalConsent) { + return new Promise((resolve, reject) => { + function cmpResponseCallback(tcfData, success) { + logInfo('Received a response from CMP', tcfData); + if (success) { + try { + setProvisionalConsent(parseConsentData(tcfData)); + } catch (e) { + } + + if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { + try { + gdprDataHandler.setConsentData(parseConsentData(tcfData)); + resolve(); + } catch (e) { + reject(e); + } + } + } else { + reject(Error('CMP unable to register callback function. Please check CMP setup.')) + } + } + + const cmp = cmpClient({ + apiName: '__tcfapi', + apiVersion: CMP_VERSION, + apiArgs: ['command', 'version', 'callback', 'parameter'], + }); + + if (!cmp) { + reject(new Error('TCF2 CMP not found.')) + } + if (cmp.isDirect) { + logInfo('Detected CMP API is directly accessible, calling it now...'); + } else { + logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); + } + + cmp({ + command: 'addEventListener', + callback: cmpResponseCallback + }) + }) +} + +function parseConsentData(consentObject) { + function checkData() { + // if CMP does not respond with a gdprApplies boolean, use defaultGdprScope (gdprScope) + const gdprApplies = consentObject && typeof consentObject.gdprApplies === 'boolean' ? consentObject.gdprApplies : gdprScope; + const tcString = consentObject && consentObject.tcString; + return !!( + (typeof gdprApplies !== 'boolean') || + (gdprApplies === true && (!tcString || !isStr(tcString))) + ); + } + + if (checkData()) { + throw Object.assign(new Error(`CMP returned unexpected value during lookup process.`), {args: [consentObject]}) + } else { + return toConsentData(consentObject); + } +} + +function toConsentData(cmpConsentObject) { + const consentData = { + consentString: (cmpConsentObject) ? cmpConsentObject.tcString : undefined, + vendorData: (cmpConsentObject) || undefined, + gdprApplies: cmpConsentObject && typeof cmpConsentObject.gdprApplies === 'boolean' ? cmpConsentObject.gdprApplies : gdprScope + }; + if (cmpConsentObject && cmpConsentObject.addtlConsent && isStr(cmpConsentObject.addtlConsent)) { + consentData.addtlConsent = cmpConsentObject.addtlConsent; + } + consentData.apiVersion = CMP_VERSION; + return consentData; +} + +/** + * Simply resets the module's consentData variable back to undefined, mainly for testing purposes + */ +export function resetConsentData() { + consentConfig = {}; + gdprDataHandler.reset(); +} + +const parseConfig = configParser({ + namespace: 'gdpr', + displayName: 'TCF', + consentDataHandler: gdprDataHandler, + cmpHandlers: cmpCallMap, + parseConsentData, + getNullConsent: () => toConsentData(null) +}) +/** + * A configuration function that initializes some module variables, as well as add a hook into the requestBids function + * @param {{cmp:string, timeout:number, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int)) + */ +export function setConsentConfig(config) { + // if `config.gdpr`, `config.usp` or `config.gpp` exist, assume new config format. + // else for backward compatability, just use `config` + config = config && (config.gdpr || config.usp || config.gpp ? config.gdpr : config); + if (config?.consentData?.getTCData != null) { + config.consentData = config.consentData.getTCData; + } + gdprScope = config?.defaultGdprScope === true; + dsaPlatform = !!config?.dsaPlatform; + consentConfig = parseConfig({gdpr: config}); + return consentConfig.loadConsentData?.()?.catch?.(() => null); +} +config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); + +export function enrichFPDHook(next, fpd) { + return next(fpd.then(ortb2 => { + const consent = gdprDataHandler.getConsentData(); + if (consent) { + if (typeof consent.gdprApplies === 'boolean') { + deepSetValue(ortb2, 'regs.ext.gdpr', consent.gdprApplies ? 1 : 0); + } + deepSetValue(ortb2, 'user.ext.consent', consent.consentString); + } + if (dsaPlatform) { + deepSetValue(ortb2, 'regs.ext.dsa.dsarequired', 3); + } + return ortb2; + })); +} + +enrichFPD.before(enrichFPDHook); + +export function setOrtbAdditionalConsent(ortbRequest, bidderRequest) { + // this is not a standardized name for addtlConsent, so keep this as an ORTB library processor rather than an FPD enrichment + const addtl = bidderRequest.gdprConsent?.addtlConsent; + if (addtl && typeof addtl === 'string') { + deepSetValue(ortbRequest, 'user.ext.ConsentedProvidersSettings.consented_providers', addtl); + } +} + +registerOrtbProcessor({type: REQUEST, name: 'gdprAddtlConsent', fn: setOrtbAdditionalConsent}) diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 78ec13cb891..29a67af0631 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -163,10 +163,12 @@ export const requestBidsHook = timedAuctionHook('usp', function requestBidsHook( /** * This function checks the consent data provided by USPAPI to ensure it's in an expected state. * If it's bad, we exit the module depending on config settings. - * If it's good, then we store the value and exits the module. - * @param {object} consentObject required; object returned by USPAPI that contains user's consent choices - * @param {function(string)} onSuccess callback accepting the resolved consent USP consent string - * @param {function(string, ...{}?)} onError callback accepting error message and any extra error arguments (used purely for logging) + * If it's good, then we store the value and exit the module. + * + * @param {Object} consentObject - The object returned by USPAPI that contains the user's consent choices. + * @param {Object} callbacks - An object containing the callback functions. + * @param {function(string): void} callbacks.onSuccess - Callback accepting the resolved USP consent string. + * @param {function(string, ...Object?): void} callbacks.onError - Callback accepting an error message and any extra error arguments (used purely for logging). */ function processUspData(consentObject, {onSuccess, onError}) { const valid = !!(consentObject && consentObject.usPrivacy); diff --git a/modules/consumableBidAdapter.js b/modules/consumableBidAdapter.js index 30b081e53d3..e01078890f9 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -33,7 +33,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests[]} validBidRequests An array of bids + * @param {Object} bidderRequest The bidder's request info. * @return ServerRequest Info describing the request to the server. */ @@ -61,7 +62,8 @@ export const spec = { source: [{ 'name': 'prebidjs', 'version': '$prebid.version$' - }] + }], + lang: bidderRequest.ortb2.device.language, }, validBidRequests[0].params); if (bidderRequest && bidderRequest.gdprConsent) { @@ -299,6 +301,7 @@ function retrieveAd(decision, unitId, unitName) { function handleEids(data, validBidRequests) { let bidUserIdAsEids = deepAccess(validBidRequests, '0.userIdAsEids'); if (isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) { + bidUserIdAsEids = bidUserIdAsEids.filter(e => typeof e === 'object'); deepSetValue(data, 'user.eids', bidUserIdAsEids); } else { deepSetValue(data, 'user.eids', undefined); diff --git a/modules/contentexchangeBidAdapter.js b/modules/contentexchangeBidAdapter.js index a6aa9262061..96a55d657d2 100644 --- a/modules/contentexchangeBidAdapter.js +++ b/modules/contentexchangeBidAdapter.js @@ -1,216 +1,21 @@ -import { isFn, deepAccess, logMessage } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'contentexchange'; const AD_URL = 'https://eu2.adnetwork.agency/pbjs'; const SYNC_URL = 'https://sync2.adnetwork.agency'; const GVLID = 864; -function isBidResponseValid (bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency || !bid.meta) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData (bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, adFormat } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - placementId, - bidId, - adFormat, - schain, - bidfloor - }; - - switch (adFormat) { - case BANNER: - placement.sizes = mediaTypes[BANNER].sizes; - break; - case VIDEO: - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - break; - case NATIVE: - placement.native = mediaTypes[NATIVE]; - break; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - export const spec = { code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && - params && - params.placementId && - params.adFormat - ); - switch (params.adFormat) { - case BANNER: - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - break; - case VIDEO: - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - break; - case NATIVE: - valid = valid && Boolean(mediaTypes[NATIVE]); - break; - default: - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - - // TODO: does the fallback to 'window.location' make sense? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/contxtfulBidAdapter.js b/modules/contxtfulBidAdapter.js new file mode 100644 index 00000000000..f7d263ae74f --- /dev/null +++ b/modules/contxtfulBidAdapter.js @@ -0,0 +1,228 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { _each, buildUrl, isStr, isEmptyStr, logInfo, logError } from '../src/utils.js'; +import { sendBeacon, ajax } from '../src/ajax.js'; +import { config as pbjsConfig } from '../src/config.js'; +import { + isBidRequestValid, + interpretResponse, + getUserSyncs as getUserSyncsLib, +} from '../libraries/teqblazeUtils/bidderUtils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; + +// Constants +const BIDDER_CODE = 'contxtful'; +const BIDDER_ENDPOINT = 'prebid.receptivity.io'; +const MONITORING_ENDPOINT = 'monitoring.receptivity.io'; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_TTL = 300; +const DEFAULT_SAMPLING_RATE = 1.0; +const PREBID_VERSION = '$prebid.version$'; + +// ORTB conversion +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_TTL + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const reqData = buildRequest(imps, bidderRequest, context); + return reqData; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + return bidResponse; + } +}); + +// Get Bid Floor +const _getRequestBidFloor = (mediaTypes, paramsBidFloor, bid) => { + const bidMediaType = Object.keys(mediaTypes)[0] || 'banner'; + const bidFloor = { floor: 0, currency: 'USD' }; + + if (typeof bid.getFloor === 'function') { + const { currency, floor } = bid.getFloor({ + mediaType: bidMediaType, + size: '*' + }); + floor && (bidFloor.floor = floor); + currency && (bidFloor.currency = currency); + } else if (paramsBidFloor) { + bidFloor.floor = paramsBidFloor + } + + return bidFloor; +} + +// Get Parameters from the config. +const extractParameters = (config) => { + const version = config?.contxtful?.version; + if (!isStr(version) || isEmptyStr(version)) { + throw Error(`contxfulBidAdapter: contxtful.version should be a non-empty string`); + } + + const customer = config?.contxtful?.customer; + if (!isStr(customer) || isEmptyStr(customer)) { + throw Error(`contxfulBidAdapter: contxtful.customer should be a non-empty string`); + } + + return { version, customer }; +} + +// Construct the Payload towards the Bidding endpoint +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + const ortb2 = converter.toORTB({ bidderRequest: bidderRequest, bidRequests: validBidRequests }); + + const bidRequests = []; + _each(validBidRequests, bidRequest => { + const { + mediaTypes = {}, + params = {}, + } = bidRequest; + bidRequest.bidFloor = _getRequestBidFloor(mediaTypes, params.bidfloor, bidRequest); + bidRequests.push(bidRequest) + }); + const config = pbjsConfig.getConfig(); + config.pbjsVersion = PREBID_VERSION; + const { version, customer } = extractParameters(config) + const adapterUrl = buildUrl({ + protocol: 'https', + host: BIDDER_ENDPOINT, + pathname: `/${version}/prebid/${customer}/bid`, + }); + + // See https://docs.prebid.org/dev-docs/bidder-adaptor.html + let req = { + url: adapterUrl, + method: 'POST', + data: { + ortb2, + bidRequests, + bidderRequest, + config, + }, + }; + + return req; +}; + +// Prepare a sync object compatible with getUserSyncs. +const constructUrl = (userSyncsDefault, userSyncServer) => { + const urlSyncServer = (userSyncServer?.url ?? '').split('?'); + const userSyncUrl = userSyncsDefault?.url || ''; + const baseSyncUrl = urlSyncServer[0] || ''; + + let url = `${baseSyncUrl}${userSyncUrl}`; + + if (urlSyncServer.length > 1) { + const urlParams = urlSyncServer[1]; + url += url.includes('?') ? `&${urlParams}` : `?${urlParams}`; + } + + return { + ...userSyncsDefault, + url, + }; +}; + +// Returns the list of user synchronization objects. +const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + // Get User Sync Defaults from pbjs lib + const userSyncsDefaultLib = getUserSyncsLib('')(syncOptions, null, gdprConsent, uspConsent, gppConsent); + const userSyncsDefault = userSyncsDefaultLib?.find(item => item.url !== undefined); + + // Map Server Responses to User Syncs list + const serverSyncsData = serverResponses?.flatMap(response => response.body || []) + .map(data => data.syncs) + .find(syncs => Array.isArray(syncs) && syncs.length > 0) || []; + const userSyncs = serverSyncsData + .map(sync => constructUrl(userSyncsDefault, sync)) + .filter(Boolean); // Filter out nulls + return userSyncs; +}; + +// Retrieve the sampling rate for events +const getSamplingRate = (bidderConfig, eventType) => { + const entry = Object.entries(bidderConfig?.contxtful?.sampling ?? {}).find(([key, value]) => key.toLowerCase() === eventType.toLowerCase()); + return entry ? entry[1] : DEFAULT_SAMPLING_RATE; +}; + +const logBidderError = ({ error, bidderRequest }) => { + if (error) { + let jsonReason = { + message: error.reason?.message, + stack: error.reason?.stack, + }; + error.reason = jsonReason; + } + logEvent('onBidderError', { error, bidderRequest }); +}; + +// Handles the logging of events +const logEvent = (eventType, data) => { + try { + // Log event + logInfo(BIDDER_CODE, `[${eventType}] ${JSON.stringify(data)}`); + + // Get Config + const bidderConfig = pbjsConfig.getConfig(); + const { version, customer } = extractParameters(bidderConfig); + + // Sampled monitoring + if (['onBidBillable', 'onAdRenderSucceeded'].includes(eventType)) { + const randomNumber = Math.random(); + const samplingRate = getSamplingRate(bidderConfig, eventType); + if (!(randomNumber <= samplingRate)) { + return; // Don't sample + } + } else if (!['onTimeout', 'onBidderError', 'onBidWon'].includes(eventType)) { + // Unsupported event type. + return; + } + + const payload = { type: eventType, data }; + const eventUrl = buildUrl({ + protocol: 'https', + host: MONITORING_ENDPOINT, + pathname: `/${version}/prebid/${customer}/log/${eventType}`, + }); + + // Try sending a beacon + if (sendBeacon(eventUrl, JSON.stringify(payload))) { + logInfo(BIDDER_CODE, `[${eventType}] Logging data sent using Beacon and payload: ${JSON.stringify(data)}`); + } else { + // Fallback to using ajax + ajax(eventUrl, null, JSON.stringify(payload), { + method: 'POST', + contentType: 'application/json', + withCredentials: true, + }); + logInfo(BIDDER_CODE, `[${eventType}] Logging data sent using Ajax and payload: ${JSON.stringify(data)}`); + } + } catch (error) { + logError(BIDDER_CODE, `Failed to log event: ${eventType}`); + } +}; + +// Bidder public specification +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onBidWon: function (bid) { logEvent('onBidWon', bid); }, + onBidBillable: function (bid) { logEvent('onBidBillable', bid); }, + onAdRenderSucceeded: function (bid) { logEvent('onAdRenderSucceeded', bid); }, + onSetTargeting: function (bid) { }, + onTimeout: function (timeoutData) { logEvent('onTimeout', timeoutData); }, + onBidderError: logBidderError, +}; + +registerBidder(spec); diff --git a/modules/contxtfulBidAdapter.md b/modules/contxtfulBidAdapter.md new file mode 100644 index 00000000000..87a78c38a85 --- /dev/null +++ b/modules/contxtfulBidAdapter.md @@ -0,0 +1,98 @@ +# Overview + +``` +Module Name: Contxtful Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@contxtful.com +``` + +# Description + +The Contxtful Bidder Adapter supports all mediatypes and connects to demand sources for bids. + +# Configuration +## Global Configuration +Contxtful uses the global configuration to store params once instead of duplicating for each ad unit. +Also, enabling user syncing greatly increases match rates and monetization. +Be sure to call `pbjs.setConfig()` only once. + +```javascript +pbjs.setConfig({ + debug: false, + contxtful: { + customer: '', // Required + version: '', // Required + }, + userSync: { + filterSettings: { + iframe: { + bidders: ['contxtful'], + filter: 'include' + } + } + } + // [...] +}); +``` + +## Bidder Setting +Contxtful leverages local storage for user syncing. + +```javascript +pbjs.bidderSettings = { + contxtful: { + storageAllowed: true + } +} +``` + +# Example Ad-units configuration +```javascript +var adUnits = [ + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [{ + bidder: 'contxtful', + }] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [{ + bidder: 'contxtful', + }] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [{ + bidder: 'contxtful', + }] + } +]; +``` diff --git a/modules/contxtfulRtdProvider.js b/modules/contxtfulRtdProvider.js index 6d4b2a2ce29..b97e2759df7 100644 --- a/modules/contxtfulRtdProvider.js +++ b/modules/contxtfulRtdProvider.js @@ -1,8 +1,8 @@ /** - * Contxtful Technologies Inc - * This RTD module provides receptivity feature that can be accessed using the - * getReceptivity() function. The value returned by this function enriches the ad-units - * that are passed within the `getTargetingData` functions and GAM. + * Contxtful Technologies Inc. + * This RTD module provides receptivity that can be accessed using the + * getTargetingData and getBidRequestData functions. The receptivity enriches ad units + * and bid requests. */ import { submodule } from '../src/hook.js'; @@ -10,18 +10,101 @@ import { logInfo, logError, isStr, + mergeDeep, isEmptyStr, + isEmpty, buildUrl, + isArray, + generateUUID, + getWinDimensions, + canAccessWindowTop, + deepAccess, + getSafeframeGeometry, + getWindowSelf, + getWindowTop, + inIframe, + isSafeFrameWindow, } from '../src/utils.js'; import { loadExternalScript } from '../src/adloader.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { getGptSlotInfoForAdUnitCode } from '../libraries/gptUtils/gptUtils.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; +// Constants const MODULE_NAME = 'contxtful'; const MODULE = `${MODULE_NAME}RtdProvider`; -const CONTXTFUL_RECEPTIVITY_DOMAIN = 'api.receptivity.io'; +const CONTXTFUL_HOSTNAME_DEFAULT = 'api.receptivity.io'; +const CONTXTFUL_DEFER_DEFAULT = 0; -let initialReceptivity = null; -let contxtfulModule = null; +// Functions +let _sm; +function sm() { + return _sm ??= generateUUID(); +} + +const storageManager = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: MODULE_NAME, +}); + +let rxApi = null; + +/** + * Return current receptivity value for the requester. + * @param { String } requester + * @return { { Object } } + */ +function getRxEngineReceptivity(requester) { + return rxApi?.receptivity(requester); +} + +function getItemFromSessionStorage(key) { + try { + return storageManager.getDataFromSessionStorage(key); + } catch (error) { + logError(MODULE, error); + } +} + +function loadSessionReceptivity(requester) { + let sessionStorageValue = getItemFromSessionStorage(requester); + if (!sessionStorageValue) { + return null; + } + + try { + // Check expiration of the cached value + let sessionStorageReceptivity = JSON.parse(sessionStorageValue); + let expiration = parseInt(sessionStorageReceptivity?.exp); + if (expiration < new Date().getTime()) { + return null; + } + + let rx = sessionStorageReceptivity?.rx; + return rx; + } catch { + return null; + } +} + +/** + * Prepare a receptivity batch + * @param {Array.} requesters + * @param {Function} method + * @returns A batch + */ +function prepareBatch(requesters, method) { + return requesters.reduce((acc, requester) => { + const receptivity = method(requester); + if (!isEmpty(receptivity)) { + return { ...acc, [requester]: receptivity }; + } else { + return acc; + } + }, {}); +} /** * Init function used to start sub module @@ -30,12 +113,13 @@ let contxtfulModule = null; */ function init(config) { logInfo(MODULE, 'init', config); - initialReceptivity = null; - contxtfulModule = null; + rxApi = null; try { - const {version, customer, hostname} = extractParameters(config); - initCustomer(version, customer, hostname); + initCustomer(config); + + observeLastCursorPosition(); + return true; } catch (error) { logError(MODULE, error); @@ -51,7 +135,7 @@ function init(config) { * @return { { version: String, customer: String, hostname: String } } * @throws params.{name} should be a non-empty string */ -function extractParameters(config) { +export function extractParameters(config) { const version = config?.params?.version; if (!isStr(version) || isEmptyStr(version)) { throw Error(`${MODULE}: params.version should be a non-empty string`); @@ -62,89 +146,406 @@ function extractParameters(config) { throw Error(`${MODULE}: params.customer should be a non-empty string`); } - const hostname = config?.params?.hostname || CONTXTFUL_RECEPTIVITY_DOMAIN; + const hostname = config?.params?.hostname || CONTXTFUL_HOSTNAME_DEFAULT; + const defer = config?.params?.defer || CONTXTFUL_DEFER_DEFAULT; - return {version, customer, hostname}; + return { version, customer, hostname, defer }; } /** * Initialize sub module for a customer. * This will load the external resources for the sub module. - * @param { String } version - * @param { String } customer - * @param { String } hostname + * @param { String } config */ -function initCustomer(version, customer, hostname) { +function initCustomer(config) { + const { version, customer, hostname, defer } = extractParameters(config); const CONNECTOR_URL = buildUrl({ protocol: 'https', host: hostname, - pathname: `/${version}/prebid/${customer}/connector/p.js`, + pathname: `/${version}/prebid/${customer}/connector/rxConnector.js`, }); - const externalScript = loadExternalScript(CONNECTOR_URL, MODULE_NAME); - addExternalScriptEventListener(externalScript); + addConnectorEventListener(customer, config); + + const loadScript = () => loadExternalScript(CONNECTOR_URL, MODULE_TYPE_RTD, MODULE_NAME, undefined, undefined, { 'data-sm': sm() }); + // Optionally defer the loading of the script + if (Number.isFinite(defer) && defer > 0) { + setTimeout(loadScript, defer); + } else { + loadScript(); + } } /** * Add event listener to the script tag for the expected events from the external script. - * @param { HTMLScriptElement } script + * @param { String } tagId + * @param { String } prebidConfig */ -function addExternalScriptEventListener(script) { - if (!script) { - return; +function addConnectorEventListener(tagId, prebidConfig) { + window.addEventListener( + 'rxConnectorIsReady', + async ({ detail: { [tagId]: rxConnector } }) => { + if (!rxConnector) { + return; + } + // Fetch the customer configuration + const { rxApiBuilder, fetchConfig } = rxConnector; + let config = await fetchConfig(tagId); + if (!config) { + return; + } + config['prebid'] = prebidConfig || {}; + rxApi = await rxApiBuilder(config); + + // Remove listener now that we can use rxApi. + removeListeners(); + } + ); +} + +/** + * Set targeting data for ad server + * @param { [String] } adUnits + * @param {*} config + * @param {*} _userConsent + * @return {{ code: { ReceptivityState: String } }} + */ +function getTargetingData(adUnits, config, _userConsent) { + try { + if (String(config?.params?.adServerTargeting) === 'false') { + return {}; + } + logInfo(MODULE, 'getTargetingData'); + + const requester = config?.params?.customer; + const rx = + getRxEngineReceptivity(requester) || + loadSessionReceptivity(requester) || + {}; + + if (isEmpty(rx)) { + return {}; + } + + return adUnits.reduce((targets, code) => { + targets[code] = rx; + return targets; + }, {}); + } catch (error) { + logError(MODULE, error); + return {}; + } +} + +function getVisibilityStateElement(domElement, windowTop) { + if ('checkVisibility' in domElement) { + return domElement.checkVisibility(); } - script.addEventListener('initialReceptivity', ({ detail }) => { - let receptivityState = detail?.ReceptivityState; - if (isStr(receptivityState) && !isEmptyStr(receptivityState)) { - initialReceptivity = receptivityState; + const elementCss = windowTop.getComputedStyle(domElement, null); + return elementCss.display !== 'none'; +} + +function getElementFromTopWindowRecurs(element, currentWindow) { + try { + if (getWindowTop() === currentWindow) { + return element; + } else { + const frame = currentWindow.frameElement; + const frameClientRect = getBoundingClientRect(frame); + const elementClientRect = getBoundingClientRect(element); + if (frameClientRect.width !== elementClientRect.width || frameClientRect.height !== elementClientRect.height) { + return undefined; + } + return getElementFromTopWindowRecurs(frame, currentWindow.parent); } - }); + } catch (err) { + logError(MODULE, err); + return undefined; + } +} - script.addEventListener('rxEngineIsReady', ({ detail: api }) => { - contxtfulModule = api; - }); +function getDivIdPosition(divId) { + if (!isSafeFrameWindow() && !canAccessWindowTop()) { + return {}; + } + + const position = {}; + + if (isSafeFrameWindow()) { + const { self } = getSafeframeGeometry() ?? {}; + + if (!self) { + return {}; + } + + position.x = Math.round(self.t); + position.y = Math.round(self.l); + } else { + try { + // window.top based computing + const wt = getWindowTop(); + const d = wt.document; + + let domElement; + + if (inIframe() === true) { + const ws = getWindowSelf(); + const currentElement = ws.document.getElementById(divId); + domElement = getElementFromTopWindowRecurs(currentElement, ws); + } else { + domElement = wt.document.getElementById(divId); + } + + if (!domElement) { + return {}; + } + + let box = getBoundingClientRect(domElement); + const docEl = d.documentElement; + const body = d.body; + const clientTop = (d.clientTop ?? body.clientTop) ?? 0; + const clientLeft = (d.clientLeft ?? body.clientLeft) ?? 0; + const scrollTop = (wt.scrollY ?? docEl.scrollTop) ?? body.scrollTop; + const scrollLeft = (wt.scrollX ?? docEl.scrollLeft) ?? body.scrollLeft; + + position.visibility = getVisibilityStateElement(domElement, wt); + position.x = Math.round(box.left + scrollLeft - clientLeft); + position.y = Math.round(box.top + scrollTop - clientTop); + } catch (err) { + logError(MODULE, err); + return {}; + } + } + + return position; +} + +function tryGetDivIdPosition(divIdMethod) { + let divId = divIdMethod(); + if (divId) { + const divIdPosition = getDivIdPosition(divId); + if (divIdPosition.x !== undefined && divIdPosition.y !== undefined) { + return divIdPosition; + } + } + return undefined; +} + +function tryMultipleDivIdPositions(adUnit) { + let divMethods = [ + // ortb2\ + () => { + adUnit.ortb2Imp = adUnit.ortb2Imp || {}; + const ortb2Imp = deepAccess(adUnit, 'ortb2Imp'); + return deepAccess(ortb2Imp, 'ext.data.divId'); + }, + // gpt + () => getGptSlotInfoForAdUnitCode(adUnit.code).divId, + // adunit code + () => adUnit.code + ]; + + for (const divMethod of divMethods) { + let divPosition = tryGetDivIdPosition(divMethod); + if (divPosition) { + return divPosition; + } + } +} + +function tryGetAdUnitPosition(adUnit) { + let adUnitPosition = {}; + adUnit.ortb2Imp = adUnit.ortb2Imp || {}; + + // try to get position with the divId + const divIdPosition = tryMultipleDivIdPositions(adUnit); + if (divIdPosition) { + adUnitPosition.p = { x: divIdPosition.x, y: divIdPosition.y }; + adUnitPosition.v = divIdPosition.visibility; + adUnitPosition.t = 'div'; + return adUnitPosition; + } + + // try to get IAB position + const iabPos = adUnit?.mediaTypes?.banner?.pos; + if (iabPos !== undefined) { + adUnitPosition.p = iabPos; + adUnitPosition.t = 'iab'; + return adUnitPosition; + } + + return undefined; +} + +function getAdUnitPositions(bidReqConfig) { + const adUnits = bidReqConfig.adUnits || []; + let adUnitPositions = {}; + + for (const adUnit of adUnits) { + let adUnitPosition = tryGetAdUnitPosition(adUnit); + if (adUnitPosition) { + adUnitPositions[adUnit.code] = adUnitPosition; + } + } + + return adUnitPositions; } /** - * Return current receptivity. - * @return { { ReceptivityState: String } } + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} onDone Called on completion + * @param {Object} config Configuration for Contxtful RTD module + * @param {Object} userConsent */ -function getReceptivity() { +function getBidRequestData(reqBidsConfigObj, onDone, config, userConsent) { + function onReturn() { + onDone(); + } + logInfo(MODULE, 'getBidRequestData'); + const bidders = config?.params?.bidders || []; + if (isEmpty(bidders) || !isArray(bidders)) { + onReturn(); + return; + } + + let ortb2Fragment; + let getContxtfulOrtb2Fragment = rxApi?.getOrtb2Fragment; + if (typeof (getContxtfulOrtb2Fragment) == 'function') { + ortb2Fragment = getContxtfulOrtb2Fragment(bidders, reqBidsConfigObj); + } else { + const adUnitsPositions = getAdUnitPositions(reqBidsConfigObj); + + let fromApi = rxApi?.receptivityBatched?.(bidders) || {}; + let fromStorage = prepareBatch(bidders, (bidder) => loadSessionReceptivity(`${config?.params?.customer}_${bidder}`)); + + let sources = [fromStorage, fromApi]; + + let rxBatch = Object.assign(...sources); + + let singlePointEvents = btoa(JSON.stringify({ ui: getUiEvents() })); + ortb2Fragment = {}; + ortb2Fragment.bidder = Object.fromEntries( + bidders + .map(bidderCode => { + return [bidderCode, { + user: { + data: [ + { + name: MODULE_NAME, + ext: { + rx: rxBatch[bidderCode], + events: singlePointEvents, + pos: btoa(JSON.stringify(adUnitsPositions)), + sm: sm(), + params: { + ev: config.params?.version, + ci: config.params?.customer, + }, + }, + }, + ], + }, + } + ] + })); + } + + mergeDeep(reqBidsConfigObj.ortb2Fragments, ortb2Fragment); + + onReturn(); +} + +function getUiEvents() { return { - ReceptivityState: contxtfulModule?.GetReceptivity()?.ReceptivityState || initialReceptivity + position: lastCursorPosition, + screen: getScreen(), }; } -/** - * Set targeting data for ad server - * @param { [String] } adUnits - * @param {*} _config - * @param {*} _userConsent - * @return {{ code: { ReceptivityState: String } }} - */ -function getTargetingData(adUnits, _config, _userConsent) { - logInfo(MODULE, 'getTargetingData'); - if (!adUnits) { - return {}; +function getScreen() { + function getInnerSize() { + const { innerWidth, innerHeight } = getWinDimensions(); + + let w = innerWidth; + let h = innerHeight; + + if (w && h) { + return [w, h]; + } } - const receptivity = getReceptivity(); - if (!receptivity?.ReceptivityState) { - return {}; + function getDocumentSize() { + const windowDimensions = getWinDimensions(); + + let w = windowDimensions.document.body.clientWidth; + let h = windowDimensions.document.body.clientHeight; + + if (w && h) { + return [w, h]; + } } - return adUnits.reduce((targets, code) => { - targets[code] = receptivity; - return targets; - }, {}); + // If we cannot access or cast the window dimensions, we get None. + // If we cannot collect the size from the window we try to use the root document dimensions + let [width, height] = getInnerSize() || getDocumentSize() || [0, 0]; + let topLeft = { x: window.scrollX, y: window.scrollY }; + + return { + topLeft, + width, + height, + timestampMs: performance.now(), + }; +} + +let lastCursorPosition; + +function observeLastCursorPosition() { + function pointerEventToPosition(event) { + lastCursorPosition = { + x: event.clientX, + y: event.clientY, + timestampMs: performance.now() + }; + } + + function touchEventToPosition(event) { + let touch = event.touches.item(0); + if (!touch) { + return; + } + + lastCursorPosition = { + x: touch.clientX, + y: touch.clientY, + timestampMs: performance.now() + }; + } + + addListener('pointermove', pointerEventToPosition); + addListener('touchmove', touchEventToPosition); +} + +let listeners = {}; +function addListener(name, listener) { + listeners[name] = listener; + + window.addEventListener(name, listener); +} + +function removeListeners() { + for (const name in listeners) { + window.removeEventListener(name, listeners[name]); + delete listeners[name]; + } } export const contxtfulSubmodule = { name: MODULE_NAME, init, - extractParameters, getTargetingData, + getBidRequestData, }; submodule('realTimeData', contxtfulSubmodule); diff --git a/modules/contxtfulRtdProvider.md b/modules/contxtfulRtdProvider.md index dfefca2067a..8fef61b3d96 100644 --- a/modules/contxtfulRtdProvider.md +++ b/modules/contxtfulRtdProvider.md @@ -2,25 +2,42 @@ **Module Name:** Contxtful RTD Provider **Module Type:** RTD Provider -**Maintainer:** [prebid@contxtful.com](mailto:prebid@contxtful.com) +**Maintainer:** [contact@contxtful.com](mailto:contact@contxtful.com) # Description The Contxtful RTD module offers a unique feature—Receptivity. Receptivity is an efficiency metric, enabling the qualification of any instant in a session in real time based on attention. The core idea is straightforward: the likelihood of an ad’s success increases when it grabs attention and is presented in the right context at the right time. -To utilize this module, you need to register for an account with [Contxtful](https://contxtful.com). For inquiries, please contact [prebid@contxtful.com](mailto:prebid@contxtful.com). - -# Configuration +To utilize this module, you need to register for an account with [Contxtful](https://contxtful.com). For inquiries, please reach out to [contact@contxtful.com](mailto:contact@contxtful.com). ## Build Instructions To incorporate this module into your `prebid.js`, compile the module using the following command: ```sh -gulp build --modules=contxtfulRtdProvider, +gulp build --modules=rtdModule,contxtfulRtdProvider, +``` + +## Testing + +To run the test server locally: +```sh +gulp serve --modules=rtdModule,contxtfulRtdProvider, --fix --nolint --notest +chrome http://localhost:9999/integrationExamples/gpt/contxtfulRtdProvider_example.html +``` + +To run the unit tests: + +```bash +gulp test ``` -## Module Configuration +To run the unit tests for a particular file: +```bash +gulp test --file "test/spec/modules/contxtfulRtdProvider_spec.js" --nolint +``` + +## Configuration Configure the `contxtfulRtdProvider` by passing the required settings through the `setConfig` function in `prebid.js`. @@ -35,8 +52,11 @@ pbjs.setConfig({ "name": "contxtful", "waitForIt": true, "params": { - "version": "", - "customer": "" + "version": "Contact contact@contxtful.com for the API version", + "customer": "Contact contact@contxtful.com for the customer ID", + "hostname": "api.receptivity.io", // Optional, default: "api.receptivity.io" + "bidders": ["bidderCode1", "bidderCode", "..."], // list of bidders + "adServerTargeting": true, // Optional, default: true } } ] @@ -44,22 +64,42 @@ pbjs.setConfig({ }); ``` -### Configuration Parameters +## Parameters -| Name | Type | Scope | Description | -|------------|----------|----------|-------------------------------------------| -| `version` | `string` | Required | Specifies the API version of Contxtful. | -| `customer` | `string` | Required | Your unique customer identifier. | +| Name | Type | Scope | Description | +|---------------------|----------|----------|--------------------------------------------| +| `version` | `String` | Required | Specifies the version of the Contxtful Receptivity API. | +| `customer` | `String` | Required | Your unique customer identifier. | +| `hostname` | `String` | Optional | Target URL for CONTXTFUL external JavaScript file. Default is "api.receptivity.io". Changing default behaviour is not recommended. Please reach out to contact@contxtful.com if you experience issues. | +| `adServerTargeting` | `Boolean`| Optional | Enables the `getTargetingData` to inject targeting value in ad units. Setting to true enables the feature, false disables the feature. Default is true | +| `bidders` | `Array` | Optional | Setting this array enables Receptivity in the `ortb2` object through `getBidRequestData` for all the listed `bidders`. Default is `[]` (an empty array). RECOMMENDED : Add all the active bidders like this `["bidderCode1", "bidderCode", "..."]` | -# Usage +## Usage: Injection in Ad Servers The `contxtfulRtdProvider` module loads an external JavaScript file and authenticates with Contxtful APIs. The `getTargetingData` function then adds a `ReceptivityState` to each ad slot, which can have one of two values: `Receptive` or `NonReceptive`. ```json { "adUnitCode1": { "ReceptivityState": "Receptive" }, - "adUnitCode2": { "ReceptivityState": "NonReceptive" } + "adUnitCode2": { "ReceptivityState": "Receptive" } } ``` -This module also integrates seamlessly with Google Ad Manager, ensuring that the `ReceptivityState` is available as early as possible in the ad serving process. \ No newline at end of file +This module also integrates seamlessly with Google Ad Manager, ensuring that the `ReceptivityState` is available as early as possible in the ad serving process. + +## Usage: Injection in ortb2 for bidders + +Setting the `bidders` field in the configuration parameters enables Receptivity in the `ortb2` object through `getBidRequestData` for all the listed bidders. +On a Bid Request Event, all bidders in the configuration will inherit the Receptivity data through `ortb2` +Default is `[]` (an empty array) + +RECOMMENDED : Add all the bidders active like this `["bidderCode1", "bidderCode", "..."]` + +## Links + +- [Basic Prebid.js Example](https://docs.prebid.org/dev-docs/examples/basic-example.html) +- [How Bid Adapters Should Read First Party Data](https://docs.prebid.org/features/firstPartyData.html#how-bid-adapters-should-read-first-party-data) +- [getBidRequestData](https://docs.prebid.org/dev-docs/add-rtd-submodule.html#getbidrequestdata) +- [getTargetingData](https://docs.prebid.org/dev-docs/add-rtd-submodule.html#gettargetingdata) +- [Contxtful Documentation](https://documentation.contxtful.com/) + diff --git a/modules/conversantAnalyticsAdapter.js b/modules/conversantAnalyticsAdapter.js index 44e712d54f7..a08eed41c53 100644 --- a/modules/conversantAnalyticsAdapter.js +++ b/modules/conversantAnalyticsAdapter.js @@ -1,7 +1,6 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import { EVENTS } from '../src/constants.js'; -import {getGlobal} from '../src/prebidGlobal.js'; import adapterManager from '../src/adapterManager.js'; import {logInfo, logWarn, logError, logMessage, deepAccess, isInteger} from '../src/utils.js'; import {getRefererInfo} from '../src/refererDetection.js'; @@ -515,7 +514,7 @@ cnvrHelper.createPayload = function(payloadType, auctionId, timestamp) { cnvrSampleRate: initOptions.cnvr_sample_rate, auction: { auctionId: auctionId, - preBidVersion: getGlobal().version, + preBidVersion: '$prebid.version$', sid: initOptions.site_id, auctionTimestamp: timestamp }, @@ -617,7 +616,7 @@ cnvrHelper.sendErrorData = function(eventType, exception) { url: cnvrHelper.getPageUrl() }; - // eslint-disable-next-line no-undef + ajax(ERROR_URL, function () {}, JSON.stringify(error), {contentType: 'text/plain'}); } diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index ebcad38d866..f7f3326de33 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -13,9 +13,8 @@ import { parseUrl, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; import {ORTB_MTYPES} from '../libraries/ortbConverter/processors/mediaType.js'; @@ -61,6 +60,7 @@ const converter = ortbConverter({ request: function (buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context); request.at = 1; + request.cur = ['USD']; if (context.bidRequests) { const bidRequest = context.bidRequests[0]; setSiteId(bidRequest, request); @@ -96,7 +96,7 @@ const converter = ortbConverter({ }, response(buildResponse, bidResponses, ortbResponse, context) { const response = buildResponse(bidResponses, ortbResponse, context); - return response.bids; + return response; }, overrides: { imp: { @@ -127,7 +127,7 @@ export const spec = { code: BIDDER_CODE, gvlid: GVLID, aliases: ['cnvr', 'epsilon'], // short code - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. @@ -177,21 +177,8 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, bidRequest) { - return converter.fromORTB({request: bidRequest.data, response: serverResponse.body}); - }, - - /** - * Covert bid param types for S2S - * @param {Object} params bid params - * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol - * @return {Object} params bid params - */ - transformBidParams: function(params, isOpenRtb) { - return convertTypes({ - 'site_id': 'string', - 'secure': 'number', - 'mobile': 'number' - }, params); + const ortbBids = converter.fromORTB({request: bidRequest.data, response: serverResponse.body}); + return ortbBids; }, /** diff --git a/modules/copper6sspBidAdapter.js b/modules/copper6sspBidAdapter.js new file mode 100644 index 00000000000..e05ed241cc6 --- /dev/null +++ b/modules/copper6sspBidAdapter.js @@ -0,0 +1,21 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'copper6ssp'; +const AD_URL = 'https://endpoint.copper6.com/pbjs'; +const SYNC_URL = 'https://сsync.copper6.com'; +const GVLID = 1356; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/copper6sspBidAdapter.md b/modules/copper6sspBidAdapter.md new file mode 100755 index 00000000000..a414187022d --- /dev/null +++ b/modules/copper6sspBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Copper6SSP Bidder Adapter +Module Type: Copper6SSP Bidder Adapter +Maintainer: info@copper6.com +``` + +# Description + +Connects to Copper6SSP exchange for bids. +Copper6SSP 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: 'copper6ssp', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'copper6ssp', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'copper6ssp', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/cpmstarBidAdapter.js b/modules/cpmstarBidAdapter.js index e076fb4b0bb..772cb8c537c 100755 --- a/modules/cpmstarBidAdapter.js +++ b/modules/cpmstarBidAdapter.js @@ -1,8 +1,8 @@ import * as utils from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {getBidIdParameter} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'cpmstar'; @@ -12,15 +12,18 @@ const ENDPOINT_PRODUCTION = 'https://server.cpmstar.com/view.aspx'; const DEFAULT_TTL = 300; const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; -function fixedEncodeURIComponent(str) { - return encodeURIComponent(str).replace(/[!'()*]/g, function(c) { - return '%' + c.charCodeAt(0).toString(16); - }); -} +export const converter = ortbConverter({ + context: { + ttl: DEFAULT_TTL, + netRevenue: DEFAULT_NET_REVENUE + } +}); export const spec = { code: BIDDER_CODE, + gvlid: 1317, supportedMediaTypes: [BANNER, VIDEO], pageID: Math.floor(Math.random() * 10e6), @@ -43,22 +46,30 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { var requests = []; - // This reference to window.top can cause issues when loaded in an iframe if not protected with a try/catch. for (var i = 0; i < validBidRequests.length; i++) { var bidRequest = validBidRequests[i]; - var referer = bidderRequest.refererInfo.page ? bidderRequest.refererInfo.page : bidderRequest.refererInfo.domain; - referer = encodeURIComponent(referer); - var e = getBidIdParameter('endpoint', bidRequest.params); - var ENDPOINT = e == 'dev' ? ENDPOINT_DEV : e == 'staging' ? ENDPOINT_STAGING : ENDPOINT_PRODUCTION; - var mediaType = spec.getMediaType(bidRequest); - var playerSize = spec.getPlayerSize(bidRequest); - var videoArgs = '&fv=0' + (playerSize ? ('&w=' + playerSize[0] + '&h=' + playerSize[1]) : ''); - var url = ENDPOINT + '?media=' + mediaType + (mediaType == VIDEO ? videoArgs : '') + - '&json=c_b&mv=1&poolid=' + getBidIdParameter('placementId', bidRequest.params) + - '&reachedTop=' + encodeURIComponent(bidderRequest.refererInfo.reachedTop) + - '&requestid=' + bidRequest.bidId + - '&referer=' + encodeURIComponent(referer); + const referer = bidderRequest.refererInfo.page ? bidderRequest.refererInfo.page : bidderRequest.refererInfo.domain; + const e = utils.getBidIdParameter('endpoint', bidRequest.params); + const ENDPOINT = e == 'dev' ? ENDPOINT_DEV : e == 'staging' ? ENDPOINT_STAGING : ENDPOINT_PRODUCTION; + const url = new URL(ENDPOINT); + const body = {}; + const mediaType = spec.getMediaType(bidRequest); + const playerSize = spec.getPlayerSize(bidRequest); + url.searchParams.set('media', mediaType); + if (mediaType == VIDEO) { + url.searchParams.set('fv', 0); + if (playerSize) { + url.searchParams.set('w', playerSize?.[0]); + url.searchParams.set('h', playerSize?.[1]); + } + } + url.searchParams.set('json', 'c_b'); + url.searchParams.set('mv', 1); + url.searchParams.set('poolid', utils.getBidIdParameter('placementId', bidRequest.params)); + url.searchParams.set('reachedTop', bidderRequest.refererInfo.reachedTop); + url.searchParams.set('requestid', bidRequest.bidId); + url.searchParams.set('referer', referer); if (bidRequest.schain && bidRequest.schain.nodes) { var schain = bidRequest.schain; @@ -67,45 +78,49 @@ export const spec = { for (var i2 = 0; i2 < schain.nodes.length; i2++) { var node = schain.nodes[i2]; schainString += '!' + - fixedEncodeURIComponent(node.asi || '') + ',' + - fixedEncodeURIComponent(node.sid || '') + ',' + - fixedEncodeURIComponent(node.hp || '') + ',' + - fixedEncodeURIComponent(node.rid || '') + ',' + - fixedEncodeURIComponent(node.name || '') + ',' + - fixedEncodeURIComponent(node.domain || ''); + (node.asi || '') + ',' + + (node.sid || '') + ',' + + (node.hp || '') + ',' + + (node.rid || '') + ',' + + (node.name || '') + ',' + + (node.domain || ''); } - url += '&schain=' + schainString; + url.searchParams.set('schain', schainString); } if (bidderRequest.gdprConsent) { if (bidderRequest.gdprConsent.consentString != null) { - url += '&gdpr_consent=' + bidderRequest.gdprConsent.consentString; + url.searchParams.set('gdpr_consent', bidderRequest.gdprConsent.consentString); } if (bidderRequest.gdprConsent.gdprApplies != null) { - url += '&gdpr=' + (bidderRequest.gdprConsent.gdprApplies ? 1 : 0); + url.searchParams.set('gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); } } if (bidderRequest.uspConsent != null) { - url += '&us_privacy=' + bidderRequest.uspConsent; + url.searchParams.set('us_privacy', bidderRequest.uspConsent); } if (config.getConfig('coppa')) { - url += '&tfcd=' + (config.getConfig('coppa') ? 1 : 0); + url.searchParams.set('tfcd', (config.getConfig('coppa') ? 1 : 0)); } - let body = {}; let adUnitCode = bidRequest.adUnitCode; if (adUnitCode) { body.adUnitCode = adUnitCode; } if (mediaType == VIDEO) { body.video = utils.deepAccess(bidRequest, 'mediaTypes.video'); + } else if (mediaType == BANNER) { + body.banner = utils.deepAccess(bidRequest, 'mediaTypes.banner'); } + const ortb = converter.toORTB({ bidderRequest, bidRequests: [bidRequest] }); + Object.assign(body, ortb); + requests.push({ method: 'POST', - url: url, + url: url.toString(), bidRequest: bidRequest, data: body }); @@ -144,7 +159,7 @@ export const spec = { width: rawBid.width || 0, height: rawBid.height || 0, currency: rawBid.currency ? rawBid.currency : DEFAULT_CURRENCY, - netRevenue: rawBid.netRevenue ? rawBid.netRevenue : true, + netRevenue: rawBid.netRevenue ? rawBid.netRevenue : DEFAULT_NET_REVENUE, ttl: rawBid.ttl ? rawBid.ttl : DEFAULT_TTL, creativeId: rawBid.creativeid || 0, meta: { @@ -191,4 +206,5 @@ export const spec = { } }; + registerBidder(spec); diff --git a/modules/cpmstarBidAdapter.md b/modules/cpmstarBidAdapter.md index c227f19bfaf..66f13479c05 100755 --- a/modules/cpmstarBidAdapter.md +++ b/modules/cpmstarBidAdapter.md @@ -3,10 +3,7 @@ ``` Module Name: Cpmstar Bidder Adapter Module Type: Bidder Adapter -Maintainer: josh@cpmstar.com -gdpr_supported: true -usp_supported: true -coppa_supported: true +Maintainer: prebid@cpmstar.com ``` # Description diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 74e732d313f..3e24a68b946 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,14 +1,13 @@ -import {getBidRequest, logError} from '../src/utils.js'; +import {getBidRequest} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; -import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; +import {interpretResponseUtil} from '../libraries/interpretResponseUtils/index.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; @@ -70,55 +69,20 @@ export const spec = { interpretResponse: function(serverResponse, {bidderRequest}) { try { - serverResponse = serverResponse.body; - const bids = []; - if (!serverResponse) { - return []; - } - if (serverResponse.error) { - let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`; - if (serverResponse.error) { - errorMessage += `: ${serverResponse.error}`; + const bids = interpretResponseUtil(serverResponse, {bidderRequest}, serverBid => { + const rtbBid = getRtbBid(serverBid); + if (rtbBid && rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { + const bid = newBid(serverBid, rtbBid, bidderRequest); + bid.mediaType = parseMediaType(rtbBid); + return bid; } - logError(errorMessage); - return bids; - } - if (serverResponse.tags) { - serverResponse.tags.forEach(serverBid => { - const rtbBid = getRtbBid(serverBid); - if (rtbBid) { - if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { - const bid = newBid(serverBid, rtbBid, bidderRequest); - bid.mediaType = parseMediaType(rtbBid); - bids.push(bid); - } - } - }); - } + }); return bids; } catch (e) { return []; } }, - transformBidParams: function(params, isOpenRtb) { - params = convertTypes({ - 'sitekey': 'string', - 'placementId': 'string', - 'keywords': transformBidderParamKeywords, - }, params); - if (isOpenRtb) { - Object.keys(params).forEach(paramKey => { - let convertedKey = convertCamelToUnderscore(paramKey); - if (convertedKey !== paramKey) { - params[convertedKey] = params[paramKey]; - delete params[paramKey]; - } - }); - } - return params; - }, - onBidWon: function(bid) { ajax(bid._prebidWon, null, null, { method: 'POST', @@ -156,7 +120,7 @@ function newBid(serverBid, rtbBid, bidderRequest) { ad: rtbBid.rtb.banner.content, ttl: TTL, creativeId: rtbBid.creative_id, - netRevenue: false, // ??? + netRevenue: true, dealId: rtbBid.deal_id, meta: null, _adUnitCode: bidRequest.adUnitCode, diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index c95cbf7af73..a38660c4f25 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -1,15 +1,14 @@ -import { deepAccess, generateUUID, isArray, logError, logInfo, logWarn, parseUrl } from '../src/utils.js'; -import { loadExternalScript } from '../src/adloader.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 -import { getStorageManager } from '../src/storageManager.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { hasPurpose1Consent } from '../src/utils/gpdr.js'; -import { Renderer } from '../src/Renderer.js'; -import { OUTSTREAM } from '../src/video.js'; -import { ajax } from '../src/ajax.js'; +import {deepSetValue, isArray, logError, logWarn, parseUrl, triggerPixel} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; +import {Renderer} from '../src/Renderer.js'; +import {OUTSTREAM} from '../src/video.js'; +import {ajax} from '../src/ajax.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {ortb25Translator} from '../libraries/ortb2.5Translator/translator.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -20,35 +19,199 @@ import { ajax } from '../src/ajax.js'; */ const GVLID = 91; -export const ADAPTER_VERSION = 36; +export const ADAPTER_VERSION = 37; const BIDDER_CODE = 'criteo'; -const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; +const CDB_ENDPOINT = 'https://grid-bidder.criteo.com/openrtb_2_5/pbjs/auction/request'; const PROFILE_ID_INLINE = 207; -export const PROFILE_ID_PUBLISHERTAG = 185; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_PREFIX = 'Criteo: '; +const TRANSLATOR = ortb25Translator(); -/* - If you don't want to use the FastBid adapter feature, you can lighten criteoBidAdapter size by : - 1. commenting the tryGetCriteoFastBid function inner content (see ref#1) - 2. removing the line 'verify' function import line (see ref#2) - - Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js -*/ -const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 144; -const FAST_BID_VERSION_LATEST = 'latest'; -const FAST_BID_VERSION_NONE = 'none'; -const PUBLISHER_TAG_URL_TEMPLATE = 'https://static.criteo.net/js/ld/publishertag.prebid' + FAST_BID_VERSION_PLACEHOLDER + '.js'; const PUBLISHER_TAG_OUTSTREAM_SRC = 'https://static.criteo.net/js/ld/publishertag.renderer.js' -const FAST_BID_PUBKEY_E = 65537; -const FAST_BID_PUBKEY_N = 'ztQYwCE5BU7T9CDM5he6rKoabstXRmkzx54zFPZkWbK530dwtLBDeaWBMxHBUT55CYyboR/EZ4efghPi3CoNGfGWezpjko9P6p2EwGArtHEeS4slhu/SpSIFMjG6fdrpRoNuIAMhq1Z+Pr/+HOd1pThFKeGFr2/NhtAg+TXAzaU='; - const OPTOUT_COOKIE_NAME = 'cto_optout'; const BUNDLE_COOKIE_NAME = 'cto_bundle'; const GUID_RETENTION_TIME_HOUR = 24 * 30 * 13; // 13 months const OPTOUT_RETENTION_TIME_HOUR = 5 * 12 * 30 * 24; // 5 years +/** + * Defines the generic oRTB converter and all customization functions. + */ +const CONVERTER = ortbConverter({ + context: { + netRevenue: true, + ttl: 60 + }, + imp, + request, + bidResponse, + response +}); + +/** + * Builds an impression object for the ORTB 2.5 request. + * + * @param {function} buildImp - The function for building an imp object. + * @param {Object} bidRequest - The bid request object. + * @param {Object} context - The context object. + * @returns {Object} The ORTB 2.5 imp object. + */ +function imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + const params = bidRequest.params; + + imp.tagid = bidRequest.adUnitCode; + deepSetValue(imp, 'ext', { + ...bidRequest.params.ext, + ...imp.ext, + rwdd: imp.rwdd, + floors: getFloors(bidRequest), + bidder: { + publishersubid: params?.publisherSubId, + zoneid: params?.zoneId, + uid: params?.uid, + }, + }); + + delete imp.rwdd // oRTB 2.6 field moved to ext + + if (!context.fledgeEnabled && imp.ext.igs?.ae) { + delete imp.ext.igs.ae; + } + + if (hasVideoMediaType(bidRequest)) { + const paramsVideo = bidRequest.params.video; + if (paramsVideo !== undefined) { + deepSetValue(imp, 'video', { + ...imp.video, + skip: imp.video.skip || paramsVideo.skip || 0, + placement: imp.video.placement || paramsVideo.placement, + minduration: imp.video.minduration || paramsVideo.minduration, + playbackmethod: imp.video.playbackmethod || paramsVideo.playbackmethod, + startdelay: imp.video.startdelay || paramsVideo.startdelay || 0, + }) + } + deepSetValue(imp, 'video.ext', { + context: bidRequest.mediaTypes.video.context, + playersizes: parseSizes(bidRequest?.mediaTypes?.video?.playerSize, parseSize), + plcmt: bidRequest.mediaTypes.video.plcmt, + poddur: bidRequest.mediaTypes.video.adPodDurationSec, + rqddurs: bidRequest.mediaTypes.video.durationRangeSec, + }) + } + + if (imp.native && typeof imp.native.request !== 'undefined') { + let requestNative = JSON.parse(imp.native.request); + + // We remove the native asset requirements if we used the bypass to generate the imp + const hasAssetRequirements = requestNative.assets && + (requestNative.assets.length !== 1 || Object.keys(requestNative.assets[0]).length); + if (!hasAssetRequirements) { + delete requestNative.assets; + } + + deepSetValue(imp, 'native.request_native', requestNative); + delete imp.native.request; + } + + return imp; +} + +/** + * Builds a request object for the ORTB 2.5 request. + * + * @param {function} buildRequest - The function for building a request object. + * @param {Array} imps - An array of ORTB 2.5 impression objects. + * @param {Object} bidderRequest - The bidder request object. + * @param {Object} context - The context object. + * @returns {Object} The ORTB 2.5 request object. + */ +function request(buildRequest, imps, bidderRequest, context) { + let request = buildRequest(imps, bidderRequest, context); + + // params.pubid should override publisher id + if (typeof context.publisherId !== 'undefined') { + if (typeof request.app !== 'undefined') { + deepSetValue(request, 'app.publisher.id', context.publisherId); + } else { + deepSetValue(request, 'site.publisher.id', context.publisherId); + } + } + + if (bidderRequest && bidderRequest.gdprConsent) { + deepSetValue(request, 'regs.ext.gdprversion', bidderRequest.gdprConsent.apiVersion); + } + + // Translate 2.6 OpenRTB request into 2.5 OpenRTB request + request = TRANSLATOR(request); + + return request; +} + +/** + * Build bid from oRTB 2.5 bid. + * + * @param buildBidResponse + * @param bid + * @param context + * @returns {*} + */ +function bidResponse(buildBidResponse, bid, context) { + context.mediaType = bid?.ext?.mediatype; + if (context.mediaType === NATIVE && typeof bid.adm_native !== 'undefined') { + bid.adm = bid.adm_native; + delete bid.adm_native; + } + + let bidResponse = buildBidResponse(bid, context); + const {bidRequest} = context; + + bidResponse.currency = bid?.ext?.cur; + + if (typeof bid?.ext?.meta !== 'undefined') { + deepSetValue(bidResponse, 'meta', { + ...bidResponse.meta, + ...bid.ext.meta + }); + } + if (typeof bid?.ext?.paf?.content_id !== 'undefined') { + deepSetValue(bidResponse, 'meta.paf.content_id', bid.ext.paf.content_id) + } + + if (bidResponse.mediaType === VIDEO) { + bidResponse.vastUrl = bid.ext?.displayurl; + // if outstream video, add a default render for it. + if (bidRequest?.mediaTypes?.video?.context === OUTSTREAM) { + bidResponse.renderer = createOutstreamVideoRenderer(bid); + } + } + + return bidResponse; +} + +/** + * Builds bid response from the oRTB 2.5 bid response. + * + * @param buildResponse + * @param bidResponses + * @param ortbResponse + * @param context + * @returns * + */ +function response(buildResponse, bidResponses, ortbResponse, context) { + let response = buildResponse(bidResponses, ortbResponse, context); + + const pafTransmission = ortbResponse?.ext?.paf?.transmission; + response.bids.forEach(bid => { + if (typeof pafTransmission !== 'undefined' && typeof bid?.meta?.paf?.content_id !== 'undefined') { + deepSetValue(bid, 'meta.paf.transmission', pafTransmission); + } else { + delete bid.meta.paf; + } + }); + + return response; +} + /** @type {BidderSpec} */ export const spec = { code: BIDDER_CODE, @@ -58,15 +221,10 @@ export const spec = { getUserSyncs: function (syncOptions, _, gdprConsent, uspConsent, gppConsent = {}) { let { gppString = '', applicableSections = [] } = gppConsent; - if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent)) { - const fastBidVersion = config.getConfig('criteo.fastBidVersion'); - if (canFastBid(fastBidVersion)) { - return []; - } - - const refererInfo = getRefererInfo(); - const origin = 'criteoPrebidAdapter'; + const refererInfo = getRefererInfo(); + const origin = 'criteoPrebidAdapter'; + if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent)) { const queryParams = []; queryParams.push(`origin=${origin}`); queryParams.push(`topUrl=${refererInfo.domain}`); @@ -120,11 +278,13 @@ export const spec = { if (response.optout) { deleteFromAllStorages(BUNDLE_COOKIE_NAME); - saveOnAllStorages(OPTOUT_COOKIE_NAME, true, OPTOUT_RETENTION_TIME_HOUR); + saveOnAllStorages(OPTOUT_COOKIE_NAME, true, OPTOUT_RETENTION_TIME_HOUR, refererInfo.domain); } else { if (response.bundle) { - saveOnAllStorages(BUNDLE_COOKIE_NAME, response.bundle, GUID_RETENTION_TIME_HOUR); + saveOnAllStorages(BUNDLE_COOKIE_NAME, response.bundle, GUID_RETENTION_TIME_HOUR, refererInfo.domain); } + + response?.callbacks?.forEach?.(triggerPixel); } }, true); @@ -191,50 +351,25 @@ export const spec = { * @return {ServerRequest} */ buildRequests: (bidRequests, bidderRequest) => { - let url; - let data; - let fpd = bidderRequest.ortb2 || {}; - - Object.assign(bidderRequest, { - publisherExt: fpd.site?.ext, - userExt: fpd.user?.ext, - ceh: config.getConfig('criteo.ceh'), - coppa: config.getConfig('coppa') - }); - - // If publisher tag not already loaded try to get it from fast bid - const fastBidVersion = config.getConfig('criteo.fastBidVersion'); - const canLoadPublisherTag = canFastBid(fastBidVersion); - if (!publisherTagAvailable() && canLoadPublisherTag) { - window.Criteo = window.Criteo || {}; - window.Criteo.usePrebidEvents = false; - - tryGetCriteoFastBid(); + bidRequests.forEach(bidRequest => { + if (hasNativeMediaType(bidRequest)) { + if (!checkNativeSendId(bidRequest)) { + logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); + } - const fastBidUrl = getFastBidUrl(fastBidVersion); - // Reload the PublisherTag after the timeout to ensure FastBid is up-to-date and tracking done properly - setTimeout(() => { - loadExternalScript(fastBidUrl, BIDDER_CODE); - }, bidderRequest.timeout); - } + // We support native request without assets requirements because we can fill them later on. + // This is a trick to fool oRTB converter isOpenRTBBidRequestValid(ortb) fn because it needs + // nativeOrtbRequest.assets to be non-empty. + if (bidRequest?.nativeOrtbRequest?.assets == null) { + logWarn(LOG_PREFIX + 'native asset requirements are missing'); + deepSetValue(bidRequest, 'nativeOrtbRequest.assets', [{}]); + } + } + }); - if (publisherTagAvailable()) { - // eslint-disable-next-line no-undef - const adapter = new Criteo.PubTag.Adapters.Prebid( - PROFILE_ID_PUBLISHERTAG, - ADAPTER_VERSION, - bidRequests, - bidderRequest, - '$prebid.version$', - { createOutstreamVideoRenderer: createOutstreamVideoRenderer } - ); - url = adapter.buildCdbUrl(); - data = adapter.buildCdbRequest(); - } else { - const context = buildContext(bidRequests, bidderRequest); - url = buildCdbUrl(context); - data = buildCdbRequest(context, bidRequests, bidderRequest); - } + const context = buildContext(bidRequests, bidderRequest); + const url = buildCdbUrl(context); + const data = CONVERTER.toORTB({bidderRequest, bidRequests, context}); if (data) { return { method: 'POST', url, data, bidRequests }; @@ -247,131 +382,24 @@ export const spec = { * @return {Bid[] | {bids: Bid[], fledgeAuctionConfigs: object[]}} */ interpretResponse: (response, request) => { - const body = response.body || response; - - if (publisherTagAvailable()) { - // eslint-disable-next-line no-undef - const adapter = Criteo.PubTag.Adapters.Prebid.GetAdapter(request); - if (adapter) { - return adapter.interpretResponse(body, request); - } + if (typeof response?.body == 'undefined') { + return []; // no bid } - const bids = []; - const fledgeAuctionConfigs = []; - - if (body && body.slots && isArray(body.slots)) { - body.slots.forEach(slot => { - const bidRequest = getAssociatedBidRequest(request.bidRequests, slot); - if (bidRequest) { - const bidId = bidRequest.bidId; - const bid = { - requestId: bidId, - cpm: slot.cpm, - currency: slot.currency, - netRevenue: true, - ttl: slot.ttl || 60, - creativeId: slot.creativecode, - width: slot.width, - height: slot.height, - dealId: slot.deal, - }; - if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { - const pafResponseMeta = { - content_id: slot.ext.paf.content_id, - transmission: response.ext.paf.transmission - }; - bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); - } - if (slot.adomain) { - bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); - } - if (slot.ext?.meta?.networkName) { - bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) - } - if (slot.ext?.dsa) { - bid.meta = Object.assign({}, bid.meta, { dsa: slot.ext.dsa }) - } - if (slot.native) { - if (bidRequest.params.nativeCallback) { - bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); - } else { - bid.native = createPrebidNativeAd(slot.native); - bid.mediaType = NATIVE; - } - } else if (slot.video) { - bid.vastUrl = slot.displayurl; - bid.mediaType = VIDEO; - const context = deepAccess(bidRequest, 'mediaTypes.video.context'); - // if outstream video, add a default render for it. - if (context === OUTSTREAM) { - bid.renderer = createOutstreamVideoRenderer(slot); - } - } else { - bid.ad = slot.creative; - } - bids.push(bid); - } - }); - } - - if (isArray(body.ext?.igi)) { - body.ext.igi.forEach((igi) => { - if (isArray(igi?.igs)) { - igi.igs.forEach((igs) => { - fledgeAuctionConfigs.push(igs); - }); - } - }); - } + const interpretedResponse = CONVERTER.fromORTB({response: response.body, request: request.data}); + const bids = interpretedResponse.bids || []; - if (fledgeAuctionConfigs.length) { + const fledgeAuctionConfigs = response.body?.ext?.igi?.filter(igi => isArray(igi?.igs)) + .flatMap(igi => igi.igs); + if (fledgeAuctionConfigs?.length) { return { bids, - fledgeAuctionConfigs, + paapi: fledgeAuctionConfigs, }; } return bids; }, - /** - * @param {TimedOutBid} timeoutData - */ - onTimeout: (timeoutData) => { - if (publisherTagAvailable() && Array.isArray(timeoutData)) { - var auctionsIds = []; - timeoutData.forEach((bid) => { - if (auctionsIds.indexOf(bid.auctionId) === -1) { - auctionsIds.push(bid.auctionId); - // eslint-disable-next-line no-undef - const adapter = Criteo.PubTag.Adapters.Prebid.GetAdapter(bid.auctionId); - adapter.handleBidTimeout(); - } - }); - } - }, - - /** - * @param {Bid} bid - */ - onBidWon: (bid) => { - if (publisherTagAvailable() && bid) { - // eslint-disable-next-line no-undef - const adapter = Criteo.PubTag.Adapters.Prebid.GetAdapter(bid.auctionId); - adapter.handleBidWon(bid); - } - }, - - /** - * @param {Bid} bid - */ - onSetTargeting: (bid) => { - if (publisherTagAvailable()) { - // eslint-disable-next-line no-undef - const adapter = Criteo.PubTag.Adapters.Prebid.GetAdapter(bid.auctionId); - adapter.handleSetTargeting(bid); - } - }, /** * @param {BidRequest[]} bidRequests @@ -398,12 +426,29 @@ function readFromAllStorages(name) { return fromCookie || fromLocalStorage || undefined; } -function saveOnAllStorages(name, value, expirationTimeHours) { +function saveOnAllStorages(name, value, expirationTimeHours, domain) { const date = new Date(); date.setTime(date.getTime() + (expirationTimeHours * 60 * 60 * 1000)); const expires = `expires=${date.toUTCString()}`; - storage.setCookie(name, value, expires); + const subDomains = domain.split('.'); + for (let i = 0; i < subDomains.length; ++i) { + // Try to write the cookie on this subdomain (we want it to be stored only on the TLD+1) + const domain = subDomains.slice(subDomains.length - i - 1, subDomains.length).join('.'); + + try { + storage.setCookie(name, value, expires, null, '.' + domain); + + // Try to read the cookie to check if we wrote it + const check = storage.getCookie(name); + if (check && check === value) { + break; + } + } catch (error) { + + } + } + storage.setDataInLocalStorage(name, value); } @@ -412,43 +457,26 @@ function deleteFromAllStorages(name) { storage.removeDataFromLocalStorage(name); } -/** - * @return {boolean} - */ -function publisherTagAvailable() { - // eslint-disable-next-line no-undef - return typeof Criteo !== 'undefined' && Criteo.PubTag && Criteo.PubTag.Adapters && Criteo.PubTag.Adapters.Prebid; -} - /** * @param {BidRequest[]} bidRequests * @param bidderRequest */ function buildContext(bidRequests, bidderRequest) { - let referrer = ''; - if (bidderRequest && bidderRequest.refererInfo) { - referrer = bidderRequest.refererInfo.page; - } const queryString = parseUrl(bidderRequest?.refererInfo?.topmostLocation).search; - const context = { - url: referrer, + return { + url: bidderRequest?.refererInfo?.page || '', debug: queryString['pbt_debug'] === '1', noLog: queryString['pbt_nolog'] === '1', - amp: false, + fledgeEnabled: bidderRequest.paapi?.enabled, + amp: bidRequests.some(bidRequest => bidRequest.params.integrationMode === 'amp'), + networkId: bidRequests.find(bidRequest => bidRequest.params?.networkId)?.params.networkId, + publisherId: bidRequests.find(bidRequest => bidRequest.params?.pubid)?.params.pubid, }; - - bidRequests.forEach(bidRequest => { - if (bidRequest.params.integrationMode === 'amp') { - context.amp = true; - } - }); - - return context; } /** - * @param {CriteoContext} context + * @param {Object} context * @return {string} */ function buildCdbUrl(context) { @@ -484,6 +512,10 @@ function buildCdbUrl(context) { url += `&optout=1`; } + if (context.networkId) { + url += `&networkId=` + context.networkId; + } + return url; } @@ -499,185 +531,6 @@ function checkNativeSendId(bidRequest) { )); } -/** - * @param {CriteoContext} context - * @param {BidRequest[]} bidRequests - * @param bidderRequest - * @return {*} - */ -function buildCdbRequest(context, bidRequests, bidderRequest) { - let networkId; - let pubid; - let schain; - let userIdAsEids; - let regs = Object.assign({}, { - coppa: bidderRequest.coppa === true ? 1 : (bidderRequest.coppa === false ? 0 : undefined) - }, bidderRequest.ortb2?.regs); - const request = { - id: generateUUID(), - publisher: { - url: context.url, - ext: bidderRequest.publisherExt, - }, - regs: regs, - slots: bidRequests.map(bidRequest => { - if (!userIdAsEids) { - userIdAsEids = bidRequest.userIdAsEids; - } - networkId = bidRequest.params.networkId || networkId; - pubid = bidRequest.params.pubid || pubid; - schain = bidRequest.schain || schain; - const slot = { - slotid: bidRequest.bidId, - impid: bidRequest.adUnitCode, - transactionid: bidRequest.ortb2Imp?.ext?.tid - }; - if (bidRequest.params.zoneId) { - slot.zoneid = bidRequest.params.zoneId; - } - if (deepAccess(bidRequest, 'ortb2Imp.ext')) { - slot.ext = bidRequest.ortb2Imp.ext; - } - - if (deepAccess(bidRequest, 'ortb2Imp.rwdd')) { - slot.rwdd = bidRequest.ortb2Imp.rwdd; - } - - if (bidRequest.params.ext) { - slot.ext = Object.assign({}, slot.ext, bidRequest.params.ext); - } - if (bidRequest.nativeOrtbRequest?.assets) { - slot.ext = Object.assign({}, slot.ext, { assets: bidRequest.nativeOrtbRequest.assets }); - } - if (bidRequest.params.uid) { - slot.ext = Object.assign({}, slot.ext, { bidder: { uid: bidRequest.params.uid } }); - } - - if (bidRequest.params.publisherSubId) { - slot.publishersubid = bidRequest.params.publisherSubId; - } - - if (bidRequest.params.nativeCallback || hasNativeMediaType(bidRequest)) { - slot.native = true; - if (!checkNativeSendId(bidRequest)) { - logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); - } - } - - if (hasBannerMediaType(bidRequest)) { - slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseSize); - } else { - slot.sizes = []; - } - - if (hasVideoMediaType(bidRequest)) { - const video = { - context: bidRequest.mediaTypes.video.context, - playersizes: parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize'), parseSize), - mimes: bidRequest.mediaTypes.video.mimes, - protocols: bidRequest.mediaTypes.video.protocols, - maxduration: bidRequest.mediaTypes.video.maxduration, - api: bidRequest.mediaTypes.video.api, - skip: bidRequest.mediaTypes.video.skip, - placement: bidRequest.mediaTypes.video.placement, - minduration: bidRequest.mediaTypes.video.minduration, - playbackmethod: bidRequest.mediaTypes.video.playbackmethod, - startdelay: bidRequest.mediaTypes.video.startdelay, - plcmt: bidRequest.mediaTypes.video.plcmt, - w: bidRequest.mediaTypes.video.w, - h: bidRequest.mediaTypes.video.h, - linearity: bidRequest.mediaTypes.video.linearity, - skipmin: bidRequest.mediaTypes.video.skipmin, - skipafter: bidRequest.mediaTypes.video.skipafter, - minbitrate: bidRequest.mediaTypes.video.minbitrate, - maxbitrate: bidRequest.mediaTypes.video.maxbitrate, - delivery: bidRequest.mediaTypes.video.delivery, - pos: bidRequest.mediaTypes.video.pos, - playbackend: bidRequest.mediaTypes.video.playbackend, - adPodDurationSec: bidRequest.mediaTypes.video.adPodDurationSec, - durationRangeSec: bidRequest.mediaTypes.video.durationRangeSec, - }; - const paramsVideo = bidRequest.params.video; - if (paramsVideo !== undefined) { - video.skip = video.skip || paramsVideo.skip || 0; - video.placement = video.placement || paramsVideo.placement; - video.minduration = video.minduration || paramsVideo.minduration; - video.playbackmethod = video.playbackmethod || paramsVideo.playbackmethod; - video.startdelay = video.startdelay || paramsVideo.startdelay || 0; - } - - slot.video = video; - } - - enrichSlotWithFloors(slot, bidRequest); - - if (!bidderRequest.fledgeEnabled && slot.ext?.ae) { - delete slot.ext.ae; - } - - return slot; - }), - }; - if (networkId) { - request.publisher.networkid = networkId; - } - - request.source = { - tid: bidderRequest.ortb2?.source?.tid - }; - - if (schain) { - request.source.ext = { - schain: schain - }; - }; - request.user = bidderRequest.ortb2?.user || {}; - request.site = bidderRequest.ortb2?.site || {}; - request.app = bidderRequest.ortb2?.app || {}; - - if (pubid) { - request.site.publisher = {...request.site.publisher, ...{ id: pubid }}; - request.app.publisher = {...request.app.publisher, ...{ id: pubid }}; - } - - request.device = bidderRequest.ortb2?.device || {}; - if (bidderRequest && bidderRequest.ceh) { - request.user.ceh = bidderRequest.ceh; - } - if (bidderRequest && bidderRequest.gdprConsent) { - request.gdprConsent = {}; - if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') { - request.gdprConsent.gdprApplies = !!(bidderRequest.gdprConsent.gdprApplies); - } - request.gdprConsent.version = bidderRequest.gdprConsent.apiVersion; - if (typeof bidderRequest.gdprConsent.consentString !== 'undefined') { - request.gdprConsent.consentData = bidderRequest.gdprConsent.consentString; - } - } - if (bidderRequest && bidderRequest.uspConsent) { - request.user.uspIab = bidderRequest.uspConsent; - } - if (bidderRequest && bidderRequest.ortb2?.device?.sua) { - request.user.ext = request.user.ext || {}; - request.user.ext.sua = bidderRequest.ortb2?.device?.sua || {}; - } - if (userIdAsEids) { - request.user.ext = request.user.ext || {}; - request.user.ext.eids = [...userIdAsEids]; - } - if (bidderRequest && bidderRequest.ortb2?.bcat) { - request.bcat = bidderRequest.ortb2.bcat; - } - if (bidderRequest && bidderRequest.ortb2?.badv) { - request.badv = bidderRequest.ortb2.badv; - } - if (bidderRequest && bidderRequest.ortb2?.bapp) { - request.bapp = bidderRequest.ortb2.bapp; - } - request.tmax = bidderRequest.timeout; - return request; -} - function parseSizes(sizes, parser = s => s) { if (sizes == undefined) { return []; @@ -693,15 +546,11 @@ function parseSize(size) { } function hasVideoMediaType(bidRequest) { - return deepAccess(bidRequest, 'mediaTypes.video') !== undefined; -} - -function hasBannerMediaType(bidRequest) { - return deepAccess(bidRequest, 'mediaTypes.banner') !== undefined; + return bidRequest?.mediaTypes?.video !== undefined; } function hasNativeMediaType(bidRequest) { - return deepAccess(bidRequest, 'mediaTypes.native') !== undefined; + return bidRequest?.mediaTypes?.native !== undefined; } function hasValidVideoMediaType(bidRequest) { @@ -710,63 +559,22 @@ function hasValidVideoMediaType(bidRequest) { var requiredMediaTypesParams = ['mimes', 'playerSize', 'maxduration', 'protocols', 'api', 'skip', 'placement', 'playbackmethod']; requiredMediaTypesParams.forEach(function (param) { - if (deepAccess(bidRequest, 'mediaTypes.video.' + param) === undefined && deepAccess(bidRequest, 'params.video.' + param) === undefined) { - isValid = false; - logError('Criteo Bid Adapter: mediaTypes.video.' + param + ' is required'); + if (param === 'placement') { + if (bidRequest?.mediaTypes?.video?.[param] === undefined && bidRequest?.params?.video?.[param] === undefined && bidRequest?.mediaTypes?.video?.plcmt === undefined && bidRequest?.params?.video?.plcmt === undefined) { + isValid = false; + logError('Criteo Bid Adapter: mediaTypes.video.' + param + ' or mediaTypes.video.plcmt is required'); + } + } else { + if (bidRequest?.mediaTypes?.video?.[param] === undefined && bidRequest?.params?.video?.[param] === undefined) { + isValid = false; + logError('Criteo Bid Adapter: mediaTypes.video.' + param + ' is required'); + } } }); return isValid; } -/** - * Create prebid compatible native ad with native payload - * @param {*} payload - * @returns prebid native ad assets - */ -function createPrebidNativeAd(payload) { - return { - sendTargetingKeys: false, // no key is added to KV by default - title: payload.products[0].title, - body: payload.products[0].description, - sponsoredBy: payload.advertiser.description, - icon: payload.advertiser.logo, - image: payload.products[0].image, - clickUrl: payload.products[0].click_url, - privacyLink: payload.privacy.optout_click_url, - privacyIcon: payload.privacy.optout_image_url, - cta: payload.products[0].call_to_action, - price: payload.products[0].price, - impressionTrackers: payload.impression_pixels.map(pix => pix.url) - }; -} - -/** - * @param {string} id - * @param {*} payload - * @param {*} callback - * @return {string} - */ -function createNativeAd(id, payload, callback) { - // Store the callback and payload in a global object to be later accessed from the creative - var slotsName = 'criteo_prebid_native_slots'; - window[slotsName] = window[slotsName] || {}; - window[slotsName][id] = { callback, payload }; - - // The creative is in an iframe so we have to get the callback and payload - // from the parent window (doesn't work with safeframes) - return ` -`; -} - function pickAvailableGetFloorFunc(bidRequest) { if (bidRequest.getFloor) { return bidRequest.getFloor; @@ -785,87 +593,58 @@ function pickAvailableGetFloorFunc(bidRequest) { return undefined; } -function enrichSlotWithFloors(slot, bidRequest) { +function getFloors(bidRequest) { try { - const slotFloors = {}; + const floors = {}; const getFloor = pickAvailableGetFloorFunc(bidRequest); if (getFloor) { if (bidRequest.mediaTypes?.banner) { - slotFloors.banner = {}; - const bannerSizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')) - bannerSizes.forEach(bannerSize => slotFloors.banner[parseSize(bannerSize).toString()] = getFloor.call(bidRequest, { size: bannerSize, mediaType: BANNER })); + floors.banner = {}; + const bannerSizes = parseSizes(bidRequest?.mediaTypes?.banner?.sizes) + bannerSizes.forEach(bannerSize => floors.banner[parseSize(bannerSize).toString()] = getFloor.call(bidRequest, { size: bannerSize, mediaType: BANNER })); } if (bidRequest.mediaTypes?.video) { - slotFloors.video = {}; - const videoSizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')) - videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = getFloor.call(bidRequest, { size: videoSize, mediaType: VIDEO })); + floors.video = {}; + const videoSizes = parseSizes(bidRequest?.mediaTypes?.video?.playerSize) + videoSizes.forEach(videoSize => floors.video[parseSize(videoSize).toString()] = getFloor.call(bidRequest, { size: videoSize, mediaType: VIDEO })); } if (bidRequest.mediaTypes?.native) { - slotFloors.native = {}; - slotFloors.native['*'] = getFloor.call(bidRequest, { size: '*', mediaType: NATIVE }); + floors.native = {}; + floors.native['*'] = getFloor.call(bidRequest, { size: '*', mediaType: NATIVE }); } - if (Object.keys(slotFloors).length > 0) { - if (!slot.ext) { - slot.ext = {} - } - Object.assign(slot.ext, { - floors: slotFloors - }); - } + return floors; } } catch (e) { logError('Could not parse floors from Prebid: ' + e); } } -export function canFastBid(fastBidVersion) { - return fastBidVersion !== FAST_BID_VERSION_NONE; -} - -export function getFastBidUrl(fastBidVersion) { - let version; - if (fastBidVersion === FAST_BID_VERSION_LATEST) { - version = ''; - } else if (fastBidVersion) { - let majorVersion = String(fastBidVersion).split('.')[0]; - if (majorVersion < 102) { - logWarn('Specifying a Fastbid version which is not supporting version selection.') - } - version = '.' + fastBidVersion; - } else { - version = '.' + FAST_BID_VERSION_CURRENT; - } - - return PUBLISHER_TAG_URL_TEMPLATE.replace(FAST_BID_VERSION_PLACEHOLDER, version); -} - -function createOutstreamVideoRenderer(slot) { - if (slot.ext.videoPlayerConfig === undefined || slot.ext.videoPlayerType === undefined) { +function createOutstreamVideoRenderer(bid) { + if (bid.ext?.videoPlayerConfig === undefined || bid.ext?.videoPlayerType === undefined) { return undefined; } const config = { - documentResolver: (bid, sourceDocument, renderDocument) => { + documentResolver: (_, sourceDocument, renderDocument) => { return renderDocument ?? sourceDocument; } } - const render = (bid, renderDocument) => { + const render = (_, renderDocument) => { let payload = { - slotid: slot.impid, - vastUrl: slot.displayurl, - vastXml: slot.creative, + slotid: bid.id, + vastUrl: bid.ext?.displayurl, + vastXml: bid.adm, documentContext: renderDocument, }; - let outstreamConfig = slot.ext.videoPlayerConfig; - - window.CriteoOutStream[slot.ext.videoPlayerType].play(payload, outstreamConfig) + let outstreamConfig = bid.ext.videoPlayerConfig; + window.CriteoOutStream[bid.ext.videoPlayerType].play(payload, outstreamConfig) }; const renderer = Renderer.install({ url: PUBLISHER_TAG_OUTSTREAM_SRC, config: config }); @@ -873,60 +652,4 @@ function createOutstreamVideoRenderer(slot) { return renderer; } -function getAssociatedBidRequest(bidRequests, slot) { - for (const request of bidRequests) { - if (request.adUnitCode === slot.impid) { - if (request.params.zoneId && parseInt(request.params.zoneId) === slot.zoneid) { - return request; - } else if (slot.native) { - if (request.mediaTypes?.native || request.nativeParams) { - return request; - } - } else if (slot.video) { - if (request.mediaTypes?.video) { - return request; - } - } else if (request.mediaTypes?.banner || request.sizes) { - return request; - } - } - } - return undefined; -} - -export function tryGetCriteoFastBid() { - // begin ref#1 - try { - const fastBidStorageKey = 'criteo_fast_bid'; - const hashPrefix = '// Hash: '; - const fastBidFromStorage = storage.getDataFromLocalStorage(fastBidStorageKey); - - if (fastBidFromStorage !== null) { - // The value stored must contain the file's encrypted hash as first line - const firstLineEndPosition = fastBidFromStorage.indexOf('\n'); - const firstLine = fastBidFromStorage.substr(0, firstLineEndPosition).trim(); - - if (firstLine.substr(0, hashPrefix.length) !== hashPrefix) { - logWarn('No hash found in FastBid'); - storage.removeDataFromLocalStorage(fastBidStorageKey); - } else { - // Remove the hash part from the locally stored value - const publisherTagHash = firstLine.substr(hashPrefix.length); - const publisherTag = fastBidFromStorage.substr(firstLineEndPosition + 1); - - if (verify(publisherTag, publisherTagHash, FAST_BID_PUBKEY_N, FAST_BID_PUBKEY_E)) { - logInfo('Using Criteo FastBid'); - eval(publisherTag); // eslint-disable-line no-eval - } else { - logWarn('Invalid Criteo FastBid found'); - storage.removeDataFromLocalStorage(fastBidStorageKey); - } - } - } - } catch (e) { - // Unable to get fast bid - } - // end ref#1 -} - registerBidder(spec); diff --git a/modules/currency.js b/modules/currency.js index c26e64a9400..b149a1934c3 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -1,4 +1,4 @@ -import {logError, logInfo, logMessage, logWarn} from '../src/utils.js'; +import {deepSetValue, logError, logInfo, logMessage, logWarn} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import { EVENTS, REJECTION_REASON } from '../src/constants.js'; import {ajax} from '../src/ajax.js'; @@ -6,11 +6,14 @@ import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; import {defer} from '../src/utils/promise.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; -import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; +import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js'; import {on as onEvent, off as offEvent} from '../src/events.js'; +import { enrichFPD } from '../src/fpd/enrichment.js'; +import { timeoutQueue } from '../libraries/timeoutQueue/timeoutQueue.js'; const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$'; const CURRENCY_RATE_PRECISION = 4; +const MODULE_NAME = 'currency'; let ratesURL; let bidResponseQueue = []; @@ -26,8 +29,12 @@ let defaultRates; export let responseReady = defer(); +const delayedAuctions = timeoutQueue(); +let auctionDelay = 0; + /** * Configuration function for currency + * @param {object} config * @param {string} [config.adServerCurrency = 'USD'] * ISO 4217 3-letter currency code that represents the target currency. (e.g. 'EUR'). If this value is present, * the currency conversion feature is activated. @@ -61,13 +68,13 @@ export let responseReady = defer(); export function setConfig(config) { ratesURL = DEFAULT_CURRENCY_RATE_URL; - if (typeof config.rates === 'object') { + if (config.rates !== null && typeof config.rates === 'object') { currencyRates.conversions = config.rates; currencyRatesLoaded = true; needToCallForCurrencyFile = false; // don't call if rates are already specified } - if (typeof config.defaultRates === 'object') { + if (config.defaultRates !== null && typeof config.defaultRates === 'object') { defaultRates = config.defaultRates; // set up the default rates to be used if the rate file doesn't get loaded in time @@ -76,6 +83,7 @@ export function setConfig(config) { } if (typeof config.adServerCurrency === 'string') { + auctionDelay = config.auctionDelay; logInfo('enabling currency support', arguments); adServerCurrency = config.adServerCurrency; @@ -105,6 +113,7 @@ export function setConfig(config) { initCurrency(); } else { // currency support is disabled, setting defaults + auctionDelay = 0; logInfo('disabling currency support'); resetCurrency(); } @@ -136,6 +145,7 @@ function loadRates() { conversionCache = {}; currencyRatesLoaded = true; processBidResponseQueue(); + delayedAuctions.resume(); } catch (e) { errorSettingsRates('Failed to parse currencyRates response: ' + response); } @@ -144,6 +154,7 @@ function loadRates() { errorSettingsRates(...args); currencyRatesLoaded = true; processBidResponseQueue(); + delayedAuctions.resume(); needToCallForCurrencyFile = true; } } @@ -155,36 +166,39 @@ function loadRates() { function initCurrency() { conversionCache = {}; - currencySupportEnabled = true; - - logInfo('Installing addBidResponse decorator for currency module', arguments); - - // Adding conversion function to prebid global for external module and on page use - getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); - getHook('addBidResponse').before(addBidResponseHook, 100); - getHook('responsesReady').before(responsesReadyHook); - onEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); - onEvent(EVENTS.AUCTION_INIT, loadRates); - loadRates(); + if (!currencySupportEnabled) { + currencySupportEnabled = true; + // Adding conversion function to prebid global for external module and on page use + getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); + getHook('addBidResponse').before(addBidResponseHook, 100); + getHook('responsesReady').before(responsesReadyHook); + enrichFPD.before(enrichFPDHook); + getHook('requestBids').before(requestBidsHook, 50); + onEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + onEvent(EVENTS.AUCTION_INIT, loadRates); + loadRates(); + } } -function resetCurrency() { - logInfo('Uninstalling addBidResponse decorator for currency module', arguments); - - getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); - getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove(); - offEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); - offEvent(EVENTS.AUCTION_INIT, loadRates); - delete getGlobal().convertCurrency; - - adServerCurrency = 'USD'; - conversionCache = {}; - currencySupportEnabled = false; - currencyRatesLoaded = false; - needToCallForCurrencyFile = true; - currencyRates = {}; - bidderCurrencyDefault = {}; - responseReady = defer(); +export function resetCurrency() { + if (currencySupportEnabled) { + getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove(); + enrichFPD.getHooks({hook: enrichFPDHook}).remove(); + getHook('requestBids').getHooks({hook: requestBidsHook}).remove(); + offEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + offEvent(EVENTS.AUCTION_INIT, loadRates); + delete getGlobal().convertCurrency; + + adServerCurrency = 'USD'; + conversionCache = {}; + currencySupportEnabled = false; + currencyRatesLoaded = false; + needToCallForCurrencyFile = true; + currencyRates = {}; + bidderCurrencyDefault = {}; + responseReady = defer(); + } } function responsesReadyHook(next, ready) { @@ -335,3 +349,23 @@ export function setOrtbCurrency(ortbRequest, bidderRequest, context) { } registerOrtbProcessor({type: REQUEST, name: 'currency', fn: setOrtbCurrency}); + +function enrichFPDHook(next, fpd) { + return next(fpd.then(ortb2 => { + deepSetValue(ortb2, 'ext.prebid.adServerCurrency', adServerCurrency); + return ortb2; + })) +} + +export const requestBidsHook = timedAuctionHook('currency', function requestBidsHook(fn, reqBidsConfigObj) { + const continueAuction = ((that) => () => fn.call(that, reqBidsConfigObj))(this); + + if (!currencyRatesLoaded && auctionDelay > 0) { + delayedAuctions.submit(auctionDelay, continueAuction, () => { + logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction ${reqBidsConfigObj.auctionId}`) + continueAuction(); + }); + } else { + continueAuction(); + } +}); diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index f878be5f66a..16370c6e911 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -2,7 +2,8 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER} from '../src/mediaTypes.js'; import {generateUUID, getParameterByName, isNumber, logError, logInfo} from '../src/utils.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; +import { sendBeacon } from '../src/ajax.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -151,14 +152,18 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - if (!bid.params?.placementId || !isNumber(bid.params.placementId)) { - logError('placementId not provided or not a number'); - return false; - } + if (!bid.params?.domainId || !isNumber(bid.params.domainId)) { + logError('domainId not provided or not a number'); + if (!bid.params?.placementId || !isNumber(bid.params.placementId)) { + logError('placementId not provided or not a number'); + return false; + } - if (!bid.params?.pageId || !isNumber(bid.params.pageId)) { - logError('pageId not provided or not a number'); - return false; + if (!bid.params?.pageId || !isNumber(bid.params.pageId)) { + logError('pageId not provided or not a number'); + return false; + } + return true; } return true; }, @@ -176,8 +181,8 @@ export const spec = { // process bid requests let processed = validBidRequests .map(bid => slotDimensions(bid)) - // Flattens the pageId and placement Id for backwards compatibility. - .map((bid) => ({...bid, pageId: bid.params?.pageId, placementId: bid.params?.placementId})); + // Flattens the pageId, domainId and placement Id for backwards compatibility. + .map((bid) => ({...bid, pageId: bid.params?.pageId, domainId: bid.params?.domainId, placementId: bid.params?.placementId})); const extensions = getCwExtension(); const payload = { @@ -224,7 +229,7 @@ export const spec = { bid: bid } } - navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) + sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) }, onBidderError: function (error, bidderRequest) { @@ -236,7 +241,7 @@ export const spec = { bidderRequest: bidderRequest } } - navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) + sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) }, getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { diff --git a/modules/cwireBidAdapter.md b/modules/cwireBidAdapter.md index 9804250b906..1d4f3c039c8 100644 --- a/modules/cwireBidAdapter.md +++ b/modules/cwireBidAdapter.md @@ -14,14 +14,15 @@ Prebid.js Adapter for C-Wire. Below, the list of C-WIRE params and where they can be set. -| Param name | URL parameter | AdUnit config | Type | Required | -|-------------|:-------------:|:-------------:|:--------:|:-------------:| -| pageId | | x | number | YES | -| placementId | | x | number | YES | -| cwgroups | x | | string | NO | -| cwcreative | x | | string | NO | -| cwdebug | x | | boolean | NO | -| cwfeatures | x | | string | NO | +| Param name | URL parameter | AdUnit config | Type | Required | +|-------------|:-------------:|:-------------:|:--------:|:--------:| +| pageId | | x | number | NO | +| domainId | | x | number | YES | +| placementId | | x | number | NO | +| cwgroups | x | | string | NO | +| cwcreative | x | | string | NO | +| cwdebug | x | | boolean | NO | +| cwfeatures | x | | string | NO | ### adUnit configuration @@ -38,12 +39,30 @@ var adUnits = [ } }, params: { - pageId: 1422, // required - number - placementId: 2211521, // required - number + domainId: 1422, // required - number + placementId: 2211521, // optional - number } }] } ]; +// old version for the compatibility +var adUnits = [ + { + code: 'target_div_id', // REQUIRED + bids: [{ + bidder: 'cwire', + mediaTypes: { + banner: { + sizes: [[400, 600]], + } + }, + params: { + pageId: 1422, // required - number + placementId: 2211521, // required - number + } + }] + } +]; ``` ### URL parameters diff --git a/modules/dailyhuntBidAdapter.js b/modules/dailyhuntBidAdapter.js index f96e07b71bf..fd7a5c137a7 100644 --- a/modules/dailyhuntBidAdapter.js +++ b/modules/dailyhuntBidAdapter.js @@ -129,7 +129,7 @@ const createOrtbPublisherObj = (validBidRequests) => ({ ...extractKeyInfo(validB // get bidFloor Function for different creatives function getBidFloor(bid, creative) { let floorInfo = typeof (bid.getFloor) == 'function' ? bid.getFloor({ currency: 'USD', mediaType: creative, size: '*' }) : {}; - return Math.floor(floorInfo.floor || (bid.params.bidfloor ? bid.params.bidfloor : 0.0)); + return Math.floor(floorInfo?.floor || (bid.params.bidfloor ? bid.params.bidfloor : 0.0)); } const createOrtbImpObj = (bid) => { diff --git a/modules/dailymotionBidAdapter.js b/modules/dailymotionBidAdapter.js index 2be5edad78e..8b34e674fde 100644 --- a/modules/dailymotionBidAdapter.js +++ b/modules/dailymotionBidAdapter.js @@ -1,6 +1,42 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { VIDEO } from '../src/mediaTypes.js'; import { deepAccess } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { userSync } from '../src/userSync.js'; + +const DAILYMOTION_VENDOR_ID = 573; + +const dailymotionOrtbConverter = ortbConverter({ + context: { + netRevenue: true, + ttl: 600, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (typeof bidRequest.getFloor === 'function') { + const size = imp.w > 0 && imp.h > 0 ? [imp.w, imp.h] : '*'; + + const floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', // or '*' for all the mediaType + size + }) || {}; + + if (floorInfo.floor && floorInfo.currency) { + imp.bidfloor = floorInfo.floor; + imp.bidfloorcur = floorInfo.currency; + } + } + + return imp; + }, +}); + +function isArrayFilled (_array) { + return _array && Array.isArray(_array) && _array.length > 0; +} /** * Get video metadata from bid request @@ -9,27 +45,38 @@ import { deepAccess } from '../src/utils.js'; * @return video metadata */ function getVideoMetadata(bidRequest, bidderRequest) { - const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + const videoParams = deepAccess(bidRequest, 'params.video', {}); - const videoParams = { - ...videoAdUnit, - ...videoBidderParams, // Bidder Specific overrides - }; + // As per oRTB 2.5 spec, "A bid request must not contain both an App and a Site object." + // See section 3.2.14 + const siteOrAppObj = deepAccess(bidderRequest, 'ortb2.site') + ? deepAccess(bidderRequest, 'ortb2.site') + : deepAccess(bidderRequest, 'ortb2.app'); + // Content object is either from Object: Site or Object: App + const contentObj = deepAccess(siteOrAppObj, 'content') + + const contentCattax = deepAccess(contentObj, 'cattax', 0); + const isContentCattaxV1 = contentCattax === 1; + const isContentCattaxV2 = [2, 5, 6].includes(contentCattax); - // Store as object keys to ensure uniqueness - const iabcat1 = {}; - const iabcat2 = {}; + const parsedContentData = { + // Store as object keys to ensure uniqueness + iabcat1: {}, + iabcat2: {}, + }; - deepAccess(bidderRequest, 'ortb2.site.content.data', []).forEach((data) => { + deepAccess(contentObj, 'data', []).forEach((data) => { if ([4, 5, 6, 7].includes(data?.ext?.segtax)) { (Array.isArray(data.segment) ? data.segment : []).forEach((segment) => { if (typeof segment.id === 'string') { // See https://docs.prebid.org/features/firstPartyData.html#segments-and-taxonomy // Only take IAB cats of taxonomy V1 - if (data.ext.segtax === 4) iabcat1[segment.id] = 1; - // Only take IAB cats of taxonomy V2 or higher - if ([5, 6, 7].includes(data.ext.segtax)) iabcat2[segment.id] = 1; + if (data.ext.segtax === 4) { + parsedContentData.iabcat1[segment.id] = 1; + } else { + // Only take IAB cats of taxonomy V2 or higher + parsedContentData.iabcat2[segment.id] = 1; + } } }); } @@ -37,26 +84,75 @@ function getVideoMetadata(bidRequest, bidderRequest) { const videoMetadata = { description: videoParams.description || '', - duration: videoParams.duration || 0, - iabcat1: Object.keys(iabcat1), - iabcat2: Array.isArray(videoParams.iabcat2) + duration: videoParams.duration || deepAccess(contentObj, 'len', 0), + iabcat1: isArrayFilled(videoParams.iabcat1) + ? videoParams.iabcat1 + : (isArrayFilled(deepAccess(contentObj, 'cat')) && isContentCattaxV1) + ? contentObj.cat + : Object.keys(parsedContentData.iabcat1), + iabcat2: isArrayFilled(videoParams.iabcat2) ? videoParams.iabcat2 - : Object.keys(iabcat2), - id: videoParams.id || '', - lang: videoParams.lang || '', + : (isArrayFilled(deepAccess(contentObj, 'cat')) && isContentCattaxV2) + ? contentObj.cat + : Object.keys(parsedContentData.iabcat2), + id: videoParams.id || deepAccess(contentObj, 'id', ''), + lang: videoParams.lang || deepAccess(contentObj, 'language', ''), + livestream: typeof videoParams.livestream === 'number' + ? !!videoParams.livestream + : !!deepAccess(contentObj, 'livestream', 0), private: videoParams.private || false, - tags: videoParams.tags || '', - title: videoParams.title || '', + tags: videoParams.tags || deepAccess(contentObj, 'keywords', ''), + title: videoParams.title || deepAccess(contentObj, 'title', ''), + url: videoParams.url || deepAccess(contentObj, 'url', ''), topics: videoParams.topics || '', - xid: videoParams.xid || '', + isCreatedForKids: typeof videoParams.isCreatedForKids === 'boolean' + ? videoParams.isCreatedForKids + : null, + context: { + siteOrAppCat: deepAccess(siteOrAppObj, 'cat', []), + siteOrAppContentCat: deepAccess(contentObj, 'cat', []), + videoViewsInSession: ( + typeof videoParams.videoViewsInSession === 'number' && + videoParams.videoViewsInSession >= 0 + ) + ? videoParams.videoViewsInSession + : null, + autoplay: typeof videoParams.autoplay === 'boolean' + ? videoParams.autoplay + : null, + playerName: videoParams.playerName || deepAccess(contentObj, 'playerName', ''), + playerVolume: ( + typeof videoParams.playerVolume === 'number' && + videoParams.playerVolume >= 0 && + videoParams.playerVolume <= 10 + ) + ? videoParams.playerVolume + : null, + }, }; return videoMetadata; } +/** + * Check if user sync is enabled for Dailymotion + * + * @return boolean True if user sync is enabled + */ +function isUserSyncEnabled() { + const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled'); + + if (!syncEnabled) return false; + + const canSyncWithIframe = userSync.canBidderRegisterSync('iframe', 'dailymotion'); + const canSyncWithPixel = userSync.canBidderRegisterSync('image', 'dailymotion'); + + return !!(canSyncWithIframe || canSyncWithPixel); +} + export const spec = { code: 'dailymotion', - gvlid: 573, + gvlid: DAILYMOTION_VENDOR_ID, supportedMediaTypes: [VIDEO], /** @@ -67,7 +163,24 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - return typeof bid?.params?.apiKey === 'string' && bid.params.apiKey.length > 10; + if (bid?.params) { + // We only accept video adUnits + if (!bid?.mediaTypes?.[VIDEO]) return false; + + // As `context`, `placement` & `plcmt` are optional (although recommended) + // values, we check the 3 of them to see if we are in an instream video context + const isInstream = bid.mediaTypes[VIDEO].context === 'instream' || + bid.mediaTypes[VIDEO].placement === 1 || + bid.mediaTypes[VIDEO].plcmt === 1; + + // We only accept instream video context + if (!isInstream) return false; + + // We need API key + return typeof bid.params.apiKey === 'string' && bid.params.apiKey.length > 10; + } + + return false; }, /** @@ -77,53 +190,94 @@ export const spec = { * @param {BidderRequest} bidderRequest * @return ServerRequest Info describing the request to the server. */ - buildRequests: (validBidRequests = [], bidderRequest) => validBidRequests.map(bid => ({ - method: 'POST', - url: 'https://pb.dmxleo.com', - data: { - bidder_request: { - gdprConsent: { - apiVersion: bidderRequest?.gdprConsent?.apiVersion || 1, - consentString: bidderRequest?.gdprConsent?.consentString || '', - // Cast boolean in any case (eg: if value is int) to ensure type - gdprApplies: !!bidderRequest?.gdprConsent?.gdprApplies, - }, - refererInfo: { - page: bidderRequest?.refererInfo?.page || '', + buildRequests: function(validBidRequests = [], bidderRequest) { + const ortbData = dailymotionOrtbConverter.toORTB({ bidRequests: validBidRequests, bidderRequest }); + // check consent to be able to read user cookie + const allowCookieReading = + // No GDPR applies + !deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || + // OR GDPR applies and we have global consent + deepAccess(bidderRequest, 'gdprConsent.vendorData.hasGlobalConsent') === true || + ( + // Vendor consent + deepAccess(bidderRequest, `gdprConsent.vendorData.vendor.consents.${DAILYMOTION_VENDOR_ID}`) === true && + + // Purposes with legal basis "consent". These are not flexible, so if publisher requires legitimate interest (2) it cancels them + [1, 3, 4].every(v => + deepAccess(bidderRequest, `gdprConsent.vendorData.publisher.restrictions.${v}.${DAILYMOTION_VENDOR_ID}`) !== 0 && + deepAccess(bidderRequest, `gdprConsent.vendorData.publisher.restrictions.${v}.${DAILYMOTION_VENDOR_ID}`) !== 2 && + deepAccess(bidderRequest, `gdprConsent.vendorData.purpose.consents.${v}`) === true + ) && + + // Purposes with legal basis "legitimate interest" (default) or "consent" (when specified as such by publisher) + [2, 7, 9, 10].every(v => + deepAccess(bidderRequest, `gdprConsent.vendorData.publisher.restrictions.${v}.${DAILYMOTION_VENDOR_ID}`) !== 0 && + (deepAccess(bidderRequest, `gdprConsent.vendorData.publisher.restrictions.${v}.${DAILYMOTION_VENDOR_ID}`) === 1 + ? deepAccess(bidderRequest, `gdprConsent.vendorData.purpose.consents.${v}`) === true + : deepAccess(bidderRequest, `gdprConsent.vendorData.purpose.legitimateInterests.${v}`) === true) + ) + ); + + return validBidRequests.map(bid => ({ + method: 'POST', + url: 'https://pb.dmxleo.com', + data: { + pbv: '$prebid.version$', + ortb: ortbData, + bidder_request: { + gdprConsent: { + apiVersion: deepAccess(bidderRequest, 'gdprConsent.apiVersion', 1), + consentString: deepAccess(bidderRequest, 'gdprConsent.consentString', ''), + // Cast boolean in any case (eg: if value is int) to ensure type + gdprApplies: !!deepAccess(bidderRequest, 'gdprConsent.gdprApplies'), + }, + refererInfo: { + page: deepAccess(bidderRequest, 'refererInfo.page', ''), + }, + uspConsent: deepAccess(bidderRequest, 'uspConsent', ''), + gppConsent: { + gppString: deepAccess(bidderRequest, 'gppConsent.gppString') || + deepAccess(bidderRequest, 'ortb2.regs.gpp', ''), + applicableSections: deepAccess(bidderRequest, 'gppConsent.applicableSections') || + deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', []), + }, }, - uspConsent: bidderRequest?.uspConsent || '', - gppConsent: { - gppString: deepAccess(bidderRequest, 'gppConsent.gppString') || - deepAccess(bidderRequest, 'ortb2.regs.gpp', ''), - applicableSections: deepAccess(bidderRequest, 'gppConsent.applicableSections') || - deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', []), + config: { + api_key: bid.params.apiKey, + ts: bid.params.dmTs, }, - }, - config: { - api_key: bid.params.apiKey - }, - // Cast boolean in any case (value should be 0 or 1) to ensure type - coppa: !!deepAccess(bidderRequest, 'ortb2.regs.coppa'), - request: { - adUnitCode: bid.adUnitCode || '', - auctionId: bid.auctionId || '', - bidId: bid.bidId || '', - mediaTypes: { - video: { - playerSize: bid.mediaTypes?.[VIDEO]?.playerSize || [], - api: bid.mediaTypes?.[VIDEO]?.api || [], - startDelay: bid.mediaTypes?.[VIDEO]?.startdelay || 0, + userSyncEnabled: isUserSyncEnabled(), + request: { + adUnitCode: deepAccess(bid, 'adUnitCode', ''), + auctionId: deepAccess(bid, 'auctionId', ''), + bidId: deepAccess(bid, 'bidId', ''), + mediaTypes: { + video: { + api: bid.mediaTypes?.[VIDEO]?.api || [], + mimes: bid.mediaTypes?.[VIDEO]?.mimes || [], + minduration: bid.mediaTypes?.[VIDEO]?.minduration || 0, + maxduration: bid.mediaTypes?.[VIDEO]?.maxduration || 0, + playbackmethod: bid.mediaTypes?.[VIDEO]?.playbackmethod || [], + plcmt: bid.mediaTypes?.[VIDEO]?.plcmt, + protocols: bid.mediaTypes?.[VIDEO]?.protocols || [], + skip: bid.mediaTypes?.[VIDEO]?.skip || 0, + skipafter: bid.mediaTypes?.[VIDEO]?.skipafter || 0, + skipmin: bid.mediaTypes?.[VIDEO]?.skipmin || 0, + startdelay: bid.mediaTypes?.[VIDEO]?.startdelay, + w: bid.mediaTypes?.[VIDEO]?.w || 0, + h: bid.mediaTypes?.[VIDEO]?.h || 0, + }, }, + sizes: bid.sizes || [], }, - sizes: bid.sizes || [], + video_metadata: getVideoMetadata(bid, bidderRequest), }, - video_metadata: getVideoMetadata(bid, bidderRequest), - }, - options: { - withCredentials: true, - crossOrigin: true, - }, - })), + options: { + withCredentials: allowCookieReading, + crossOrigin: true, + }, + })); + }, /** * Map the response from the server into a list of bids. @@ -133,7 +287,38 @@ export const spec = { * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: serverResponse => serverResponse?.body ? [serverResponse.body] : [], + interpretResponse: serverResponse => serverResponse?.body?.cpm ? [serverResponse.body] : [], + + /** + * Retrieves user synchronization URLs based on provided options and consents. + * + * @param {object} syncOptions - Options for synchronization. + * @param {object[]} serverResponses - Array of server responses. + * @returns {object[]} - Array of synchronization URLs. + */ + getUserSyncs: (syncOptions, serverResponses) => { + if (!!serverResponses?.length && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + const iframeSyncs = []; + const pixelSyncs = []; + + serverResponses.forEach((response) => { + (response?.body?.userSyncs || []).forEach((syncUrl) => { + if (syncUrl.type === 'image') { + pixelSyncs.push({ url: syncUrl.url, type: 'image' }); + } + + if (syncUrl.type === 'iframe') { + iframeSyncs.push({ url: syncUrl.url, type: 'iframe' }); + } + }); + }); + + if (syncOptions.iframeEnabled) return iframeSyncs; + return pixelSyncs; + } + + return []; + }, }; registerBidder(spec); diff --git a/modules/dailymotionBidAdapter.md b/modules/dailymotionBidAdapter.md index 795273c9229..12d5bc1c04d 100644 --- a/modules/dailymotionBidAdapter.md +++ b/modules/dailymotionBidAdapter.md @@ -1,4 +1,4 @@ -# Overview +### Overview ``` Module Name: Dailymotion Bid Adapter @@ -6,13 +6,24 @@ Module Type: Bidder Adapter Maintainer: ad-leo-engineering@dailymotion.com ``` -# Description +### Description Dailymotion prebid adapter. +Supports video ad units in instream context. -# Configuration options +### Usage -Before calling this adapter, you need to set at least the API key in the bid parameters: +Make sure to have the following modules listed while building prebid : `priceFloors,dailymotionBidAdapter` + +`priceFloors` module is needed to retrieve the price floor: https://docs.prebid.org/dev-docs/modules/floors.html + +```shell +gulp build --modules=priceFloors,dailymotionBidAdapter +``` + +### Configuration options + +Before calling this adapter, you need to at least set a video adUnit in an instream context and the API key in the bid parameters: ```javascript const adUnits = [ @@ -21,17 +32,96 @@ const adUnits = [ bidder: 'dailymotion', params: { apiKey: 'fake_api_key' - } - }] + }, + }], + code: 'test-ad-unit', + mediaTypes: { + video: { + context: 'instream', + }, + }, } ]; ``` `apiKey` is your publisher API key. For testing purpose, you can use "dailymotion-testing". -# Test Parameters +#### User Sync -By setting the following bid parameters, you'll get a constant response to any request, to validate your adapter integration: +To enable user synchronization, add the following code. Dailymotion highly recommends using iframes and/or pixels for user syncing. This feature enhances DSP user match rates, resulting in higher bid rates and bid prices. Ensure that `pbjs.setConfig()` is called only once. + +```javascript +pbjs.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: '*', // Or add dailymotion to your list included bidders + filter: 'include' + }, + image: { + bidders: '*', // Or add dailymotion to your list of included bidders + filter: 'include' + }, + }, + }, +}); +``` + +#### Price floor + +The price floor can be set at the ad unit level, for example : + +```javascript +const adUnits = [{ + floors: { + currency: 'USD', + schema: { + fields: [ 'mediaType', 'size' ] + }, + values: { + 'video|300x250': 2.22, + 'video|*': 1 + } + }, + bids: [{ + bidder: 'dailymotion', + params: { + apiKey: 'dailymotion-testing', + } + }], + code: 'test-ad-unit', + mediaTypes: { + video: { + playerSize: [300, 250], + context: 'instream', + }, + } +}]; + +// Do not forget to set an empty object for "floors" to active the price floor module +pbjs.setConfig({floors: {}}); +``` + +The following request will be sent to Dailymotion Prebid Service : + +```javascript +{ + "pbv": "9.23.0-pre", + "ortb": { + "imp": [ + { + ... + "bidfloor": 2.22, + "bidfloorcur": "USD" + } + ], + } + ... +} +``` + +Or the price floor can be set at the package level, for example : ```javascript const adUnits = [ @@ -39,21 +129,86 @@ const adUnits = [ bids: [{ bidder: 'dailymotion', params: { - apiKey: 'dailymotion-testing' + apiKey: 'dailymotion-testing', + } + }], + code: 'test-ad-unit', + mediaTypes: { + video: { + playerSize: [1280,720], + context: 'instream', + }, + } + } +]; + +pbjs.setConfig({ + floors: { + data: { + currency: 'USD', + schema: { + fields: [ 'mediaType', 'size' ] + }, + values: { + 'video|300x250': 2.22, + 'video|*': 1 + } } - }] + } +}) +``` + +This will send the following bid floor in the request to Daiymotion Prebid Service : + +```javascript +{ + "pbv": "9.23.0-pre", + "ortb": { + "imp": [ + { + ... + "bidfloor": 1, + "bidfloorcur": "USD" + } + ], + ... + } +} +``` + +You can also [set dynamic floors](https://docs.prebid.org/dev-docs/modules/floors.html#bid-adapter-interface). + +### Test Parameters + +By setting the following bid parameters, you'll get a constant response to any request, to validate your adapter integration: + +```javascript +const adUnits = [ + { + bids: [{ + bidder: 'dailymotion', + params: { + apiKey: 'dailymotion-testing', + }, + }], + code: 'test-ad-unit', + mediaTypes: { + video: { + context: 'instream', + }, + }, } ]; ``` Please note that failing to set these will result in the adapter not bidding at all. -# Sample video AdUnit +### Sample video AdUnit To allow better targeting, you should provide as much context about the video as possible. -There are two ways of doing this depending on if you're using Dailymotion player or a third party one. +There are three ways of doing this depending on if you're using Dailymotion player or a third party one. -If you are using the Dailymotion player, you should only provide the video `xid` in your ad unit, example: +If you are using the Dailymotion player, you must provide the video `xid` in the `video.id` field of your ad unit, example: ```javascript const adUnits = [ @@ -61,7 +216,13 @@ const adUnits = [ bids: [{ bidder: 'dailymotion', params: { - apiKey: 'dailymotion-testing' + apiKey: 'dailymotion-testing', + video: { + id: 'x123456' // Dailymotion infrastructure unique video ID + autoplay: false, + playerName: 'dailymotion', + playerVolume: 8 + }, } }], code: 'test-ad-unit', @@ -69,9 +230,9 @@ const adUnits = [ video: { api: [2, 7], context: 'instream', - playerSize: [ [1280, 720] ], - startDelay: 0, - xid: 'x123456' // Dailymotion infrastructure unique video ID + startdelay: 0, + w: 1280, + h: 720, }, } } @@ -79,9 +240,9 @@ const adUnits = [ ``` This will automatically fetch the most up-to-date information about the video. -If you provide any other metadata in addition to the `xid`, they will be ignored. +Please note that if you provide any video metadata not listed above, they will be replaced by the ones fetched from the `video.id`. -If you are using a third party video player, you should not provide any `xid` and instead fill the following members: +If you are using a third party video player, you should fill the following members: ```javascript const adUnits = [ @@ -91,7 +252,23 @@ const adUnits = [ params: { apiKey: 'dailymotion-testing', video: { - description: 'overriden video description' + description: 'this is a video description', + duration: 556, + iabcat1: ['IAB-2'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + livestream: 0, + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/testvideo' + topics: 'topic_1, topic_2', + isCreatedForKids: false, + videoViewsInSession: 1, + autoplay: false, + playerName: 'video.js', + playerVolume: 8 } } }], @@ -100,41 +277,54 @@ const adUnits = [ video: { api: [2, 7], context: 'instream', - description: 'this is a video description', - duration: 556, - iabcat2: ['6', '17'], - id: '54321', - lang: 'FR', - playerSize: [ [1280, 720] ], - private: false, - startDelay: 0, - tags: 'tag_1,tag_2,tag_3', - title: 'test video', - topics: 'topic_1, topic_2', + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [7, 8, 11, 12, 13, 14], + startdelay: 0, + w: 1280, + h: 720, }, } } ]; ``` -Each of the following video metadata fields can be added in mediaTypes.video or bids.params.video. -If a field exists in both places, it will be overridden by bids.params.video. +Each of the following video metadata fields can be added in bids.params.video. * `description` - Video description * `duration` - Video duration in seconds -* `iabcat2` - List of IAB category IDs from the [2.0 taxonomy](https://github.com/InteractiveAdvertisingBureau/Taxonomies/blob/main/Content%20Taxonomies/Content%20Taxonomy%202.0.tsv) +* `iabcat1` - List of IAB category IDs from the [1.0 taxonomy](https://github.com/InteractiveAdvertisingBureau/Taxonomies/blob/main/Content%20Taxonomies/Content%20Taxonomy%201.0.tsv) +* `iabcat2` - List of IAB category IDs from the [2.0 taxonomy](https://github.com/InteractiveAdvertisingBureau/Taxonomies/blob/main/Content%20Taxonomies/Content%20Taxonomy%202.0.tsv) and above * `id` - Video unique ID in host video infrastructure * `lang` - ISO 639-1 code for main language used in the video +* `livestream` - 0 = not live, 1 = content is live * `private` - True if video is not publicly available * `tags` - Tags for the video, comma separated * `title` - Video title +* `url` - URL of the content * `topics` - Main topics for the video, comma separated -* `xid` - Dailymotion video identifier (only applicable if using the Dailymotion player) +* `isCreatedForKids` - [The content is created for children as primary audience](https://faq.dailymotion.com/hc/en-us/articles/360020920159-Content-created-for-kids) -# Integrating the adapter +The following contextual information can also be added in bids.params.video. -To use the adapter with any non-test request, you first need to ask an API key from Dailymotion. Please contact us through **DailymotionPrebid.js@dailymotion.com**. +* `autoplay` - Playback was launched without user interaction +* `playerName` - Name of the player used to display the video +* `playerVolume` - Player volume between 0 (muted, 0%) and 10 (100%) +* `videoViewsInSession` - Number of videos viewed within the current user session -You will then be able to use it within the bid parameters before making a bid request. +If you already specify [First-Party data](https://docs.prebid.org/features/firstPartyData.html) through the `ortb2` object when calling [`pbjs.requestBids(requestObj)`](https://docs.prebid.org/dev-docs/publisher-api-reference/requestBids.html), we will collect the following values and fallback to bids.params.video values when applicable. See the mapping below. -This API key will ensure proper identification of your inventory and allow you to get real bids. +| From ortb2 | Metadata fields | +|---------------------------------------------------------------------------------|-----------------| +| `ortb2.site.content.cat` OR `ortb2.site.content.data` where `ext.segtax` is `4` | `iabcat1` | +| `ortb2.site.content.data` where `ext.segtax` is `5`, `6` or `7` | `iabcat2` | +| `ortb2.site.content.id` | `id` | +| `ortb2.site.content.language` | `lang` | +| `ortb2.site.content.livestream` | `livestream` | +| `ortb2.site.content.keywords` | `tags` | +| `ortb2.site.content.title` | `title` | +| `ortb2.site.content.url` | `url` | +| `ortb2.*` | N/A | diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 395706994fe..3cd974b2b13 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; +import {deepAccess, getWinDimensions, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; @@ -203,14 +203,15 @@ export const spec = { get_client_info: function () { let botTest = new BotClientTests(); let win = getWindowTop(); + const windowDimensions = getWinDimensions(); return { - 'wiw': win.innerWidth, - 'wih': win.innerHeight, - 'saw': screen ? screen.availWidth : null, - 'sah': screen ? screen.availHeight : null, + 'wiw': windowDimensions.innerWidth, + 'wih': windowDimensions.innerHeight, + 'saw': windowDimensions.screen.availWidth, + 'sah': windowDimensions.screen.availHeight, 'scd': screen ? screen.colorDepth : null, - 'sw': screen ? screen.width : null, - 'sh': screen ? screen.height : null, + 'sw': windowDimensions.screen.width, + 'sh': windowDimensions.screen.height, 'whl': win.history.length, 'wxo': win.pageXOffset, 'wyo': win.pageYOffset, @@ -417,7 +418,7 @@ export const spec = { // INITIATE USER SYNCING getUserSyncs: function(options, rtbResponse, gdprConsent) { const syncs = []; - let bidResponse = rtbResponse[0].body; + let bidResponse = rtbResponse?.[0]?.body ?? null; let scope = this; // LISTEN FOR SYNC DATA FROM IFRAME TYPE SYNC diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index b5a096521a1..2f8e397d959 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -227,7 +227,6 @@ function buildVideoRequest(bidRequest, bidderRequest) { maxbitrate: deepAccess(bidRequest, 'mediaTypes.video.maxbitrate'), delivery: deepAccess(bidRequest, 'mediaTypes.video.delivery'), linearity: deepAccess(bidRequest, 'mediaTypes.video.linearity'), - placement: deepAccess(bidRequest, 'mediaTypes.video.placement'), skip: deepAccess(bidRequest, 'mediaTypes.video.skip'), skipafter: deepAccess(bidRequest, 'mediaTypes.video.skipafter') }; diff --git a/modules/debugging/bidInterceptor.js b/modules/debugging/bidInterceptor.js index 3afaacaeb81..828535f88b8 100644 --- a/modules/debugging/bidInterceptor.js +++ b/modules/debugging/bidInterceptor.js @@ -1,10 +1,6 @@ -import { - deepAccess, - deepClone, - deepEqual, - delayExecution, - mergeDeep -} from '../../src/utils.js'; +import {BANNER, VIDEO} from '../../src/mediaTypes.js'; +import {deepAccess, deepClone, delayExecution, hasNonSerializableProperty, mergeDeep} from '../../src/utils.js'; +import responseResolvers from './responses.js'; /** * @typedef {Number|String|boolean|null|undefined} Scalar @@ -22,9 +18,9 @@ Object.assign(BidInterceptor.prototype, { }, serializeConfig(ruleDefs) { const isSerializable = (ruleDef, i) => { - const serializable = deepEqual(ruleDef, JSON.parse(JSON.stringify(ruleDef)), {checkTypes: true}); + const serializable = !hasNonSerializableProperty(ruleDef); if (!serializable && !deepAccess(ruleDef, 'options.suppressWarnings')) { - this.logger.logWarn(`Bid interceptor rule definition #${i + 1} is not serializable and will be lost after a refresh. Rule definition: `, ruleDef); + this.logger.logWarn(`Bid interceptor rule definition #${i + 1} contains non-serializable properties and will be lost after a refresh. Rule definition: `, ruleDef); } return serializable; } @@ -143,39 +139,54 @@ Object.assign(BidInterceptor.prototype, { return (bid, ...args) => { const response = this.responseDefaults(bid); mergeDeep(response, replFn({args: [bid, ...args]})); - if (!response.hasOwnProperty('ad') && !response.hasOwnProperty('adUrl')) { - response.ad = this.defaultAd(bid, response); - } + const resolver = responseResolvers[response.mediaType]; + resolver && resolver(bid, response); response.isDebug = true; return response; } }, paapiReplacer(paapiDef, ruleNo) { + function wrap(configs = []) { + return configs.map(config => { + return Object.keys(config).some(k => !['config', 'igb'].includes(k)) + ? {config} + : config + }); + } if (Array.isArray(paapiDef)) { - return () => paapiDef; + return () => wrap(paapiDef); } else if (typeof paapiDef === 'function') { - return paapiDef + return (...args) => wrap(paapiDef(...args)) } else { this.logger.logError(`Invalid 'paapi' definition for debug bid interceptor (in rule #${ruleNo})`); } }, responseDefaults(bid) { - return { + const response = { requestId: bid.bidId, cpm: 3.5764, currency: 'EUR', - width: 300, - height: 250, ttl: 360, creativeId: 'mock-creative-id', netRevenue: false, meta: {} }; - }, - defaultAd(bid, bidResponse) { - return ``; + + if (!bid.mediaType) { + response.mediaType = Object.keys(bid.mediaTypes ?? {})[0] ?? BANNER; + } + let size; + if (response.mediaType === BANNER) { + size = bid.mediaTypes?.banner?.sizes?.[0] ?? [300, 250]; + } else if (response.mediaType === VIDEO) { + size = bid.mediaTypes?.video?.playerSize?.[0] ?? [600, 500]; + } + if (Array.isArray(size)) { + ([response.width, response.height] = size); + } + return response; }, /** * Match a candidate bid against all registered rules. diff --git a/modules/debugging/debugging.js b/modules/debugging/debugging.js index 2fd1731dc4e..e7d602f4711 100644 --- a/modules/debugging/debugging.js +++ b/modules/debugging/debugging.js @@ -31,6 +31,7 @@ export function disableDebugging({hook, logger}) { } } +// eslint-disable-next-line no-restricted-properties function saveDebuggingConfig(debugConfig, {sessionStorage = window.sessionStorage, DEBUG_KEY} = {}) { if (!debugConfig.enabled) { try { @@ -49,6 +50,7 @@ function saveDebuggingConfig(debugConfig, {sessionStorage = window.sessionStorag } } +// eslint-disable-next-line no-restricted-properties export function getConfig(debugging, {getStorage = () => window.sessionStorage, DEBUG_KEY, config, hook, logger} = {}) { if (debugging == null) return; let sessionStorage; @@ -70,6 +72,7 @@ export function getConfig(debugging, {getStorage = () => window.sessionStorage, export function sessionLoader({DEBUG_KEY, storage, config, hook, logger}) { let overrides; try { + // eslint-disable-next-line no-restricted-properties storage = storage || window.sessionStorage; overrides = JSON.parse(storage.getItem(DEBUG_KEY)); } catch (e) { @@ -102,11 +105,12 @@ export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest ({bids, bidRequest} = interceptBids({ bids, bidRequest, - addBid: cbs.onBid, - addPaapiConfig: (config, bidRequest) => cbs.onPaapi({bidId: bidRequest.bidId, config}), + addBid: wrapCallback(cbs.onBid), + addPaapiConfig: wrapCallback((config, bidRequest) => cbs.onPaapi({bidId: bidRequest.bidId, ...config})), done })); if (bids.length === 0) { + cbs.onResponse?.({}); // trigger onResponse so that the bidder may be marked as "timely" if necessary done(); } else { next(spec, bids, bidRequest, ajax, wrapCallback, {...cbs, onCompletion: done}); diff --git a/modules/debugging/pbsInterceptor.js b/modules/debugging/pbsInterceptor.js index 5c2eb458b18..dcde50927ad 100644 --- a/modules/debugging/pbsInterceptor.js +++ b/modules/debugging/pbsInterceptor.js @@ -17,7 +17,7 @@ export function makePbsInterceptor({createBid}) { function addBid(bid, bidRequest) { onBid({ adUnit: bidRequest.adUnitCode, - bid: Object.assign(createBid(STATUS.GOOD, bidRequest), bid) + bid: Object.assign(createBid(STATUS.GOOD, bidRequest), {requestBidder: bidRequest.bidder}, bid) }) } bidRequests = bidRequests @@ -29,7 +29,7 @@ export function makePbsInterceptor({createBid}) { adUnitCode: bidRequest.adUnitCode, ortb2: bidderRequest.ortb2, ortb2Imp: bidRequest.ortb2Imp, - config + ...config }) }, done diff --git a/modules/debugging/responses.js b/modules/debugging/responses.js new file mode 100644 index 00000000000..939a838e70d --- /dev/null +++ b/modules/debugging/responses.js @@ -0,0 +1,98 @@ +import {BANNER, NATIVE, VIDEO} from '../../src/mediaTypes.js'; +import {Renderer} from '../../src/Renderer.js'; +import {getGptSlotInfoForAdUnitCode} from '../../libraries/gptUtils/gptUtils.js'; + +const ORTB_NATIVE_ASSET_TYPES = ['img', 'video', 'link', 'data', 'title']; + +export default { + [BANNER]: (bid, bidResponse) => { + if (!bidResponse.hasOwnProperty('ad') && !bidResponse.hasOwnProperty('adUrl')) { + let [size, repeat] = (bidResponse.width ?? bidResponse.wratio) < (bidResponse.height ?? bidResponse.hratio) ? [bidResponse.width, 'repeat-y'] : [bidResponse.height, 'repeat-x']; + size = size == null ? '100%' : `${size}px`; + bidResponse.ad = `
`; + } + }, + [VIDEO]: (bid, bidResponse) => { + if (!bidResponse.hasOwnProperty('vastXml') && !bidResponse.hasOwnProperty('vastUrl')) { + bidResponse.vastXml = 'GDFPDemo00:00:11'; + bidResponse.renderer = Renderer.install({ + url: 'https://cdn.jwplayer.com/libraries/l5MchIxB.js', + }); + bidResponse.renderer.setRender(function (bid, doc) { + const parentId = getGptSlotInfoForAdUnitCode(bid.adUnitCode).divId ?? bid.adUnitCode; + const div = doc.createElement('div'); + div.id = `${parentId}-video-player`; + doc.getElementById(parentId).appendChild(div); + const player = window.jwplayer(div.id).setup({ + debug: true, + width: bidResponse.width, + height: bidResponse.height, + advertising: { + client: 'vast', + outstream: true, + endstate: 'close' + }, + }); + player.on('ready', async function () { + if (bid.vastUrl) { + player.loadAdTag(bid.vastUrl); + } else { + player.loadAdXml(bid.vastXml); + } + }); + }) + } + }, + [NATIVE]: (bid, bidResponse) => { + if (!bidResponse.hasOwnProperty('native')) { + bidResponse.native = { + ortb: { + link: { + url: 'https://www.link.example', + clicktrackers: ['https://impression.example'] + }, + assets: bid.nativeOrtbRequest.assets.map(mapDefaultNativeOrtbAsset) + } + } + } + } +} + +function mapDefaultNativeOrtbAsset(asset) { + const assetType = ORTB_NATIVE_ASSET_TYPES.find(type => asset.hasOwnProperty(type)); + switch (assetType) { + case 'img': + return { + ...asset, + img: { + type: 3, + w: 600, + h: 500, + url: 'https://vcdn.adnxs.com/p/creative-image/27/c0/52/67/27c05267-5a6d-4874-834e-18e218493c32.png', + } + } + case 'video': + return { + ...asset, + video: { + vasttag: 'GDFPDemo00:00:11' + } + } + case 'data': { + return { + ...asset, + data: { + value: '5 stars' + } + } + } + case 'title': { + return { + ...asset, + title: { + text: 'Prebid Native Example' + } + } + } + } +} diff --git a/modules/deepintentBidAdapter.js b/modules/deepintentBidAdapter.js index 0a64ed88ca5..9c67eb02fa9 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -1,35 +1,17 @@ -import { generateUUID, deepSetValue, deepAccess, isArray, isInteger, logError, logWarn } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.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'; +import { COMMON_ORTB_VIDEO_PARAMS, formatResponse } from '../libraries/deepintentUtils/index.js'; const BIDDER_CODE = 'deepintent'; const GVL_ID = 541; const BIDDER_ENDPOINT = 'https://prebid.deepintent.com/prebid'; const USER_SYNC_URL = 'https://cdn.deepintent.com/syncpixel.html'; const DI_M_V = '1.0.0'; export const ORTB_VIDEO_PARAMS = { - 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), - 'minduration': (value) => isInteger(value), - 'maxduration': (value) => isInteger(value), - 'protocols': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 10), - 'w': (value) => isInteger(value), - 'h': (value) => isInteger(value), - 'startdelay': (value) => isInteger(value), - 'placement': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 5), - 'linearity': (value) => [1, 2].indexOf(value) !== -1, - 'skip': (value) => [0, 1].indexOf(value) !== -1, - 'skipmin': (value) => isInteger(value), - 'skipafter': (value) => isInteger(value), - 'sequence': (value) => isInteger(value), - 'battr': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 17), - 'maxextended': (value) => isInteger(value), - 'minbitrate': (value) => isInteger(value), - 'maxbitrate': (value) => isInteger(value), - 'boxingallowed': (value) => [0, 1].indexOf(value) !== -1, - 'playbackmethod': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6), - 'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1, + ...COMMON_ORTB_VIDEO_PARAMS, + 'plcmt': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 5), 'delivery': (value) => [1, 2, 3].indexOf(value) !== -1, 'pos': (value) => [0, 1, 2, 3, 4, 5, 6, 7].indexOf(value) !== -1, - 'api': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6) }; export const spec = { code: BIDDER_CODE, @@ -155,29 +137,13 @@ function clean(obj) { } } -function formatResponse(bid) { - return { - requestId: bid && bid.impid ? bid.impid : undefined, - cpm: bid && bid.price ? bid.price : 0.0, - width: bid && bid.w ? bid.w : 0, - height: bid && bid.h ? bid.h : 0, - ad: bid && bid.adm ? bid.adm : '', - meta: { - advertiserDomains: bid && bid.adomain ? bid.adomain : [] - }, - creativeId: bid && bid.crid ? bid.crid : undefined, - netRevenue: false, - currency: bid && bid.cur ? bid.cur : 'USD', - ttl: 300, - dealId: bid && bid.dealId ? bid.dealId : undefined - } -} - function buildImpression(bid) { let impression = {}; + const floor = getFloor(bid); impression = { id: bid.bidId, tagid: bid.params.tagId || '', + ...(!isNaN(floor) && { bidfloor: floor }), secure: window.location.protocol === 'https:' ? 1 : 0, displaymanager: 'di_prebid', displaymanagerver: DI_M_V, @@ -192,6 +158,23 @@ function buildImpression(bid) { return impression; } +function getFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return bidRequest.params?.bidfloor; + } + + let floor = bidRequest.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + function _buildVideo(bid) { const videoObj = {}; const videoAdUnitParams = deepAccess(bid, 'mediaTypes.video', {}); diff --git a/modules/deepintentBidAdapter.md b/modules/deepintentBidAdapter.md index 84c375d69a4..73f1918085e 100644 --- a/modules/deepintentBidAdapter.md +++ b/modules/deepintentBidAdapter.md @@ -15,64 +15,70 @@ Module that connects to Deepintent's demand sources. # Banner Test Request ``` var adUnits = [ - { - code: 'di_adUnit1', - mediaTypes: { - banner: { - sizes: [[300, 250]], // a display size, only first one will be picked up since multiple ad sizes are not supported yet - } - } - bids: [ - { - bidder: 'deepintent', - params: { - tagId: '1300', // Required parameter - w: 300, // Width and Height here will override sizes in mediatype - h: 250, - pos: 1, - custom: { // Custom parameters in form of key value pairs - user_min_age: 18 - } - } - } - ] - } - ]; + { + code: 'di_adUnit1', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size, only first one will be picked up since multiple ad sizes are not supported yet + }, + }, + bids: [ + { + bidder: 'deepintent', + params: { + tagId: '1300', // Required parameter + bidfloor: 1.5, // optional + w: 300, // Width and Height here will override sizes in mediatype + h: 250, + pos: 1, + custom: { + // Custom parameters in form of key value pairs + user_min_age: 18, + }, + }, + }, + ], + }, + ]; ``` # Sample Video Ad Unit ``` -var adVideoAdUnits = [ -{ - code: 'test-div-video', - mediaTypes: { - video: { - playerSize: [640, 480], // required - context: 'instream' //required - } - }, - bids: [{ - bidder: 'deepintent', - params: { - tagId: '1300', // Required parameter // required - video: { - mimes: ['video/mp4','video/x-flv'], // required - skippable: true, // optional - minduration: 5, // optional - maxduration: 30, // optional - startdelay: 5, // optional - playbackmethod: [1,3], // optional - api: [ 1, 2 ], // optional - protocols: [ 2, 3 ], // optional - battr: [ 13, 14 ], // optional - linearity: 1, // optional - placement: 2, // optional - minbitrate: 10, // optional - maxbitrate: 10 // optional - } - } - }] -}] + var adVideoAdUnits = [ + { + code: 'test-div-video', + mediaTypes: { + video: { + playerSize: [640, 480], // required + context: 'instream', //required + }, + }, + bids: [ + { + bidder: 'deepintent', + params: { + tagId: '1300', // Required parameter // required + bidfloor: 1.5, // optional + video: { + mimes: ['video/mp4', 'video/x-flv'], // required + skippable: true, // optional + minduration: 5, // optional + maxduration: 30, // optional + startdelay: 5, // optional + playbackmethod: [1, 3], // optional + api: [1, 2], // optional + protocols: [2, 3], // optional + battr: [13, 14], // optional + linearity: 1, // optional + plcmt: 2, // optional + minbitrate: 10, // optional + maxbitrate: 10, // optional + }, + }, + }, + ], + }, + ]; ``` ###Recommended User Sync Configuration @@ -84,6 +90,4 @@ pbjs.setConfig({ enabledBidders: ['deepintent'], syncDelay: 3000 }}); - - ``` diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index 2d3eae980cd..a1f1e29a4ce 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -8,6 +8,7 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {isPlainObject} from '../src/utils.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -49,7 +50,13 @@ export const deepintentDpesSubmodule = { eids: { 'deepintentId': { source: 'deepintent.com', - atype: 3 + atype: 3, + getValue: (userIdData) => { + if (isPlainObject(userIdData) && userIdData?.id) { + return userIdData.id; + } + return userIdData; + } }, }, }; diff --git a/modules/deltaprojectsBidAdapter.js b/modules/deltaprojectsBidAdapter.js index 870378a13dd..2111643b344 100644 --- a/modules/deltaprojectsBidAdapter.js +++ b/modules/deltaprojectsBidAdapter.js @@ -1,5 +1,6 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; import { _each, _map, @@ -8,9 +9,9 @@ import { isFn, isNumber, logError, - logWarn + logWarn, + setOnAny } from '../src/utils.js'; -import {config} from '../src/config.js'; export const BIDDER_CODE = 'deltaprojects'; export const BIDDER_ENDPOINT_URL = 'https://d5p.de17a.com/dogfight/prebid'; @@ -73,7 +74,7 @@ function buildRequests(validBidRequests, bidderRequest) { // build bid specific return validBidRequests.map(validBidRequest => { - const openRTBRequest = buildOpenRTBRequest(validBidRequest, id, site, device, user, tmax, regs); + const openRTBRequest = buildOpenRTBRequest(validBidRequest, bidderRequest, id, site, device, user, tmax, regs); return { method: 'POST', url: BIDDER_ENDPOINT_URL, @@ -84,9 +85,9 @@ function buildRequests(validBidRequests, bidderRequest) { }); } -function buildOpenRTBRequest(validBidRequest, id, site, device, user, tmax, regs) { +function buildOpenRTBRequest(validBidRequest, bidderRequest, id, site, device, user, tmax, regs) { // build cur - const currency = config.getConfig('currency.adServerCurrency') || deepAccess(validBidRequest, 'params.currency'); + const currency = getCurrencyFromBidderRequest(bidderRequest) || deepAccess(validBidRequest, 'params.currency'); const cur = currency && [currency]; // build impression @@ -228,22 +229,12 @@ export function getBidFloor(bid, mediaType, size, currency) { if (isFn(bid.getFloor)) { const bidFloorCurrency = currency || 'USD'; const bidFloor = bid.getFloor({currency: bidFloorCurrency, mediaType: mediaType, size: size}); - if (isNumber(bidFloor.floor)) { + if (isNumber(bidFloor?.floor)) { return bidFloor; } } } -/** -- Helper methods -- */ -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; - } - } -} - /** -- Register -- */ export const spec = { code: BIDDER_CODE, diff --git a/modules/dexertoBidAdapter.js b/modules/dexertoBidAdapter.js new file mode 100644 index 00000000000..af06341e9e6 --- /dev/null +++ b/modules/dexertoBidAdapter.js @@ -0,0 +1,31 @@ +import { + BANNER +} from '../src/mediaTypes.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { + getBannerRequest, + getBannerResponse, +} from '../libraries/audUtils/bidderUtils.js'; + +const ENDPOINT_URL = 'https://rtb.dexerto.media/hb/dexerto'; +// Export const spec +export const spec = { + code: 'dexerto', + supportedMediaTypes: BANNER, + // Determines whether or not the given bid request is valid + isBidRequestValid: (bid) => { + return !!(bid.params.placement_id); + }, + // Make a server request from the list of BidRequests + buildRequests: (bidRequests, bidderRequest) => { + return getBannerRequest(bidRequests, bidderRequest, ENDPOINT_URL); + }, + // Unpack the response from the server into a list of bids. + interpretResponse: (bidResponse, bidRequest) => { + return getBannerResponse(bidResponse, BANNER); + } +} + +registerBidder(spec); diff --git a/modules/dexertoBidAdapter.md b/modules/dexertoBidAdapter.md new file mode 100644 index 00000000000..ad4d7bb42eb --- /dev/null +++ b/modules/dexertoBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: Dexerto Bidder Adapter +Module Type: Bidder Adapter +Maintainer: niels.claes@dexerto.com +``` + +# Description + +Dexerto currently supports the BANNER type ads through prebid js + +Module that connects to dexerto's demand sources. + +# Banner Test Request +``` + var adUnits = [ + { + code: 'display-ad', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + } + bids: [ + { + bidder: 'dexerto', + params: { + placement_id: 110003, // Required parameter + width: 300, // Optional parameter + height: 250, // Optional parameter + bid_floor: 0.1 // Optional parameter + } + } + ] + } + ]; +``` diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index abf58aceb45..c40ca46e593 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -2,29 +2,30 @@ * This module adds [DFP support]{@link https://www.doubleclickbygoogle.com/} for Video to Prebid. */ -import {registerVideoSupport} from '../src/adServerManager.js'; -import {targeting} from '../src/targeting.js'; +import { getSignals } from '../libraries/gptUtils/gptUtils.js'; +import { registerVideoSupport } from '../src/adServerManager.js'; +import { getPPID } from '../src/adserver.js'; +import { auctionManager } from '../src/auctionManager.js'; +import { config } from '../src/config.js'; +import { EVENTS } from '../src/constants.js'; +import * as events from '../src/events.js'; +import { getHook } from '../src/hook.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { targeting } from '../src/targeting.js'; import { - isNumber, buildUrl, - deepAccess, formatQS, isEmpty, + isNumber, logError, + logWarn, parseSizesInput, - parseUrl, - uniques + parseUrl } from '../src/utils.js'; -import {config} from '../src/config.js'; -import {getHook, submodule} from '../src/hook.js'; -import {auctionManager} from '../src/auctionManager.js'; -import {gdprDataHandler} from '../src/adapterManager.js'; -import * as events from '../src/events.js'; -import { EVENTS } from '../src/constants.js'; -import {getPPID} from '../src/adserver.js'; -import {getRefererInfo} from '../src/refererDetection.js'; -import {CLIENT_SECTIONS} from '../src/fpd/oneClient.js'; - +import {DEFAULT_DFP_PARAMS, DFP_ENDPOINT, gdprParams} from '../libraries/dfpUtils/dfpUtils.js'; +import { vastLocalCache } from '../src/videoCache.js'; +import { fetch } from '../src/ajax.js'; +import XMLUtil from '../libraries/xmlUtils/xmlUtils.js'; /** * @typedef {Object} DfpVideoParams * @@ -54,20 +55,12 @@ import {CLIENT_SECTIONS} from '../src/fpd/oneClient.js'; * @param {string} [url] video adserver url */ -/** Safe defaults which work on pretty much all video calls. */ -const defaultParamConstants = { - env: 'vp', - gdfp_req: 1, - output: 'vast', - unviewed_position_start: 1, -}; - -export const adpodUtils = {}; - export const dep = { ri: getRefererInfo } +export const VAST_TAG_URI_TAGNAME = 'VASTAdTagURI'; + /** * Merge all the bid data and publisher-supplied options into a single URL, and then return it. * @@ -102,7 +95,7 @@ export function buildDfpVideoUrl(options) { const derivedParams = { correlator: Date.now(), - sz: parseSizesInput(deepAccess(adUnit, 'mediaTypes.video.playerSize')).join('|'), + sz: parseSizesInput(adUnit?.mediaTypes?.video?.playerSize).join('|'), url: encodeURIComponent(location.href), }; @@ -115,21 +108,16 @@ export function buildDfpVideoUrl(options) { let encodedCustomParams = getCustParams(bid, options, urlSearchComponent && urlSearchComponent.cust_params); const queryParams = Object.assign({}, - defaultParamConstants, + DEFAULT_DFP_PARAMS, urlComponents.search, derivedParams, options.params, - { cust_params: encodedCustomParams } + { cust_params: encodedCustomParams }, + gdprParams() ); const descriptionUrl = getDescriptionUrl(bid, options, 'params'); if (descriptionUrl) { queryParams.description_url = descriptionUrl; } - const gdprConsent = gdprDataHandler.getConsentData(); - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } - if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; } - if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } - } if (!queryParams.ppid) { const ppid = getPPID(); @@ -151,7 +139,7 @@ export function buildDfpVideoUrl(options) { return 'preroll'; } }, - vconp: () => Array.isArray(video?.playbackmethod) && video.playbackmethod.every(m => m === 7) ? '2' : undefined, + vconp: () => Array.isArray(video?.playbackmethod) && video.playbackmethod.some(m => m === 7) ? '2' : undefined, vpa() { // playbackmethod = 3 is play on click; 1, 2, 4, 5, 6 are autoplay if (Array.isArray(video?.playbackmethod)) { @@ -181,20 +169,7 @@ export function buildDfpVideoUrl(options) { const fpd = auctionManager.index.getBidRequest(options.bid || {})?.ortb2 ?? auctionManager.index.getAuction(options.bid || {})?.getFPD()?.global; - function getSegments(sections, segtax) { - return sections - .flatMap(section => deepAccess(fpd, section) || []) - .filter(datum => datum.ext?.segtax === segtax) - .flatMap(datum => datum.segment?.map(seg => seg.id)) - .filter(ob => ob) - .filter(uniques) - } - - const signals = Object.entries({ - IAB_AUDIENCE_1_1: getSegments(['user.data'], 4), - IAB_CONTENT_2_2: getSegments(CLIENT_SECTIONS.map(section => `${section}.content.data`), 6) - }).map(([taxonomy, values]) => values.length ? {taxonomy, values} : null) - .filter(ob => ob); + const signals = getSignals(fpd); if (signals.length) { queryParams.ppsj = btoa(JSON.stringify({ @@ -202,11 +177,7 @@ export function buildDfpVideoUrl(options) { })) } - return buildUrl(Object.assign({ - protocol: 'https', - host: 'securepubads.g.doubleclick.net', - pathname: '/gampad/ads' - }, urlComponents, { search: queryParams })); + return buildUrl(Object.assign({}, DFP_ENDPOINT, urlComponents, { search: queryParams })); } export function notifyTranslationModule(fn) { @@ -215,100 +186,11 @@ export function notifyTranslationModule(fn) { if (config.getConfig('brandCategoryTranslation.translationFile')) { getHook('registerAdserver').before(notifyTranslationModule); } -/** - * @typedef {Object} DfpAdpodOptions - * - * @param {string} code Ad Unit code - * @param {Object} params Query params which should be set on the DFP request. - * These will override this module's defaults whenever they conflict. - * @param {function} callback Callback function to execute when master tag is ready - */ - -/** - * Creates master tag url for long-form - * @param {DfpAdpodOptions} options - * @returns {string} A URL which calls DFP with custom adpod targeting key values to compete with rest of the demand in DFP - */ -export function buildAdpodVideoUrl({code, params, callback} = {}) { - // TODO: the public API for this does not take in enough info to fill all DFP params (adUnit/bid), - // and is marked "alpha": https://docs.prebid.org/dev-docs/publisher-api-reference/adServers.dfp.buildAdpodVideoUrl.html - if (!params || !callback) { - logError(`A params object and a callback is required to use pbjs.adServers.dfp.buildAdpodVideoUrl`); - return; - } - - const derivedParams = { - correlator: Date.now(), - sz: getSizeForAdUnit(code), - url: encodeURIComponent(location.href), - }; - - function getSizeForAdUnit(code) { - let adUnit = auctionManager.getAdUnits() - .filter((adUnit) => adUnit.code === code) - let sizes = deepAccess(adUnit[0], 'mediaTypes.video.playerSize'); - return parseSizesInput(sizes).join('|'); - } - - adpodUtils.getTargeting({ - 'codes': [code], - 'callback': createMasterTag - }); - - function createMasterTag(err, targeting) { - if (err) { - callback(err, null); - return; - } - - let initialValue = { - [adpodUtils.TARGETING_KEY_PB_CAT_DUR]: undefined, - [adpodUtils.TARGETING_KEY_CACHE_ID]: undefined - }; - let customParams = {}; - if (targeting[code]) { - customParams = targeting[code].reduce((acc, curValue) => { - if (Object.keys(curValue)[0] === adpodUtils.TARGETING_KEY_PB_CAT_DUR) { - acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] = (typeof acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] !== 'undefined') ? acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] + ',' + curValue[adpodUtils.TARGETING_KEY_PB_CAT_DUR] : curValue[adpodUtils.TARGETING_KEY_PB_CAT_DUR]; - } else if (Object.keys(curValue)[0] === adpodUtils.TARGETING_KEY_CACHE_ID) { - acc[adpodUtils.TARGETING_KEY_CACHE_ID] = curValue[adpodUtils.TARGETING_KEY_CACHE_ID] - } - return acc; - }, initialValue); - } - - let encodedCustomParams = encodeURIComponent(formatQS(customParams)); - - const queryParams = Object.assign({}, - defaultParamConstants, - derivedParams, - params, - { cust_params: encodedCustomParams } - ); - - const gdprConsent = gdprDataHandler.getConsentData(); - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } - if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; } - if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } - } - - const masterTag = buildUrl({ - protocol: 'https', - host: 'securepubads.g.doubleclick.net', - pathname: '/gampad/ads', - search: queryParams - }); - - callback(null, masterTag); - } -} - /** * Builds a video url from a base dfp video url and a winning bid, appending * Prebid-specific key-values. * @param {Object} components base video adserver url parsed into components object - * @param {AdapterBidResponse} bid winning bid object to append parameters from + * @param {Object} bid winning bid object to append parameters from * @param {Object} options Options which should be used to construct the URL (used for custom params). * @return {string} video url */ @@ -325,18 +207,18 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { /** * Returns the encoded vast url if it exists on a bid object, only if prebid-cache * is disabled, and description_url is not already set on a given input - * @param {AdapterBidResponse} bid object to check for vast url + * @param {Object} bid object to check for vast url * @param {Object} components the object to check that description_url is NOT set on * @param {string} prop the property of components that would contain description_url * @return {string | undefined} The encoded vast url if it exists, or undefined */ function getDescriptionUrl(bid, components, prop) { - return deepAccess(components, `${prop}.description_url`) || encodeURIComponent(dep.ri().page); + return components?.[prop]?.description_url || encodeURIComponent(dep.ri().page); } /** * Returns the encoded `cust_params` from the bid.adserverTargeting and adds the `hb_uuid`, and `hb_cache_id`. Optionally the options.params.cust_params - * @param {AdapterBidResponse} bid + * @param {Object} bid * @param {Object} options this is the options passed in from the `buildDfpVideoUrl` function * @return {Object} Encoded key value pairs for cust_params */ @@ -363,7 +245,7 @@ function getCustParams(bid, options, urlCustParams) { events.emit(EVENTS.SET_TARGETING, {[adUnit.code]: prebidTargetingSet}); // merge the prebid + publisher targeting sets - const publisherTargetingSet = deepAccess(options, 'params.cust_params'); + const publisherTargetingSet = options?.params?.cust_params; const targetingSet = Object.assign({}, prebidTargetingSet, publisherTargetingSet); let encodedParams = encodeURIComponent(formatQS(targetingSet)); if (urlCustParams) { @@ -373,10 +255,72 @@ function getCustParams(bid, options, urlCustParams) { return encodedParams; } +async function getVastForLocallyCachedBids(gamVastWrapper, localCacheMap) { + try { + const xmlUtil = XMLUtil(); + const xmlDoc = xmlUtil.parse(gamVastWrapper); + const vastAdTagUriElement = xmlDoc.querySelectorAll(VAST_TAG_URI_TAGNAME)[0]; + + if (!vastAdTagUriElement || !vastAdTagUriElement.textContent) { + return gamVastWrapper; + } + + const uuidExp = new RegExp(`[A-Fa-f0-9]{8}-(?:[A-Fa-f0-9]{4}-){3}[A-Fa-f0-9]{12}`, 'gi'); + const matchResult = Array.from(vastAdTagUriElement.textContent.matchAll(uuidExp)); + const uuidCandidates = matchResult + .map(([uuid]) => uuid) + .filter(uuid => localCacheMap.has(uuid)); + + if (uuidCandidates.length != 1) { + logWarn(`Unable to determine unique uuid in ${VAST_TAG_URI_TAGNAME}`); + return gamVastWrapper; + } + const uuid = uuidCandidates[0]; + + const blobUrl = localCacheMap.get(uuid); + const base64BlobContent = await getBase64BlobContent(blobUrl); + const cdata = xmlDoc.createCDATASection(base64BlobContent); + vastAdTagUriElement.textContent = ''; + vastAdTagUriElement.appendChild(cdata); + return xmlUtil.serialize(xmlDoc); + } catch (error) { + logWarn('Unable to process xml', error); + return gamVastWrapper; + } +}; + +export async function getVastXml(options, localCacheMap = vastLocalCache) { + const vastUrl = buildDfpVideoUrl(options); + const response = await fetch(vastUrl); + if (!response.ok) { + throw new Error('Unable to fetch GAM VAST wrapper'); + } + + const gamVastWrapper = await response.text(); + + if (config.getConfig('cache.useLocal')) { + const vastXml = await getVastForLocallyCachedBids(gamVastWrapper, localCacheMap); + return vastXml; + } + + return gamVastWrapper; +} + +export async function getBase64BlobContent(blobUrl) { + const response = await fetch(blobUrl); + if (!response.ok) { + logError('Unable to fetch blob'); + throw new Error('Blob not found'); + } + // Mechanism to handle cases where VAST tags are fetched + // from a context where the blob resource is not accessible. + // like IMA SDK iframe + const blobContent = await response.text(); + const dataUrl = `data://text/xml;base64,${btoa(blobContent)}`; + return dataUrl; +} + registerVideoSupport('dfp', { buildVideoUrl: buildDfpVideoUrl, - buildAdpodVideoUrl: buildAdpodVideoUrl, - getAdpodTargeting: (args) => adpodUtils.getTargeting(args) + getVastXml }); - -submodule('adpod', adpodUtils); diff --git a/modules/dfpAdpod.js b/modules/dfpAdpod.js new file mode 100644 index 00000000000..d443e770d87 --- /dev/null +++ b/modules/dfpAdpod.js @@ -0,0 +1,95 @@ +import {submodule} from '../src/hook.js'; +import {buildUrl, deepAccess, formatQS, logError, parseSizesInput} from '../src/utils.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {DEFAULT_DFP_PARAMS, DFP_ENDPOINT, gdprParams} from '../libraries/dfpUtils/dfpUtils.js'; +import {registerVideoSupport} from '../src/adServerManager.js'; + +export const adpodUtils = {}; + +/** + * @typedef {Object} DfpAdpodOptions + * + * @param {string} code Ad Unit code + * @param {Object} params Query params which should be set on the DFP request. + * These will override this module's defaults whenever they conflict. + * @param {function} callback Callback function to execute when master tag is ready + */ + +/** + * Creates master tag url for long-form + * @param {DfpAdpodOptions} options + * @returns {string} A URL which calls DFP with custom adpod targeting key values to compete with rest of the demand in DFP + */ +export function buildAdpodVideoUrl({code, params, callback} = {}) { + // TODO: the public API for this does not take in enough info to fill all DFP params (adUnit/bid), + // and is marked "alpha": https://docs.prebid.org/dev-docs/publisher-api-reference/adServers.dfp.buildAdpodVideoUrl.html + if (!params || !callback) { + logError(`A params object and a callback is required to use pbjs.adServers.dfp.buildAdpodVideoUrl`); + return; + } + + const derivedParams = { + correlator: Date.now(), + sz: getSizeForAdUnit(code), + url: encodeURIComponent(location.href), + }; + + function getSizeForAdUnit(code) { + let adUnit = auctionManager.getAdUnits() + .filter((adUnit) => adUnit.code === code) + let sizes = deepAccess(adUnit[0], 'mediaTypes.video.playerSize'); + return parseSizesInput(sizes).join('|'); + } + + adpodUtils.getTargeting({ + 'codes': [code], + 'callback': createMasterTag + }); + + function createMasterTag(err, targeting) { + if (err) { + callback(err, null); + return; + } + + let initialValue = { + [adpodUtils.TARGETING_KEY_PB_CAT_DUR]: undefined, + [adpodUtils.TARGETING_KEY_CACHE_ID]: undefined + }; + let customParams = {}; + if (targeting[code]) { + customParams = targeting[code].reduce((acc, curValue) => { + if (Object.keys(curValue)[0] === adpodUtils.TARGETING_KEY_PB_CAT_DUR) { + acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] = (typeof acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] !== 'undefined') ? acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] + ',' + curValue[adpodUtils.TARGETING_KEY_PB_CAT_DUR] : curValue[adpodUtils.TARGETING_KEY_PB_CAT_DUR]; + } else if (Object.keys(curValue)[0] === adpodUtils.TARGETING_KEY_CACHE_ID) { + acc[adpodUtils.TARGETING_KEY_CACHE_ID] = curValue[adpodUtils.TARGETING_KEY_CACHE_ID] + } + return acc; + }, initialValue); + } + + let encodedCustomParams = encodeURIComponent(formatQS(customParams)); + + const queryParams = Object.assign({}, + DEFAULT_DFP_PARAMS, + derivedParams, + params, + { cust_params: encodedCustomParams }, + gdprParams(), + ); + + const masterTag = buildUrl({ + ...DFP_ENDPOINT, + search: queryParams + }); + + callback(null, masterTag); + } +} + +registerVideoSupport('dfp', { + buildAdpodVideoUrl: buildAdpodVideoUrl, + getAdpodTargeting: (args) => adpodUtils.getTargeting(args) +}); + +submodule('adpod', adpodUtils); diff --git a/modules/dgkeywordRtdProvider.js b/modules/dgkeywordRtdProvider.js index 14519ae2713..92f63703f42 100644 --- a/modules/dgkeywordRtdProvider.js +++ b/modules/dgkeywordRtdProvider.js @@ -101,6 +101,8 @@ export function getProfileApiUrl(customeUrl, enableReadFpid) { export function readFpidFromLocalStrage() { try { + // TODO: use storageManager + // eslint-disable-next-line no-restricted-properties const fpid = window.localStorage.getItem('ope_fpid'); if (fpid) { return fpid; diff --git a/modules/dianomiBidAdapter.js b/modules/dianomiBidAdapter.js index d4b2a4a5da5..5e43bf955ef 100644 --- a/modules/dianomiBidAdapter.js +++ b/modules/dianomiBidAdapter.js @@ -10,10 +10,14 @@ import { parseSizesInput, deepSetValue, formatQS, + setOnAny, + getWinDimensions } from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import {getUserSyncParams} from '../libraries/userSyncUtils/userSyncUtils.js'; const { getConfig } = config; @@ -98,8 +102,9 @@ export const spec = { } const device = getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; + const { innerWidth, innerHeight } = getWinDimensions(); + device.w = device.w || innerWidth; + device.h = device.h || innerHeight; device.ua = device.ua || navigator.userAgent; const paramsEndpoint = setOnAny(validBidRequests, 'params.endpoint'); @@ -113,7 +118,7 @@ export const spec = { setOnAny(validBidRequests, 'params.priceType') || 'net'; const tid = bidderRequest.ortb2?.source?.tid; - const currency = getConfig('currency.adServerCurrency'); + const currency = getCurrencyFromBidderRequest(bidderRequest); const cur = currency && [currency]; const eids = setOnAny(validBidRequests, 'userIdAsEids'); const schain = setOnAny(validBidRequests, 'schain'); @@ -126,8 +131,8 @@ export const spec = { currency: currency || 'USD', }) : {}; - const bidfloor = floorInfo.floor; - const bidfloorcur = floorInfo.currency; + const bidfloor = floorInfo?.floor; + const bidfloorcur = floorInfo?.currency; const { smartadId } = bid.params; const imp = { @@ -301,19 +306,8 @@ export const spec = { .filter(Boolean); }, getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { - const params = {}; - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - params['gdpr'] = Number(gdprConsent.gdprApplies); - } - if (typeof gdprConsent.consentString === 'string') { - params['gdpr_consent'] = gdprConsent.consentString; - } - } + const params = getUserSyncParams(gdprConsent, uspConsent); - if (uspConsent) { - params['us_privacy'] = encodeURIComponent(uspConsent); - } if (syncOptions.iframeEnabled) { // data is only assigned if params are available to pass to syncEndpoint return { @@ -355,15 +349,6 @@ function parseNative(bid) { return result; } -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; - } - } -} - function flatten(arr) { return [].concat(...arr); } diff --git a/modules/digitalMatterBidAdapter.js b/modules/digitalMatterBidAdapter.js new file mode 100644 index 00000000000..34cf84eb9d3 --- /dev/null +++ b/modules/digitalMatterBidAdapter.js @@ -0,0 +1,212 @@ +import {deepAccess, deepSetValue, getDNT, getWinDimensions, inIframe, logWarn, parseSizesInput} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; + +const BIDDER_CODE = 'digitalMatter'; +const GVLID = 1345; +const ENDPOINT_URL = 'https://adx.digitalmatter.services/' + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + aliases: ['dichange', 'digitalmatter'], + bidParameters: ['accountId', 'siteId'], + isBidRequestValid: function (bid) { + if (typeof bid.params !== 'object') { + return false; + } + if (!hasBannerMediaType(bid)) { + logWarn('Invalid bid request: missing required mediaType - banner'); + return false; + } + + return !!(bid.params.accountId && bid.params.siteId) + }, + buildRequests: function (validBidRequests, bidderRequest) { + const common = bidderRequest.ortb2 || {}; + const site = common.site; + const tid = common?.source?.tid; + const {user} = common || {}; + + if (!site.page) { + site.page = bidderRequest.refererInfo.page; + } + + const device = getDevice(common.device); + const schain = getByKey(validBidRequests, 'schain'); + const eids = getByKey(validBidRequests, 'userIdAsEids'); + const currency = config.getConfig('currency') + const cur = currency && [currency]; + + const imp = validBidRequests.map((bid, id) => { + const {accountId, siteId} = bid.params; + const bannerParams = deepAccess(bid, 'mediaTypes.banner'); + const position = deepAccess(bid, 'mediaTypes.banner.pos') ?? 0; + + return { + id: bid.adUnitCode, + bidId: bid.bidId, + accountId: accountId, + adUnitCode: bid.adUnitCode, + siteId: siteId, + banner: { + pos: position, + topframe: inIframe() ? 0 : 1, + format: bannerParams.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1] + })) + }, + sizes: parseSizesInput(bannerParams.sizes), + }; + }); + + const ext = { + prebid: { + targeting: { + includewinners: true, + includebidderkeys: false + } + } + }; + + const payload = { + id: bidderRequest.bidderRequestId, + tid, + site, + device, + user, + cur, + imp, + test: config.getConfig('debug') ? 1 : 0, + tmax: bidderRequest.timeout, + start: bidderRequest.auctionStart, + ext + }; + + if (schain) { + deepSetValue(payload, 'source.ext.schain', schain); + } + + if (eids) { + deepSetValue(payload, 'user.ext.eids', eids); + } + + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') !== undefined) { + deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(payload, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); + } + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL + 'openrtb2/auction', + data: payloadString, + }; + }, + interpretResponse: function (serverResponse) { + const body = serverResponse.body || serverResponse; + const {cur} = body; + const bids = []; + + if (body && body.bids && Array.isArray(body.bids)) { + body.bids.forEach(bidItem => { + const bid = { + requestId: bidItem.bidid, + adomain: bidItem.adomain, + cpm: bidItem.cpm, + currency: cur, + netRevenue: true, + ttl: bidItem.ttl || 300, + creativeId: bidItem.creativeid, + width: bidItem.width, + height: bidItem.height, + dealId: bidItem.dealid, + ad: bidItem.ad, + meta: bidItem.meta, + }; + + bids.push(bid); + }); + } + + return bids + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + if (usersSynced) { + return []; + } + + const userSyncs = []; + + function checkGppStatus(gppConsent) { + if (gppConsent && Array.isArray(gppConsent.applicableSections)) { + return gppConsent.applicableSections.every(sec => typeof sec === 'number' && sec <= 5); + } + return true; + } + + if (hasPurpose1Consent(gdprConsent) && checkGppStatus(gppConsent)) { + responses.forEach(response => { + if (response.body.ext && response.body.ext.usersync) { + try { + const userSync = response.body.ext.usersync; + + userSync.forEach((element) => { + let url = element.url; + let type = element.type; + + if (url) { + if ((type === 'image' || type === 'redirect') && syncOptions.pixelEnabled) { + userSyncs.push({type: 'image', url: url}); + } else if (type === 'iframe' && syncOptions.iframeEnabled) { + userSyncs.push({type: 'iframe', url: url}); + } + } + }) + } catch (e) { + // + } + } + }); + } + + return userSyncs; + } +} + +let usersSynced = false; + +function hasBannerMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.banner'); +} + +function getDevice(data) { + let dnt = data.dnt; + if (!dnt) { + dnt = getDNT() ? 1 : 0; + } + const { innerWidth, innerHeight } = getWinDimensions(); + + return { + w: data.w || innerWidth, + h: data.h || innerHeight, + ua: data.ua || navigator.userAgent, + dnt: dnt, + language: data.language || navigator.language, + } +} + +function getByKey(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = deepAccess(collection[i], key); + if (result) { + return result; + } + } +} + +registerBidder(spec); diff --git a/modules/digitalMatterBidAdapter.md b/modules/digitalMatterBidAdapter.md new file mode 100644 index 00000000000..19c0ecd631a --- /dev/null +++ b/modules/digitalMatterBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: Digital Matter Bid Adapter +Module Type: Digital Matter Bid Adapter +Maintainer: prebid@digitalmatter.ai +``` + +# Description + +Module that connects to Digital Matter demand sources + +# Banner Test Parameters + +```js +var adUnits = [ + { + code: "test-banner", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + bids: [ + { + bidder: "digitalMatter", + params: { + accountId: "1_demo_1", // string, required + siteId: "1-demo-1" // string, required + } + } + ] + } +]; +``` diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 7493dcb9af4..3d4452452ae 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -2,6 +2,12 @@ import * as utils from '../src/utils.js'; import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink, getReferrer } from '../libraries/fpdUtils/pageInfo.js'; +import { getDevice, getScreenSize } from '../libraries/fpdUtils/deviceInfo.js'; +import { getBidFloor } from '../libraries/currencyUtils/floor.js'; +import { transformSizes, normalAdSize } from '../libraries/sizeUtils/tranformSize.js'; +import { getHLen } from '../libraries/navigatorData/navigatorData.js'; +import { cookieSync } from '../libraries/cookieSync/cookieSync.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -21,6 +27,9 @@ const MEDIATYPE = [BANNER, NATIVE]; const COOKIE_KEY_SSPPID = '_ss_pp_id'; export const COOKIE_KEY_MGUID = '__mguid_'; const COOKIE_KEY_PMGUID = '__pmguid_'; +const COOKIE_KEY_PBUID = 'pub_pp_tag'; +const STORAGE_KEY_FTUID = 'fluct_ppUUIDv4'; +const STORAGE_KEY_IMUID = '__im_ppid'; const COOKIE_RETENTION_TIME = 365 * 24 * 60 * 60 * 1000; // 1 year const COOKY_SYNC_IFRAME_URL = 'https://asset.popin.cc/js/cookieSync.html'; export const THIRD_PARTY_COOKIE_ORIGIN = 'https://asset.popin.cc'; @@ -67,64 +76,6 @@ const NATIVERET = { ext: {}, }; -/** - * get page title111 - * @returns {string} - */ - -export function getPageTitle(win = window) { - try { - const ogTitle = win.top.document.querySelector('meta[property="og:title"]') - return win.top.document.title || (ogTitle && ogTitle.content) || ''; - } catch (e) { - const ogTitle = document.querySelector('meta[property="og:title"]') - return document.title || (ogTitle && ogTitle.content) || ''; - } -} - -/** - * get page description - * @returns {string} - */ -export function getPageDescription(win = window) { - let element; - - try { - element = win.top.document.querySelector('meta[name="description"]') || - win.top.document.querySelector('meta[property="og:description"]') - } catch (e) { - element = document.querySelector('meta[name="description"]') || - document.querySelector('meta[property="og:description"]') - } - - return (element && element.content) || ''; -} - -/** - * get page keywords - * @returns {string} - */ -export function getPageKeywords(win = window) { - let element; - - try { - element = win.top.document.querySelector('meta[name="keywords"]'); - } catch (e) { - element = document.querySelector('meta[name="keywords"]'); - } - - return (element && element.content) || ''; -} - -/** - * get connection downlink - * @returns {number} - */ -export function getConnectionDownLink(win = window) { - const nav = win.navigator || {}; - return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined; -} - /** * get pmg uid * 获取并生成用户的id @@ -139,7 +90,7 @@ export const getPmgUID = () => { } // Extend the expiration time of pmguid try { - storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, getCurrentTimeToUTCString()); + storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, getCookieTimeToUTCString()); } catch (e) {} return pmgUid; }; @@ -165,150 +116,14 @@ function getKv(obj, ...keys) { return o; } -/** - * get device - * @return {boolean} - */ -function getDevice() { - let check = false; - (function (a) { - let reg1 = new RegExp( - [ - '(android|bbd+|meego)', - '.+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', - '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', - '|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap', - '|windows ce|xda|xiino|android|ipad|playbook|silk', - ].join(''), - 'i' - ); - let reg2 = new RegExp( - [ - '1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)', - '|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )', - '|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55/|capi|ccwa|cdm-|cell', - '|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)', - '|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene', - '|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c', - '|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|/)|ibro|idea|ig01|ikom', - '|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |/)|klon|kpt |kwc-|kyo(c|k)', - '|le(no|xi)|lg( g|/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50/|ma(te|ui|xo)|mc(01|21|ca)', - '|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]', - '|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)', - '|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio', - '|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55/|sa(ge|ma|mm|ms', - '|ny|va)|sc(01|h-|oo|p-)|sdk/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al', - '|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)', - '|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|', - 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)', - '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-', - '|your|zeto|zte-', - ].join(''), - 'i' - ); - if (reg1.test(a) || reg2.test(a.substr(0, 4))) { - check = true; - } - })(navigator.userAgent || navigator.vendor || window.opera); - return check; -} - -/** - * get BidFloor - * @param {*} bid - * @param {*} mediaType - * @param {*} sizes - * @returns - */ -function getBidFloor(bid) { - if (!utils.isFn(bid.getFloor)) { - return utils.deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0; - } -} - -/** - * get sizes for rtb - * @param {Array|Object} requestSizes - * @return {Object} - */ -function transformSizes(requestSizes) { - let sizes = []; - let sizeObj = {}; - - if ( - utils.isArray(requestSizes) && - requestSizes.length === 2 && - !utils.isArray(requestSizes[0]) - ) { - sizeObj.width = parseInt(requestSizes[0], 10); - sizeObj.height = parseInt(requestSizes[1], 10); - sizes.push(sizeObj); - } else if (typeof requestSizes === 'object') { - for (let i = 0; i < requestSizes.length; i++) { - let size = requestSizes[i]; - sizeObj = {}; - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - sizes.push(sizeObj); - } - } - - return sizes; -} - // Support sizes -const popInAdSize = [ - { w: 300, h: 250 }, - { w: 300, h: 600 }, - { w: 728, h: 90 }, - { w: 970, h: 250 }, - { w: 320, h: 50 }, - { w: 160, h: 600 }, - { w: 320, h: 180 }, - { w: 320, h: 100 }, - { w: 336, h: 280 }, -]; - -/** - * get screen size - * - * @returns {Array} eg: "['widthxheight']" - */ -function getScreenSize() { - return utils.parseSizesInput([window.screen.width, window.screen.height]); -} +const popInAdSize = normalAdSize; /** - * @param {BidRequest} bidRequest - * @param bidderRequest - * @returns {string} - */ -function getReferrer(bidRequest = {}, bidderRequest = {}) { - let pageUrl; - if (bidRequest.params && bidRequest.params.referrer) { - pageUrl = bidRequest.params.referrer; - } else { - pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); - } - return pageUrl; -} - -/** - * get current time to UTC string + * get cookie time to UTC string * @returns utc string */ -export function getCurrentTimeToUTCString() { +export function getCookieTimeToUTCString() { const date = new Date(); date.setTime(date.getTime() + COOKIE_RETENTION_TIME); return date.toUTCString(); @@ -358,7 +173,7 @@ function getItems(validBidRequests, bidderRequest) { let items = []; items = validBidRequests.map((req, i) => { let ret = {}; - // eslint-disable-next-line no-debugger + let mediaTypes = getKv(req, 'mediaTypes'); const bidFloor = getBidFloor(req); @@ -381,9 +196,8 @@ function getItems(validBidRequests, bidderRequest) { } } if (!matchSize) { - matchSize = sizes[0] - ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } - : { h: 0, w: 0 }; + const { height = 0, width = 0 } = sizes[0] || {}; + matchSize = { h: height, w: width }; } ret = { id: id, @@ -414,7 +228,7 @@ function getItems(validBidRequests, bidderRequest) { export const buildUTMTagData = (url) => { if (!storage.cookiesAreEnabled()) return; - const urlParams = utils.parseUrl(url).search; + const urlParams = utils.parseUrl(url).search || {}; const UTMParams = {}; Object.keys(urlParams).forEach(key => { if (/^utm_/.test(key)) { @@ -423,7 +237,7 @@ export const buildUTMTagData = (url) => { }); UTMValue = JSON.parse(storage.getCookie(UTM_KEY) || '{}'); Object.assign(UTMValue, UTMParams); - storage.setCookie(UTM_KEY, JSON.stringify(UTMValue), getCurrentTimeToUTCString()); + storage.setCookie(UTM_KEY, JSON.stringify(UTMValue), getCookieTimeToUTCString()); } /** @@ -434,10 +248,10 @@ export const buildUTMTagData = (url) => { * @return {Object} */ function getParam(validBidRequests, bidderRequest) { - const pubcid = utils.deepAccess(validBidRequests[0], 'crumbs.pubcid'); const sharedid = utils.deepAccess(validBidRequests[0], 'userId.sharedid.id') || - utils.deepAccess(validBidRequests[0], 'userId.pubcid'); + utils.deepAccess(validBidRequests[0], 'userId.pubcid') || + utils.deepAccess(validBidRequests[0], 'crumbs.pubcid'); const eids = validBidRequests[0].userIdAsEids || validBidRequests[0].userId; let isMobile = getDevice() ? 1 : 0; @@ -455,11 +269,32 @@ function getParam(validBidRequests, bidderRequest) { const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); const firstPartyData = bidderRequest.ortb2; const tpData = utils.deepAccess(bidderRequest, 'ortb2.user.data') || undefined; - const topWindow = window.top; const title = getPageTitle(); const desc = getPageDescription(); const keywords = getPageKeywords(); - + let ext = {}; + try { + ext = { + eids, + firstPartyData, + ssppid: storage.getCookie(COOKIE_KEY_SSPPID) || undefined, + pmguid: getPmgUID(), + ssftUid: storage.getDataFromLocalStorage(STORAGE_KEY_FTUID) || undefined, + ssimUid: storage.getDataFromLocalStorage(STORAGE_KEY_IMUID) || undefined, + sspbid: storage.getCookie(COOKIE_KEY_PBUID) || undefined, + tpData, + utm: storage.getCookie(UTM_KEY), + page: { + title: title ? title.slice(0, 150) : undefined, + desc: desc ? desc.slice(0, 300) : undefined, + keywords: keywords ? keywords.slice(0, 100) : undefined, + hLen: getHLen(), + }, + device: { + nbw: getConnectionDownLink(), + } + } + } catch (error) {} try { buildUTMTagData(page); } catch (error) { } @@ -480,27 +315,10 @@ function getParam(validBidRequests, bidderRequest) { ua: navigator.userAgent, language: /en/.test(navigator.language) ? 'en' : navigator.language, }, - ext: { - eids, - firstPartyData, - ssppid: storage.getCookie(COOKIE_KEY_SSPPID) || undefined, - pmguid: getPmgUID(), - tpData, - page: { - title: title ? title.slice(0, 100) : undefined, - desc: desc ? desc.slice(0, 300) : undefined, - keywords: keywords ? keywords.slice(0, 100) : undefined, - hLen: topWindow.history?.length || undefined, - }, - device: { - nbw: getConnectionDownLink(), - hc: topWindow.navigator?.hardwareConcurrency || undefined, - dm: topWindow.navigator?.deviceMemory || undefined, - } - }, + ext, user: { buyeruid: storage.getCookie(COOKIE_KEY_MGUID) || undefined, - id: sharedid || pubcid, + id: sharedid, }, tmax: timeout, site: { @@ -561,14 +379,15 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - if (!globals['token']) return; + const pbToken = globals['token']; + if (!pbToken) return; let payload = getParam(validBidRequests, bidderRequest); - const payloadString = JSON.stringify(payload); + return { method: 'POST', - url: ENDPOINT_URL + globals['token'], + url: `${ENDPOINT_URL}${pbToken}`, data: payloadString, }; }, @@ -666,42 +485,7 @@ export const spec = { }, getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) { - const origin = encodeURIComponent(location.origin || `https://${location.host}`); - let syncParamUrl = `dm=${origin}`; - - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - if (syncOptions.iframeEnabled) { - window.addEventListener('message', function handler(event) { - if (!event.data || event.origin != THIRD_PARTY_COOKIE_ORIGIN) { - return; - } - - this.removeEventListener('message', handler); - - event.stopImmediatePropagation(); - - const response = event.data; - if (!response.optout && response.mguid) { - storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); - } - }, true); - return [ - { - type: 'iframe', - url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` - } - ]; - } + return cookieSync(syncOptions, gdprConsent, uspConsent, BIDDER_CODE, THIRD_PARTY_COOKIE_ORIGIN, COOKY_SYNC_IFRAME_URL, getCookieTimeToUTCString()); }, /** diff --git a/modules/djaxBidAdapter.js b/modules/djaxBidAdapter.js new file mode 100644 index 00000000000..7a9e359f520 --- /dev/null +++ b/modules/djaxBidAdapter.js @@ -0,0 +1,113 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { ajax } from '../src/ajax.js'; +import {Renderer} from '../src/Renderer.js'; + +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; +const BIDDER_CODE = 'djax'; +const DOMAIN = 'https://revphpe.djaxbidder.com/header_bidding_vast/'; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +function outstreamRender(bidAd) { + bidAd.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bidAd.width, bidAd.height], + width: bidAd.width, + height: bidAd.height, + targetId: bidAd.adUnitCode, + adResponse: bidAd.adResponse, + rendererOptions: { + showVolume: false, + allowFullscreen: false + } + }); + }); +} + +function createRenderer(bidAd, rendererParams, adUnitCode) { + const renderer = Renderer.install({ + id: rendererParams.id, + url: rendererParams.url, + loaded: false, + config: {'player_height': bidAd.height, 'player_width': bidAd.width}, + adUnitCode + }); + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + return renderer; +} + +function sendResponseToServer(data) { + ajax(DOMAIN + 'www/admin/plugins/Prebid/tracking/track.php', null, JSON.stringify(data), { + withCredentials: false, + method: 'POST', + crossOrigin: true + }); +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_AD_TYPES, + + isBidRequestValid: function(bid) { + return (typeof bid.params !== 'undefined' && parseInt(utils.getValue(bid.params, 'publisherId')) > 0); + }, + + buildRequests: function(validBidRequests) { + return { + method: 'POST', + url: DOMAIN + 'www/admin/plugins/Prebid/getAd.php', + options: { + withCredentials: false, + crossOrigin: true + }, + data: validBidRequests, + }; + }, + + interpretResponse: function(serverResponse, request) { + const response = serverResponse.body; + const bidResponses = []; + var bidRequestResponses = []; + + utils._each(response, function(bidAd) { + bidAd.adResponse = { + content: bidAd.vastXml, + height: bidAd.height, + width: bidAd.width + }; + + bidAd.renderer = bidAd.context === 'outstream' ? createRenderer(bidAd, { + id: bidAd.adUnitCode, + url: RENDERER_URL + }, bidAd.adUnitCode) : undefined; + bidResponses.push(bidAd); + }); + + bidRequestResponses.push({ + function: 'saveResponses', + request: request, + response: bidResponses + }); + sendResponseToServer(bidRequestResponses); + return bidResponses; + }, + + onBidWon: function(bid) { + let wonBids = []; + wonBids.push(bid); + wonBids[0].function = 'onBidWon'; + sendResponseToServer(wonBids); + }, + + onTimeout: function(details) { + details.unshift({ 'function': 'onTimeout' }); + sendResponseToServer(details); + } +}; + +registerBidder(spec); diff --git a/modules/djaxBidAdapter.md b/modules/djaxBidAdapter.md new file mode 100644 index 00000000000..d36a92de458 --- /dev/null +++ b/modules/djaxBidAdapter.md @@ -0,0 +1,51 @@ +# Overview + +``` +Module Name: Djax Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@djaxtech.com +``` + +# Description + +Module that connects to Djax + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "djax", + params: { + publisherId: '2' // string - required + } + } + ] + } + ]; +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + playerSize: [[480, 320]], // a display size + } + }, + bids: [ + { + bidder: "djax", + params: { + publisherId: '12' // string - required + } + } + ] + } + ]; \ No newline at end of file diff --git a/modules/docereeAdManagerBidAdapter.js b/modules/docereeAdManagerBidAdapter.js index d3765f5a130..4d7f9e34d45 100644 --- a/modules/docereeAdManagerBidAdapter.js +++ b/modules/docereeAdManagerBidAdapter.js @@ -3,11 +3,13 @@ import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'docereeadmanager'; const END_POINT = 'https://dai.doceree.com/drs/quest'; +const GVLID = 1063; export const spec = { code: BIDDER_CODE, url: '', supportedMediaTypes: [BANNER], + gvlid: GVLID, isBidRequestValid: (bid) => { const { placementId } = bid.params; @@ -20,12 +22,12 @@ export const spec = { } return true; }, - buildRequests: (validBidRequests) => { + buildRequests: (validBidRequests, bidderRequest) => { const serverRequests = []; const { data } = config.getConfig('docereeadmanager.user') || {}; validBidRequests.forEach(function (validBidRequest) { - const payload = getPayload(validBidRequest, data); + const payload = getPayload(validBidRequest, data, bidderRequest); if (!payload) { return; @@ -70,35 +72,58 @@ export const spec = { }, }; -function getPayload(bid, userData) { +export function getPageUrl() { + let url = ''; + try { + url = window.location.href; + } catch (error) { + } + return url; +} + +const handleConsent = (consentValue) => { + try { + if (consentValue === 0 || consentValue === '0') { + consentValue = '0'; + } + } catch (error) { + + } + return consentValue; +} + +export function getPayload(bid, userData, bidderRequest) { if (!userData || !bid) { return false; } const { bidId, params } = bid; - const { placementId } = params; + const { placementId, publisherUrl } = params; const { userid, email, firstname, lastname, - specialization, hcpid, + dob, + specialization, gender, city, state, zipcode, - hashedNPI, hashedhcpid, hashedemail, hashedmobile, country, + hashedNPI, organization, - dob, + platformUid, + mobile, + userconsent } = userData; const data = { - userid: userid || '', + userid: platformUid || userid || '', email: email || '', firstname: firstname || '', lastname: lastname || '', @@ -108,18 +133,32 @@ function getPayload(bid, userData) { city: city || '', state: state || '', zipcode: zipcode || '', - hashedNPI: hashedNPI || '', pb: 1, adunit: placementId || '', requestId: bidId || '', - hashedhcpid: hashedhcpid || '', + hashedhcpid: hashedhcpid || hashedNPI || '', hashedemail: hashedemail || '', hashedmobile: hashedmobile || '', country: country || '', organization: organization || '', dob: dob || '', - userconsent: 1, + upref: handleConsent(userconsent) || '', + mobile: mobile || '', + pageurl: getPageUrl() || publisherUrl || '' }; + + try { + if (bidderRequest && bidderRequest.gdprConsent) { + const { gdprApplies, consentString } = bidderRequest.gdprConsent; + data['consent'] = { + 'gdpr': gdprApplies ? 1 : 0, + 'gdprstr': consentString || '', + } + } + } catch (error) { + + } + return { data, }; diff --git a/modules/dochaseBidAdapter.js b/modules/dochaseBidAdapter.js new file mode 100644 index 00000000000..46b5b720f47 --- /dev/null +++ b/modules/dochaseBidAdapter.js @@ -0,0 +1,41 @@ +import { + BANNER, + NATIVE +} from '../src/mediaTypes.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { + getBannerRequest, + getBannerResponse, + getNativeResponse, +} from '../libraries/audUtils/bidderUtils.js'; + +const URL = 'https://rtb.dochaseadx.com/hb'; +// Export const spec +export const spec = { + code: 'dochase', + supportedMediaTypes: [BANNER, NATIVE], + // Determines whether given bid request is valid or not + isBidRequestValid: (bidRParam) => { + return !!(bidRParam.params.placement_id); + }, + // Make a server request from the list of BidRequests + buildRequests: (bidRq, serverRq) => { + // Get Requests based on media types + return getBannerRequest(bidRq, serverRq, URL); + }, + // Unpack the response from the server into a list of bids. + interpretResponse: (bidRes, bidReq) => { + let Response = {}; + const media = JSON.parse(bidReq.data)[0].MediaType; + if (media == BANNER) { + Response = getBannerResponse(bidRes, BANNER); + } else if (media == NATIVE) { + Response = getNativeResponse(bidRes, bidReq, NATIVE); + } + return Response; + } +} + +registerBidder(spec); diff --git a/modules/dochaseBidAdapter.md b/modules/dochaseBidAdapter.md new file mode 100644 index 00000000000..77355b4a7dd --- /dev/null +++ b/modules/dochaseBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: Dochase Bidder Adapter +Module Type: Bidder Adapter +Maintainer: dochaseops@dochase.com +``` + +# Description + +Dochase currently supports the BANNER and NATIVE type ads through prebid js + +Module that connects to Dochase's demand sources. + +# Test Request +``` + var adUnits = [ + { + code: 'display-ad', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + } + bids: [ + { + bidder: 'dochase', + params: { + placement_id: 5550, // Required parameter + width: 300, // Optional parameter + height: 250, // Optional parameter + bid_floor: 0.5 // Optional parameter + } + } + ] + }, + { + code: 'native-ad-container', + mediaTypes: { + native: { + title: { required: true, len: 100 }, + image: { required: true, sizes: [300, 250] }, + sponsored: { required: false }, + clickUrl: { required: true }, + desc: { required: true }, + icon: { required: false, sizes: [50, 50] }, + cta: { required: false } + } + }, + bids: [ + { + bidder: 'dochase', + params: { + placement_id: 5551, // Required parameter + bid_floor: 1 // Optional parameter + } + } + ] + } + ]; +``` diff --git a/modules/driftpixelBidAdapter.js b/modules/driftpixelBidAdapter.js new file mode 100644 index 00000000000..5dd0d3a5835 --- /dev/null +++ b/modules/driftpixelBidAdapter.js @@ -0,0 +1,18 @@ +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {buildRequests, getUserSyncs, interpretResponse, isBidRequestValid} from '../libraries/xeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'driftpixel'; +const ENDPOINT = 'https://pbjs.driftpixel.live'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['driftpixel'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests: (validBidRequests, bidderRequest) => buildRequests(validBidRequests, bidderRequest, ENDPOINT), + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/driftpixelBidAdapter.md b/modules/driftpixelBidAdapter.md new file mode 100644 index 00000000000..277c363bbf0 --- /dev/null +++ b/modules/driftpixelBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: Driftpixel Bidder Adapter +Module Type: Driftpixel Bidder Adapter +Maintainer: developer@driftpixel.ai +``` + +# Description + +Module that connects to driftpixel.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'driftpixel', + params: { + env: 'driftpixel', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'driftpixel', + params: { + env: 'driftpixel', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/dsp_genieeBidAdapter.js b/modules/dsp_genieeBidAdapter.js index 57aafd47fc8..f97c13379f3 100644 --- a/modules/dsp_genieeBidAdapter.js +++ b/modules/dsp_genieeBidAdapter.js @@ -3,6 +3,7 @@ import { BANNER } from '../src/mediaTypes.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { deepAccess, deepSetValue } from '../src/utils.js'; import { config } from '../src/config.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -71,7 +72,7 @@ export const spec = { if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || // gdpr USPConsent(bidderRequest.uspConsent) || // usp config.getConfig('coppa') || // coppa - invalidCurrency(config.getConfig('currency.adServerCurrency')) // currency validation + invalidCurrency(getCurrencyFromBidderRequest(bidderRequest)) // currency validation ) { return { method: 'GET', diff --git a/modules/dspxBidAdapter.js b/modules/dspxBidAdapter.js index ea47c64094d..acb5fb64d81 100644 --- a/modules/dspxBidAdapter.js +++ b/modules/dspxBidAdapter.js @@ -1,18 +1,31 @@ -import {deepAccess, getBidIdParameter, isFn, logError, logMessage, logWarn} from '../src/utils.js'; +import {deepAccess, logMessage, getBidIdParameter, logError, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {Renderer} from '../src/Renderer.js'; import {includes} from '../src/polyfill.js'; +import { + fillUsersIds, + handleSyncUrls, + objectToQueryString, + isBannerRequest, + getVideoContext, + convertMediaInfoForRequest, + getMediaTypesInfo, + getBidFloor, + siteContentToString, + assignDefinedValues, + extractUserSegments, + interpretResponse +} from '../libraries/dspxUtils/bidderUtils.js'; +import {Renderer} from '../src/Renderer.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest */ - const BIDDER_CODE = 'dspx'; const ENDPOINT_URL = 'https://buyer.dspx.tv/request/'; const ENDPOINT_URL_DEV = 'https://dcbuyer.dspx.tv/request/'; const GVLID = 602; -const VIDEO_ORTB_PARAMS = ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', +const VIDEO_ORTB_PARAMS = ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'plcmt', 'linearity', 'skip', 'skipmin', 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', 'api', 'companiontype', 'ext']; @@ -63,24 +76,18 @@ export const spec = { rnd: rnd, ref: referrer, bid_id: bidId, - pbver: '$prebid.version$' + pbver: '$prebid.version$', }; + payload.pfilter = {}; if (params.pfilter !== undefined) { payload.pfilter = params.pfilter; } if (bidderRequest && bidderRequest.gdprConsent) { - if (payload.pfilter !== undefined) { - if (!payload.pfilter.gdpr_consent) { - payload.pfilter.gdpr_consent = bidderRequest.gdprConsent.consentString; - payload.pfilter.gdpr = bidderRequest.gdprConsent.gdprApplies; - } - } else { - payload.pfilter = { - 'gdpr_consent': bidderRequest.gdprConsent.consentString, - 'gdpr': bidderRequest.gdprConsent.gdprApplies - }; + if (!payload.pfilter.gdpr_consent) { + payload.pfilter.gdpr_consent = bidderRequest.gdprConsent.consentString; + payload.pfilter.gdpr = bidderRequest.gdprConsent.gdprApplies; } } @@ -94,48 +101,10 @@ export const spec = { payload.prebidDevMode = 1; } - // fill userId params - if (bidRequest.userId) { - if (bidRequest.userId.netId) { - payload.did_netid = bidRequest.userId.netId; - } - if (bidRequest.userId.id5id) { - payload.did_id5 = bidRequest.userId.id5id.uid || '0'; - if (bidRequest.userId.id5id.ext.linkType !== undefined) { - payload.did_id5_linktype = bidRequest.userId.id5id.ext.linkType; - } - } - let uId2 = deepAccess(bidRequest, 'userId.uid2.id'); - if (uId2) { - payload.did_uid2 = uId2; - } - let sharedId = deepAccess(bidRequest, 'userId.sharedid.id'); - if (sharedId) { - payload.did_sharedid = sharedId; - } - let pubcId = deepAccess(bidRequest, 'userId.pubcid'); - if (pubcId) { - payload.did_pubcid = pubcId; - } - let crumbsPubcid = deepAccess(bidRequest, 'crumbs.pubcid'); - if (crumbsPubcid) { - payload.did_cpubcid = crumbsPubcid; - } - } - - if (bidRequest.schain) { - payload.schain = bidRequest.schain; - } - - if (payload.pfilter === undefined || !payload.pfilter.floorprice) { + if (!payload.pfilter.floorprice) { let bidFloor = getBidFloor(bidRequest); if (bidFloor > 0) { - if (payload.pfilter !== undefined) { - payload.pfilter.floorprice = bidFloor; - } else { - payload.pfilter = { 'floorprice': bidFloor }; - } - // payload.bidFloor = bidFloor; + payload.pfilter.floorprice = bidFloor; } } @@ -146,6 +115,7 @@ export const spec = { payload.pbcode = pbcode; } + // media types payload.media_types = convertMediaInfoForRequest(mediaTypesInfo); if (mediaTypesInfo[VIDEO] !== undefined) { payload.vctx = getVideoContext(bidRequest); @@ -159,6 +129,33 @@ export const spec = { .forEach(key => payload.vpl[key] = videoParams[key]); } + // iab content + let content = deepAccess(bidderRequest, 'ortb2.site.content'); + if (content) { + let stringContent = siteContentToString(content); + if (stringContent) { + payload.pfilter.iab_content = stringContent; + } + } + + // Google Topics + const segments = extractUserSegments(bidderRequest); + if (segments) { + assignDefinedValues(payload, { + segtx: segments.segtax, + segcl: segments.segclass, + segs: segments.segments + }); + } + + // schain + if (bidRequest.schain) { + payload.schain = bidRequest.schain; + } + + // fill userId params + fillUsersIds(bidRequest, payload); + return { method: 'GET', url: endpoint, @@ -169,282 +166,20 @@ export const spec = { interpretResponse: function(serverResponse, bidRequest) { logMessage('DSPx: serverResponse', serverResponse); logMessage('DSPx: bidRequest', bidRequest); - const bidResponses = []; - const response = serverResponse.body; - const crid = response.crid || 0; - const cpm = response.cpm / 1000000 || 0; - if (cpm !== 0 && crid !== 0) { - const dealId = response.dealid || ''; - const currency = response.currency || 'EUR'; - const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue; - const bidResponse = { - requestId: response.bid_id, - cpm: cpm, - width: response.width, - height: response.height, - creativeId: crid, - dealId: dealId, - currency: currency, - netRevenue: netRevenue, - type: response.type, - ttl: 60, - meta: { - advertiserDomains: response.adomain || [] - } - }; - - if (response.vastUrl) { - bidResponse.vastUrl = response.vastUrl; - bidResponse.mediaType = 'video'; - } - if (response.vastXml) { - bidResponse.vastXml = response.vastXml; - bidResponse.mediaType = 'video'; - } - if (response.renderer) { - bidResponse.renderer = newRenderer(bidRequest, response); - } - - if (response.videoCacheKey) { - bidResponse.videoCacheKey = response.videoCacheKey; - } - - if (response.adTag) { - bidResponse.ad = response.adTag; - } - - if (response.bid_appendix) { - Object.keys(response.bid_appendix).forEach(fieldName => { - bidResponse[fieldName] = response.bid_appendix[fieldName]; - }); - } - - bidResponses.push(bidResponse); - } - return bidResponses; + return interpretResponse(serverResponse, bidRequest, (bidRequest, response) => newRenderer(bidRequest, response)); }, getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { - if (!serverResponses || serverResponses.length === 0) { - return []; - } - - const syncs = [] - - let gdprParams = ''; - if (gdprConsent) { - if ('gdprApplies' in gdprConsent && typeof gdprConsent.gdprApplies === 'boolean') { - gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - gdprParams = `gdpr_consent=${gdprConsent.consentString}`; - } - } - - if (serverResponses.length > 0 && serverResponses[0].body.userSync) { - if (syncOptions.iframeEnabled) { - serverResponses[0].body.userSync.iframeUrl.forEach((url) => syncs.push({ - type: 'iframe', - url: appendToUrl(url, gdprParams) - })); - } - if (syncOptions.pixelEnabled) { - serverResponses[0].body.userSync.imageUrl.forEach((url) => syncs.push({ - type: 'image', - url: appendToUrl(url, gdprParams) - })); - } - } - return syncs; + return handleSyncUrls(syncOptions, serverResponses, gdprConsent); } } -function appendToUrl(url, what) { - if (!what) { - return url; - } - return url + (url.indexOf('?') !== -1 ? '&' : '?') + what; -} - -function objectToQueryString(obj, prefix) { - let str = []; - let p; - for (p in obj) { - if (obj.hasOwnProperty(p)) { - let k = prefix ? prefix + '[' + p + ']' : p; - let v = obj[p]; - str.push((v !== null && typeof v === 'object') - ? objectToQueryString(v, k) - : encodeURIComponent(k) + '=' + encodeURIComponent(v)); - } - } - return str.join('&'); -} - -/** - * Check if it's a banner bid request - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {boolean} True if it's a banner bid - */ -function isBannerRequest(bid) { - return bid.mediaType === 'banner' || !!deepAccess(bid, 'mediaTypes.banner') || !isVideoRequest(bid); -} - -/** - * Check if it's a video bid request - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {boolean} True if it's a video bid - */ -function isVideoRequest(bid) { - return bid.mediaType === 'video' || !!deepAccess(bid, 'mediaTypes.video'); -} - -/** - * Get video sizes - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {object} - */ -function getVideoSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); -} - -/** - * Get video context - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {object} - */ -function getVideoContext(bid) { - return deepAccess(bid, 'mediaTypes.video.context') || 'unknown'; -} - -/** - * Get banner sizes - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {object} True if it's a video bid - */ -function getBannerSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); -} - -/** - * Parse size - * @param size - * @returns {object} sizeObj - */ -function parseSize(size) { - let sizeObj = {} - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - return sizeObj; -} - -/** - * Parse sizes - * @param sizes - * @returns {{width: number , height: number }[]} - */ -function parseSizes(sizes) { - if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]]) - return sizes.map(size => parseSize(size)); - } - return [parseSize(sizes)]; // or a single one ? (ie. [728,90]) -} - -/** - * Get MediaInfo object for server request - * - * @param mediaTypesInfo - * @returns {*} - */ -function convertMediaInfoForRequest(mediaTypesInfo) { - let requestData = {}; - Object.keys(mediaTypesInfo).forEach(mediaType => { - requestData[mediaType] = mediaTypesInfo[mediaType].map(size => { - return size.width + 'x' + size.height; - }).join(','); - }); - return requestData; -} - -/** - * Get media types info - * - * @param bid - */ -function getMediaTypesInfo(bid) { - let mediaTypesInfo = {}; - - if (bid.mediaTypes) { - Object.keys(bid.mediaTypes).forEach(mediaType => { - if (mediaType === BANNER) { - mediaTypesInfo[mediaType] = getBannerSizes(bid); - } - if (mediaType === VIDEO) { - mediaTypesInfo[mediaType] = getVideoSizes(bid); - } - }); - } else { - mediaTypesInfo[BANNER] = getBannerSizes(bid); - } - return mediaTypesInfo; -} - -/** - * Get Bid Floor - * @param bid - * @returns {number|*} - */ -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'EUR', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - -/** - * Create a new renderer - * - * @param bidRequest - * @param response - * @returns {Renderer} - */ -function newRenderer(bidRequest, response) { - logMessage('DSPx: newRenderer', bidRequest, response); - const renderer = Renderer.install({ - id: response.renderer.id || response.bid_id, - url: (bidRequest.params && bidRequest.params.rendererUrl) || response.renderer.url, - config: response.renderer.options || deepAccess(bidRequest, 'renderer.options'), - loaded: false - }); - - try { - renderer.setRender(outstreamRender); - } catch (err) { - logWarn('Prebid Error calling setRender on renderer', err); - } - return renderer; -} - /** * Outstream Render Function * * @param bid */ function outstreamRender(bid) { - logMessage('DSPx: outstreamRender bid:', bid); + logMessage('[DSPx][outstreamRender] bid:', bid); const embedCode = createOutstreamEmbedCode(bid); try { const inIframe = getBidIdParameter('iframe', bid.renderer.config); @@ -455,7 +190,7 @@ function outstreamRender(bid) { if (typeof window.dspxRender === 'function') { window.dspxRender(bid); } else { - logError('[dspx][renderer] Error: dspxRender function is not found'); + logError('[DSPx][outstreamRender] Error: dspxRender function is not found'); } return; } @@ -466,13 +201,13 @@ function outstreamRender(bid) { if (typeof window.dspxRender === 'function') { window.dspxRender(bid); } else { - logError('[dspx][renderer] Error: dspxRender function is not found'); + logError('[DSPx][outstreamRender] Error: dspxRender function is not found'); } } else if (slot) { - logError('[dspx][renderer] Error: slot not found'); + logError('[DSPx][outstreamRender] Error: slot not found'); } } catch (err) { - logError('[dspx][renderer] Error:' + err.message) + logError('[DSPx][outstreamRender] Error:' + err.message) } } @@ -508,4 +243,28 @@ function createOutstreamEmbedCode(bid) { return fragment; } +/** + * Create a new renderer + * + * @param bidRequest + * @param response + * @returns {Renderer} + */ +function newRenderer(bidRequest, response) { + logMessage('[DSPx] newRenderer', bidRequest, response); + const renderer = Renderer.install({ + id: response.renderer.id || response.bid_id, + url: (bidRequest.params && bidRequest.params.rendererUrl) || response.renderer.url, + config: response.renderer.options || deepAccess(bidRequest, 'renderer.options'), + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logWarn('[DSPx]Prebid Error calling setRender on renderer', err); + } + return renderer; +} + registerBidder(spec); diff --git a/modules/dynamicAdBoostRtdProvider.js b/modules/dynamicAdBoostRtdProvider.js index 697bd7340d3..a68567b1ca3 100644 --- a/modules/dynamicAdBoostRtdProvider.js +++ b/modules/dynamicAdBoostRtdProvider.js @@ -8,6 +8,7 @@ import { submodule } from '../src/hook.js' import { loadExternalScript } from '../src/adloader.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { deepAccess, deepSetValue, isEmptyStr } from '../src/utils.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -72,7 +73,7 @@ function loadLmScript(keyId) { let viewableAdUnits = Object.keys(dynamicAdBoostAdUnits); let viewableAdUnitsCSV = viewableAdUnits.join(','); const scriptUrl = `${SCRIPT_URL}/${keyId}.js?viewableAdUnits=${viewableAdUnitsCSV}`; - loadExternalScript(scriptUrl, MODULE_NAME); + loadExternalScript(scriptUrl, MODULE_TYPE_RTD, MODULE_NAME); observer.disconnect(); } diff --git a/modules/e_volutionBidAdapter.js b/modules/e_volutionBidAdapter.js index 2ce6cda16d1..e87e39599a0 100644 --- a/modules/e_volutionBidAdapter.js +++ b/modules/e_volutionBidAdapter.js @@ -1,183 +1,19 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { isFn, deepAccess, logMessage } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { isBidRequestValid, buildRequests, interpretResponse } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'e_volution'; const GVLID = 957; const AD_URL = 'https://service.e-volution.ai/?c=o&m=multi'; -const URL_SYNC = 'https://service.e-volution.ai/?c=o&m=sync'; -const NO_SYNC = true; - -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; - } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl); - case NATIVE: - return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); - default: - return false; - } -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - -function getUserId(eids, id, source, uidExt) { - if (id) { - var uid = { id }; - if (uidExt) { - uid.ext = uidExt; - } - eids.push({ - source, - uids: [ uid ] - }); - } -} export const spec = { code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - noSync: NO_SYNC, - - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let winTop = window; - let location; - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page); - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - let placements = []; - let request = { - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - 'secure': 1, - 'host': location.host, - 'page': location.pathname, - 'placements': placements - }; - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent; - } - } - const len = validBidRequests.length; - - for (let i = 0; i < len; i++) { - let bid = validBidRequests[i]; - - const placement = { - placementId: bid.params.placementId, - bidId: bid.bidId, - bidfloor: getBidFloor(bid), - eids: [] - }; - - if (bid.userId) { - getUserId(placement.eids, bid.userId.id5id, 'id5-sync.com'); - } - - if (bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { - placement.traffic = BANNER; - placement.sizes = bid.mediaTypes[BANNER].sizes; - } else if (bid.mediaTypes && bid.mediaTypes[VIDEO] && bid.mediaTypes[VIDEO].playerSize) { - placement.traffic = VIDEO; - placement.wPlayer = bid.mediaTypes[VIDEO].playerSize[0]; - placement.hPlayer = bid.mediaTypes[VIDEO].playerSize[1]; - placement.minduration = bid.mediaTypes[VIDEO].minduration; - placement.maxduration = bid.mediaTypes[VIDEO].maxduration; - placement.mimes = bid.mediaTypes[VIDEO].mimes; - placement.protocols = bid.mediaTypes[VIDEO].protocols; - placement.startdelay = bid.mediaTypes[VIDEO].startdelay; - placement.placement = bid.mediaTypes[VIDEO].placement; - placement.skip = bid.mediaTypes[VIDEO].skip; - placement.skipafter = bid.mediaTypes[VIDEO].skipafter; - placement.minbitrate = bid.mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = bid.mediaTypes[VIDEO].maxbitrate; - placement.delivery = bid.mediaTypes[VIDEO].delivery; - placement.playbackmethod = bid.mediaTypes[VIDEO].playbackmethod; - placement.api = bid.mediaTypes[VIDEO].api; - placement.linearity = bid.mediaTypes[VIDEO].linearity; - } else if (bid.mediaTypes && bid.mediaTypes[NATIVE]) { - placement.traffic = NATIVE; - placement.native = bid.mediaTypes[NATIVE]; - } - - if (bid.schain) { - placements.schain = bid.schain; - } - - placements.push(placement); - } - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses) => { - if (NO_SYNC) { - return false - } else { - return [{ - type: 'image', - url: URL_SYNC - }]; - } - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse }; registerBidder(spec); diff --git a/modules/ebdrBidAdapter.js b/modules/ebdrBidAdapter.js deleted file mode 100644 index e830f8a94f7..00000000000 --- a/modules/ebdrBidAdapter.js +++ /dev/null @@ -1,156 +0,0 @@ -import {getBidIdParameter, logInfo} from '../src/utils.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -const BIDDER_CODE = 'ebdr'; -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [ BANNER, VIDEO ], - isBidRequestValid: function(bid) { - return !!(bid && bid.params && bid.params.zoneid); - }, - buildRequests: function(bids) { - const rtbServerDomain = 'dsp.bnmla.com'; - let domain = window.location.host; - let page = window.location.pathname + location.search + location.hash; - let ebdrImps = []; - const ebdrReq = {}; - let ebdrParams = {}; - let zoneid = ''; - let requestId = ''; - bids.forEach(bid => { - logInfo('Log bid', bid); - let bidFloor = getBidIdParameter('bidfloor', bid.params); - let whArr = getWidthAndHeight(bid); - let _mediaTypes = (bid.mediaTypes && bid.mediaTypes.video) ? VIDEO : BANNER; - zoneid = getBidIdParameter('zoneid', bid.params); - requestId = bid.bidderRequestId; - ebdrImps.push({ - id: bid.bidId, - [_mediaTypes]: { - w: whArr[0], - h: whArr[1] - }, - bidfloor: bidFloor - }); - ebdrReq[bid.bidId] = {mediaTypes: _mediaTypes, - w: whArr[0], - h: whArr[1] - }; - // TODO: fix lat and long to only come from request - ebdrParams['latitude'] = '0'; - ebdrParams['longitude'] = '0'; - ebdrParams['ifa'] = (getBidIdParameter('IDFA', bid.params).length > getBidIdParameter('ADID', bid.params).length) ? getBidIdParameter('IDFA', bid.params) : getBidIdParameter('ADID', bid.params); - }); - let ebdrBidReq = { - id: requestId, - imp: ebdrImps, - site: { - domain: domain, - page: page - }, - device: { - geo: { - lat: ebdrParams.latitude, - log: ebdrParams.longitude - }, - ifa: ebdrParams.ifa - } - }; - return { - method: 'GET', - url: 'https://' + rtbServerDomain + '/hb?' + '&zoneid=' + zoneid + '&br=' + encodeURIComponent(JSON.stringify(ebdrBidReq)), - bids: ebdrReq - }; - }, - interpretResponse: function(serverResponse, bidRequest) { - logInfo('Log serverResponse', serverResponse); - logInfo('Log bidRequest', bidRequest); - let ebdrResponseImps = []; - const ebdrResponseObj = serverResponse.body; - if (!ebdrResponseObj || !ebdrResponseObj.seatbid || ebdrResponseObj.seatbid.length === 0 || !ebdrResponseObj.seatbid[0].bid || ebdrResponseObj.seatbid[0].bid.length === 0) { - return []; - } - ebdrResponseObj.seatbid[0].bid.forEach(ebdrBid => { - let responseCPM; - responseCPM = parseFloat(ebdrBid.price); - let adm; - let type; - let _mediaTypes; - let vastURL; - if (bidRequest.bids[ebdrBid.id].mediaTypes == BANNER) { - adm = decodeURIComponent(ebdrBid.adm) - type = 'ad'; - _mediaTypes = BANNER; - } else { - adm = ebdrBid.adm - type = 'vastXml' - _mediaTypes = VIDEO; - if (ebdrBid.nurl) { - vastURL = ebdrBid.nurl; - } - } - let response = { - requestId: ebdrBid.id, - [type]: adm, - mediaType: _mediaTypes, - creativeId: ebdrBid.crid, - cpm: responseCPM, - width: ebdrBid.w, - height: ebdrBid.h, - currency: 'USD', - netRevenue: true, - ttl: 3600, - meta: { - advertiserDomains: ebdrBid.adomain || [] - } - }; - if (vastURL) { - response.vastUrl = vastURL; - } - ebdrResponseImps.push(response); - }); - return ebdrResponseImps; - }, - getUserSyncs: function(syncOptions, serverResponses) { - const syncs = [] - if (syncOptions.pixelEnabled) { - const ebdrResponseObj = serverResponses.body; - if (!ebdrResponseObj || !ebdrResponseObj.seatbid || ebdrResponseObj.seatbid.length === 0 || !ebdrResponseObj.seatbid[0].bid || ebdrResponseObj.seatbid[0].bid.length === 0) { - return []; - } - ebdrResponseObj.seatbid[0].bid.forEach(ebdrBid => { - if (ebdrBid.iurl && ebdrBid.iurl.length > 0) { - syncs.push({ - type: 'image', - url: ebdrBid.iurl - }); - } - }); - } - return syncs; - } -} -function getWidthAndHeight(bid) { - let adW = null; - let adH = null; - // Handing old bidder only has size object - if (bid.sizes && bid.sizes.length) { - let sizeArrayLength = bid.sizes.length; - if (sizeArrayLength === 2 && typeof bid.sizes[0] === 'number' && typeof bid.sizes[1] === 'number') { - adW = bid.sizes[0]; - adH = bid.sizes[1]; - } - } - let _mediaTypes = bid.mediaTypes && bid.mediaTypes.video ? VIDEO : BANNER; - if (bid.mediaTypes && bid.mediaTypes[_mediaTypes]) { - if (_mediaTypes == BANNER && bid.mediaTypes[_mediaTypes].sizes && bid.mediaTypes[_mediaTypes].sizes[0] && bid.mediaTypes[_mediaTypes].sizes[0].length === 2) { - adW = bid.mediaTypes[_mediaTypes].sizes[0][0]; - adH = bid.mediaTypes[_mediaTypes].sizes[0][1]; - } else if (_mediaTypes == VIDEO && bid.mediaTypes[_mediaTypes].playerSize && bid.mediaTypes[_mediaTypes].playerSize.length === 2) { - adW = bid.mediaTypes[_mediaTypes].playerSize[0]; - adH = bid.mediaTypes[_mediaTypes].playerSize[1]; - } - } - return [adW, adH]; -} -registerBidder(spec); diff --git a/modules/ebdrBidAdapter.md b/modules/ebdrBidAdapter.md deleted file mode 100644 index 64483797b25..00000000000 --- a/modules/ebdrBidAdapter.md +++ /dev/null @@ -1,53 +0,0 @@ -# Overview - -``` -Module Name: EngageBDR Bid Adapter -Module Type: Bidder Adapter -Maintainer: tech@engagebdr.com -``` - -# Description - -Adapter that connects to EngageBDR's demand sources. - -# Test Parameters -``` - var adUnits = [{ - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]], - } - }, - bids: [{ - bidder: 'ebdr', - params: { - zoneid: '99999', - bidfloor: '1.00', - IDFA:'xxx-xxx', - ADID:'xxx-xxx', - latitude:'34.089811', - longitude:'-118.392805' - } - }] - },{ - code: 'test-video', - mediaTypes: { - video: { - context: 'instream', - playerSize: [300, 250] - } - }, - bids: [{ - bidder: 'ebdr', - params: { - zoneid: '99998', - bidfloor: '1.00', - IDFA:'xxx-xxx', - ADID:'xxx-xxx', - latitude:'34.089811', - longitude:'-118.392805' - } - }] - }]; -``` diff --git a/modules/eclickadsBidAdapter.js b/modules/eclickadsBidAdapter.js new file mode 100644 index 00000000000..27e3926afe3 --- /dev/null +++ b/modules/eclickadsBidAdapter.js @@ -0,0 +1,80 @@ +import { NATIVE } from '../src/mediaTypes.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getDevice } from '../libraries/fpdUtils/deviceInfo.js'; + +// ***** ECLICKADS ADAPTER ***** +export const BIDDER_CODE = 'eclickads'; +const DEFAULT_CURRENCY = ['USD']; +const DEFAULT_TTL = 1000; +export const ENDPOINT = 'https://g.eclick.vn/rtb_hb_request?fosp_uid='; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [NATIVE], + isBidRequestValid: (bid) => { + return !!bid && !!bid.params && !!bid.bidder && !!bid.params.zid; + }, + buildRequests: (validBidRequests = [], bidderRequest) => { + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + const ortb2ConfigFPD = bidderRequest.ortb2.site.ext?.data || {}; + const ortb2Device = bidderRequest.ortb2.device; + const ortb2Site = bidderRequest.ortb2.site; + + const isMobile = getDevice(); + const imp = []; + const fENDPOINT = ENDPOINT + (ortb2ConfigFPD.fosp_uid || ''); + const request = { + deviceWidth: ortb2Device.w, + deviceHeight: ortb2Device.h, + ua: ortb2Device.ua, + language: ortb2Device.language, + device: isMobile ? 'mobile' : 'desktop', + host: ortb2Site.domain, + page: ortb2Site.page, + imp, + myvne_id: ortb2ConfigFPD.myvne_id || '', + orig_aid: ortb2ConfigFPD.orig_aid, + fosp_aid: ortb2ConfigFPD.fosp_aid, + fosp_uid: ortb2ConfigFPD.fosp_uid, + id: ortb2ConfigFPD.id, + }; + + validBidRequests.map((bid) => { + imp.push({ + requestId: bid.bidId, + adUnitCode: bid.adUnitCode, + zid: bid.params.zid, + }); + }); + + return { + method: 'POST', + url: fENDPOINT, + data: request, + }; + }, + interpretResponse: (serverResponse) => { + const seatbid = serverResponse.body?.seatbid || []; + return seatbid.reduce((bids, bid) => { + return [ + ...bids, + { + id: bid.id, + impid: bid.impid, + adUnitCode: bid.adUnitCode, + cpm: bid.cpm, + ttl: bid.ttl || DEFAULT_TTL, + requestId: bid.requestId, + creativeId: bid.creativeId, + netRevenue: bid.netRevenue, + currency: bid.currency || DEFAULT_CURRENCY, + adserverTargeting: { + hb_ad_eclickads: bid.ad, + }, + }, + ]; + }, []); + }, +}; +registerBidder(spec); diff --git a/modules/eclickadsBidAdapter.md b/modules/eclickadsBidAdapter.md new file mode 100644 index 00000000000..39ba19d5249 --- /dev/null +++ b/modules/eclickadsBidAdapter.md @@ -0,0 +1,55 @@ +# Overview + +Module Name: EClickAds Bid Adapter +Type: Bidder Adapter +Maintainer: vietlv14@fpt.com + +# Description + +This module connects to EClickAds exchange for bidding NATIVE ADS via prebid.js + +# Test Parameters + +``` +var adUnits = [{ + code: 'test-div', + mediaTypes: { + native: { + title: { + required: true, + len: 50 + }, + body: { + required: true, + len: 350 + }, + url: { + required: true + }, + image: { + required: true, + sizes : [300, 175] + }, + sponsoredBy: { + required: true + } + } + }, + bids: [{ + bidder: 'eclickads', + params: { + zid:"7096" + } + }] +}]; +``` + +# Notes: + +- EClickAdsBidAdapter need serveral params inside bidder config as following + - user.myvne_id + - site.orig_aid + - site.fosp_aid + - site.id + - site.orig_aid +- EClickAdsBidAdapter will set bid.adserverTargeting.hb_ad_eclickads targeting key while submitting bid to AdServer diff --git a/modules/edge226BidAdapter.js b/modules/edge226BidAdapter.js index 6d1e2466abe..ae235f02f64 100644 --- a/modules/edge226BidAdapter.js +++ b/modules/edge226BidAdapter.js @@ -1,188 +1,17 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'edge226'; const AD_URL = 'https://ssp.dauup.com/pbjs'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse }; registerBidder(spec); diff --git a/modules/ehealthcaresolutionsBidAdapter.js b/modules/ehealthcaresolutionsBidAdapter.js new file mode 100644 index 00000000000..9df4c38e4f2 --- /dev/null +++ b/modules/ehealthcaresolutionsBidAdapter.js @@ -0,0 +1,41 @@ +import { + BANNER, + NATIVE +} from '../src/mediaTypes.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { + getBannerRequest, + getBannerResponse, + getNativeResponse, +} from '../libraries/audUtils/bidderUtils.js'; + +const ENDPOINT_URL = 'https://rtb.ehealthcaresolutions.com/hb'; +// Export const spec +export const spec = { + code: 'ehealthcaresolutions', + supportedMediaTypes: [BANNER, NATIVE], + // Determines whether or not the given bid request is valid + isBidRequestValid: (bParam) => { + return !!(bParam.params.placement_id); + }, + // Make a server request from the list of BidRequests + buildRequests: (bidRequests, serverRequest) => { + // Get Requests based on media types + return getBannerRequest(bidRequests, serverRequest, ENDPOINT_URL); + }, + // Unpack the response from the server into a list of bids. + interpretResponse: (bResponse, bRequest) => { + let Response = {}; + const mediaType = JSON.parse(bRequest.data)[0].MediaType; + if (mediaType == BANNER) { + Response = getBannerResponse(bResponse, BANNER); + } else if (mediaType == NATIVE) { + Response = getNativeResponse(bResponse, bRequest, NATIVE); + } + return Response; + } +} + +registerBidder(spec); diff --git a/modules/ehealthcaresolutionsBidAdapter.md b/modules/ehealthcaresolutionsBidAdapter.md new file mode 100644 index 00000000000..fdf859404d2 --- /dev/null +++ b/modules/ehealthcaresolutionsBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: eHealthcareSolutions Bidder Adapter +Module Type: Bidder Adapter +Maintainer: info@ehsmail.com +``` + +# Description + +eHealthcareSolutions currently supports the BANNER and NATIVE type ads through prebid js + +Module that connects to eHealthcareSolutions's demand sources. + +# Test Request +``` + var adUnits = [ + { + code: 'display-ad', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + } + bids: [ + { + bidder: 'ehealthcaresolutions', + params: { + placement_id: 111519, // Required parameter + width: 300, // Optional parameter + height: 250, // Optional parameter + bid_floor: 0.5 // Optional parameter + } + } + ] + }, + { + code: 'native-ad-container', + mediaTypes: { + native: { + title: { required: true, len: 100 }, + image: { required: true, sizes: [300, 250] }, + sponsored: { required: false }, + clickUrl: { required: true }, + desc: { required: true }, + icon: { required: false, sizes: [50, 50] }, + cta: { required: false } + } + }, + bids: [ + { + bidder: 'eHealthcareSolutions', + params: { + placement_id: 111519, // Required parameter + bid_floor: 1 // Optional parameter + } + } + ] + } + ]; +``` diff --git a/modules/eightPodAnalyticsAdapter.js b/modules/eightPodAnalyticsAdapter.js new file mode 100644 index 00000000000..e91f9412ef5 --- /dev/null +++ b/modules/eightPodAnalyticsAdapter.js @@ -0,0 +1,205 @@ +import {logError, logInfo, logMessage} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import { EVENTS } from '../src/constants.js'; +import adapterManager from '../src/adapterManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js' +import {getStorageManager} from '../src/storageManager.js'; + +const analyticsType = 'endpoint'; +const MODULE_NAME = `eightPod`; +const MODULE = `${MODULE_NAME}AnalyticProvider`; + +/** + * Custom tracking server that gets internal events from EightPod's ad unit + */ +const trackerUrl = 'https://demo.8pod.com/tracker/track'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}) + +const { + BID_WON +} = EVENTS; + +export let queue = []; +let context = {}; + +/** + * Create eightPod Analytic adapter + */ +let eightPodAnalytics = Object.assign(adapter({url: trackerUrl, analyticsType}), { + /** + * Execute on bid won - setup basic settings, save context about EightPod's bid. We will send it with our events later + */ + track({ eventType, args }) { + switch (eventType) { + case BID_WON: + if (args.bidder === 'eightPod') { + context[args.adUnitCode] = makeContext(args); + + eightPodAnalytics.setupPage(args); + break; + } + } + }, + + /** + * Execute on bid won upload events from local storage + */ + setupPage() { + queue = this.getEventFromLocalStorage(); + }, + + /** + * Subscribe on internal ad unit tracking events + */ + eventSubscribe() { + window.addEventListener('message', async (event) => { + const data = event.data; + + const frameElement = event.source?.frameElement; + const parentElement = frameElement?.parentElement; + const adUnitCode = parentElement?.id; + + trackEvent(data, adUnitCode); + }); + + if (!this._interval) { + this._interval = setInterval(sendEvents, 10_000); + } + }, + resetQueue() { + queue = []; + }, + getContext() { + return context; + }, + resetContext() { + context = {}; + }, + getEventFromLocalStorage, +}); + +/** + * Create context of event, who emits it + */ +function makeContext(args) { + const params = args?.params?.[0]; + return { + bidId: args.seatBidId, + variantId: args.creativeId || '', + campaignId: args.cid || '', + publisherId: params.publisherId, + placementId: params.placementId, + }; +} + +/** + * Create event, add context and push it to queue + */ +export function trackEvent(event, adUnitCode) { + if (!event.detail) { + return; + } + + const fullEvent = { + context: eightPodAnalytics.getContext()[adUnitCode], + eventType: event.detail.type, + eventClass: 'adunit', + timestamp: new Date().getTime(), + eventName: event.detail.name, + payload: event.detail.payload + }; + + logMessage(fullEvent); + addEvent(fullEvent); +} + +/** + * Push event to queue, save event list in local storage + */ +function addEvent(eventPayload) { + queue.push(eventPayload); + storage.setDataInLocalStorage(`EIGHT_POD_EVENTS`, JSON.stringify(queue), null); +} + +/** + * Gets previously saved event that has not been sent + */ +function getEventFromLocalStorage() { + const storedEvents = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage('EIGHT_POD_EVENTS') : null; + + if (storedEvents) { + return JSON.parse(storedEvents); + } else { + return []; + } +} + +/** + * Send event to our custom tracking server and reset queue + */ +function sendEvents() { + eightPodAnalytics.eventsStorage = queue; + + if (queue.length) { + try { + sendEventsApi(queue, { + success: () => { + resetLocalStorage(); + eightPodAnalytics.resetQueue(); + }, + error: (e) => { + logError(MODULE, 'Cant send events', e); + } + }) + } catch (e) { + logError(MODULE, 'Cant send events', e); + } + } +} + +/** + * Send event to our custom tracking server + */ +function sendEventsApi(eventList, callbacks) { + ajax(trackerUrl, callbacks, JSON.stringify(eventList), {keepalive: true}); +} + +/** + * Remove saved events in success scenario + */ +const resetLocalStorage = () => { + storage.setDataInLocalStorage(`EIGHT_POD_EVENTS`, JSON.stringify([]), null); +} + +// save the base class function +eightPodAnalytics.originEnableAnalytics = eightPodAnalytics.enableAnalytics; +eightPodAnalytics.eventsStorage = []; + +// override enableAnalytics so we can get access to the config passed in from the page +// Subscribe on events from adUnit +eightPodAnalytics.enableAnalytics = function (config) { + eightPodAnalytics.originEnableAnalytics(config); + logInfo(MODULE, 'init', config); + eightPodAnalytics.eventSubscribe(); +}; + +eightPodAnalytics.disableAnalytics = ((orig) => { + return function () { + if (this._interval) { + clearInterval(this._interval); + this._interval = null; + } + return orig.apply(this, arguments); + } +})(eightPodAnalytics.disableAnalytics) + +/** + * Register Analytics Adapter + */ +adapterManager.registerAnalyticsAdapter({ + adapter: eightPodAnalytics, + code: MODULE_NAME +}); + +export default eightPodAnalytics; diff --git a/modules/eightPodAnalyticsAdapter.md b/modules/eightPodAnalyticsAdapter.md new file mode 100644 index 00000000000..fe37bf34459 --- /dev/null +++ b/modules/eightPodAnalyticsAdapter.md @@ -0,0 +1,19 @@ +# Overview +Module Name: 8pod Analytics by 8Pod + +Module Type: Analytics Adapter + +Maintainer: bianca@8pod.com + +# Description + +Analytics adapter for prebid provided by 8pod. It gets events from eightPod's ad unit and send it to our tracking server to improve user experience. +Please, use it ONLY with eightPodBidAdapter. + +# Analytics Adapter configuration example + +``` +{ + provider: 'eightPod' +} +``` diff --git a/modules/eightPodBidAdapter.js b/modules/eightPodBidAdapter.js new file mode 100644 index 00000000000..536bc4b4036 --- /dev/null +++ b/modules/eightPodBidAdapter.js @@ -0,0 +1,249 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { registerBidder } from '../src/adapters/bidderFactory.js' +import { BANNER } from '../src/mediaTypes.js' +import * as utils from '../src/utils.js' + +export const BIDDER_CODE = 'eightPod' +const url = 'https://demo.8pod.com/bidder/rtb/eightpod_exchange/bid'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + isBannerBid, + isVideoBid, + onBidWon +} + +registerBidder(spec) + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context) + return req + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context) + return response.bids + }, + imp(buildImp, bidRequest, context) { + return buildImp(bidRequest, context) + }, + bidResponse +}) + +function hasRequiredParams(bidRequest) { + return !!bidRequest?.params?.placementId +} + +function isBidRequestValid(bidRequest) { + return hasRequiredParams(bidRequest) +} + +function buildRequests(bids, bidderRequest) { + let bannerBids = bids.filter((bid) => isBannerBid(bid)) + let requests = bannerBids.length + ? createRequest(bannerBids, bidderRequest, BANNER) + : [] + + return requests +} + +function bidResponse(buildBidResponse, bid, context) { + bid.nurl = replacePriceInUrl(bid.nurl, bid.price); + + const bidResponse = buildBidResponse(bid, context); + + bidResponse.height = context?.imp?.banner?.format?.[0].h; + bidResponse.width = context?.imp?.banner?.format?.[0].w; + bidResponse.cid = bid.cid; + + bidResponse.burl = replacePriceInUrl(bid.burl, bidResponse.originalCpm || bidResponse.cpm); + + return bidResponse; +} + +function onBidWon(bid) { + if (bid.burl) { + utils.triggerPixel(bid.burl) + } +} +function replacePriceInUrl(url, price) { + return url.replace(/\${AUCTION_PRICE}/, price) +} + +export function parseUserAgent() { + const ua = navigator.userAgent.toLowerCase(); + + // Check if it's iOS + if (/iphone|ipad|ipod/.test(ua)) { + // Extract iOS version and device type + const iosInfo = /(iphone|ipad|ipod) os (\d+[._]\d+)|((iphone|ipad|ipod)(\D+cpu) os (\d+(?:[._\s]\d+)?))/.exec(ua); + return { + platform: 'ios', + version: iosInfo ? iosInfo[1] : '', + device: iosInfo ? iosInfo[2].replace('_', '.') : '' + }; + } else if (/android/.test(ua)) { + // Check if it's Android + // Extract Android version + const androidVersion = /android (\d+([._]\d+)?)/.exec(ua); + return { + platform: 'android', + version: androidVersion ? androidVersion[1].replace('_', '.') : '', + device: '' + }; + } else { + // If neither iOS nor Android, return unknown + return { + platform: 'Unknown', + version: '', + device: '' + }; + } +} + +export function getPageKeywords(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="keywords"]'); + } catch (e) { + element = document.querySelector('meta[name="keywords"]'); + } + + return ((element && element.content) || '').replaceAll(' ', ''); +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const requests = bidRequests.map((bidRequest) => { + const data = converter.toORTB({ + bidRequests: [bidRequest], + bidderRequest, + context: { mediaType }, + }); + + data.adSlotPositionOnScreen = 'ABOVE_THE_FOLD'; + data.at = 1; + + const userId = + utils.deepAccess(bidRequest, 'userId.unifiedId.id') || + utils.deepAccess(bidRequest, 'userId.id5id.uid') || + utils.deepAccess(bidRequest, 'userId.idl_env'); + + const params = getBidderParams(bidRequest); + data.device = { + ...data.device, + devicetype: 4, + geo: { + country: params.country || 'GRB' + }, + language: params.language || data.device.language, + } + data.site = { + ...data.site, + keywords: getPageKeywords(window), + publisher: { + id: params.publisherId + } + } + data.imp = [ + { + ...data.imp?.[0], + secure: 1, + pmp: params.dealId + ? { + ...data.pmp, + deals: [ + { + id: params.dealId, + }, + ], + private_auction: 1, + } + : data.pmp, + } + ] + data.adSlotPlacementId = params.placementId; + + if (userId) { + data.user = { + id: userId + } + } + + const req = { + method: 'POST', + url: url && params.trace ? url + '?trace=true' : url, + options: { withCredentials: false }, + data + } + return req + }) + + return requests; +} + +function getBidderParams(bid) { + return bid?.params ? bid.params : undefined; +} + +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video') +} + +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner') +} + +function interpretResponse(resp, req) { + const impressionId = resp.body.seatbid[0].bid[0].impid; + const bidResponses = converter.fromORTB({ request: req.data, response: resp.body }); + const ad = bidResponses[0].ad; + const trackingTag = ` + + + + + ` + + bidResponses[0].ad = ad.replace('', trackingTag + ''); + return bidResponses; +} diff --git a/modules/eightPodBidAdapter.md b/modules/eightPodBidAdapter.md new file mode 100644 index 00000000000..afefd5717de --- /dev/null +++ b/modules/eightPodBidAdapter.md @@ -0,0 +1,36 @@ +# Overview +Module Name: 8pod Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: bianca@8pod.com + +# Description + +Connect to 8pod for bids. + +This adapter requires setup and approval from the 8pod team. + +Please add eightPodAnalytics to collect user behavior and improve user experience as well. + +# Bidder Adapter configuration example + +``` +var adUnits = [{ + code: 'something', + mediaTypes: { + banner: { + sizes: [[350, 550]], + }, + }, + bids: [ + { + bidder: 'eightPod', + params: { + placementId: 13144370, + publisherId: 'publisherID-488864646', + }, + }, + ], + }]; +``` diff --git a/modules/emtvBidAdapter.js b/modules/emtvBidAdapter.js index 7a2fdae8adf..822fa683d9e 100644 --- a/modules/emtvBidAdapter.js +++ b/modules/emtvBidAdapter.js @@ -1,211 +1,19 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'emtv'; const AD_URL = 'https://us-east-ep.engagemedia.tv/pbjs'; const SYNC_URL = 'https://cs.engagemedia.tv'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: config.getConfig('bidderTimeout') - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/enrichmentFpdModule.js b/modules/enrichmentFpdModule.js deleted file mode 100644 index 59d5d326109..00000000000 --- a/modules/enrichmentFpdModule.js +++ /dev/null @@ -1,2 +0,0 @@ -// Logic from this module was moved into core since approx. 7.27 -// TODO: remove this in v8 diff --git a/modules/eplanningAnalyticsAdapter.js b/modules/eplanningAnalyticsAdapter.js deleted file mode 100644 index 45a0be54715..00000000000 --- a/modules/eplanningAnalyticsAdapter.js +++ /dev/null @@ -1,130 +0,0 @@ -import { logError } from '../src/utils.js'; -import {ajax} from '../src/ajax.js'; -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; -import adapterManager from '../src/adapterManager.js'; -import { EVENTS } from '../src/constants.js'; - -const analyticsType = 'endpoint'; -const EPL_HOST = 'https://ads.us.e-planning.net/hba/1/'; - -function auctionEndHandler(args) { - return {auctionId: args.auctionId}; -} - -function auctionInitHandler(args) { - return { - auctionId: args.auctionId, - time: args.timestamp - }; -} - -function bidRequestedHandler(args) { - return { - auctionId: args.auctionId, - time: args.start, - bidder: args.bidderCode, - bids: args.bids.map(function(bid) { - return { - time: bid.startTime, - bidder: bid.bidder, - placementCode: bid.placementCode, - auctionId: bid.auctionId, - sizes: bid.sizes - }; - }), - }; -} - -function bidResponseHandler(args) { - return { - bidder: args.bidder, - size: args.size, - auctionId: args.auctionId, - cpm: args.cpm, - time: args.responseTimestamp, - }; -} - -function bidWonHandler(args) { - return { - auctionId: args.auctionId, - size: args.width + 'x' + args.height, - }; -} - -function bidTimeoutHandler(args) { - return args.map(function(bid) { - return { - bidder: bid.bidder, - auctionId: bid.auctionId - }; - }) -} - -function callHandler(evtype, args) { - let handler = null; - - if (evtype === EVENTS.AUCTION_INIT) { - handler = auctionInitHandler; - eplAnalyticsAdapter.context.events = []; - } else if (evtype === EVENTS.AUCTION_END) { - handler = auctionEndHandler; - } else if (evtype === EVENTS.BID_REQUESTED) { - handler = bidRequestedHandler; - } else if (evtype === EVENTS.BID_RESPONSE) { - handler = bidResponseHandler - } else if (evtype === EVENTS.BID_TIMEOUT) { - handler = bidTimeoutHandler; - } else if (evtype === EVENTS.BID_WON) { - handler = bidWonHandler; - } - - if (handler) { - eplAnalyticsAdapter.context.events.push({ec: evtype, p: handler(args)}); - } -} - -var eplAnalyticsAdapter = Object.assign(adapter( - { - EPL_HOST, - analyticsType - }), -{ - track({eventType, args}) { - if (typeof args !== 'undefined') { - callHandler(eventType, args); - } - - if (eventType === EVENTS.AUCTION_END) { - try { - let strjson = JSON.stringify(eplAnalyticsAdapter.context.events); - ajax(eplAnalyticsAdapter.context.host + eplAnalyticsAdapter.context.ci + '?d=' + encodeURIComponent(strjson)); - } catch (err) {} - } - } -} -); - -eplAnalyticsAdapter.originEnableAnalytics = eplAnalyticsAdapter.enableAnalytics; - -eplAnalyticsAdapter.enableAnalytics = function (config) { - if (!config.options.ci) { - logError('Client ID (ci) option is not defined. Analytics won\'t work'); - return; - } - - eplAnalyticsAdapter.context = { - events: [], - host: config.options.host || EPL_HOST, - ci: config.options.ci - }; - - eplAnalyticsAdapter.originEnableAnalytics(config); -}; - -adapterManager.registerAnalyticsAdapter({ - adapter: eplAnalyticsAdapter, - code: 'eplanning' -}); - -export default eplAnalyticsAdapter; diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index d57804c04e6..5b3f55b9da6 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,9 +1,11 @@ -import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined} from '../src/utils.js'; +import {isEmpty, parseSizesInput, isGptPubadsDefined, getWinDimensions} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {serializeSupplyChain} from '../libraries/schainSerializer/schainSerializer.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; const BIDDER_CODE = 'eplanning'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -39,6 +41,7 @@ export const spec = { const method = 'GET'; const dfpClientId = '1'; const sec = 'ROS'; + const schain = bidRequests[0].schain; let url; let params; const urlConfig = getUrlConfig(bidRequests); @@ -70,7 +73,9 @@ export const spec = { if (pcrs) { params.crs = pcrs; } - + if (schain && schain.nodes.length <= 2) { + params.sch = serializeSupplyChain(schain, ['asi', 'sid', 'hp', 'rid', 'name', 'domain']); + } if (referrerUrl) { params.fr = cutUrl(referrerUrl); } @@ -174,7 +179,7 @@ function getUserAgent() { return window.navigator.userAgent; } function getInnerWidth() { - return getWindowSelf().innerWidth; + return getWinDimensions().innerWidth; } function isMobileUserAgent() { return getUserAgent().match(/(mobile)|(ip(hone|ad))|(android)|(blackberry)|(nokia)|(phone)|(opera\smini)/i); @@ -274,7 +279,7 @@ function getFloorStr(bid) { currency: DOLLAR_CODE, mediaType: '*', size: '*' - }); + }) || {}; if (bidFloor.floor) { return '|' + encodeURIComponent(bidFloor.floor); @@ -360,7 +365,7 @@ function waitForElementsPresent(elements) { if (index < 0) { elements.forEach(code => { let div = _getAdSlotHTMLElement(code); - if (div && div.contains(ad) && div.getBoundingClientRect().width > 0) { + if (div && div.contains(ad) && getBoundingClientRect(div).width > 0) { index = elements.indexOf(div.id); adView = div; } diff --git a/modules/equativBidAdapter.js b/modules/equativBidAdapter.js new file mode 100644 index 00000000000..77c37f5f79f --- /dev/null +++ b/modules/equativBidAdapter.js @@ -0,0 +1,299 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { tryAppendQueryString } from '../libraries/urlUtils/urlUtils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { deepAccess, deepSetValue, logError, logWarn, mergeDeep } from '../src/utils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + +const BIDDER_CODE = 'equativ'; +const COOKIE_SYNC_ORIGIN = 'https://apps.smartadserver.com'; +const COOKIE_SYNC_URL = `${COOKIE_SYNC_ORIGIN}/diff/templates/asset/csync.html`; +const LOG_PREFIX = 'Equativ:'; +const PID_STORAGE_NAME = 'eqt_pid'; + +let nwid = 0; + +let impIdMap = {}; + +/** + * Assigns values to new properties, removes temporary ones from an object + * and remove temporary default bidfloor of -1 + * @param {*} obj An object + * @param {string} key A name of the new property + * @param {string} tempKey A name of the temporary property to be removed + * @returns {*} An updated object + */ +function cleanObject(obj, key, tempKey) { + const newObj = {}; + + for (const prop in obj) { + if (prop === key) { + if (Object.prototype.hasOwnProperty.call(obj, tempKey)) { + newObj[key] = obj[tempKey]; + } + } else if (prop !== tempKey) { + newObj[prop] = obj[prop]; + } + } + + newObj.bidfloor === -1 && delete newObj.bidfloor; + + return newObj; +} + +/** + * Returns a floor price provided by the Price Floors module or the floor price set in the publisher parameters + * @param {*} bid + * @param {string} mediaType A media type + * @param {number} width A width of the ad + * @param {number} height A height of the ad + * @param {string} currency A floor price currency + * @returns {number} Floor price + */ +function getFloor(bid, mediaType, width, height, currency) { + return bid.getFloor?.({ currency, mediaType, size: [width, height] }) + .floor || bid.params.bidfloor || -1; +} + +/** + * Evaluates impressions for validity. The entry evaluated is considered valid if NEITHER of these conditions are met: + * 1) it has a `video` property defined for `mediaTypes.video` which is an empty object + * 2) it has a `native` property defined for `mediaTypes.native` which is an empty object + * @param {*} bidReq A bid request object to evaluate + * @returns boolean + */ +function isValid(bidReq) { + return !(bidReq.mediaTypes.video && JSON.stringify(bidReq.mediaTypes.video) === '{}') && !(bidReq.mediaTypes.native && JSON.stringify(bidReq.mediaTypes.native) === '{}'); +} + +/** + * Generates a 14-char string id + * @returns {string} + */ +function makeId() { + const length = 14; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let counter = 0; + let str = ''; + + while (counter++ < length) { + str += characters.charAt(Math.floor(Math.random() * characters.length)); + } + + return str; +} + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + code: BIDDER_CODE, + gvlid: 45, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + /** + * @param bidRequests + * @param bidderRequest + * @returns {ServerRequest[]} + */ + buildRequests: (bidRequests, bidderRequest) => { + if (bidRequests.filter(isValid).length === 0) { + logError(`${LOG_PREFIX} No useful bid requests to process. No requests will be sent.`, bidRequests); + return undefined; + } + + const requests = []; + + bidRequests.forEach(bid => { + const data = converter.toORTB({bidRequests: [bid], bidderRequest}); + requests.push({ + data, + method: 'POST', + url: 'https://ssb-global.smartadserver.com/api/bid?callerId=169', + }) + }); + + return requests; + }, + + /** + * @param serverResponse + * @param bidRequest + * @returns {Bid[]} + */ + interpretResponse: (serverResponse, bidRequest) => { + if (bidRequest.data?.imp?.length) { + bidRequest.data.imp.forEach(imp => imp.id = impIdMap[imp.id]); + } + + if (serverResponse.body?.seatbid?.length) { + serverResponse.body.seatbid + .filter(seat => seat?.bid?.length) + .forEach(seat => + seat.bid.forEach(bid => bid.impid = impIdMap[bid.impid]) + ); + } + + return converter.fromORTB({ + request: bidRequest.data, + response: serverResponse.body, + }); + }, + + /** + * @param bidRequest + * @returns {boolean} + */ + isBidRequestValid: (bidRequest) => { + return !!( + deepAccess(bidRequest, 'params.networkId') || + deepAccess(bidRequest, 'ortb2.site.publisher.id') || + deepAccess(bidRequest, 'ortb2.app.publisher.id') || + deepAccess(bidRequest, 'ortb2.dooh.publisher.id') + ); + }, + + /** + * @param syncOptions + * @returns {{type: string, url: string}[]} + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent) => { + if (syncOptions.iframeEnabled) { + window.addEventListener('message', function handler(event) { + if (event.origin === COOKIE_SYNC_ORIGIN && event.data.action === 'getConsent') { + event.source.postMessage({ + action: 'consentResponse', + id: event.data.id, + consents: gdprConsent.vendorData.vendor.consents + }, event.origin); + + if (event.data.pid) { + storage.setDataInLocalStorage(PID_STORAGE_NAME, event.data.pid); + } + + this.removeEventListener('message', handler); + } + }); + + let url = tryAppendQueryString(COOKIE_SYNC_URL + '?', 'nwid', nwid); + url = tryAppendQueryString(url, 'gdpr', (gdprConsent.gdprApplies ? '1' : '0')); + + return [{ type: 'iframe', url }]; + } + + return []; + } +}; + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const { siteId, pageId, formatId } = bidRequest.params; + + delete imp.dt; + + imp.secure = 1; + imp.tagid = bidRequest.adUnitCode; + + if (!deepAccess(bidRequest, 'ortb2Imp.rwdd') && deepAccess(bidRequest, 'mediaTypes.video.ext.rewarded')) { + mergeDeep(imp, { rwdd: bidRequest.mediaTypes.video.ext.rewarded }); + } + + const bidder = { ...(siteId && { siteId }), ...(pageId && { pageId }), ...(formatId && { formatId }) }; + if (Object.keys(bidder).length) { + mergeDeep(imp.ext, { bidder }); + } + + return imp; + }, + + request(buildRequest, imps, bidderRequest, context) { + const bid = context.bidRequests[0]; + const currency = config.getConfig('currency.adServerCurrency') || 'USD'; + const splitImps = []; + + imps.forEach(item => { + const floorMap = {}; + + const updateFloorMap = (type, name, width = 0, height = 0) => { + const floor = getFloor(bid, type, width, height, currency); + + if (!floorMap[floor]) { + floorMap[floor] = { + ...item, + bidfloor: floor + }; + } + + if (!floorMap[floor][name]) { + floorMap[floor][name] = type === 'banner' ? { format: [] } : item[type]; + } + + if (type === 'banner') { + floorMap[floor][name].format.push({ w: width, h: height }); + } + }; + + if (item.banner?.format?.length) { + item.banner.format.forEach(format => updateFloorMap('banner', 'bannerTemp', format?.w, format?.h)); + } + updateFloorMap('native', 'nativeTemp'); + updateFloorMap('video', 'videoTemp', item.video?.w, item.video?.h); + + Object.values(floorMap).forEach(obj => { + [ + ['banner', 'bannerTemp'], + ['native', 'nativeTemp'], + ['video', 'videoTemp'] + ].forEach(([name, tempName]) => obj = cleanObject(obj, name, tempName)); + + if (obj.banner || obj.video || obj.native) { + const id = makeId(); + impIdMap[id] = obj.id; + obj.id = id; + + splitImps.push(obj); + } + }); + }); + + const req = buildRequest(splitImps, bidderRequest, context); + + let env = ['ortb2.site.publisher', 'ortb2.app.publisher', 'ortb2.dooh.publisher'].find(propPath => deepAccess(bid, propPath)) || 'ortb2.site.publisher'; + nwid = deepAccess(bid, env + '.id') || bid.params.networkId; + deepSetValue(req, env.replace('ortb2.', '') + '.id', nwid); + + [ + { path: 'mediaTypes.video', props: ['mimes', 'placement'] }, + { path: 'ortb2Imp.audio', props: ['mimes'] }, + { path: 'mediaTypes.native.ortb', props: ['privacy', 'plcmttype', 'eventtrackers'] }, + ].forEach(({ path, props }) => { + if (deepAccess(bid, path)) { + props.forEach(prop => { + if (!deepAccess(bid, `${path}.${prop}`)) { + logWarn(`${LOG_PREFIX} Property "${path}.${prop}" is missing from request. Request will proceed, but the use of "${prop}" is strongly encouraged.`, bid); + } + }); + } + }); + + const pid = storage.getDataFromLocalStorage(PID_STORAGE_NAME); + if (pid) { + deepSetValue(req, 'user.buyeruid', pid); + } + + return req; + } +}); + +registerBidder(spec); diff --git a/modules/equativBidAdapter.md b/modules/equativBidAdapter.md new file mode 100644 index 00000000000..ceee6d19bdc --- /dev/null +++ b/modules/equativBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: Equativ Bidder Adapter (beta) +Module Type: Bidder Adapter +Maintainer: support@equativ.com +``` + +# Description + +Connect to Equativ for bids. + +The Equativ adapter requires setup and approval from the Equativ team. Please reach out to your technical account manager for more information. + +# Test Parameters + +## Web or In-app +```javascript +var adUnits = [ + { + code: '/589236/banner_1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'equativ', + params: { + networkId: 13, // mandatory if no ortb2.(site or app).publisher.id set + siteId: 20743, // optional + pageId: 89653, // optional + formatId: 291, // optional + } + } + ] + } +]; +``` \ No newline at end of file diff --git a/modules/escalaxBidAdapter.js b/modules/escalaxBidAdapter.js new file mode 100644 index 00000000000..027e41d7c56 --- /dev/null +++ b/modules/escalaxBidAdapter.js @@ -0,0 +1,106 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'escalax'; +const ESCALAX_SOURCE_ID_MACRO = '[sourceId]'; +const ESCALAX_ACCOUNT_ID_MACRO = '[accountId]'; +const ESCALAX_SUBDOMAIN_MACRO = '[subdomain]'; +const ESCALAX_URL = `https://${ESCALAX_SUBDOMAIN_MACRO}.escalax.io/bid?type=pjs&partner=${ESCALAX_SOURCE_ID_MACRO}&token=${ESCALAX_ACCOUNT_ID_MACRO}`; +const ESCALAX_DEFAULT_CURRENCY = 'USD'; +const ESCALAX_DEFAULT_SUBDOMAIN = 'bidder_us'; + +function createImp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + imp.ext = { + [BIDDER_CODE]: { + sourceId: bidRequest.params.sourceId, + accountId: bidRequest.params.accountId, + } + }; + if (!imp.bidfloor) imp.bidfloor = bidRequest.params.bidfloor; + return imp; +} + +function createRequest(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + request.test = config.getConfig('debug') ? 1 : 0; + if (!request.cur) request.cur = [bid.params.currency || ESCALAX_DEFAULT_CURRENCY]; + return request; +} + +function createBidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = 'USD'; + return bidResponse; +} + +function getSubdomain() { + const regionMap = { + 'Europe': 'bidder_eu', + 'Africa': 'bidder_eu', + 'Atlantic': 'bidder_eu', + 'Arctic': 'bidder_eu', + 'Asia': 'bidder_apac', + 'Australia': 'bidder_apac', + 'Antarctica': 'bidder_apac', + 'Pacific': 'bidder_apac', + 'Indian': 'bidder_apac', + 'America': 'bidder_us' + }; + + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; + return regionMap[region] || 'bidder_us'; + } catch (err) { + return 'bidder_us'; + } +} + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 20, + }, + imp: createImp, + request: createRequest, + bidResponse: createBidResponse +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.params.sourceId) && Boolean(bid.params.accountId); + }, + + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests && validBidRequests.length === 0) return []; + const { sourceId, accountId } = validBidRequests[0].params; + const subdomain = getSubdomain(); + const endpointURL = ESCALAX_URL + .replace(ESCALAX_SUBDOMAIN_MACRO, subdomain || ESCALAX_DEFAULT_SUBDOMAIN) + .replace(ESCALAX_SOURCE_ID_MACRO, sourceId) + .replace(ESCALAX_ACCOUNT_ID_MACRO, accountId); + const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); + return { + method: 'POST', + url: endpointURL, + data: request + }; + }, + + interpretResponse: (response, request) => { + if (response?.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; + } + return []; + }, +}; + +registerBidder(spec); diff --git a/modules/escalaxBidAdapter.md b/modules/escalaxBidAdapter.md new file mode 100644 index 00000000000..7cd45cabdc6 --- /dev/null +++ b/modules/escalaxBidAdapter.md @@ -0,0 +1,80 @@ +# Overview + +``` +Module Name: Escalax SSP Bidder Adapter +Module Type: Bidder Adapter +Maintainer: connect@escalax.io +``` + +# Description + +Escalax Bidding adapter requires setup before beginning. Please contact us at + +# Test Parameters + +```js +const adUnits = [ + { + code: "banner1", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: "escalax", + params: { + accountId: "hash", + sourceId: "sourceId", + }, + }, + ], + }, + { + code: "native_example", + mediaTypes: { + native: {}, + }, + bids: [ + { + bidder: "escalax", + params: { + accountId: "hash", + sourceId: "sourceId", + }, + }, + ], + }, + { + code: "video1", + sizes: [640, 480], + mediaTypes: { + video: { + minduration: 0, + maxduration: 999, + boxingallowed: 1, + skip: 0, + mimes: ["application/javascript", "video/mp4"], + w: 1920, + h: 1080, + protocols: [2], + linearity: 1, + api: [1, 2], + }, + }, + bids: [ + { + bidder: "escalax", + params: { + accountId: "hash", + sourceId: "sourceId", + }, + }, + ], + }, +]; +``` diff --git a/modules/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index ce01abb9e71..5516656f467 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -2,16 +2,15 @@ import {ortbConverter} from '../libraries/ortbConverter/converter.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; -import {getBidIdParameter} from '../src/utils.js'; +import {getBidIdParameter, logInfo, mergeDeep} from '../src/utils.js'; /** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests */ const BIDDER_CODE = 'eskimi'; -// const ENDPOINT = 'https://hb.eskimi.com/bids' -const ENDPOINT = 'https://sspback.eskimi.com/bid-request' - const DEFAULT_BID_TTL = 30; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_NET_REVENUE = true; @@ -21,7 +20,7 @@ const VIDEO_ORTB_PARAMS = [ 'mimes', 'minduration', 'maxduration', - 'placement', + 'plcmt', 'protocols', 'startdelay', 'skip', @@ -37,7 +36,13 @@ const VIDEO_ORTB_PARAMS = [ const BANNER_ORTB_PARAMS = [ 'battr' -] +]; + +const REGION_SUBDOMAIN_SUFFIX = { + EU: '', + US: '-us-e', + APAC: '-asia' +}; export const spec = { code: BIDDER_CODE, @@ -46,15 +51,23 @@ export const spec = { isBidRequestValid, buildRequests, interpretResponse, + getUserSyncs, /** * 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) { + logInfo('Bid won: ', bid); if (bid.burl) { utils.triggerPixel(bid.burl); } - } + }, + onTimeout: function (timeoutData) { + logInfo('Timeout: ', timeoutData); + }, + onBidderError: function ({error, bidderRequest}) { + logInfo('Error: ', error, bidderRequest); + }, } registerBidder(spec); @@ -67,7 +80,7 @@ const CONVERTER = ortbConverter({ }, imp(buildImp, bidRequest, context) { let imp = buildImp(bidRequest, context); - imp.secure = Number(window.location.protocol === 'https:'); + imp.secure = bidRequest.ortb2Imp?.secure ?? 1; if (!imp.bidfloor && bidRequest.params.bidFloor) { imp.bidfloor = bidRequest.params.bidFloor; imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' @@ -80,7 +93,24 @@ const CONVERTER = ortbConverter({ } return imp; - } + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + at: 1, + ext: { + pv: '$prebid.version$' + } + }) + const bid = context.bidRequests[0]; + if (bid.params.coppa) { + utils.deepSetValue(req, 'regs.coppa', 1); + } + if (bid.params.test) { + req.test = 1 + } + return req; + }, }); function isBidRequestValid(bidRequest) { @@ -88,7 +118,7 @@ function isBidRequestValid(bidRequest) { } function isPlacementIdValid(bidRequest) { - return utils.isNumber(bidRequest.params.placementId); + return !!parseInt(bidRequest.params.placementId); } function isValidBannerRequest(bidRequest) { @@ -102,9 +132,17 @@ function isValidVideoRequest(bidRequest) { return utils.isArray(videoSizes) && videoSizes.length > 0 && videoSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); } -function buildRequests(validBids, bidderRequest) { - let videoBids = validBids.filter(bid => isVideoBid(bid)); - let bannerBids = validBids.filter(bid => isBannerBid(bid)); +/** + * Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test. + * Make a server request from the list of BidRequests. + * + * @param {*} validBidRequests + * @param {*} bidderRequest + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + let videoBids = validBidRequests.filter(bid => isVideoBid(bid)); + let bannerBids = validBidRequests.filter(bid => isBannerBid(bid)); let requests = []; bannerBids.forEach(bid => { @@ -119,14 +157,14 @@ function buildRequests(validBids, bidderRequest) { } function interpretResponse(response, request) { - return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids; + return CONVERTER.fromORTB({request: request.data, response: response.body}).bids; } function buildVideoImp(bidRequest, imp) { const videoAdUnitParams = utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}`, {}); const videoBidderParams = utils.deepAccess(bidRequest, `params.${VIDEO}`, {}); - const videoParams = { ...videoAdUnitParams, ...videoBidderParams }; + const videoParams = {...videoAdUnitParams, ...videoBidderParams}; const videoSizes = (videoAdUnitParams && videoAdUnitParams.playerSize) || []; @@ -142,17 +180,17 @@ function buildVideoImp(bidRequest, imp) { }); if (imp.video && videoParams?.context === 'outstream') { - imp.video.placement = imp.video.placement || 4; + imp.video.plcmt = imp.video.plcmt || 4; } - return { ...imp }; + return {...imp}; } function buildBannerImp(bidRequest, imp) { const bannerAdUnitParams = utils.deepAccess(bidRequest, `mediaTypes.${BANNER}`, {}); const bannerBidderParams = utils.deepAccess(bidRequest, `params.${BANNER}`, {}); - const bannerParams = { ...bannerAdUnitParams, ...bannerBidderParams }; + const bannerParams = {...bannerAdUnitParams, ...bannerBidderParams}; let sizes = bidRequest.mediaTypes.banner.sizes; @@ -167,15 +205,15 @@ function buildBannerImp(bidRequest, imp) { } }); - return { ...imp }; + return {...imp}; } function createRequest(bidRequests, bidderRequest, mediaType) { - const data = CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } }) + const data = CONVERTER.toORTB({bidRequests, bidderRequest, context: {mediaType}}) const bid = bidRequests.find((b) => b.params.placementId) if (!data.site) data.site = {} - data.site.ext = { placementId: bid.params.placementId } + data.site.ext = {placementId: parseInt(bid.params.placementId)} if (bidderRequest.gdprConsent) { if (!data.user) data.user = {}; @@ -192,9 +230,12 @@ function createRequest(bidRequests, bidderRequest, mediaType) { return { method: 'POST', - url: ENDPOINT, + url: getBidRequestUrlByRegion(), data: data, - options: { contentType: 'application/json;charset=UTF-8', withCredentials: false } + options: { + withCredentials: true, + contentType: 'application/json;charset=UTF-8', + } } } @@ -205,3 +246,84 @@ function isVideoBid(bid) { function isBannerBid(bid) { return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); } + +/** + * @param syncOptions + * @param responses + * @param gdprConsent + * @param uspConsent + * @param gppConsent + * @return {{type: (string), url: (*|string)}[]} + */ +function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let query = []; + let syncUrl = getUserSyncUrlByRegion(); + // GDPR Consent Params in UserSync url + if (gdprConsent) { + query.push('gdpr=' + (gdprConsent.gdprApplies & 1)); + query.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + // US Privacy Consent + if (uspConsent) { + query.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + // Global Privacy Platform Consent + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + query.push('gpp=' + encodeURIComponent(gppConsent.gppString)); + query.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(','))); + } + return [{ + type: pixelType, + url: `${syncUrl}${query.length > 0 ? '&' + query.join('&') : ''}` + }]; + } +} + +/** + * Get Bid Request endpoint url by region + * @return {string} + */ +function getBidRequestUrlByRegion() { + return `https://ittr${getRegionSubdomainSuffix()}.eskimi.com/prebidjs`; +} + +/** + * Get User Sync endpoint url by region + * @return {string} + */ +function getUserSyncUrlByRegion() { + return `https://ittpx${getRegionSubdomainSuffix()}.eskimi.com/sync?sp_id=137`; +} + +/** + * Get subdomain URL suffix by region + * @return {string} + */ +function getRegionSubdomainSuffix() { + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; + + switch (region) { + case 'Europe': + case 'Africa': + case 'Atlantic': + case 'Arctic': + return REGION_SUBDOMAIN_SUFFIX['EU']; + case 'Asia': + case 'Australia': + case 'Antarctica': + case 'Pacific': + case 'Indian': + return REGION_SUBDOMAIN_SUFFIX['APAC']; + case 'America': + return REGION_SUBDOMAIN_SUFFIX['US']; + default: + return REGION_SUBDOMAIN_SUFFIX['EU']; + } + } catch (err) { + return REGION_SUBDOMAIN_SUFFIX['EU']; + } +} diff --git a/modules/eskimiBidAdapter.md b/modules/eskimiBidAdapter.md index b3494a217eb..30db251b7a8 100644 --- a/modules/eskimiBidAdapter.md +++ b/modules/eskimiBidAdapter.md @@ -48,6 +48,35 @@ Banner and video formats are supported. Where: -* placementId - Placement ID of the ad unit (required) -* bcat, badv, bapp, battr - ORTB blocking parameters as specified by OpenRTB 2.5 +* `placementId` - Placement ID of the ad unit (required) +* `bcat`, `badv`, `bapp`, `battr` - ORTB blocking parameters as specified by OpenRTB 2.5 +# ## Configuration + +Eskimi recommends the UserSync configuration below. Without it, the Eskimi adapter will not able to perform user syncs, which lowers match rate and reduces monetization. + +```javascript +pbjs.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: ['eskimi'], + filter: 'include' + } + }, + syncDelay: 6000 + }}); +``` + +### Bidder Settings + +The Eskimi bid adapter uses browser local storage. Since Prebid.js 7.x, the access to it must be explicitly set. + +```js +// https://docs.prebid.org/dev-docs/publisher-api-reference/bidderSettings.html +pbjs.bidderSettings = { + eskimi: { + storageAllowed: true + } +} +``` diff --git a/modules/etargetBidAdapter.js b/modules/etargetBidAdapter.js index cced180e061..1653d849297 100644 --- a/modules/etargetBidAdapter.js +++ b/modules/etargetBidAdapter.js @@ -1,8 +1,9 @@ -import { deepSetValue, isFn, isPlainObject } from '../src/utils.js'; +import { deepClone, deepSetValue, isFn, isPlainObject } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'etarget'; +const GVL_ID = 29; const countryMap = { 1: 'sk', 2: 'cz', @@ -19,6 +20,7 @@ const countryMap = { } export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [ BANNER, VIDEO ], isBidRequestValid: function (bid) { return !!(bid.params.refid && bid.params.country); @@ -26,7 +28,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { var i, l, bid, reqParams, netRevenue, gdprObject; var request = []; - var bids = JSON.parse(JSON.stringify(validBidRequests)); + var bids = deepClone(validBidRequests); var lastCountry = 'sk'; var floors = []; for (i = 0, l = bids.length; i < l; i++) { diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index d98dc02cdce..b97dc91effb 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -14,6 +14,13 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // eslint-disable-next-line prebid/validate-imports import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'euid'; const MODULE_REVISION = Uid2CodeVersion; const PREBID_VERSION = '$prebid.version$'; @@ -75,16 +82,16 @@ export const euidIdSubmodule = { /** * performs action to obtain id and return a value. * @function - * @param {SubmoduleConfig} [configparams] + * @param {SubmoduleConfig} [config] * @param {ConsentData|undefined} consentData - * @returns {euidId} + * @returns {IdResponse} */ getId(config, consentData) { - if (consentData?.gdprApplies !== true) { + if (consentData?.gdpr?.gdprApplies !== true) { logWarn('EUID is intended for use within the EU. The module will not run when GDPR does not apply.'); return; } - if (!hasWriteToDeviceConsent(consentData)) { + if (!hasWriteToDeviceConsent(consentData?.gdpr)) { // The module cannot operate without this permission. _logWarn(`Unable to use EUID module due to insufficient consent. The EUID module requires storage permission.`) return; @@ -103,7 +110,6 @@ export const euidIdSubmodule = { mappedConfig.cstg = { serverPublicKey: config?.params?.serverPublicKey, subscriptionId: config?.params?.subscriptionId, - optoutCheck: 1, ...extractIdentityFromParams(config?.params ?? {}) } } diff --git a/modules/exadsBidAdapter.js b/modules/exadsBidAdapter.js new file mode 100644 index 00000000000..e50c141f4b0 --- /dev/null +++ b/modules/exadsBidAdapter.js @@ -0,0 +1,514 @@ +import * as utils from '../src/utils.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER = 'exads'; + +const PARTNERS = { + ORTB_2_4: 'ortb_2_4' +}; + +const GVL_ID = 1084; + +const htmlImageOutput = 'html'; +const htmlVideoOutput = 'html'; + +const adPartnerHandlers = { + [PARTNERS.ORTB_2_4]: { + request: handleReqORTB2Dot4, + response: handleResORTB2Dot4, + validation: handleValidORTB2Dot4 + } +}; + +function handleReqORTB2Dot4(validBidRequest, endpointUrl, bidderRequest) { + utils.logInfo(`Calling endpoint for ortb_2_4:`, endpointUrl); + const gdprConsent = getGdprConsentChoice(bidderRequest); + const envParams = getEnvParams(); + + // Make a dynamic bid request to the ad partner's endpoint + let bidRequestData = { + 'id': validBidRequest.bidId, // NOT bid.bidderRequestId or bid.auctionId + 'at': 1, + 'imp': [], + 'bcat': validBidRequest.params.bcat, + 'badv': validBidRequest.params.badv, + 'site': { + 'id': validBidRequest.params.siteId, + 'name': validBidRequest.params.siteName, + 'domain': envParams.domain, + 'page': envParams.page, + 'keywords': validBidRequest.params.keywords + }, + 'device': { + 'ua': envParams.userAgent, + 'ip': validBidRequest.params.userIp, + 'geo': { + 'country': validBidRequest.params.country + }, + 'language': envParams.lang, + 'os': envParams.osName, + 'js': 0, + 'ext': { + 'accept_language': envParams.language + } + }, + 'user': { + 'id': validBidRequest.params.userId, + }, + 'ext': { + 'sub': 0, + 'prebid': { + 'channel': { + 'name': 'pbjs', + 'version': '$prebid.version$' + } + } + } + }; + + if (gdprConsent && gdprConsent.gdprApplies) { + bidRequestData.user['ext'] = { + consent: gdprConsent.consentString + } + } + + if (validBidRequest.params.dsa && ( + hasValue(validBidRequest.params.dsa.dsarequired) || + hasValue(validBidRequest.params.dsa.pubrender) || + hasValue(validBidRequest.params.dsa.datatopub))) { + bidRequestData.regs = { + 'ext': { + 'dsa': { + 'dsarequired': validBidRequest.params.dsa.dsarequired, + 'pubrender': validBidRequest.params.dsa.pubrender, + 'datatopub': validBidRequest.params.dsa.datatopub + } + } + } + } + + const impData = imps.get(validBidRequest.params.impressionId); + + // Banner setup + const bannerMediaType = utils.deepAccess(validBidRequest, 'mediaTypes.banner'); + if (bannerMediaType != null) { + impData.mediaType = BANNER; + bidRequestData.imp = bannerMediaType.sizes.map(size => { + return ({ + 'id': validBidRequest.params.impressionId, + 'bidfloor': validBidRequest.params.bidfloor, + 'bidfloorcur': validBidRequest.params.bidfloorcur, + 'banner': { + 'w': size[0], + 'h': size[1], + 'mimes': validBidRequest.params.mimes ? validBidRequest.params.mimes : undefined, + 'ext': { + image_output: htmlImageOutput, + video_output: htmlVideoOutput, + } + }, + }); + }); + } + + const nativeMediaType = utils.deepAccess(validBidRequest, 'mediaTypes.native'); + + if (nativeMediaType != null) { + impData.mediaType = NATIVE; + const nativeVersion = '1.2'; + + const native = { + 'native': { + 'ver': nativeVersion, + 'plcmttype': 4, + 'plcmtcnt': validBidRequest.params.native.plcmtcnt + } + }; + + native.native.assets = bidRequestData.imp = nativeMediaType.ortb.assets.map(asset => { + const newAsset = asset; + if (newAsset.img != null) { + newAsset.img.wmin = newAsset.img.h; + newAsset.img.hmin = newAsset.img.w; + } + return newAsset; + }); + + const imp = [{ + 'id': validBidRequest.params.impressionId, + 'bidfloor': validBidRequest.params.bidfloor, + 'bidfloorcur': validBidRequest.params.bidfloorcur, + 'native': { + 'request': JSON.stringify(native), + 'ver': nativeVersion + }, + }]; + + bidRequestData.imp = imp; + }; + + const videoMediaType = utils.deepAccess(validBidRequest, 'mediaTypes.video'); + + if (videoMediaType != null) { + impData.mediaType = VIDEO; + const imp = [{ + 'id': validBidRequest.params.impressionId, + 'bidfloor': validBidRequest.params.bidfloor, + 'bidfloorcur': validBidRequest.params.bidfloorcur, + 'video': { + 'mimes': videoMediaType.mimes, + 'protocols': videoMediaType.protocols, + }, + 'ext': validBidRequest.params.imp.ext + }]; + + bidRequestData.imp = imp; + } + + utils.logInfo('PAYLOAD', bidRequestData, JSON.stringify(bidRequestData)); + utils.logInfo('FINAL URL', endpointUrl); + + return makeBidRequest(endpointUrl, bidRequestData); +}; + +function handleResORTB2Dot4(serverResponse, request, adPartner) { + utils.logInfo('on handleResORTB2Dot4 -> request:', request); + utils.logInfo('on handleResORTB2Dot4 -> request json data:', JSON.parse(request.data)); + utils.logInfo('on handleResORTB2Dot4 -> serverResponse:', serverResponse); + + let bidResponses = []; + const bidRq = JSON.parse(request.data); + + if (serverResponse.hasOwnProperty('body') && serverResponse.body.hasOwnProperty('id')) { + utils.logInfo('Ad server response', serverResponse.body.id); + + const requestId = serverResponse.body.id; + const currency = serverResponse.body.cur; + + serverResponse.body.seatbid.forEach((seatbid, seatIndex) => { + seatbid.bid.forEach((bidData, bidIndex) => { + utils.logInfo('serverResponse.body.seatbid[' + seatIndex + '].bid[' + bidIndex + ']', bidData); + + const bidResponseAd = bidData.adm; + const bannerInfo = utils.deepAccess(bidRq.imp[0], 'banner'); + const nativeInfo = utils.deepAccess(bidRq.imp[0], 'native'); + const videoInfo = utils.deepAccess(bidRq.imp[0], 'video'); + + let w; let h = 0; + let mediaType = ''; + const native = {}; + + if (bannerInfo != null) { + w = bidRq.imp[0].banner.w; + h = bidRq.imp[0].banner.h; + mediaType = BANNER; + } else if (nativeInfo != null) { + const responseADM = JSON.parse(bidResponseAd); + responseADM.native.assets.forEach(asset => { + if (asset.img != null) { + const imgAsset = JSON.parse(bidRq.imp[0].native.request) + .native.assets.filter(asset => asset.img != null).map(asset => asset.img); + w = imgAsset[0].w; + h = imgAsset[0].h; + native.image = { + url: asset.img.url, + height: h, + width: w + } + } else if (asset.title != null) { + native.title = asset.title.text; + } else if (asset.data != null) { + native.body = asset.data.value; + } else { + utils.logWarn('bidResponse->', 'wrong asset type or null'); + } + }); + + if (responseADM.native) { + if (responseADM.native.link) { + native.clickUrl = responseADM.native.link.url; + } + if (responseADM.native.eventtrackers) { + native.impressionTrackers = []; + + responseADM.native.eventtrackers.forEach(tracker => { + if (tracker.method == 1) { + native.impressionTrackers.push(tracker.url); + } + }); + } + } + mediaType = NATIVE; + } else if (videoInfo != null) { + mediaType = VIDEO; + } + + const metaData = {}; + + if (hasValue(bidData.ext.dsa)) { + metaData.dsa = bidData.ext.dsa; + } + + const bidResponse = { + requestId: requestId, + currency: currency, + ad: bidData.adm, + cpm: bidData.price, + creativeId: bidData.crid, + cid: bidData.cid, + width: w, + ttl: 360, + height: h, + netRevenue: true, + mediaType: mediaType, + meta: metaData, + nurl: bidData.nurl.replace(/^http:\/\//i, 'https://') + }; + + if (mediaType == 'native') { + bidResponse.native = native; + } + + if (mediaType == 'video') { + bidResponse.vastXml = bidData.adm; + bidResponse.width = bidData.w; + bidResponse.height = bidData.h; + } + + utils.logInfo('bidResponse->', bidResponse); + + bidResponses.push(bidResponse); + }); + }); + } else { + imps.delete(bidRq.imp[0].id); + utils.logInfo('NO Ad server response ->', serverResponse.body.id); + } + + utils.logInfo('interpretResponse -> bidResponses:', bidResponses); + + return bidResponses; +} + +function makeBidRequest(url, data) { + const payloadString = JSON.stringify(data); + + return { + method: 'POST', + url: url, + data: payloadString + } +} + +function getUrl(adPartner, bid) { + let endpointUrlMapping = { + [PARTNERS.ORTB_2_4]: bid.params.endpoint + '?idzone=' + bid.params.zoneId + '&fid=' + bid.params.fid + }; + + return endpointUrlMapping[adPartner] ? endpointUrlMapping[adPartner] : 'defaultEndpoint'; +} + +function getEnvParams() { + const envParams = { + lang: '', + userAgent: '', + osName: '', + page: '', + domain: '', + language: '' + }; + + // TODO: all of this is already in first party data + envParams.domain = window.location.hostname; + envParams.page = window.location.protocol + '//' + window.location.host + window.location.pathname; + envParams.lang = navigator.language.indexOf('-') > -1 + ? navigator.language.split('-')[0] + : navigator.language; + envParams.userAgent = navigator.userAgent; + if (envParams.userAgent.match(/Windows/i)) { + envParams.osName = 'Windows'; + } else if (envParams.userAgent.match(/Mac OS|Macintosh/i)) { + envParams.osName = 'MacOS'; + } else if (envParams.userAgent.match(/Unix/i)) { + envParams.osName = 'Unix'; + } else if (envParams.userAgent.match(/Android/i)) { + envParams.osName = 'Android'; + } else if (envParams.userAgent.match(/iPhone|iPad|iPod/i)) { + envParams.osName = 'iOS'; + } else if (envParams.userAgent.match(/Linux/i)) { + envParams.osName = 'Linux'; + } else { + envParams.osName = 'Unknown'; + } + + let browserLanguage = navigator.language || navigator.userLanguage; + let acceptLanguage = browserLanguage.replace('_', '-'); + + envParams.language = acceptLanguage; + + utils.logInfo('Domain -> ', envParams.domain); + utils.logInfo('Page -> ', envParams.page); + utils.logInfo('Lang -> ', envParams.lang); + utils.logInfo('OS -> ', envParams.osName); + utils.logInfo('User Agent -> ', envParams.userAgent); + utils.logInfo('Primary Language -> ', envParams.language); + + return envParams; +} + +export const imps = new Map(); + +/* + Common mandatory parameters: + - endpoint + - userIp + - userIp - the minimum constraint is having the propery, empty or not + - zoneId + - partner + - fid + - siteId + - impressionId + - country + - mediaTypes?.banner or mediaTypes?.native or mediaTypes?.video + + for native parameters + - assets - it should contain the img property + + for video parameters + - mimes - it has to contain one mime type at least + - procols - it should contain one protocol at least + +*/ +function handleValidORTB2Dot4(bid) { + const bannerInfo = bid.mediaTypes?.banner; + const nativeInfo = bid.mediaTypes?.native; + const videoInfo = bid.mediaTypes?.video; + const isValid = ( + hasValue(bid.params.endpoint) && + hasValue(bid.params.userIp) && + bid.params.hasOwnProperty('userIp') && + hasValue(bid.params.zoneId) && + hasValue(bid.params.partner) && + hasValue(bid.params.fid) && + hasValue(bid.params.siteId) && + hasValue(bid.params.impressionId) && + hasValue(bid.params.country) && + hasValue(bid.params.country.length > 0) && + ((!hasValue(bid.params.bcat) || + bid.params.bcat.length > 0)) && + ((!hasValue(bid.params.badv) || + bid.params.badv.length > 0)) && + (bannerInfo || nativeInfo || videoInfo) && + (nativeInfo ? bid.params.native && + nativeInfo.ortb.assets && + nativeInfo.ortb.assets.some(asset => !!asset.img) : true) && + (videoInfo ? (videoInfo.mimes && + videoInfo.mimes.length > 0 && + videoInfo.protocols && + videoInfo.protocols.length > 0) : true)); + if (!isValid) { + utils.logError('Validation Error'); + } + + return isValid; +} + +function hasValue(value) { + return ( + value !== undefined && + value !== null + ); +} + +function getGdprConsentChoice(bidderRequest) { + const hasGdprConsent = + hasValue(bidderRequest) && + hasValue(bidderRequest.gdprConsent); + + if (hasGdprConsent) { + return bidderRequest.gdprConsent; + } + + return null; +} + +export const spec = { + aliases: ['exads'], // short code + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + isBidRequestValid: function (bid) { + utils.logInfo('on isBidRequestValid -> bid:', bid); + + if (!bid.params.partner) { + utils.logError('Validation Error', 'bid.params.partner missed'); + return false; + } else if (!Object.values(PARTNERS).includes(bid.params.partner)) { + utils.logError('Validation Error', 'bid.params.partner is not valid'); + return false; + } + + let adPartner = bid.params.partner; + + if (adPartnerHandlers[adPartner] && adPartnerHandlers[adPartner]['validation']) { + return adPartnerHandlers[adPartner]['validation'](bid); + } else { + // Handle unknown or unsupported ad partners + return false; + } + }, + buildRequests: function (validBidRequests, bidderRequest) { + utils.logInfo('on buildRequests -> validBidRequests:', validBidRequests); + utils.logInfo('on buildRequests -> bidderRequest:', bidderRequest); + + return validBidRequests.map(bid => { + let adPartner = bid.params.partner; + + imps.set(bid.params.impressionId, { adPartner: adPartner, mediaType: null }); + + let endpointUrl = getUrl(adPartner, bid); + + // Call the handler for the ad partner, passing relevant parameters + if (adPartnerHandlers[adPartner]['request']) { + return adPartnerHandlers[adPartner]['request'](bid, endpointUrl, bidderRequest); + } else { + // Handle unknown or unsupported ad partners + return null; + } + }); + }, + interpretResponse: function (serverResponse, request) { + const bid = JSON.parse(request.data); + const impData = imps.get(bid.imp[0].id); + const adPartner = impData.adPartner; + + // Call the handler for the ad partner, passing relevant parameters + if (adPartnerHandlers[adPartner]['response']) { + return adPartnerHandlers[adPartner]['response'](serverResponse, request, adPartner); + } else { + // Handle unknown or unsupported ad partners + return null; + } + }, + onTimeout: function (timeoutData) { + utils.logWarn(`onTimeout -> timeoutData:`, timeoutData); + }, + onBidWon: function (bid) { + utils.logInfo(`onBidWon -> bid:`, bid); + if (bid.nurl) { + utils.triggerPixel(bid.nurl); + } + }, + onSetTargeting: function (bid) { + utils.logInfo(`onSetTargeting -> bid:`, bid); + }, + onBidderError: function (bid) { + imps.delete(bid.bidderRequest.bids[0].params.impressionId); + utils.logInfo('onBidderError -> bid:', bid); + }, +}; + +registerBidder({ + code: BIDDER, + gvlid: GVL_ID, + ...spec +}); diff --git a/modules/exadsBidAdapter.md b/modules/exadsBidAdapter.md new file mode 100644 index 00000000000..4c8eedffdd0 --- /dev/null +++ b/modules/exadsBidAdapter.md @@ -0,0 +1,484 @@ +# Overview + +**Module Name**: Exads Bidder Adapter + +**Module Type**: Bidder Adapter + +**Maintainer**: + +## Description + +Module that connects to EXADS' bidder for bids. + +## Build + +If you don't need to use the prebidJS video module, please remove the videojsVideoProvider module. + +```bash +gulp build --modules=consentManagement,exadsBidAdapter,videojsVideoProvider +``` + +### Configuration + +Use `setConfig` to instruct Prebid.js to initilize the exadsBidAdapter, as specified below. + +* Set "debug" as true if you need to read logs; +* Set "gdprApplies" as true if you need to pass gdpr consent string; +* The tcString is the iabtcf consent string for gdpr; +* Uncomment the cache instruction if you need to configure a cache server (e.g. for instream video) + +```js +pbjs.setConfig({ + debug: false, + //cache: { url: "https://prebid.adnxs.com/pbc/v1/cache" }, + consentManagement: { + gdpr: { + cmpApi: 'static', + timeout: 1000000, + defaultGdprScope: true, + consentData: { + getTCData: { + tcString: consentString, + gdprApplies: false // set to true to pass the gdpr consent string + } + } + } + } +}); +``` + +Add the `video` config if you need to render videos using the video module. +For more info navigate to . + +```js +pbjs.setConfig({ + video: { + providers: [{ + divId: 'player', // the id related to the videojs tag in your body + vendorCode: 2, // videojs, + playerConfig: { + params: { + adPluginConfig: { + numRedirects: 10 + }, + vendorConfig: { + controls: true, + autoplay: true, + preload: "auto", + } + } + } + },] + }, +}); +``` + +### Test Parameters + +Now you will find the different parameters to set, based on publisher website. They are optional unless otherwise specified. + +#### RTB Banner 2.4 + +* **zoneId** (required) - you can get it from the endpoint created after configuring the zones (integer) +* **fid** (required) - you can get it from the endpoint created after configuring the zones (string) +* **partner** (required) - currently we support rtb 2.4 ("ortb_2_4") only (string) +* **siteId** (recommended) - unique Site ID (string) +* **siteName** site name (string) +* **banner.sizes** (required) - one integer array - [width, height] +* **userIp** (required) - IP address of the user, ipv4 or ipv6 (string) +* **userId** (*required) - unique user ID (string).*If you cannot generate a user ID, you can leave it empty (""). The request will get a response as long as "user" object is included in the request +* **country** - country ISO3 +* **impressionId** (required) - unique impression ID within this bid request (string) +* **keywords** - keywords can be used to ensure ad zones get the right type of advertising. Keywords should be a string of comma-separated words +* **bidfloor** - minimum bid for this impression (CPM) / click (CPC) and account currency (float) +* **bidfloorcur** - currency for minimum bid value specified using ISO-4217 alpha codes (string) +* **bcat** - blocked advertiser categories using the IAB content categories (string array) +* **badv** - block list of advertisers by their domains (string array) +* **mimes** - list of supported mime types. We support: image/jpeg, image/jpg, image/png, image/png, image/gif, image/webp, video/mp4 (string array) +* **dsa** - DSA transparency information + * **dsarequired** - flag to indicate if DSA information should be made available (integer) + *0 - Not required + * 1 - Supported, bid responses with or without DSA object will be accepted + *2 - Required, bid responses without DSA object will not be accepted + * 3 - Required, bid responses without DSA object will not be accepted, Publisher is an Online Platform + * **pubrender** - flag to indicate if the publisher will render the DSA Transparency info (integer) + * 0 - Publisher can't render + * 1 - Publisher could render depending on adrender + * 2 - Publisher will render + * **datatopub** - independent of pubrender, the publisher may need the transparency data for audit purposes (integer) + * 0 - do not send transparency data + * 1 - optional to send transparency data + * 2 - send transparency data +* **endpoint** (required) - EXADS endpoint (URL) + +##### RTB Banner 2.4 (Image) + +```js + +adUnits = + [{ code: 'postbid_iframe', // the frame where to render the creative + mediaTypes: { + banner: { + sizes: [300, 250] + } + }, + bids: [{ + bidder: 'exads', + params: { + zoneId: 12345, + fid: '829a896f011475d50da0d82cfdd1af8d9cdb07ff', + partner: 'ortb_2_4', + siteId: '123', + siteName: 'test.com', + userIp: '0.0.0.0', + userId: '1234', + country: 'IRL', + impressionId: impression_id.toString(), + keywords: 'lifestyle, humour', + bidfloor: 0.00000011, + bidfloorcur: 'EUR', + bcat: ['IAB25', 'IAB7-39','IAB8-18','IAB8-5','IAB9-9'], + badv: ['first.com', 'second.com'], + mimes: ['image/jpg'], + dsa: { + dsarequired: 3, + pubrender: 0, + datatopub: 2 + }, + endpoint: 'https://your-ad-network.com/rtb.php' + } + }] + }]; +``` + +##### RTB Banner 2.4 (Video) + +```js +adUnits = + [{ code: 'postbid_iframe', // the frame where to render the creative + mediaTypes: { + banner: { + sizes: [900, 250] + } + }, + bids: [{ + bidder: 'exads', + params: { + zoneId: 12345, + fid: '829a896f011475d50da0d82cfdd1af8d9cdb07ff', + partner: 'ortb_2_4', + siteId: '123', + siteName: 'test.com', + userIp: '0.0.0.0', + userId: '1234', + country: 'IRL', + impressionId: '1234', + keywords: 'lifestyle, humour', + bidfloor: 0.00000011, + bidfloorcur: 'EUR', + bcat: ['IAB25', 'IAB7-39','IAB8-18','IAB8-5','IAB9-9'], + badv: ['first.com', 'second.com'], + mimes: ['image/jpg'], + dsa: { + dsarequired: 3, + pubrender: 0, + datatopub: 2 + }, + endpoint: 'https://your-ad-network.com/rtb.php' + } + }] + }]; +``` + +#### RTB 2.4 Video (Instream/OutStream/Video Slider) - VAST XML or VAST TAG (url) + +* **zoneId** (required) - you can get it from the endpoint created after configuring the zones (integer) +* **fid** (required) - you can get it from the endpoint created after configuring the zones (string) +* **partner** (required) - currently we support rtb 2.4 ("ortb_2_4") only (string) +* **siteId** (recommended) - unique Site ID (string) +* **siteName** site name (string) +* **userIp** (required) - IP address of the user, ipv4 or ipv6 (string) +* **userId** (required) - unique user ID (string). *If you cannot generate a user ID, you can leave it empty (""). The request will get a response as long as "user" object is included in the request +* **country** - Country ISO3 (string) +* **impressionId** (required) - unique impression ID within this bid request (string) +* **keywords** - keywords can be used to ensure ad zones get the right type of advertising. Keywords should be a string of comma-separated words +* **bidfloor** - minimum bid for this impression (CPM) / click (CPC) and account currency (float) +* **bidfloorcur** - currency for minimum bid value specified using ISO-4217 alpha codes (string) +* **bcat** - blocked advertiser categories using the IAB content categories (string array) +* **badv** - block list of advertisers by their domains (string array) +* **mediaTypes.video** (required) + * **mimes** (required) - list of supported mime types (string array) + * **protocols** (required) - list of supported video bid response protocols (integer array) + * **context** - (recommended) - the video context, either 'instream', 'outstream'. Defaults to ‘instream’ (string) +* **dsa** - DSA transparency information + * **dsarequired** - flag to indicate if DSA information should be made available (integer) + *0 - Not required + * 1 - Supported, bid responses with or without DSA object will be accepted + *2 - Required, bid responses without DSA object will not be accepted + * 3 - Required, bid responses without DSA object will not be accepted, Publisher is an Online Platform + * **pubrender** - flag to indicate if the publisher will render the DSA Transparency info (integer) + * 0 - Publisher can't render + * 1 - Publisher could render depending on adrender + * 2 - Publisher will render + * **datatopub** - independent of pubrender, the publisher may need the transparency data for audit purposes (integer) + * 0 - do not send transparency data + * 1 - optional to send transparency data + * 2 - send transparency data +* **endpoint** (required) - EXADS endpoint (URL) + +```js +adUnits = [{ + code: 'postbid_iframe', + mediaTypes: { + video: { + mimes: ['video/mp4'], + context: 'instream', + protocols: [3, 6] + } + }, + bids: [{ + bidder: 'exads', + params: { + zoneId: 12345, + fid: '829a896f011475d50da0d82cfdd1af8d9cdb07ff', + partner: 'ortb_2_4', + siteId: '123', + siteName: 'test.com', + userIp: '0.0.0.0', + userId: '1234', + impressionId: '1234', + imp: { + ext: { + video_cta: 0 + } + }, + dsa: { + dsarequired: 3, + pubrender: 0, + datatopub: 2 + }, + country: 'IRL', + keywords: 'lifestyle, humour', + bidfloor: 0.00000011, + bidfloorcur: 'EUR', + bcat: ['IAB25', 'IAB7-39','IAB8-18','IAB8-5','IAB9-9'], + badv: ['first.com', 'second.com'], + endpoint: 'https://your-ad-network.com/rtb.php' + } + }] +}]; +``` + +#### RTB 2.4 Native + +* **zoneId** (required) - you can get it from the endpoint created after configuring the zones (integer) +* **fid** (required) - you can get it from the endpoint created after configuring the zones (string) +* **partner** (required) - currently we support rtb 2.4 ("ortb_2_4") only (string) +* **siteId** (recommended) - unique Site ID (string) +* **siteName** site name (string) +* **userIp** (required) - IP address of the user, ipv4 or ipv6 (string) +* **userId** (*required) - unique user ID (string).*If you cannot generate a user ID, you can leave it empty (""). The request will get a response as long as "user" object is included in the request +* **country** - country ISO3 (string) +* **impressionId** (required) - unique impression ID within this bid request (string) +* **keywords** - keywords can be used to ensure ad zones get the right type of advertising. Keywords should be a string of comma-separated words +* **bidfloor** - minimum bid for this impression (CPM) / click (CPC) and account currency (float) +* **bidfloorcur** - currency for minimum bid value specified using ISO-4217 alpha codes (string) +* **bcat** - blocked advertiser categories using the IAB content categories (string array) +* **badv** - block list of advertisers by their domains (string array) +* **dsa** - DSA transparency information + * **dsarequired** - flag to indicate if DSA information should be made available (integer) + *0 - Not required + * 1 - Supported, bid responses with or without DSA object will be accepted + *2 - Required, bid responses without DSA object will not be accepted + * 3 - Required, bid responses without DSA object will not be accepted, Publisher is an Online Platform + * **pubrender** - flag to indicate if the publisher will render the DSA Transparency info (integer) + * 0 - Publisher can't render + * 1 - Publisher could render depending on adrender + * 2 - Publisher will render + * **datatopub** - independent of pubrender, the publisher may need the transparency data for audit purposes (integer) + * 0 - do not send transparency data + * 1 - optional to send transparency data + * 2 - send transparency data +* **native.plcmtcnt** - the number of identical placements in this Layout (integer) +* **assets (title)** + * **id** - unique asset ID, assigned by exchange. Typically a counter for the array (integer): + *1 - image asset ID + * 2 - title asset ID + * 3 - description asset ID + * **required** - set to 1 if asset is required or 0 if asset is optional (integer) + * **title** + * len (required) - maximum length of the text in the title element (integer) +* **assets (data)** + * **id** - unique asset ID, assigned by exchange. Typically a counter for the array (integer): + *1 - image asset ID + * 2 - title asset ID + * 3 - description asset ID + * **data** + * **type** - type ID of the element supported by the publisher (integer). We support: + *1 - sponsored - sponsored By message where response should contain the brand name of the sponsor + * 2 - desc - descriptive text associated with the product or service being advertised + * **len** - maximum length of the text in the element’s response (integer) +* **assets (img)** + * **id** - unique asset ID, assigned by exchange. Typically a counter for the array (integer): + *1 - image asset ID + * 2 - title asset ID + * 3 - description asset ID + * **required** - set to 1 if asset is required or 0 if asset is optional (integer) + * **img** + * **type** - type ID of the image element supported by the publisher. We support: + *1 - icon image (integer) + * 3 - large image preview for the ad (integer) + * **w** - width of the image in pixels, optional (integer) + * **h** - height of the image in pixels, optional (integer) +* **endpoint** (required) - EXADS endpoint (URL) + +```js +adUnits = [{ + code: 'postbid_iframe', + mediaTypes: { + native: { + ortb: { + assets: [{ + id: 2, + required: 1, + title: { + len: 124 + } + }, + { + id: 3, + data: { + type: 1, + len: 50 + } + }, + { + id: 1, + required: 1, + img: { + type: 3, + w: 300, + h: 300 + } + }] + } + } + }, + bids: [{ + bidder: 'exads', + params: { + zoneId: 12345, + fid: '829a896f011475d50da0d82cfdd1af8d9cdb07ff', + partner: 'ortb_2_4', + siteId: '123', + siteName: 'test.com', + userIp: '0.0.0.0', + userId: '1234', + impressionId: '1234', + native: { + plcmtcnt: 4 + }, + dsa: { + dsarequired: 3, + pubrender: 0, + datatopub: 2 + }, + country: 'IRL', + keywords: 'lifestyle, humour', + bidfloor: 0.00000011, + bidfloorcur: 'EUR', + bcat: ['IAB25', 'IAB7-39','IAB8-18','IAB8-5','IAB9-9'], + badv: ['first.com', 'second.com'], + endpoint: 'https://your-ad-network.com/rtb.php' + } + }] +}]; +``` + +## DSA Transparency + +All DSA information, returned by the ad server, can be found into the **meta** tag of the response. As: + +```js +"meta": { + "dsa": { + "behalf": "...", + "paid": "...", + "transparency": [ + { + "params": [ + ... + ] + } + ], + "adrender": ... + } +} +``` + +For more information navigate to . + +## Tools and suggestions + +This section contains some suggestions that allow to set some parameters automatically. + +### User Ip / Country + +In order to detect the current user ip there are different approaches. An example is using public web services as ```https://api.ipify.org```. + +Example of usage (to add to the publisher websites): + +```html + +``` + +The same service gives the possibility to detect the country as well. Check the official web page about possible limitations of the free licence. + +### Impression Id + +Each advertising request has to be identified uniquely by an id. +One possible approach is using a classical hash function. + +```html + +``` + +### User Id + +The approach used for impression id could be used for generating a unique user id. +Also, it is recommended to store the id locally, e.g. by the browser localStorage. + +```html + +``` diff --git a/modules/excoBidAdapter.js b/modules/excoBidAdapter.js new file mode 100644 index 00000000000..eeadf4204f5 --- /dev/null +++ b/modules/excoBidAdapter.js @@ -0,0 +1,306 @@ + +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { pbsExtensions } from '../libraries/pbsExtensions/pbsExtensions.js'; +import { + mergeDeep, + deepAccess, + deepSetValue, + logInfo, + logWarn, + insertUserSyncIframe, + logError, + isStr, + generateUUID, +} from '../src/utils.js'; + +export const SID = window.excoPid || generateUUID(); +export const ENDPOINT = '//v.ex.co/se/openrtb/hb/pbjs'; +const SYNC_URL = '//cdn.ex.co/sync/e15e216-l/cookie_sync.html'; +export const BIDDER_CODE = 'exco'; +const VERSION = '0.0.1'; +const CURRENCY = 'USD'; + +const SYNC = { + done: false, +}; + +export class AdapterHelpers { + doSync(gdprConsent = { consentString: '', gdprApplies: false }, accountId) { + insertUserSyncIframe( + this.createSyncUrl(gdprConsent, accountId) + ); + } + + createSyncUrl({ consentString, gppString, applicableSections, gdprApplies }, network) { + try { + const url = new URL(window.location.protocol + SYNC_URL); + const networks = [ '368531133' ]; + + if (network) { + networks.push(network); + } + + url.searchParams.set('network', networks.join(',')); + url.searchParams.set('gdpr', encodeURIComponent((Number(gdprApplies) || 0).toString())); + url.searchParams.set('gdpr_consent', encodeURIComponent(consentString || '')); + + if (gppString && applicableSections?.length) { + url.searchParams.set('gpp', encodeURIComponent(gppString)); + url.searchParams.set('gpp_sid', encodeURIComponent(applicableSections.join(','))); + } + + return url.toString(); + } catch (error) { /* Do nothing */ } + + return null; + } + + addOrtbFirstPartyData(data, bidRequests) { + const params = bidRequests[0].params || {}; + const key = data.app ? 'app' : 'site'; + + if (data[key] && data[key].publisher && params.publisherId) { + mergeDeep(data[key].publisher, { + id: bidRequests[0].params.publisherId + }); + } + } + + getExtData(bidRequests, bidderRequest) { + return { + version: VERSION, + pbversion: '$prebid.version$', + sid: SID, + aid: bidRequests[0]?.auctionId || bidderRequest.bidderRequestId, + rc: bidRequests[0]?.bidRequestsCount, + brc: bidRequests[0]?.bidderRequestsCount, + } + } + + createRequest(converter, bidRequests, bidderRequest, mediaType) { + const data = converter.toORTB({ bidRequests, bidderRequest, context: { mediaType } }); + + data.ext[BIDDER_CODE] = this.getExtData(bidRequests, bidderRequest); + + return { method: 'POST', url: ENDPOINT, data }; + } + + isVideoBid(bid) { + return deepAccess(bid, 'mediaTypes.video'); + } + + isBannerBid(bid) { + return deepAccess(bid, 'mediaTypes.banner') || !this.isVideoBid(bid); + } + + adoptVideoImp(imp, bidRequest) { + imp.id = bidRequest.adUnitCode; + + if (bidRequest.params) { + imp.tagId = bidRequest.params.tagId; + } + } + + adoptBannerImp(imp, bidRequest) { + if (bidRequest.params) { + imp.tagId = bidRequest.params.tagId; + } + } + + adoptBidResponse(bidResponse, bid, context) { + bidResponse.bidderCode = BIDDER_CODE; + + bidResponse.vastXml = bidResponse.ad || bid.adm; + + bidResponse.ad = bid.ad; + bidResponse.adUrl = bid.adUrl; + + bidResponse.mediaType = bid.mediaType || VIDEO; + bidResponse.meta.mediaType = bid.mediaType || VIDEO; + bidResponse.meta.advertiserDomains = bidResponse.meta.advertiserDomains || []; + + bidResponse.creativeId = bidResponse.creativeId || `creative-${Date.now()}`; + bidResponse.netRevenue = bid.netRevenue || true; + bidResponse.currency = CURRENCY; + + bidResponse.cpm = bid.cpm; + + const { bidRequest } = context; + + bidResponse.width = bid.w || deepAccess(bidRequest, 'mediaTypes.video.w') || deepAccess(bidRequest, 'params.video.playerWidth'); + bidResponse.height = bid.h || deepAccess(bidRequest, 'mediaTypes.video.h') || deepAccess(bidRequest, 'params.video.playerHeight'); + + if (deepAccess(bid, 'ext.bidder.rp.advid')) { + deepSetValue(bidResponse, 'meta.advertiserId', bid.ext.bidder.rp.advid); + } + + return bidResponse; + } + + log(severity, message) { + const msg = `${BIDDER_CODE.toUpperCase()}: ${message}`; + + if (severity === 'warn') { + logWarn(msg); + } + if (severity === 'error') { + logError(msg); + } + if (severity === 'info') { + logInfo(msg); + } + } +} + +const helpers = new AdapterHelpers(); + +/** + * @description https://github.com/prebid/Prebid.js/blob/master/libraries/ortbConverter/README.md + */ +export const converter = ortbConverter({ + request(buildRequest, imps, bidderRequest, context) { + const data = buildRequest(imps, bidderRequest, context); + + if (data.cur && !data.cur.includes('USD')) { + helpers.log('warn', 'Warning - EX.CO adapter is supporting USD only. processing with USD'); + } + + data.cur = [CURRENCY]; + data.test = config.getConfig('debug') ? 1 : 0; + + helpers.addOrtbFirstPartyData(data, context.bidRequests || []); + + return data; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + imp.secure = window.location.protocol === 'http:' ? 0 : 1; + + if (imp.video) { + helpers.adoptVideoImp(imp, bidRequest) + } + + if (imp.banner) { + helpers.adoptBannerImp(imp, bidRequest); + } + + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + return helpers.adoptBidResponse(bidResponse, bid, context); + }, + context: { + ttl: 3000, + }, + processors: pbsExtensions +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {import('../src/auction.js').BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + const props = ['accountId', 'publisherId', 'tagId']; + const missing = props.filter(prop => !bid.params[prop]); + const nonStr = props.filter(prop => !isStr(bid.params[prop])); + const existingLegacy = ['cId', 'pId'].filter(prop => bid.params[prop]); + const message = `Bid will not be sent for ad unit '${bid.adUnitCode}'`; + const suggestion = 'wrap it in quotes in your config'; + + if (existingLegacy.length) { + existingLegacy.forEach(prop => { + helpers.log('warn', `Warn: '${prop}' was deprecated.`); + }); + } + + if (missing.length) { + missing.forEach(prop => { + helpers.log('warn', `Error: '${prop}' is missing. ${message}`); + }); + + return false; + } + + if (nonStr.length) { + nonStr.forEach(prop => { + helpers.log('warn', `Error: '${prop}' must be a string (${suggestion}). ${message}`); + }); + + return false; + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {import('../src/auction.js').Bid[]} bids - an array of bids + * @param {import('../src/auction.js').BidderRequest} bidderRequest - bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bids, bidderRequest) { + const videoBids = bids.filter(bid => helpers.isVideoBid(bid)); + const bannerBids = bids.filter(bid => helpers.isBannerBid(bid)); + const requests = []; + + if (bannerBids.length) { + requests.push( + helpers.createRequest(converter, bannerBids, bidderRequest, BANNER) + ); + } + + if (videoBids.length) { + requests.push( + helpers.createRequest(converter, videoBids, bidderRequest, VIDEO) + ); + } + + return requests; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {object} response A successful response from the server. + * @return {import('../src/auction.js').Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (response, request) { + const body = response?.body?.Result || response?.body || {}; + return converter.fromORTB({response: body, request: request?.data}).bids || []; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {import('../src/adapters/bidderFactory.js').SyncOptions} syncOptions Which user syncs are allowed? + * @param {object[]} serverResponses List of server's responses. + * @return {import('../src/adapters/bidderFactory.js').UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function ( + syncOptions, + serverResponses, + gdprConsent, + uspConsent + ) { + if (syncOptions.iframeEnabled && !SYNC.done) { + helpers.doSync(gdprConsent); + SYNC.done = true; + } + + return []; + }, +}; + +registerBidder(spec); diff --git a/modules/excoBidAdapter.md b/modules/excoBidAdapter.md new file mode 100644 index 00000000000..f00a3a45030 --- /dev/null +++ b/modules/excoBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +**Module Name:** Exco Bid Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** Itadmin@ex.co + +# Description + +Module that connects to Exco's demand sources. + +# Test Parameters + ```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'exco', + params: { + accountId: 'accountId', + publisherId: 'publisherId', + tagId: 'tagId', + } + } + ] + } +]; +``` diff --git a/modules/fabrickIdSystem.js b/modules/fabrickIdSystem.js index f62bafcf637..a11bd50b4f9 100644 --- a/modules/fabrickIdSystem.js +++ b/modules/fabrickIdSystem.js @@ -51,7 +51,7 @@ export const fabrickIdSubmodule = { try { const configParams = (config && config.params) || {}; if (window.fabrickMod1) { - window.fabrickMod1(configParams, consentData, cacheIdObj); + window.fabrickMod1(configParams, consentData?.gdpr, cacheIdObj); } if (!configParams || !configParams.apiKey || typeof configParams.apiKey !== 'string') { logError('fabrick submodule requires an apiKey.'); @@ -96,7 +96,7 @@ export const fabrickIdSubmodule = { success: response => { if (window.fabrickMod2) { return window.fabrickMod2( - callback, response, configParams, consentData, cacheIdObj); + callback, response, configParams, consentData?.gdpr, cacheIdObj); } else { let responseObj; if (response) { diff --git a/modules/fanAdapter.js b/modules/fanAdapter.js new file mode 100644 index 00000000000..cdcc8d19889 --- /dev/null +++ b/modules/fanAdapter.js @@ -0,0 +1,176 @@ +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + +const BIDDER_CODE = 'freedomadnetwork'; +const BASE_URL = 'https://srv.freedomadnetwork.com'; + +/** + * Build OpenRTB request from bidRequest and bidderRequest + * + * @param {BidRequest} bid + * @param {BidderRequest} bidderRequest + * @returns {Request} + */ +function buildBidRequest(bid, bidderRequest) { + const payload = { + id: bid.bidId, + tmax: bidderRequest.timeout, + placements: [bid.params.placementId], + at: 1, + user: {} + } + + const gdprConsent = utils.deepAccess(bidderRequest, 'gdprConsent'); + if (!!gdprConsent && gdprConsent.gdprApplies) { + payload.user.gdpr = 1; + payload.user.consent = gdprConsent.consentString; + } + + const uspConsent = utils.deepAccess(bidderRequest, 'uspConsent'); + if (uspConsent) { + payload.user.usp = uspConsent; + } + + return { + method: 'POST', + url: BASE_URL + '/pb/req', + data: JSON.stringify(payload), + options: { + contentType: 'application/json', + withCredentials: false, + customHeaders: { + 'Accept-Language': 'en;q=10', + }, + }, + originalBidRequest: bid + } +} + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid: function(bid) { + if (!bid) { + utils.logWarn(BIDDER_CODE, 'Invalid bid', bid); + + return false; + } + + if (!bid.params) { + utils.logWarn(BIDDER_CODE, 'bid.params is required'); + + return false; + } + + if (!bid.params.placementId) { + utils.logWarn(BIDDER_CODE, 'bid.params.placementId is required'); + + return false; + } + + var banner = utils.deepAccess(bid, 'mediaTypes.banner'); + if (banner === undefined) { + return false; + } + + return true; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + return validBidRequests.map(bid => buildBidRequest(bid, bidderRequest)); + }, + + /** + * 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. + */ + interpretResponse: function (serverResponse, bidRequest) { + const serverBody = serverResponse.body; + let bidResponses = []; + + if (!serverBody) { + return bidResponses; + } + + serverBody.forEach((response) => { + const bidResponse = { + requestId: response.id, + bidid: response.bidid, + impid: response.impid, + userId: response.userId, + cpm: response.cpm, + currency: response.currency, + width: response.width, + height: response.height, + ad: response.payload, + ttl: response.ttl, + creativeId: response.crid, + netRevenue: response.netRevenue, + trackers: response.trackers, + meta: { + mediaType: response.mediaType, + advertiserDomains: response.domains, + } + }; + + bidResponses.push(bidResponse); + }); + + return bidResponses; + }, + + /** + * 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) { + return; + } + + const payload = { + id: bid.bidid, + impid: bid.impid, + t: bid.cpm, + u: bid.userId, + } + + ajax(BASE_URL + '/pb/imp', null, JSON.stringify(payload), { + method: 'POST', + customHeaders: { + 'Accept-Language': 'en;q=10', + }, + }); + + if (bid.trackers && bid.trackers.length > 0) { + for (var i = 0; i < bid.trackers.length; i++) { + if (bid.trackers[i].type == 0) { + utils.triggerPixel(bid.trackers[i].url); + } + } + } + }, + onSetTargeting: function(bid) {}, + onBidderError: function(error) { + utils.logError(`${BIDDER_CODE} bidder error`, error); + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + return syncs; + }, + onTimeout: function(timeoutData) {}, + supportedMediaTypes: [BANNER, NATIVE] +} + +registerBidder(spec); diff --git a/modules/fanAdapter.md b/modules/fanAdapter.md new file mode 100644 index 00000000000..caa18ca8552 --- /dev/null +++ b/modules/fanAdapter.md @@ -0,0 +1,40 @@ +# Freedom Ad Network Bidder Adapter + +# Overview + +``` +Module Name: Freedom Ad Network Bidder Adapter +Module Type: Bidder Adapter +Maintainer: info@freedomadnetwork.com +``` + +## Description + +Module that connects to FAN's demand sources. + +## Bid Parameters + +| Name | Scope | Type | Description | Example | +|---------------|----------|--------------------|-----------------------------------------|-------------------------------------------------| +| `placementId` | required | String | The Placement Id provided by FAN. | `e6203f1e-bd6d-4f42-9895-d1a19cdb83c8` | + +## Example + +### Banner Ads + +```javascript +var adUnits = [{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'freedomadnetwork', + params: { + placementId: 'e6203f1e-bd6d-4f42-9895-d1a19cdb83c8' + } + }] +}]; +``` diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 5ee9906b5df..8535ce15d19 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -90,7 +90,7 @@ const VERSION = '1.0.6'; /** * @typedef {object} FeedAdServerResponse - * @extends ServerResponse + * @augments ServerResponse * @inner * * @property {FeedAdApiBidResponse[]} body - the body of a FeedAd server response diff --git a/modules/finativeBidAdapter.js b/modules/finativeBidAdapter.js index 87580a209bb..0cdcae15e61 100644 --- a/modules/finativeBidAdapter.js +++ b/modules/finativeBidAdapter.js @@ -3,9 +3,9 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {NATIVE} from '../src/mediaTypes.js'; -import {_map, deepAccess, deepSetValue, isEmpty} from '../src/utils.js'; -import {config} from '../src/config.js'; +import {_map, deepSetValue, isEmpty, setOnAny} from '../src/utils.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; const BIDDER_CODE = 'finative'; const DEFAULT_CUR = 'EUR'; @@ -64,7 +64,7 @@ export const spec = { validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); const pt = setOnAny(validBidRequests, 'params.pt') || setOnAny(validBidRequests, 'params.priceType') || 'net'; const tid = bidderRequest.ortb2?.source?.tid; - const cur = [config.getConfig('currency.adServerCurrency') || DEFAULT_CUR]; + const cur = [getCurrencyFromBidderRequest(bidderRequest) || DEFAULT_CUR]; let url = bidderRequest.refererInfo.referer; const imp = validBidRequests.map((bid, id) => { @@ -224,15 +224,6 @@ function parseNative(bid) { return result; } -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; - } - } -} - function flatten(arr) { return [].concat(...arr); } diff --git a/modules/fintezaAnalyticsAdapter.js b/modules/fintezaAnalyticsAdapter.js index ab41272c85f..78777cd6478 100644 --- a/modules/fintezaAnalyticsAdapter.js +++ b/modules/fintezaAnalyticsAdapter.js @@ -70,7 +70,8 @@ function initFirstVisit() { let cookies; try { - cookies = parseCookies(document.cookie); + // TODO: commented out because of rule violations + cookies = {} // parseCookies(document.cookie); } catch (a) { cookies = {}; } @@ -91,7 +92,8 @@ function initFirstVisit() { return visitDate; } - +// TODO: commented out because of rule violations +/* function trim(string) { if (string.trim) { return string.trim(); @@ -130,6 +132,7 @@ function parseCookies(cookie) { return values; } +*/ function getRandAsStr(digits) { let str = ''; @@ -172,7 +175,8 @@ function initSession() { let isNew = false; try { - cookies = parseCookies(document.cookie); + // TODO: commented out because of rule violations + cookies = {} // parseCookies(document.cookie); } catch (a) { cookies = {}; } @@ -263,7 +267,8 @@ function getTrackRequestLastTime() { ); } - cookie = parseCookies(document.cookie); + // TODO: commented out because of rule violations + cookie = {} // parseCookies(document.cookie); cookie = cookie[ TRACK_TIME_KEY ]; if (cookie) { return parseInt(cookie, 10); diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js deleted file mode 100644 index bda4494faaf..00000000000 --- a/modules/fledgeForGpt.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * GPT-specific slot configuration logic for PAAPI. - */ -import {submodule} from '../src/hook.js'; -import {deepAccess, logInfo, logWarn} from '../src/utils.js'; -import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; -import {config} from '../src/config.js'; -import {getGlobal} from '../src/prebidGlobal.js'; - -// import parent module to keep backwards-compat for NPM consumers after paapi was split from fledgeForGpt -// there's a special case in webpack.conf.js to avoid duplicating build output on non-npm builds -// TODO: remove this in prebid 9 -// eslint-disable-next-line prebid/validate-imports -import './paapi.js'; -const MODULE = 'fledgeForGpt'; - -let getPAAPIConfig; - -// for backwards compat, we attempt to automatically set GPT configuration as soon as we -// have the auction configs available. Disabling this allows one to call pbjs.setPAAPIConfigForGPT at their -// own pace. -let autoconfig = true; - -Object.entries({ - [MODULE]: MODULE, - 'paapi': 'paapi.gpt' -}).forEach(([topic, ns]) => { - const configKey = `${ns}.autoconfig`; - config.getConfig(topic, (cfg) => { - autoconfig = deepAccess(cfg, configKey, true); - }); -}); - -export function slotConfigurator() { - const PREVIOUSLY_SET = {}; - return function setComponentAuction(adUnitCode, auctionConfigs, reset = true) { - const gptSlot = getGptSlotForAdUnitCode(adUnitCode); - if (gptSlot && gptSlot.setConfig) { - let previous = PREVIOUSLY_SET[adUnitCode] ?? {}; - let configsBySeller = Object.fromEntries(auctionConfigs.map(cfg => [cfg.seller, cfg])); - const sellers = Object.keys(configsBySeller); - if (reset) { - configsBySeller = Object.assign(previous, configsBySeller); - previous = Object.fromEntries(sellers.map(seller => [seller, null])); - } else { - sellers.forEach(seller => { - previous[seller] = null; - }); - } - Object.keys(previous).length ? PREVIOUSLY_SET[adUnitCode] = previous : delete PREVIOUSLY_SET[adUnitCode]; - const componentAuction = Object.entries(configsBySeller) - .map(([configKey, auctionConfig]) => ({configKey, auctionConfig})); - if (componentAuction.length > 0) { - gptSlot.setConfig({componentAuction}); - logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); - } - } else if (auctionConfigs.length > 0) { - logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); - } - }; -} - -const setComponentAuction = slotConfigurator(); - -export function onAuctionConfigFactory(setGptConfig = setComponentAuction) { - return function onAuctionConfig(auctionId, configsByAdUnit, markAsUsed) { - if (autoconfig) { - Object.entries(configsByAdUnit).forEach(([adUnitCode, cfg]) => { - setGptConfig(adUnitCode, cfg?.componentAuctions ?? []); - markAsUsed(adUnitCode); - }); - } - } -} - -export function setPAAPIConfigFactory( - getConfig = (filters) => getPAAPIConfig(filters, true), - setGptConfig = setComponentAuction) { - /** - * Configure GPT slots with PAAPI auction configs. - * `filters` are the same filters accepted by `pbjs.getPAAPIConfig`; - */ - return function(filters = {}) { - let some = false; - Object.entries( - getConfig(filters) || {} - ).forEach(([au, config]) => { - if (config != null) { - some = true; - } - setGptConfig(au, config?.componentAuctions || [], true); - }) - if (!some) { - logInfo(`${MODULE}: No component auctions available to set`); - } - } -} -/** - * Configure GPT slots with PAAPI component auctions. Accepts the same filter arguments as `pbjs.getPAAPIConfig`. - */ -getGlobal().setPAAPIConfigForGPT = setPAAPIConfigFactory(); - -submodule('paapi', { - name: 'gpt', - onAuctionConfig: onAuctionConfigFactory(), - init(params) { - getPAAPIConfig = params.getPAAPIConfig; - } -}); diff --git a/modules/flippBidAdapter.js b/modules/flippBidAdapter.js index f9c424d9da5..95fd67c779b 100644 --- a/modules/flippBidAdapter.js +++ b/modules/flippBidAdapter.js @@ -25,6 +25,7 @@ const DEFAULT_CREATIVE_TYPE = 'NativeX'; const VALID_CREATIVE_TYPES = ['DTX', 'NativeX']; const FLIPP_USER_KEY = 'flipp-uid'; const COMPACT_DEFAULT_HEIGHT = 600; +const STANDARD_DEFAULT_HEIGHT = 1800; let userKey = null; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -166,7 +167,10 @@ export const spec = { if (!isEmpty(res) && !isEmpty(res.decisions) && !isEmpty(res.decisions.inline)) { return res.decisions.inline.map(decision => { const placement = placements.find(p => p.prebid.requestId === decision.prebid?.requestId); - const height = placement.options?.startCompact ? COMPACT_DEFAULT_HEIGHT : decision.height; + const customData = decision.contents[0]?.data?.customData; + const height = placement.options?.startCompact + ? customData?.compactHeight ?? COMPACT_DEFAULT_HEIGHT + : customData?.standardHeight ?? STANDARD_DEFAULT_HEIGHT; return { bidderCode: BIDDER_CODE, requestId: decision.prebid?.requestId, diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index c0ae55efc89..8ccafab76bb 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -41,7 +41,7 @@ export const spec = { _each(validBidRequests, (request) => { const impExt = request.ortb2Imp?.ext; - const data = Object(); + const data = {}; data.page = page; data.adUnitCode = request.adUnitCode; @@ -72,7 +72,17 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(data, 'regs.coppa', 1); } - + if (bidderRequest.gppConsent) { + deepSetValue(data, 'regs.gpp', { + string: bidderRequest.gppConsent.gppString, + sid: bidderRequest.gppConsent.applicableSections + }); + } else if (bidderRequest.ortb2?.regs?.gpp) { + deepSetValue(data, 'regs.gpp', { + string: bidderRequest.ortb2.regs.gpp, + sid: bidderRequest.ortb2.regs.gpp_sid + }); + } data.sizes = []; _each(request.sizes, (size) => { data.sizes.push({ diff --git a/modules/fpdModule/index.js b/modules/fpdModule/index.js index 0b0c0906f84..608b1763f9b 100644 --- a/modules/fpdModule/index.js +++ b/modules/fpdModule/index.js @@ -5,7 +5,7 @@ import { config } from '../../src/config.js'; import { module, getHook } from '../../src/hook.js'; import {logError} from '../../src/utils.js'; -import {GreedyPromise} from '../../src/utils/promise.js'; +import {PbPromise} from '../../src/utils/promise.js'; import {timedAuctionHook} from '../../src/utils/perfMetrics.js'; let submodules = []; @@ -20,12 +20,12 @@ export function reset() { export function processFpd({global = {}, bidder = {}} = {}) { let modConf = config.getConfig('firstPartyData') || {}; - let result = GreedyPromise.resolve({global, bidder}); + let result = PbPromise.resolve({global, bidder}); submodules.sort((a, b) => { return ((a.queue || 1) - (b.queue || 1)); }).forEach(submodule => { result = result.then( - ({global, bidder}) => GreedyPromise.resolve(submodule.processFpd(modConf, {global, bidder})) + ({global, bidder}) => PbPromise.resolve(submodule.processFpd(modConf, {global, bidder})) .catch((err) => { logError(`Error in FPD module ${submodule.name}`, err); return {}; diff --git a/modules/fpdModule/index.md b/modules/fpdModule/index.md index 238881db96b..42ae663b090 100644 --- a/modules/fpdModule/index.md +++ b/modules/fpdModule/index.md @@ -44,6 +44,5 @@ pbjs.setConfig({ At least one of the submodules must be included in order to successfully run the corresponding above operations. -enrichmentFpdModule validationFpdModule -topicsFpdModule \ No newline at end of file +topicsFpdModule diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index e11aa3f8fb7..fc85edc483b 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -76,7 +76,7 @@ function getPricing(xmlNode) { var priceNode = pricingExtNode.querySelector('Price'); princingData = { currency: priceNode.getAttribute('currency'), - price: priceNode.textContent || priceNode.innerText + price: priceNode.textContent }; } else { logWarn('PREBID - ' + BIDDER_CODE + ': No bid received or missing pricing extension.'); @@ -110,7 +110,7 @@ function getAdvertiserDomain(xmlNode) { // Currently we only return one Domain if (brandExtNode) { var domainNode = brandExtNode.querySelector('Domain'); - domain.push(domainNode.textContent || domainNode.innerText); + domain.push(domainNode.textContent); } else { logWarn('PREBID - ' + BIDDER_CODE + ': No bid received or missing StickyBrand extension.'); } @@ -231,7 +231,7 @@ function getBidFloor(bid, config) { mediaType: typeof bid.mediaTypes['banner'] == 'object' ? 'banner' : 'video', size: '*', }); - return bidFloor.floor; + return bidFloor?.floor; } catch (e) { return -1; } diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 1794c3f76f4..7474703974d 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -8,7 +8,6 @@ import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; -import {uspDataHandler} from '../src/adapterManager.js'; import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; @@ -142,7 +141,7 @@ export const ftrackIdSubmodule = { } // Creates an async script element and appends it to the document - loadExternalScript(config.params.url, MODULE_NAME); + loadExternalScript(config.params.url, MODULE_TYPE_UID, MODULE_NAME); } }; }, @@ -192,18 +191,18 @@ export const ftrackIdSubmodule = { isThereConsent: function(consentData) { let consentValue = true; - + const {gdpr, usp} = consentData ?? {}; /* * Scenario 1: GDPR * if GDPR Applies is true|1, we do not have consent * if GDPR Applies does not exist or is false|0, we do not NOT have consent */ - if (consentData && consentData.gdprApplies && (consentData.gdprApplies === true || consentData.gdprApplies === 1)) { + if (gdpr?.gdprApplies === true || gdpr?.gdprApplies === 1) { consentInfo.gdpr.applies = 1; consentValue = false; } // If consentString exists, then we store it even though we are not using it - if (consentData && consentData.consentString !== 'undefined' && !utils.isEmpty(consentData.consentString) && !utils.isEmptyStr(consentData.consentString)) { + if (typeof gdpr?.consentString !== 'undefined' && !utils.isEmpty(gdpr.consentString) && !utils.isEmptyStr(gdpr.consentString)) { consentInfo.gdpr.consentString = consentData.consentString; } @@ -213,7 +212,6 @@ export const ftrackIdSubmodule = { * parse the us_privacy string to see if we have consent * for version 1 of us_privacy strings, if 'Opt-Out Sale' is 'Y' we do not track */ - const usp = uspDataHandler.getConsentData(); let usPrivacyVersion; // let usPrivacyOptOut; let usPrivacyOptOutSale; diff --git a/modules/gameraRtdProvider.js b/modules/gameraRtdProvider.js new file mode 100644 index 00000000000..96c4bac5f87 --- /dev/null +++ b/modules/gameraRtdProvider.js @@ -0,0 +1,107 @@ +import { submodule } from '../src/hook.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { + isPlainObject, + logError, + mergeDeep, + deepClone, +} from '../src/utils.js'; + +const MODULE_NAME = 'gamera'; +const MODULE = `${MODULE_NAME}RtdProvider`; + +/** + * Initialize the Gamera RTD Module. + * @param {Object} config + * @param {Object} userConsent + * @returns {boolean} + */ +function init(config, userConsent) { + return true; +} + +/** + * Modify bid request data before auction + * @param {Object} reqBidsConfigObj - The bid request config object + * @param {function} callback - Callback function to execute after data handling + * @param {Object} config - Module configuration + * @param {Object} userConsent - User consent data + */ +function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + // Check if window.gamera.getPrebidSegments is available + if (typeof window.gamera?.getPrebidSegments !== 'function') { + window.gamera = window.gamera || {}; + window.gamera.cmd = window.gamera.cmd || []; + window.gamera.cmd.push(function () { + enrichAuction(reqBidsConfigObj, callback, config, userConsent); + }); + return; + } + + enrichAuction(reqBidsConfigObj, callback, config, userConsent); +} + +/** + * Enriches the auction with user and content segments from Gamera's on-page script + * @param {Object} reqBidsConfigObj - The bid request config object + * @param {Function} callback - Callback function to execute after data handling + * @param {Object} config - Module configuration + * @param {Object} userConsent - User consent data + */ +function enrichAuction(reqBidsConfigObj, callback, config, userConsent) { + try { + /** + * @function external:"window.gamera".getPrebidSegments + * @description Retrieves user and content segments from Gamera's on-page script + * @param {Function|null} onSegmentsUpdateCallback - Callback for segment updates (not used here) + * @param {Object} config - Module configuration + * @param {Object} userConsent - User consent data + * @returns {Object|undefined} segments - The targeting segments object containing: + * @property {Object} [user] - User-level attributes to merge into ortb2.user + * @property {Object} [site] - Site-level attributes to merge into ortb2.site + * @property {Object.} [adUnits] - Ad unit specific attributes, keyed by adUnitCode, + * to merge into each ad unit's ortb2Imp + */ + const segments = window.gamera.getPrebidSegments(null, deepClone(config || {}), deepClone(userConsent || {})) || {}; + + // Initialize ortb2Fragments and its nested objects + reqBidsConfigObj.ortb2Fragments = reqBidsConfigObj.ortb2Fragments || {}; + reqBidsConfigObj.ortb2Fragments.global = reqBidsConfigObj.ortb2Fragments.global || {}; + + // Add user-level data + if (segments.user && isPlainObject(segments.user)) { + reqBidsConfigObj.ortb2Fragments.global.user = reqBidsConfigObj.ortb2Fragments.global.user || {}; + mergeDeep(reqBidsConfigObj.ortb2Fragments.global.user, segments.user); + } + + // Add site-level data + if (segments.site && isPlainObject(segments.site)) { + reqBidsConfigObj.ortb2Fragments.global.site = reqBidsConfigObj.ortb2Fragments.global.site || {}; + mergeDeep(reqBidsConfigObj.ortb2Fragments.global.site, segments.site); + } + + // Add adUnit-level data + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits || []; + adUnits.forEach(adUnit => { + const gameraData = segments.adUnits && segments.adUnits[adUnit.code]; + if (!gameraData || !isPlainObject(gameraData)) { + return; + } + + adUnit.ortb2Imp = adUnit.ortb2Imp || {}; + mergeDeep(adUnit.ortb2Imp, gameraData); + }); + } catch (error) { + logError(MODULE, 'Error getting segments:', error); + } + + callback(); +} + +export const subModuleObj = { + name: MODULE_NAME, + init: init, + getBidRequestData: getBidRequestData, +}; + +submodule('realTimeData', subModuleObj); diff --git a/modules/gameraRtdProvider.md b/modules/gameraRtdProvider.md new file mode 100644 index 00000000000..44260b88d1f --- /dev/null +++ b/modules/gameraRtdProvider.md @@ -0,0 +1,51 @@ +# Overview + +Module Name: Gamera Rtd Provider +Module Type: Rtd Provider +Maintainer: aleksa@gamera.ai + +# Description + +RTD provider for Gamera.ai that enriches bid requests with real-time data, by populating the [First Party Data](https://docs.prebid.org/features/firstPartyData.html) attributes. +The module integrates with Gamera's AI-powered audience segmentation system to provide enhanced bidding capabilities. +The Gamera RTD Provider works in conjunction with the Gamera script, which must be available on the page for the module to enrich bid requests. To learn more about the Gamera script, please visit the [Gamera website](https://gamera.ai/). + +ORTB2 enrichments that gameraRtdProvider can provide: + * `ortb2.site` + * `ortb2.user` + * `AdUnit.ortb2Imp` + +# Integration + +## Build + +Include the Gamera RTD module in your Prebid.js build: + +```bash +gulp build --modules=rtdModule,gameraRtdProvider +``` + +## Configuration + +Configure the module in your Prebid.js configuration: + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'gamera', + params: { + // Optional configuration parameters + } + }] + } +}); +``` + +### Configuration Parameters + +The module currently supports basic initialization without required parameters. Future versions may include additional configuration options. + +## Support + +For more information or support, please contact gareth@gamera.ai. diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index 1c279cdb9b8..32be0f0ee13 100644 --- a/modules/gamoshiBidAdapter.js +++ b/modules/gamoshiBidAdapter.js @@ -17,7 +17,8 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {includes} from '../src/polyfill.js'; const ENDPOINTS = { - 'gamoshi': 'https://rtb.gamoshi.io' + 'gamoshi': 'https://rtb.gamoshi.io', + 'cleanmedianet': 'https://bidder.cleanmediaads.com' }; const DEFAULT_TTL = 360; @@ -66,7 +67,7 @@ export const helper = { export const spec = { code: 'gamoshi', - aliases: ['gambid', '9MediaOnline'], + aliases: ['gambid', 'cleanmedianet'], supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { @@ -81,7 +82,9 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { const {adUnitCode, mediaTypes, params, sizes, bidId} = bidRequest; - const baseEndpoint = params['rtbEndpoint'] || ENDPOINTS['gamoshi']; + + const bidderCode = bidderRequest.bidderCode || 'gamoshi'; + const baseEndpoint = params['rtbEndpoint'] || ENDPOINTS[bidderCode] || 'https://rtb.gamoshi.io'; const rtbEndpoint = `${baseEndpoint}/r/${params.supplyPartnerId}/bidr?rformat=open_rtb&reqformat=rtb_json&bidder=prebid` + (params.query ? '&' + params.query : ''); const rtbBidRequest = { id: bidderRequest.bidderRequestId, @@ -157,7 +160,7 @@ export const spec = { maxduration: bidRequest.mediaTypes.video.maxduration, api: bidRequest.mediaTypes.video.api, skip: bidRequest.mediaTypes.video.skip || bidRequest.params.video.skip, - placement: bidRequest.mediaTypes.video.placement || bidRequest.params.video.placement, + plcmt: bidRequest.mediaTypes.video.plcmt || bidRequest.params.video.plcmt, minduration: bidRequest.mediaTypes.video.minduration || bidRequest.params.video.minduration, playbackmethod: bidRequest.mediaTypes.video.playbackmethod || bidRequest.params.video.playbackmethod, startdelay: bidRequest.mediaTypes.video.startdelay || bidRequest.params.video.startdelay diff --git a/modules/genericAnalyticsAdapter.js b/modules/genericAnalyticsAdapter.js index 7f721863912..ce37e5c02fe 100644 --- a/modules/genericAnalyticsAdapter.js +++ b/modules/genericAnalyticsAdapter.js @@ -142,7 +142,7 @@ export function defaultHandler({url, method, batchSize, ajax = ajaxBuilder()}) { const serialize = method === 'GET' ? (data) => ({data: JSON.stringify(data)}) : (data) => JSON.stringify(data); return function (events) { - ajax(url, callbacks, serialize(extract(events)), {method}) + ajax(url, callbacks, serialize(extract(events)), {method, keepalive: true}) } } diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index 46f7e7f7d6d..09e717a112f 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -23,6 +23,7 @@ import { EVENTS } from '../src/constants.js'; import { loadExternalScript } from '../src/adloader.js'; import { auctionManager } from '../src/auctionManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -53,6 +54,10 @@ export let wrapper let wrapperReady; /** @type {boolean} */; let preloaded; +/** @type {object} */; +let refererInfo = getRefererInfo(); +/** @type {object} */; +let overrides = window.grumi?.overrides; /** * fetches the creative wrapper @@ -75,9 +80,8 @@ export function setWrapper(responseText) { } export function getInitialParams(key) { - let refererInfo = getRefererInfo(); let params = { - wver: 'pbjs', + wver: '1.1.1', wtype: 'pbjs-module', key, meta: { @@ -105,7 +109,7 @@ export function preloadClient(key) { insertElement(iframe); iframe.contentWindow.grumi = getInitialParams(key); let url = getClientUrl(key); - loadExternalScript(url, SUBMODULE_NAME, markAsLoaded, iframe.contentDocument); + loadExternalScript(url, MODULE_TYPE_RTD, SUBMODULE_NAME, markAsLoaded, iframe.contentDocument); } /** @@ -141,7 +145,7 @@ export function getMacros(bid, key) { '%_hbcid!': bid.creativeId || '', '%_hbadomains': bid.meta && bid.meta.advertiserDomains, '%%PATTERN:hb_pb%%': bid.pbHg, - '%%SITE%%': location.hostname, + '%%SITE%%': overrides?.site || refererInfo.domain, '%_pimp%': PV_ID, '%_hbCpm!': bid.cpm, '%_hbCurrency!': bid.currency @@ -256,7 +260,7 @@ function fireBillableEventsForApplicableBids(params) { function setupInPage(params) { window.grumi = params; window.grumi.fromPrebid = true; - loadExternalScript(getInPageUrl(params.key), SUBMODULE_NAME); + loadExternalScript(getInPageUrl(params.key), MODULE_TYPE_RTD, SUBMODULE_NAME); } function init(config, userConsent) { diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index a8888893333..67a0e1e91be 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -152,7 +152,7 @@ function getBidFloor(bidRequest, currency) { currency: currency || DEFAULT_CURRENCY, mediaType: bidRequest.mediaType, size: bidRequest.sizes || '*' - }); + }) || {}; } return { diff --git a/modules/globalsunBidAdapter.js b/modules/globalsunBidAdapter.js index 5b5d97c2cac..1684509b7b9 100644 --- a/modules/globalsunBidAdapter.js +++ b/modules/globalsunBidAdapter.js @@ -1,212 +1,19 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'globalsun'; const AD_URL = 'https://endpoint.globalsun.io/pbjs'; const SYNC_URL = 'https://cs.globalsun.io'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index d7af51f7312..e0a5861f40c 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -1,3 +1,7 @@ +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { tryAppendQueryString } from '../libraries/urlUtils/urlUtils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; import { createTrackPixelHtml, deepAccess, @@ -7,10 +11,6 @@ import { isEmpty, logError } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; -import {BANNER} from '../src/mediaTypes.js'; -import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -50,7 +50,7 @@ export const spec = { const bidRequests = []; const urlInfo = getUrlInfo(bidderRequest.refererInfo); - const cur = getCurrencyType(); + const cur = getCurrencyType(bidderRequest); const dnt = getDNT() ? '1' : '0'; for (let i = 0; i < validBidRequests.length; i++) { @@ -156,11 +156,8 @@ export const spec = { }; -function getCurrencyType() { - if (config.getConfig('currency.adServerCurrency')) { - return config.getConfig('currency.adServerCurrency'); - } - return 'JPY'; +function getCurrencyType(bidderRequest) { + return getCurrencyFromBidderRequest(bidderRequest) || 'JPY'; } function getUrlInfo(refererInfo) { diff --git a/modules/goldbachBidAdapter.js b/modules/goldbachBidAdapter.js index 9f9913b7023..1b59bcb63ec 100644 --- a/modules/goldbachBidAdapter.js +++ b/modules/goldbachBidAdapter.js @@ -1,1133 +1,423 @@ -import {Renderer} from '../src/Renderer.js'; -import { - createTrackPixelHtml, - deepAccess, - deepClone, - getBidRequest, - getParameterByName, - isArray, - isArrayOfNums, - isEmpty, - isFn, - isNumber, - isPlainObject, - isStr, - logError, - logInfo, - logMessage -} from '../src/utils.js'; -import {config} from '../src/config.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {find, includes} from '../src/polyfill.js'; -import {INSTREAM, OUTSTREAM} from '../src/video.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; -import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; -import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; -import {chunk} from '../libraries/chunk/chunk.js'; - -/** - * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest - * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid - */ - +import { ajax } from '../src/ajax.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { Renderer } from '../src/Renderer.js'; +import { deepAccess } from '../src/utils.js'; + +/* General config */ +const IS_LOCAL_MODE = false; const BIDDER_CODE = 'goldbach'; -const URL = 'https://ib.adnxs.com/ut/v3/prebid'; -const PRICING_URL = 'https://templates.da-services.ch/01_universal/burda_prebid/1.0/json/sizeCPMMapping.json'; -const URL_SIMPLE = 'https://ib.adnxs-simple.com/ut/v3/prebid'; -const VIDEO_TARGETING = ['id', 'minduration', 'maxduration', - 'skippable', 'playback_method', 'frameworks', 'context', 'skipoffset']; -const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api']; -const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; -const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately -const DEBUG_PARAMS = ['enabled', 'dongle', 'member_id', 'debug_timeout']; -const DEFAULT_PRICE_MAPPING = { - '0x0': 2.5, - '300x600': 5, - '800x250': 6, - '350x600': 6 -}; -let PRICE_MAPPING; -const VIDEO_MAPPING = { - playback_method: { - 'unknown': 0, - 'auto_play_sound_on': 1, - 'auto_play_sound_off': 2, - 'click_to_play': 3, - 'mouse_over': 4, - 'auto_play_sound_unknown': 5 - }, - context: { - 'unknown': 0, - 'pre_roll': 1, - 'mid_roll': 2, - 'post_roll': 3, - 'outstream': 4, - 'in-banner': 5 - } -}; -const NATIVE_MAPPING = { - body: 'description', - body2: 'desc2', - cta: 'ctatext', - image: { - serverName: 'main_image', - requiredParams: { required: true } - }, - icon: { - serverName: 'icon', - requiredParams: { required: true } - }, - sponsoredBy: 'sponsored_by', - privacyLink: 'privacy_link', - salePrice: 'saleprice', - displayUrl: 'displayurl' -}; -const SOURCE = 'pbjs'; -const MAX_IMPS_PER_REQUEST = 15; -const SCRIPT_TAG_START = ' { - if (Array.isArray(bid.params.placementId)) { - const ids = bid.params.placementId; - for (let i = 0; i < ids.length; i++) { - const newBid = Object.assign({}, bid, {params: {placementId: ids[i]}}); - localBidRequests.push(newBid) - } - } else { - localBidRequests.push(bid); - } - }); - const tags = localBidRequests.map(bidToTag); - const userObjBid = find(bidRequests, hasUserInfo); - let userObj = {}; - if (config.getConfig('coppa') === true) { - userObj = { 'coppa': true }; - } - if (userObjBid) { - Object.keys(userObjBid.params.user) - .filter(param => includes(USER_PARAMS, param)) - .forEach((param) => { - let uparam = convertCamelToUnderscore(param); - if (param === 'segments' && isArray(userObjBid.params.user[param])) { - let segs = []; - userObjBid.params.user[param].forEach(val => { - if (isNumber(val)) { - segs.push({'id': val}); - } else if (isPlainObject(val)) { - segs.push(val); - } - }); - userObj[uparam] = segs; - } else if (param !== 'segments') { - userObj[uparam] = userObjBid.params.user[param]; - } - }); - } - - const appDeviceObjBid = find(bidRequests, hasAppDeviceInfo); - let appDeviceObj; - if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app) { - appDeviceObj = {}; - Object.keys(appDeviceObjBid.params.app) - .filter(param => includes(APP_DEVICE_PARAMS, param)) - .forEach(param => appDeviceObj[param] = appDeviceObjBid.params.app[param]); - } - - const appIdObjBid = find(bidRequests, hasAppId); - let appIdObj; - if (appIdObjBid && appIdObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { - appIdObj = { - appid: appIdObjBid.params.app.id - }; - } - - let debugObj = {}; - let debugObjParams = {}; - const debugBidRequest = find(bidRequests, hasDebug); - if (debugBidRequest && debugBidRequest.debug) { - debugObj = debugBidRequest.debug; - } - - if (debugObj && debugObj.enabled) { - Object.keys(debugObj) - .filter(param => includes(DEBUG_PARAMS, param)) - .forEach(param => { - debugObjParams[param] = debugObj[param]; - }); - } - - const memberIdBid = find(bidRequests, hasMemberId); - const member = memberIdBid ? parseInt(memberIdBid.params.member, 10) : 0; - const schain = bidRequests[0].schain; - const omidSupport = find(bidRequests, hasOmidSupport); - - const payload = { - tags: [...tags], - user: userObj, - sdk: { - source: SOURCE, - version: '$prebid.version$' - }, - schain: schain - }; - - if (omidSupport) { - payload['iab_support'] = { - omidpn: 'Appnexus', - omidpv: '$prebid.version$' - }; - } - - if (member > 0) { - payload.member_id = member; - } - - if (appDeviceObjBid) { - payload.device = appDeviceObj; - } - if (appIdObjBid) { - payload.app = appIdObj; - } - - if (config.getConfig('adpod.brandCategoryExclusion')) { - payload.brand_category_uniqueness = true; - } - - if (debugObjParams.enabled) { - payload.debug = debugObjParams; - logInfo('Debug Auction Settings:\n\n' + JSON.stringify(debugObjParams, null, 4)); - } - - if (bidderRequest && bidderRequest.gdprConsent) { - // note - objects for impbus use underscore instead of camelCase - payload.gdpr_consent = { - consent_string: bidderRequest.gdprConsent.consentString, - consent_required: bidderRequest.gdprConsent.gdprApplies - }; - - if (bidderRequest.gdprConsent.addtlConsent && bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1) { - let ac = bidderRequest.gdprConsent.addtlConsent; - // pull only the ids from the string (after the ~) and convert them to an array of ints - let acStr = ac.substring(ac.indexOf('~') + 1); - payload.gdpr_consent.addtl_consent = acStr.split('.').map(id => parseInt(id, 10)); - } - } - - if (bidderRequest && bidderRequest.uspConsent) { - payload.us_privacy = bidderRequest.uspConsent; - } - - if (bidderRequest && bidderRequest.refererInfo) { - let refererinfo = { - // TODO: this collects everything it finds, except for topmostLocation - 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(',') - }; - payload.referrer_detection = refererinfo; - } - - const hasAdPodBid = find(bidRequests, hasAdPod); - if (hasAdPodBid) { - bidRequests.filter(hasAdPod).forEach(adPodBid => { - const adPodTags = createAdPodRequest(tags, adPodBid); - // don't need the original adpod placement because it's in adPodTags - const nonPodTags = payload.tags.filter(tag => tag.uuid !== adPodBid.bidId); - payload.tags = [...nonPodTags, ...adPodTags]; - }); - } - - if (bidRequests[0].userId) { - let eids = []; - - addUserId(eids, deepAccess(bidRequests[0], `userId.criteoId`), 'criteo.com', null); - addUserId(eids, deepAccess(bidRequests[0], `userId.netId`), 'netid.de', null); - addUserId(eids, deepAccess(bidRequests[0], `userId.idl_env`), 'liveramp.com', null); - addUserId(eids, deepAccess(bidRequests[0], `userId.tdid`), 'adserver.org', 'TDID'); - addUserId(eids, deepAccess(bidRequests[0], `userId.uid2.id`), 'uidapi.com', 'UID2'); - - if (eids.length) { - payload.eids = eids; - } - } - - if (tags[0].publisher_id) { - payload.publisher_id = tags[0].publisher_id; - } - - const request = formatRequest(payload, bidderRequest); - // add pricing endpoint - return [{method: 'GET', url: PRICING_URL, options: {withCredentials: false}}, request]; - }, - - parseAndMapCpm: function(serverResponse) { - const responseBody = serverResponse.body; - if (Array.isArray(responseBody) && responseBody.length) { - let localData = {}; - responseBody.forEach(cpmPerSize => { - Object.keys(cpmPerSize).forEach(size => { - let obj = {}; - obj[size] = cpmPerSize[size]; - localData = Object.assign({}, localData, obj) - }) - }) - PRICE_MAPPING = localData; - return null; - } - - if (responseBody.version) { - const localPriceMapping = PRICE_MAPPING || DEFAULT_PRICE_MAPPING; - if (responseBody.tags && Array.isArray(responseBody.tags) && responseBody.tags.length) { - responseBody.tags.forEach((tag) => { - if (tag.ads && Array.isArray(tag.ads) && tag.ads.length) { - tag.ads.forEach(ad => { - if (ad.ad_type === 'banner') { - const size = `${ad.rtb.banner.width}x${ad.rtb.banner.height}`; - if (localPriceMapping[size]) { - ad.cpm = localPriceMapping[size]; - } else { - ad.cpm = localPriceMapping['0x0']; - } - } - }) - } - }); - } - } - return responseBody; - }, - - /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: function (serverResponse, { bidderRequest }) { - serverResponse = this.parseAndMapCpm(serverResponse); - if (!serverResponse) return []; - const bids = []; - if (serverResponse.error) { - let errorMessage = `in response for ${bidderRequest.bidderCode} adapter : ${serverResponse.error}`; - logError(errorMessage); - return bids; - } - - if (serverResponse.tags) { - serverResponse.tags.forEach(serverBid => { - const rtbBid = getRtbBid(serverBid); - if (rtbBid) { - if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { - const bid = newBid(serverBid, rtbBid, bidderRequest); - bid.mediaType = parseMediaType(rtbBid); - bids.push(bid); - } - } - }); - } - - if (serverResponse.debug && serverResponse.debug.debug_info) { - let debugHeader = 'AppNexus Debug Auction for Prebid\n\n' - let debugText = debugHeader + serverResponse.debug.debug_info - debugText = debugText - .replace(/(|)/gm, '\t') // Tables - .replace(/(<\/td>|<\/th>)/gm, '\n') // Tables - .replace(/^
/gm, '') // Remove leading
- .replace(/(
\n|
)/gm, '\n') //
- .replace(/

(.*)<\/h1>/gm, '\n\n===== $1 =====\n\n') // Header H1 - .replace(/(.*)<\/h[2-6]>/gm, '\n\n*** $1 ***\n\n') // Headers - .replace(/(<([^>]+)>)/igm, ''); // Remove any other tags - logMessage(debugText); - } - - return bids; - }, - - getUserSyncs: function (syncOptions, responses, gdprConsent) { - if (syncOptions.iframeEnabled && hasPurpose1Consent({gdprConsent})) { - return [{ - type: 'iframe', - url: 'https://acdn.adnxs.com/dmp/async_usersync.html' - }]; - } - }, - - transformBidParams: function (params, isOpenRtb) { - params = convertTypes({ - 'member': 'string', - 'invCode': 'string', - 'placementId': 'number', - 'keywords': transformBidderParamKeywords, - 'publisherId': 'number' - }, params); - - if (isOpenRtb) { - params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; - if (params.usePaymentRule) { delete params.usePaymentRule; } - - Object.keys(params).forEach(paramKey => { - let convertedKey = convertCamelToUnderscore(paramKey); - if (convertedKey !== paramKey) { - params[convertedKey] = params[paramKey]; - delete params[paramKey]; - } - }); - } - - return params; - }, - - /** - * Add element selector to javascript tracker to improve native viewability - * @param {Bid} bid - */ - onBidWon: function (bid) { - if (bid.native) { - reloadViewabilityScriptWithCorrectParameters(bid); - } +const URL = 'https://goldlayer-api.prod.gbads.net/bid/pbjs'; +const URL_LOCAL = 'http://localhost:3000/bid/pbjs'; +const LOGGING_PERCENTAGE_REGULAR = 0.0001; +const LOGGING_PERCENTAGE_ERROR = 0.001; +const LOGGING_URL = 'https://l.da-services.ch/pb'; + +/* Renderer settings */ +const RENDERER_OPTIONS = { + OUTSTREAM_GP: { + MIN_HEIGHT: 300, + MIN_WIDTH: 300, + URL: 'https://goldplayer.prod.gbads.net/scripts/goldplayer.js' } }; -function reloadViewabilityScriptWithCorrectParameters(bid) { - let viewJsPayload = getAppnexusViewabilityScriptFromJsTrackers(bid.native.javascriptTrackers); - - if (viewJsPayload) { - let prebidParams = 'pbjs_adid=' + bid.adId + ';pbjs_auc=' + bid.adUnitCode; - - let jsTrackerSrc = getViewabilityScriptUrlFromPayload(viewJsPayload); - - let newJsTrackerSrc = jsTrackerSrc.replace('dom_id=%native_dom_id%', prebidParams); - - // find iframe containing script tag - let frameArray = document.getElementsByTagName('iframe'); - - // boolean var to modify only one script. That way if there are muliple scripts, - // they won't all point to the same creative. - let modifiedAScript = false; - - // first, loop on all ifames - for (let i = 0; i < frameArray.length && !modifiedAScript; i++) { - let currentFrame = frameArray[i]; - try { - // IE-compatible, see https://stackoverflow.com/a/3999191/2112089 - let nestedDoc = currentFrame.contentDocument || currentFrame.contentWindow.document; - - if (nestedDoc) { - // if the doc is present, we look for our jstracker - let scriptArray = nestedDoc.getElementsByTagName('script'); - for (let j = 0; j < scriptArray.length && !modifiedAScript; j++) { - let currentScript = scriptArray[j]; - if (currentScript.getAttribute('data-src') == jsTrackerSrc) { - currentScript.setAttribute('src', newJsTrackerSrc); - currentScript.setAttribute('data-src', ''); - if (currentScript.removeAttribute) { - currentScript.removeAttribute('data-src'); - } - modifiedAScript = true; - } - } - } - } catch (exception) { - // trying to access a cross-domain iframe raises a SecurityError - // this is expected and ignored - if (!(exception instanceof DOMException && exception.name === 'SecurityError')) { - // all other cases are raised again to be treated by the calling function - throw exception; - } - } - } - } -} - -function strIsAppnexusViewabilityScript(str) { - let regexMatchUrlStart = str.match(VIEWABILITY_URL_START); - let viewUrlStartInStr = regexMatchUrlStart != null && regexMatchUrlStart.length >= 1; - - let regexMatchFileName = str.match(VIEWABILITY_FILE_NAME); - let fileNameInStr = regexMatchFileName != null && regexMatchFileName.length >= 1; +/* Event types */ +const EVENTS = { + BID_WON: 'bid_won', + TARGETING: 'targeting_set', + RENDER: 'creative_render', + TIMEOUT: 'timeout', + ERROR: 'error' +}; - return str.startsWith(SCRIPT_TAG_START) && fileNameInStr && viewUrlStartInStr; -} +/* Targeting mapping */ +const TARGETING_KEYS = { + // request level + GEO_LAT: 'lat', + GEO_LON: 'long', + GEO_ZIP: 'zip', + CONNECTION_TYPE: 'connection', + // slot level + VIDEO_DURATION: 'duration', +}; -function getAppnexusViewabilityScriptFromJsTrackers(jsTrackerArray) { - let viewJsPayload; - if (isStr(jsTrackerArray) && strIsAppnexusViewabilityScript(jsTrackerArray)) { - viewJsPayload = jsTrackerArray; - } else if (isArray(jsTrackerArray)) { - for (let i = 0; i < jsTrackerArray.length; i++) { - let currentJsTracker = jsTrackerArray[i]; - if (strIsAppnexusViewabilityScript(currentJsTracker)) { - viewJsPayload = currentJsTracker; - } +/* Native mapping */ +export const OPENRTB = { + NATIVE: { + IMAGE_TYPE: { + ICON: 1, + MAIN: 3, + }, + ASSET_ID: { + TITLE: 1, + IMAGE: 2, + ICON: 3, + BODY: 4, + CTA: 5, + SPONSORED: 6, } } - return viewJsPayload; -} - -function getViewabilityScriptUrlFromPayload(viewJsPayload) { - // extracting the content of the src attribute - // -> substring between src=" and " - let indexOfFirstQuote = viewJsPayload.indexOf('src="') + 5; // offset of 5: the length of 'src=' + 1 - let indexOfSecondQuote = viewJsPayload.indexOf('"', indexOfFirstQuote); - let jsTrackerSrc = viewJsPayload.substring(indexOfFirstQuote, indexOfSecondQuote); - return jsTrackerSrc; -} - -function formatRequest(payload, bidderRequest) { - let request = []; - let options = { - withCredentials: true - }; - - let endpointUrl = URL; - - if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) { - endpointUrl = URL_SIMPLE; - } - - if (getParameterByName('apn_test').toUpperCase() === 'TRUE' || config.getConfig('apn_test') === true) { - options.customHeaders = { - 'X-Is-Test': 1 - }; - } - - if (payload.tags.length > MAX_IMPS_PER_REQUEST) { - const clonedPayload = deepClone(payload); - - chunk(payload.tags, MAX_IMPS_PER_REQUEST).forEach(tags => { - clonedPayload.tags = tags; - const payloadString = JSON.stringify(clonedPayload); - request.push({ - method: 'POST', - url: endpointUrl, - data: payloadString, - bidderRequest, - options - }); - }); - } else { - const payloadString = JSON.stringify(payload); - request = { - method: 'POST', - url: endpointUrl, - data: payloadString, - bidderRequest, - options - }; - } +}; - return request; -} +/* Mapping */ +const convertToCustomTargeting = (bidderRequest) => { + const customTargeting = {}; -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) { - logError('Prebid Error calling setRender on renderer', err); - } - - renderer.setEventHandlers({ - impression: () => logMessage('Outstream video impression event'), - loaded: () => logMessage('Outstream video loaded event'), - ended: () => { - logMessage('Outstream renderer video event'); - document.querySelector(`#${adUnitCode}`).style.display = 'none'; + // geo - lat/long + if (bidderRequest?.ortb2?.device?.geo) { + if (bidderRequest?.ortb2?.device?.geo?.lon) { + customTargeting[TARGETING_KEYS.GEO_LON] = bidderRequest.ortb2.device.geo.lon; } - }); - return renderer; -} - -/** - * Unpack the Server's Bid into a Prebid-compatible one. - * @param serverBid - * @param rtbBid - * @param bidderRequest - * @return Bid - */ -function newBid(serverBid, rtbBid, bidderRequest) { - const bidRequest = getBidRequest(serverBid.uuid, [bidderRequest]); - const bid = { - requestId: serverBid.uuid, - cpm: rtbBid.cpm, - creativeId: rtbBid.creative_id, - dealId: rtbBid.deal_id, - currency: 'USD', - netRevenue: true, - ttl: 300, - adUnitCode: bidRequest.adUnitCode, - appnexus: { - buyerMemberId: rtbBid.buyer_member_id, - dealPriority: rtbBid.deal_priority, - dealCode: rtbBid.deal_code + if (bidderRequest?.ortb2?.device?.geo?.lat) { + customTargeting[TARGETING_KEYS.GEO_LAT] = bidderRequest.ortb2.device.geo.lat; } - }; - - // WE DON'T FULLY SUPPORT THIS ATM - future spot for adomain code; creating a stub for 5.0 compliance - if (rtbBid.adomain) { - bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [] }); - } - - if (rtbBid.advertiser_id) { - bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); } - if (rtbBid.rtb.video) { - // shared video properties used for all 3 contexts - Object.assign(bid, { - width: rtbBid.rtb.video.player_width, - height: rtbBid.rtb.video.player_height, - vastImpUrl: rtbBid.notify_url, - ttl: 3600 - }); - - const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); - switch (videoContext) { - case ADPOD: - const primaryCatId = (APPNEXUS_CATEGORY_MAPPING[rtbBid.brand_category_id]) ? APPNEXUS_CATEGORY_MAPPING[rtbBid.brand_category_id] : null; - bid.meta = Object.assign({}, bid.meta, { primaryCatId }); - const dealTier = rtbBid.deal_priority; - bid.video = { - context: ADPOD, - durationSeconds: Math.floor(rtbBid.rtb.video.duration_ms / 1000), - dealTier - }; - bid.vastUrl = rtbBid.rtb.video.asset_url; + // connection + if (bidderRequest?.ortb2?.device?.connectiontype) { + switch (bidderRequest.ortb2.device.connectiontype) { + case 1: + customTargeting[TARGETING_KEYS.CONNECTION_TYPE] = 'ethernet'; break; - case OUTSTREAM: - bid.adResponse = serverBid; - bid.adResponse.ad = bid.adResponse.ads[0]; - bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; - bid.vastXml = rtbBid.rtb.video.content; - - if (rtbBid.renderer_url) { - const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid); - const rendererOptions = deepAccess(videoBid, 'renderer.options'); - bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions); - } + case 2: + customTargeting[TARGETING_KEYS.CONNECTION_TYPE] = 'wifi'; break; - case INSTREAM: - bid.vastUrl = rtbBid.notify_url + '&redir=' + encodeURIComponent(rtbBid.rtb.video.asset_url); + case 4: + customTargeting[TARGETING_KEYS.CONNECTION_TYPE] = '2G'; + break; + case 5: + customTargeting[TARGETING_KEYS.CONNECTION_TYPE] = '3G'; + break; + case 6: + customTargeting[TARGETING_KEYS.CONNECTION_TYPE] = '4G'; + break; + case 0: + case 3: + default: break; } - } else if (rtbBid.rtb[NATIVE]) { - const nativeAd = rtbBid.rtb[NATIVE]; - - // setting up the jsTracker: - // we put it as a data-src attribute so that the tracker isn't called - // until we have the adId (see onBidWon) - let jsTrackerDisarmed = rtbBid.viewability.config.replace('src=', 'data-src='); - - let jsTrackers = nativeAd.javascript_trackers; - - if (jsTrackers == undefined) { - jsTrackers = jsTrackerDisarmed; - } else if (isStr(jsTrackers)) { - jsTrackers = [jsTrackers, jsTrackerDisarmed]; - } else { - jsTrackers.push(jsTrackerDisarmed); - } - - bid[NATIVE] = { - title: nativeAd.title, - body: nativeAd.desc, - body2: nativeAd.desc2, - cta: nativeAd.ctatext, - rating: nativeAd.rating, - sponsoredBy: nativeAd.sponsored, - privacyLink: nativeAd.privacy_link, - address: nativeAd.address, - downloads: nativeAd.downloads, - likes: nativeAd.likes, - phone: nativeAd.phone, - price: nativeAd.price, - salePrice: nativeAd.saleprice, - clickUrl: nativeAd.link.url, - displayUrl: nativeAd.displayurl, - clickTrackers: nativeAd.link.click_trackers, - impressionTrackers: nativeAd.impression_trackers, - javascriptTrackers: jsTrackers - }; - if (nativeAd.main_img) { - bid['native'].image = { - url: nativeAd.main_img.url, - height: nativeAd.main_img.height, - width: nativeAd.main_img.width, - }; - } - if (nativeAd.icon) { - bid['native'].icon = { - url: nativeAd.icon.url, - height: nativeAd.icon.height, - width: nativeAd.icon.width, - }; - } - } else { - Object.assign(bid, { - width: rtbBid.rtb.banner.width, - height: rtbBid.rtb.banner.height, - ad: rtbBid.rtb.banner.content - }); - try { - if (rtbBid.rtb.trackers) { - const url = rtbBid.rtb.trackers[0].impression_urls[0]; - const tracker = createTrackPixelHtml(url); - bid.ad += tracker; - } - } catch (error) { - logError('Error appending tracking pixel', error); - } - } - - return bid; -} - -function bidToTag(bid) { - const tag = {}; - tag.sizes = transformSizes(bid.sizes); - tag.primary_size = tag.sizes[0]; - tag.ad_types = []; - tag.uuid = bid.bidId; - if (bid.params.placementId) { - tag.id = parseInt(bid.params.placementId, 10); - } else { - tag.code = bid.params.invCode; - } - tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; - tag.use_pmt_rule = bid.params.usePaymentRule || false; - tag.prebid = true; - tag.disable_psa = true; - let bidFloor = getBidFloor(bid); - if (bidFloor) { - tag.reserve = bidFloor; - } - if (bid.params.position) { - tag.position = { 'above': 1, 'below': 2 }[bid.params.position] || 0; - } - if (bid.params.trafficSourceCode) { - tag.traffic_source_code = bid.params.trafficSourceCode; - } - if (bid.params.privateSizes) { - tag.private_sizes = transformSizes(bid.params.privateSizes); - } - if (bid.params.supplyType) { - tag.supply_type = bid.params.supplyType; - } - if (bid.params.pubClick) { - tag.pubclick = bid.params.pubClick; - } - if (bid.params.extInvCode) { - tag.ext_inv_code = bid.params.extInvCode; - } - if (bid.params.publisherId) { - tag.publisher_id = parseInt(bid.params.publisherId, 10); - } - if (bid.params.externalImpId) { - tag.external_imp_id = bid.params.externalImpId; - } - tag.keywords = getANKeywordParam(bid.ortb2, bid.params.keywords); - - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); - if (gpid) { - tag.gpid = gpid; - } - - if (bid.mediaType === NATIVE || deepAccess(bid, `mediaTypes.${NATIVE}`)) { - tag.ad_types.push(NATIVE); - if (tag.sizes.length === 0) { - tag.sizes = transformSizes([1, 1]); - } - - if (bid.nativeParams) { - const nativeRequest = buildNativeRequest(bid.nativeParams); - tag[NATIVE] = { layouts: [nativeRequest] }; - } - } - - const videoMediaType = deepAccess(bid, `mediaTypes.${VIDEO}`); - const context = deepAccess(bid, 'mediaTypes.video.context'); - - if (videoMediaType && context === 'adpod') { - tag.hb_source = 7; - } else { - tag.hb_source = 1; - } - if (bid.mediaType === VIDEO || videoMediaType) { - tag.ad_types.push(VIDEO); - } - - // instream gets vastUrl, outstream gets vastXml - if (bid.mediaType === VIDEO || (videoMediaType && context !== 'outstream')) { - tag.require_asset_url = true; - } - - if (bid.params.video) { - tag.video = {}; - // place any valid video params on the tag - Object.keys(bid.params.video) - .filter(param => includes(VIDEO_TARGETING, param)) - .forEach(param => { - switch (param) { - case 'context': - case 'playback_method': - let type = bid.params.video[param]; - type = (isArray(type)) ? type[0] : type; - tag.video[param] = VIDEO_MAPPING[param][type]; - break; - // Deprecating tags[].video.frameworks in favor of tags[].video_frameworks - case 'frameworks': - break; - default: - tag.video[param] = bid.params.video[param]; - } - }); - - if (bid.params.video.frameworks && isArray(bid.params.video.frameworks)) { - tag['video_frameworks'] = bid.params.video.frameworks; - } - } - - // use IAB ORTB values if the corresponding values weren't already set by bid.params.video - if (videoMediaType) { - tag.video = tag.video || {}; - Object.keys(videoMediaType) - .filter(param => includes(VIDEO_RTB_TARGETING, param)) - .forEach(param => { - switch (param) { - case 'minduration': - case 'maxduration': - if (typeof tag.video[param] !== 'number') tag.video[param] = videoMediaType[param]; - break; - case 'skip': - if (typeof tag.video['skippable'] !== 'boolean') tag.video['skippable'] = (videoMediaType[param] === 1); - break; - case 'skipafter': - if (typeof tag.video['skipoffset'] !== 'number') tag.video['skippoffset'] = videoMediaType[param]; - break; - case 'playbackmethod': - if (typeof tag.video['playback_method'] !== 'number') { - let type = videoMediaType[param]; - type = (isArray(type)) ? type[0] : type; - - // we only support iab's options 1-4 at this time. - if (type >= 1 && type <= 4) { - tag.video['playback_method'] = type; - } - } - break; - case 'api': - if (!tag['video_frameworks'] && isArray(videoMediaType[param])) { - // need to read thru array; remove 6 (we don't support it), swap 4 <> 5 if found (to match our adserver mapping for these specific values) - let apiTmp = videoMediaType[param].map(val => { - let v = (val === 4) ? 5 : (val === 5) ? 4 : val; - - if (v >= 1 && v <= 5) { - return v; - } - }).filter(v => v); - tag['video_frameworks'] = apiTmp; - } - break; - } - }); - } - - if (bid.renderer) { - tag.video = Object.assign({}, tag.video, { custom_renderer_present: true }); } - if (bid.params.frameworks && isArray(bid.params.frameworks)) { - tag['banner_frameworks'] = bid.params.frameworks; + // zip + if (bidderRequest?.ortb2?.device?.geo?.zip) { + customTargeting[TARGETING_KEYS.GEO_ZIP] = bidderRequest.ortb2.device.geo.zip; } - if (bid.mediaTypes?.banner) { - tag.ad_types.push(BANNER); - } - - if (tag.ad_types.length === 0) { - delete tag.ad_types; - } - - return tag; + return customTargeting; } -/* Turn bid request sizes into ut-compatible format */ -function transformSizes(requestSizes) { - let sizes = []; - let sizeObj = {}; +const convertToCustomSlotTargeting = (validBidRequest) => { + const customTargeting = {}; - if (isArray(requestSizes) && requestSizes.length === 2 && - !isArray(requestSizes[0])) { - sizeObj.width = parseInt(requestSizes[0], 10); - sizeObj.height = parseInt(requestSizes[1], 10); - sizes.push(sizeObj); - } else if (typeof requestSizes === 'object') { - for (let i = 0; i < requestSizes.length; i++) { - let size = requestSizes[i]; - sizeObj = {}; - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - sizes.push(sizeObj); + // Video duration + if (validBidRequest.mediaTypes?.[VIDEO]) { + if (validBidRequest.params?.video?.maxduration) { + const duration = validBidRequest.params?.video?.maxduration; + if (duration <= 15) customTargeting[TARGETING_KEYS.VIDEO_DURATION] = 'M'; + if (duration > 15 && duration <= 30) customTargeting[TARGETING_KEYS.VIDEO_DURATION] = 'XL'; + if (duration > 30) customTargeting[TARGETING_KEYS.VIDEO_DURATION] = 'XXL'; } } - return sizes; + return customTargeting } -function hasUserInfo(bid) { - return !!bid.params.user; -} +const convertToProprietaryData = (validBidRequests, bidderRequest) => { + const requestData = { + mock: false, + debug: false, + timestampStart: undefined, + timestampEnd: undefined, + config: { + publisher: { + id: undefined, + } + }, + gdpr: { + consent: undefined, + consentString: undefined, + }, + contextInfo: { + contentUrl: undefined, + bidderResources: undefined, + }, + appInfo: { + id: undefined, + }, + userInfo: { + ip: undefined, + ua: undefined, + ifa: undefined, + ppid: [], + }, + slots: [], + targetings: {}, + }; -function hasMemberId(bid) { - return !!parseInt(bid.params.member, 10); -} + // Set timestamps + requestData.timestampStart = Date.now(); + requestData.timestampEnd = Date.now() + (!isNaN(bidderRequest.timeout) ? Number(bidderRequest.timeout) : 0); -function hasAppDeviceInfo(bid) { - if (bid.params) { - return !!bid.params.app + // Set config + if (validBidRequests[0]?.params?.publisherId) { + requestData.config.publisher.id = validBidRequests[0].params.publisherId; } -} -function hasAppId(bid) { - if (bid.params && bid.params.app) { - return !!bid.params.app.id + // Set GDPR + if (bidderRequest?.gdprConsent) { + requestData.gdpr.consent = bidderRequest.gdprConsent.gdprApplies; + requestData.gdpr.consentString = bidderRequest.gdprConsent.consentString; } - return !!bid.params.app -} -function hasDebug(bid) { - return !!bid.debug -} + // Set contextInfo + requestData.contextInfo.contentUrl = bidderRequest.refererInfo?.canonicalUrl || bidderRequest.refererInfo?.topmostLocation || bidderRequest?.ortb2?.site?.page; -function hasAdPod(bid) { - return ( - bid.mediaTypes && - bid.mediaTypes.video && - bid.mediaTypes.video.context === ADPOD - ); -} + // Set appInfo + requestData.appInfo.id = bidderRequest?.ortb2?.site?.domain || bidderRequest.refererInfo?.page; -function hasOmidSupport(bid) { - let hasOmid = false; - const bidderParams = bid.params; - const videoParams = bid.params.video; - if (bidderParams.frameworks && isArray(bidderParams.frameworks)) { - hasOmid = includes(bid.params.frameworks, 6); - } - if (!hasOmid && videoParams && videoParams.frameworks && isArray(videoParams.frameworks)) { - hasOmid = includes(bid.params.video.frameworks, 6); - } - return hasOmid; -} - -/** - * Expand an adpod placement into a set of request objects according to the - * total adpod duration and the range of duration seconds. Sets minduration/ - * maxduration video property according to requireExactDuration configuration - */ -function createAdPodRequest(tags, adPodBid) { - const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; - - const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = Math.max(...durationRangeSec); - - const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); - let request = fill(...tagToDuplicate, numberOfPlacements); - - if (requireExactDuration) { - const divider = Math.ceil(numberOfPlacements / durationRangeSec.length); - const chunked = chunk(request, divider); + // Set userInfo + requestData.userInfo.ip = bidderRequest?.ortb2?.device?.ip || navigator.ip; + requestData.userInfo.ua = bidderRequest?.ortb2?.device?.ua || navigator.userAgent; - // each configured duration is set as min/maxduration for a subset of requests - durationRangeSec.forEach((duration, index) => { - chunked[index].map(tag => { - setVideoProperty(tag, 'minduration', duration); - setVideoProperty(tag, 'maxduration', duration); + // Set userInfo.ppid + requestData.userInfo.ppid = (validBidRequests || []).reduce((ppids, validBidRequest) => { + const extractedPpids = []; + (validBidRequest.userIdAsEids || []).forEach((eid) => { + (eid?.uids || []).forEach(uid => { + if (uid?.ext?.stype === 'ppuid') { + const isExistingInExtracted = !!extractedPpids.find(id => id.source === eid.source); + const isExistingInPpids = !!ppids.find(id => id.source === eid.source); + if (!isExistingInExtracted && !isExistingInPpids) extractedPpids.push({source: eid.source, id: uid.id}); + } }); - }); + }) + return [...ppids, ...extractedPpids]; + }, []); + + // Set userInfo.ifa + if (bidderRequest.ortb2?.device?.ifa) { + requestData.userInfo.ifa = bidderRequest.ortb2.device.ifa; } else { - // all maxdurations should be the same - request.map(tag => setVideoProperty(tag, 'maxduration', maxDuration)); + requestData.userInfo.ifa = validBidRequests.find(validBidRequest => { + return !!validBidRequest.ortb2?.device?.ifa; + }); } - return request; -} - -function getAdPodPlacementNumber(videoParams) { - const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = Math.min(...durationRangeSec); - const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); - - return requireExactDuration - ? Math.max(numberOfPlacements, durationRangeSec.length) - : numberOfPlacements; -} - -function setVideoProperty(tag, key, value) { - if (isEmpty(tag.video)) { tag.video = {}; } - tag.video[key] = value; -} - -function getRtbBid(tag) { - return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb); -} - -function buildNativeRequest(params) { - const request = {}; - - // map standard prebid native asset identifier to /ut parameters - // e.g., tag specifies `body` but /ut only knows `description`. - // mapping may be in form {tag: ''} or - // {tag: {serverName: '', requiredParams: {...}}} - Object.keys(params).forEach(key => { - // check if one of the forms is used, otherwise - // a mapping wasn't specified so pass the key straight through - const requestKey = - (NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverName) || - NATIVE_MAPPING[key] || - key; - - // required params are always passed on request - const requiredParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].requiredParams; - request[requestKey] = Object.assign({}, requiredParams, params[key]); - - // convert the sizes of image/icon assets to proper format (if needed) - const isImageAsset = !!(requestKey === NATIVE_MAPPING.image.serverName || requestKey === NATIVE_MAPPING.icon.serverName); - if (isImageAsset && request[requestKey].sizes) { - let sizes = request[requestKey].sizes; - if (isArrayOfNums(sizes) || (isArray(sizes) && sizes.length > 0 && sizes.every(sz => isArrayOfNums(sz)))) { - request[requestKey].sizes = transformSizes(request[requestKey].sizes); + // Set slots + requestData.slots = validBidRequests.map((validBidRequest) => { + const slot = { + id: validBidRequest.params?.slotId, + sizes: [ + ...(validBidRequest.sizes || []), + ...(validBidRequest.mediaTypes?.[VIDEO]?.sizes ? validBidRequest.mediaTypes[VIDEO].sizes : []) + ], + targetings: { + ...validBidRequest?.params?.customTargeting, + ...convertToCustomSlotTargeting(validBidRequest) } - } - - if (requestKey === NATIVE_MAPPING.privacyLink) { - request.privacy_supported = true; - } + }; + return slot; }); - return request; -} + // Set targetings + requestData.targetings = convertToCustomTargeting(bidderRequest); -/** - * 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) { - var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); - if (el[0]) { - el[0].style.setProperty('display', 'none'); - } + return requestData; } -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 && el[0].nextSibling.localName === 'iframe') { - el[0].nextSibling.style.setProperty('display', 'none'); - } - } catch (e) { - // element not found! +const getRendererForBid = (bidRequest, creative) => { + if (!bidRequest.renderer && creative.contextType === 'video_outstream') { + if (!creative.vastUrl && !creative.vastXml) return undefined; + + const config = { documentResolver: (_, sourceDocument, renderDocument) => renderDocument ?? sourceDocument }; + const renderer = Renderer.install({id: bidRequest.bidId, url: RENDERER_OPTIONS.OUTSTREAM_GP.URL, adUnitCode: bidRequest.adUnitCode, config}); + + renderer.setRender((bid, doc) => { + bid.renderer.push(() => { + if (doc.defaultView?.GoldPlayer) { + const options = { + vastUrl: creative.vastUrl, + vastXML: creative.vastXml, + autoplay: false, + muted: false, + controls: true, + styling: { progressbarColor: '#000' }, + videoHeight: Math.min(deepAccess(doc, 'defaultView.innerWidth') / 16 * 9, RENDERER_OPTIONS.OUTSTREAM_GP.MIN_HEIGHT), + videoVerticalHeight: Math.min(deepAccess(doc, 'defaultView.innerWidth') / 9 * 16, RENDERER_OPTIONS.OUTSTREAM_GP.MIN_WIDTH), + }; + const GP = doc.defaultView.GoldPlayer; + const player = new GP(options); + player.play(); + } + }); + }); + + return renderer; } + return undefined; } -function outstreamRender(bid) { - hidedfpContainer(bid.adUnitCode); - hideSASIframe(bid.adUnitCode); - // push to render queue because ANOutstreamVideo may not be loaded yet - bid.renderer.push(() => { - window.ANOutstreamVideo.renderAd({ - tagId: bid.adResponse.tag_id, - sizes: [bid.getSize().split('x')], - targetId: bid.adUnitCode, // target div id to render video - uuid: bid.adResponse.uuid, - adResponse: bid.adResponse, - rendererOptions: bid.renderer.getConfig() - }, handleOutstreamRendererEvents.bind(null, bid)); - }); +const getNativeAssetsForBid = (bidRequest, creative) => { + if (creative.contextType === 'native' && creative.ad) { + const nativeAssets = JSON.parse(creative.ad); + const result = { + clickUrl: encodeURI(nativeAssets?.link?.url), + impressionTrackers: nativeAssets?.imptrackers, + clickTrackers: nativeAssets?.clicktrackers, + javascriptTrackers: nativeAssets?.jstracker && [nativeAssets.jstracker], + }; + (nativeAssets?.assets || []).forEach(asset => { + switch (asset.id) { + case OPENRTB.NATIVE.ASSET_ID.TITLE: + result.title = asset.title?.text; + break; + case OPENRTB.NATIVE.ASSET_ID.IMAGE: + result.image = { + url: encodeURI(asset.img?.url), + width: asset.img?.w, + height: asset.img?.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.ICON: + result.icon = { + url: encodeURI(asset.img.url), + width: asset.img?.w, + height: asset.img?.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.BODY: + result.body = asset.data?.value; + break; + case OPENRTB.NATIVE.ASSET_ID.SPONSORED: + result.sponsoredBy = asset.data?.value; + break; + case OPENRTB.NATIVE.ASSET_ID.CTA: + result.cta = asset.data?.value; + break; + } + }); + return result; + } + return undefined; } -function handleOutstreamRendererEvents(bid, id, eventName) { - bid.renderer.handleVideoEvent({ id, eventName }); -} +const convertProprietaryResponseToBidResponses = (serverResponse, bidRequest) => { + const bidRequests = bidRequest?.bidderRequest?.bids || []; + const creativeGroups = serverResponse?.body?.creatives || {}; -function parseMediaType(rtbBid) { - const adType = rtbBid.ad_type; - if (adType === VIDEO) { - return VIDEO; - } else if (adType === NATIVE) { - return NATIVE; - } else { - return BANNER; - } + return bidRequests.reduce((bidResponses, bidRequest) => { + const matchingCreativeGroup = (creativeGroups[bidRequest.params?.slotId] || []).filter((creative) => { + if (bidRequest.mediaTypes?.[BANNER] && creative.mediaType === BANNER) return true; + if (bidRequest.mediaTypes?.[VIDEO] && creative.mediaType === VIDEO) return true; + if (bidRequest.mediaTypes?.[NATIVE] && creative.mediaType === NATIVE) return true; + return false; + }); + const matchingBidResponses = matchingCreativeGroup.map((creative) => { + return { + requestId: bidRequest.bidId, + cpm: creative.cpm, + currency: creative.currency, + width: creative.width, + height: creative.height, + creativeId: creative.creativeId, + dealId: creative.dealId, + netRevenue: creative.netRevenue, + ttl: creative.ttl, + ad: creative.ad, + vastUrl: creative.vastUrl, + vastXml: creative.vastXml, + mediaType: creative.mediaType, + meta: creative.meta, + native: getNativeAssetsForBid(bidRequest, creative), + renderer: getRendererForBid(bidRequest, creative), + }; + }); + return [...bidResponses, ...matchingBidResponses]; + }, []); } -function addUserId(eids, id, source, rti) { - if (id) { - if (rti) { - eids.push({ source, id, rti_partner: rti }); - } else { - eids.push({ source, id }); - } - } - return eids; +/* Logging */ +const sendLog = (data, percentage = 0.0001) => { + if (Math.random() > percentage) return; + const encodedData = `data=${window.btoa(JSON.stringify({...data, source: 'goldbach_pbjs', projectedAmount: (1 / percentage)}))}`; + ajax(LOGGING_URL, null, encodedData, { + withCredentials: false, + method: 'POST', + crossOrigin: true, + contentType: 'application/x-www-form-urlencoded', + }); } -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return (bid.params.reserve) ? bid.params.reserve : null; - } - - let floor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { - return floor.floor; - } - return null; +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid: function (bid) { + return typeof bid.params.publisherId === 'string' && Array.isArray(bid.sizes); + }, + buildRequests: function (validBidRequests, bidderRequest) { + const url = IS_LOCAL_MODE ? URL_LOCAL : URL; + const data = convertToProprietaryData(validBidRequests, bidderRequest); + return [{ + method: 'POST', + url: url, + data: data, + bidderRequest: bidderRequest, + options: { + withCredentials: false, + contentType: 'application/json', + } + }]; + }, + interpretResponse: function (serverResponse, request) { + return convertProprietaryResponseToBidResponses(serverResponse, request); + }, + onTimeout: function(timeoutData) { + const payload = { + event: EVENTS.TIMEOUT, + error: timeoutData, + }; + sendLog(payload, LOGGING_PERCENTAGE_ERROR); + }, + onBidWon: function(bid) { + const payload = { + event: EVENTS.BID_WON, + adUnitCode: bid.adUnitCode, + adId: bid.adId, + mediaType: bid.mediaType, + size: bid.size, + }; + sendLog(payload, LOGGING_PERCENTAGE_REGULAR); + }, + onSetTargeting: function(bid) { + const payload = { + event: EVENTS.TARGETING, + adUnitCode: bid.adUnitCode, + adId: bid.adId, + mediaType: bid.mediaType, + size: bid.size, + }; + sendLog(payload, LOGGING_PERCENTAGE_REGULAR); + }, + onBidderError: function({ error }) { + const payload = { + event: EVENTS.ERROR, + error: error, + }; + sendLog(payload, LOGGING_PERCENTAGE_ERROR); + }, + onAdRenderSucceeded: function(bid) { + const payload = { + event: EVENTS.RENDER, + adUnitCode: bid.adUnitCode, + adId: bid.adId, + mediaType: bid.mediaType, + size: bid.size, + }; + sendLog(payload, LOGGING_PERCENTAGE_REGULAR); + }, } registerBidder(spec); diff --git a/modules/goldbachBidAdapter.md b/modules/goldbachBidAdapter.md index f7c9479439b..7335c95f77f 100644 --- a/modules/goldbachBidAdapter.md +++ b/modules/goldbachBidAdapter.md @@ -1,151 +1,89 @@ -#Overview +# Goldbach Bidder Adapter -``` -Module Name: Goldbach Bid Adapter -Module Type: Bidder Adapter -Maintainer: dusan.veljovic@goldbach.com -``` +## Overview -# Description +```text +Module Name: Goldbach Bidder Adapter +Module Type: Bidder Adapter +Maintainer: benjamin.brachmann@goldbach.com +``` -Connects to Xandr exchange for bids. +## Description -Goldbach bid adapter supports Banner, Video (instream and outstream) and Native. +Module that connects to Goldbach SSP demand sources. -# Test Parameters +```shell +gulp build --modules=goldbachBidAdapter,userId,pubProvidedIdSystem ``` -var adUnits = [ - // Banner adUnit - { - code: 'banner-div', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]] - } - }, - bids: [{ - bidder: 'goldbach', - params: { - placementId: 13144370 - } - }] - }, - // Native adUnit - { - code: 'native-div', - sizes: [[1, 1]], - mediaTypes: { - native: { - title: { - required: true - }, - body: { - required: true - }, - image: { - required: true - }, - sponsoredBy: { - required: true - }, - icon: { - required: false - } - } - }, - bids: [{ - bidder: 'goldbach', - params: { - placementId: 13232354, - allowSmallerSizes: true - } - }] - }, - // Video instream adUnit - { - code: 'video-instream', - sizes: [[640, 480]], - mediaTypes: { - video: { - playerSize: [[640, 480]], - context: 'instream' + +## Test Parameters + +```javascript + var adUnits = [ + { + code: 'au-1', + mediaTypes: { + video: { + sizes: [[640, 480]], + maxduration: 30, + } + }, + bids: [ + { + bidder: 'goldbach', + params: { + publisherId: 'goldbach_debug', + slotId: '/1235/example.com/video/video/example' + } + } + ] }, - }, - bids: [{ - goldbach: 'goldbach', - params: { - placementId: 13232361, - video: { - skippable: true, - playback_methods: ['auto_play_sound_off'] - } - } - }] - }, - // Video outstream adUnit - { - code: 'video-outstream', - sizes: [[300, 250]], - mediaTypes: { - video: { - playerSize: [[300, 250]], - context: 'outstream', - // Certain ORTB 2.5 video values can be read from the mediatypes object; below are examples of supported params. - // To note - goldbach supports additional values for our system that are not part of the ORTB spec. If you want - // to use these values, they will have to be declared in the bids[].params.video object instead using the goldbach syntax. - // Between the corresponding values of the mediaTypes.video and params.video objects, the properties in params.video will - // take precedence if declared; eg in the example below, the `skippable: true` setting will be used instead of the `skip: 0`. - minduration: 1, - maxduration: 60, - skip: 0, // 1 - true, 0 - false - skipafter: 5, - playbackmethod: [2], // note - we only support options 1-4 at this time - api: [1,2,3] // note - option 6 is not supported at this time - } - }, - bids: [ - { - bidder: 'goldbach', - params: { - placementId: 13232385, - video: { - skippable: true, - playback_method: 'auto_play_sound_off' - } - } - } - ] - }, - // Banner adUnit in a App Webview - // Only use this for situations where prebid.js is in a webview of an App - // See Prebid Mobile for displaying ads via an SDK - { - code: 'banner-div', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]] - } - } - bids: [{ - bidder: 'goldbach', - params: { - placementId: 13144370, - app: { - id: "B1O2W3M4AN.com.prebid.webview", - geo: { - lat: 40.0964439, - lng: -75.3009142 - }, - device_id: { - idfa: "4D12078D-3246-4DA4-AD5E-7610481E7AE", // Apple advertising identifier - aaid: "38400000-8cf0-11bd-b23e-10b96e40000d", // Android advertising identifier - md5udid: "5756ae9022b2ea1e47d84fead75220c8", // MD5 hash of the ANDROID_ID - sha1udid: "4DFAA92388699AC6539885AEF1719293879985BF", // SHA1 hash of the ANDROID_ID - windowsadid: "750c6be243f1c4b5c9912b95a5742fc5" // Windows advertising identifier - } - } - } - }] - } -]; + { + code: 'au-2', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 50 + }, + image: { + required: true, + sizes: [300, 157] + }, + icon: { + required: true, + sizes: [30, 30] + } + } + }, + bids: [ + { + bidder: 'goldbach', + params: { + publisherId: 'goldbach_debug', + slotId: '/1235/example.com/inside-full-test-native/example' + } + } + ] + }, + { + code: 'au-3', + sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'goldbach', + params: { + publisherId: 'goldbach_debug', + slotId: '/1235/example.com/inside-full-test-banner/example' + } + } + ] + }, + ]; ``` diff --git a/modules/goldfishAdsRtdProvider.md b/modules/goldfishAdsRtdProvider.md index 4625c9a7988..9a6fd939bd1 100755 --- a/modules/goldfishAdsRtdProvider.md +++ b/modules/goldfishAdsRtdProvider.md @@ -8,7 +8,7 @@ ## Description -This RTD module provides access to the Goldfish Ads Geograph, which leverages geographic and temporal data on a privcay-first platform. This module works without using cookies, PII, emails, or device IDs across all website traffic, including unauthenticated users, and adds audience data into bid requests to increase scale and yields. +This RTD module provides access to the Goldfish Ads Geograph, which leverages geographic and temporal data on a privacy-first platform. This module works without using cookies, PII, emails, or device IDs across all website traffic, including unauthenticated users, and adds audience data into bid requests to increase scale and yields. ## Usage diff --git a/modules/gothamadsBidAdapter.js b/modules/gothamadsBidAdapter.js index ab59c6febec..bcd382e507a 100644 --- a/modules/gothamadsBidAdapter.js +++ b/modules/gothamadsBidAdapter.js @@ -1,4 +1,4 @@ -import { logMessage, deepSetValue, deepAccess, _map, logWarn } from '../src/utils.js'; +import { deepSetValue, deepAccess, _map, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; @@ -80,18 +80,9 @@ export const spec = { if (validBidRequests && validBidRequests.length === 0) return [] let accuontId = validBidRequests[0].params.accountId; const endpointURL = URL_ENDPOINT.replace(ACCOUNTID_MACROS, accuontId); - let winTop = window; let location; - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - + location = bidderRequest?.refererInfo ?? null; let bids = []; for (let bidRequest of validBidRequests) { let impObject = prepareImpObject(bidRequest); @@ -105,8 +96,8 @@ export const spec = { language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', }, site: { - page: location.pathname, - host: location.host + page: location?.page, + host: location?.domain }, source: { tid: bidderRequest?.ortb2?.source?.tid, @@ -332,7 +323,7 @@ const parseSizes = (bid, mediaType) => { const addVideoParameters = (bidRequest) => { let videoObj = {}; - let supportParamsList = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'placement', 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'] + let supportParamsList = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'] for (let param of supportParamsList) { if (bidRequest.mediaTypes.video[param] !== undefined) { diff --git a/modules/gptPreAuction.js b/modules/gptPreAuction.js index bf5b4a55dbb..a6495e3570e 100644 --- a/modules/gptPreAuction.js +++ b/modules/gptPreAuction.js @@ -1,19 +1,68 @@ +import { getSignals as getSignalsFn, getSegments as getSegmentsFn, taxonomies } from '../libraries/gptUtils/gptUtils.js'; +import { auctionManager } from '../src/auctionManager.js'; +import { config } from '../src/config.js'; +import { TARGETING_KEYS } from '../src/constants.js'; +import { getHook } from '../src/hook.js'; +import { find } from '../src/polyfill.js'; import { deepAccess, + deepSetValue, isAdUnitCodeMatchingSlot, isGptPubadsDefined, logInfo, + logWarn, pick, - deepSetValue + uniques } from '../src/utils.js'; -import {config} from '../src/config.js'; -import {getHook} from '../src/hook.js'; -import {find} from '../src/polyfill.js'; const MODULE_NAME = 'GPT Pre-Auction'; export let _currentConfig = {}; let hooksAdded = false; +export function getSegments(fpd, sections, segtax) { + return getSegmentsFn(fpd, sections, segtax); +} + +export function getSignals(fpd) { + return getSignalsFn(fpd); +} + +export function getSignalsArrayByAuctionsIds(auctionIds, index = auctionManager.index) { + const signals = auctionIds + .map(auctionId => index.getAuction({ auctionId })?.getFPD()?.global) + .map(getSignals) + .filter(fpd => fpd); + + return signals; +} + +export function getSignalsIntersection(signals) { + const result = {}; + taxonomies.forEach((taxonomy) => { + const allValues = signals + .flatMap(x => x) + .filter(x => x.taxonomy === taxonomy) + .map(x => x.values); + result[taxonomy] = allValues.length ? ( + allValues.reduce((commonElements, subArray) => { + return commonElements.filter(element => subArray.includes(element)); + }) + ) : [] + result[taxonomy] = { values: result[taxonomy] }; + }) + return result; +} + +export function getAuctionsIdsFromTargeting(targeting, am = auctionManager) { + return Object.values(targeting) + .flatMap(x => Object.entries(x)) + .filter((entry) => entry[0] === TARGETING_KEYS.AD_ID || entry[0].startsWith(TARGETING_KEYS.AD_ID + '_')) + .flatMap(entry => entry[1]) + .map(adId => am.findBidByAdId(adId)?.auctionId) + .filter(id => id != null) + .filter(uniques); +} + export const appendGptSlots = adUnits => { const { customGptSlotMatching } = _currentConfig; @@ -27,21 +76,25 @@ export const appendGptSlots = adUnits => { return acc; }, {}); + const adUnitPaths = {}; + window.googletag.pubads().getSlots().forEach(slot => { const matchingAdUnitCode = find(Object.keys(adUnitMap), customGptSlotMatching ? customGptSlotMatching(slot) : isAdUnitCodeMatchingSlot(slot)); if (matchingAdUnitCode) { + const path = adUnitPaths[matchingAdUnitCode] = slot.getAdUnitPath(); const adserver = { name: 'gam', - adslot: sanitizeSlotPath(slot.getAdUnitPath()) + adslot: sanitizeSlotPath(path) }; adUnitMap[matchingAdUnitCode].forEach((adUnit) => { deepSetValue(adUnit, 'ortb2Imp.ext.data.adserver', Object.assign({}, adUnit.ortb2Imp?.ext?.data?.adserver, adserver)); }); } }); + return adUnitPaths; }; const sanitizeSlotPath = (path) => { @@ -54,7 +107,7 @@ const sanitizeSlotPath = (path) => { return path; } -const defaultPreAuction = (adUnit, adServerAdSlot) => { +const defaultPreAuction = (adUnit, adServerAdSlot, adUnitPath) => { const context = adUnit.ortb2Imp.ext.data; // use pbadslot if supplied @@ -68,7 +121,7 @@ const defaultPreAuction = (adUnit, adServerAdSlot) => { } // find all GPT slots with this name - var gptSlots = window.googletag.pubads().getSlots().filter(slot => slot.getAdUnitPath() === adServerAdSlot); + var gptSlots = window.googletag.pubads().getSlots().filter(slot => slot.getAdUnitPath() === adUnitPath); if (gptSlots.length === 0) { return; // should never happen @@ -113,8 +166,12 @@ export const appendPbAdSlot = adUnit => { return true; }; +function warnDeprecation(adUnit) { + logWarn(`pbadslot is deprecated and will soon be removed, use gpid instead`, adUnit) +} + export const makeBidRequestsHook = (fn, adUnits, ...args) => { - appendGptSlots(adUnits); + const adUnitPaths = appendGptSlots(adUnits); const { useDefaultPreAuction, customPreAuction } = _currentConfig; adUnits.forEach(adUnit => { // init the ortb2Imp if not done yet @@ -122,21 +179,24 @@ export const makeBidRequestsHook = (fn, adUnits, ...args) => { adUnit.ortb2Imp.ext = adUnit.ortb2Imp.ext || {}; adUnit.ortb2Imp.ext.data = adUnit.ortb2Imp.ext.data || {}; const context = adUnit.ortb2Imp.ext; - // if neither new confs set do old stuff if (!customPreAuction && !useDefaultPreAuction) { + warnDeprecation(adUnit); const usedAdUnitCode = appendPbAdSlot(adUnit); // gpid should be set to itself if already set, or to what pbadslot was (as long as it was not adUnit code) if (!context.gpid && !usedAdUnitCode) { context.gpid = context.data.pbadslot; } } else { + if (context.data?.pbadslot) { + warnDeprecation(adUnit); + } let adserverSlot = deepAccess(context, 'data.adserver.adslot'); let result; if (customPreAuction) { - result = customPreAuction(adUnit, adserverSlot); + result = customPreAuction(adUnit, adserverSlot, adUnitPaths?.[adUnit.code]); } else if (useDefaultPreAuction) { - result = defaultPreAuction(adUnit, adserverSlot); + result = defaultPreAuction(adUnit, adserverSlot, adUnitPaths?.[adUnit.code]); } if (result) { context.gpid = context.data.pbadslot = result; @@ -146,6 +206,14 @@ export const makeBidRequestsHook = (fn, adUnits, ...args) => { return fn.call(this, adUnits, ...args); }; +const setPpsConfigFromTargetingSet = (next, targetingSet) => { + // set gpt config + const auctionsIds = getAuctionsIdsFromTargeting(targetingSet); + const signals = getSignalsIntersection(getSignalsArrayByAuctionsIds(auctionsIds)); + window.googletag.setConfig && window.googletag.setConfig({pps: { taxonomies: signals }}); + next(targetingSet); +}; + const handleSetGptConfig = moduleConfig => { _currentConfig = pick(moduleConfig, [ 'enabled', enabled => enabled !== false, @@ -153,18 +221,20 @@ const handleSetGptConfig = moduleConfig => { typeof customGptSlotMatching === 'function' && customGptSlotMatching, 'customPbAdSlot', customPbAdSlot => typeof customPbAdSlot === 'function' && customPbAdSlot, 'customPreAuction', customPreAuction => typeof customPreAuction === 'function' && customPreAuction, - 'useDefaultPreAuction', useDefaultPreAuction => useDefaultPreAuction === true, + 'useDefaultPreAuction', useDefaultPreAuction => useDefaultPreAuction ?? true, ]); if (_currentConfig.enabled) { if (!hooksAdded) { getHook('makeBidRequests').before(makeBidRequestsHook); + getHook('targetingDone').after(setPpsConfigFromTargetingSet) hooksAdded = true; } } else { logInfo(`${MODULE_NAME}: Turning off module`); _currentConfig = {}; getHook('makeBidRequests').getHooks({hook: makeBidRequestsHook}).remove(); + getHook('targetingDone').getHooks({hook: setPpsConfigFromTargetingSet}).remove(); hooksAdded = false; } }; diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index d6293adac82..99ce89ee4d1 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -4,9 +4,17 @@ import { EVENTS } from '../src/constants.js'; import adapterManager from '../src/adapterManager.js'; import {deepClone, generateUUID, logError, logInfo, logWarn, getParameterByName} from '../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + +/** + * @typedef {object} Message Payload message sent to the Greenbids API + */ + const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '2.2.1'; +export const ANALYTICS_VERSION = '2.3.2'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; @@ -97,6 +105,11 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER contentType: 'application/json' }); }, + /** + * + * @param {string} auctionId + * @returns {Message} + */ createCommonMessage(auctionId) { const cachedAuction = this.getCachedAuction(auctionId); return { @@ -111,13 +124,27 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER adUnits: [], }; }, + /** + * @param {Bid} bid + * @param {BIDDER_STATUS} status + */ serializeBidResponse(bid, status) { return { bidder: bid.bidder, isTimeout: (status === BIDDER_STATUS.TIMEOUT), hasBid: (status === BIDDER_STATUS.BID), + params: (bid.params && Object.keys(bid.params).length > 0) ? bid.params : {}, + ...(status === BIDDER_STATUS.BID ? { + cpm: bid.cpm, + currency: bid.currency + } : {}), }; }, + /** + * @param {*} message Greenbids API payload + * @param {Bid} bid Bid to add to the payload + * @param {BIDDER_STATUS} status Bidding status + */ addBidResponseToMessage(message, bid, status) { const adUnitCode = bid.adUnitCode.toLowerCase(); const adUnitIndex = message.adUnits.findIndex((adUnit) => { @@ -133,8 +160,11 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER if (bidderIndex === -1) { message.adUnits[adUnitIndex].bidders.push(this.serializeBidResponse(bid, status)); } else { + message.adUnits[adUnitIndex].bidders[bidderIndex].params = (bid.params && Object.keys(bid.params).length > 0) ? bid.params : {}; if (status === BIDDER_STATUS.BID) { message.adUnits[adUnitIndex].bidders[bidderIndex].hasBid = true; + message.adUnits[adUnitIndex].bidders[bidderIndex].cpm = bid.cpm; + message.adUnits[adUnitIndex].bidders[bidderIndex].currency = bid.currency; } else if (status === BIDDER_STATUS.TIMEOUT) { message.adUnits[adUnitIndex].bidders[bidderIndex].isTimeout = true; } @@ -193,9 +223,12 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); - this.sendEventMessage('/', - this.createBidMessage(auctionEndArgs, cachedAuction) - ); + const isFilteringForced = getParameterByName('greenbids_force_filtering'); + if (!isFilteringForced) { + this.sendEventMessage('/', + this.createBidMessage(auctionEndArgs, cachedAuction) + ) + }; }, handleBidTimeout(timeoutBids) { timeoutBids.forEach((bid) => { diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js new file mode 100644 index 00000000000..0ece04fe11f --- /dev/null +++ b/modules/greenbidsBidAdapter.js @@ -0,0 +1,238 @@ +import { getValue, logError, deepAccess, parseSizesInput, getBidIdParameter, logInfo, getWinDimensions } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { getHLen } from '../libraries/navigatorData/navigatorData.js'; +import { getTimeToFirstByte } from '../libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js'; +import { getReferrerInfo, getPageTitle, getPageDescription, getConnectionDownLink } from '../libraries/pageInfosUtils/pageInfosUtils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + +const BIDDER_CODE = 'greenbids'; +const GVL_ID = 1232; +const ENDPOINT_URL = 'https://hb.greenbids.ai'; +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: ['banner', 'video'], + /** + * 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. + */ + isBidRequestValid: function (bid) { + if (typeof bid.params !== 'undefined' && parseInt(getValue(bid.params, 'placementId')) > 0) { + logInfo('Greenbids bidder adapter valid bid request'); + return true; + } else { + logError('Greenbids bidder adapter requires placementId to be defined and a positive number'); + return false; + } + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} validBidRequests array of bids + * @param bidderRequest bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const bids = validBidRequests.map(bids => { + const reqObj = {}; + let placementId = getValue(bids.params, 'placementId'); + const gpid = deepAccess(bids, 'ortb2Imp.ext.gpid'); + reqObj.sizes = getSizes(bids); + reqObj.bidId = getBidIdParameter('bidId', bids); + reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bids); + reqObj.placementId = parseInt(placementId, 10); + reqObj.adUnitCode = getBidIdParameter('adUnitCode', bids); + reqObj.transactionId = bids.ortb2Imp?.ext?.tid || ''; + if (gpid) { reqObj.gpid = gpid; } + }); + const topWindow = window.top; + + const payload = { + referrer: getReferrerInfo(bidderRequest), + pageReferrer: document.referrer, + pageTitle: getPageTitle().slice(0, 300), + pageDescription: getPageDescription().slice(0, 300), + networkBandwidth: getConnectionDownLink(window.navigator), + timeToFirstByte: getTimeToFirstByte(window), + data: bids, + device: bidderRequest?.ortb2?.device || {}, + deviceWidth: screen.width, + deviceHeight: screen.height, + devicePixelRatio: topWindow.devicePixelRatio, + screenOrientation: screen.orientation?.type, + historyLength: getHLen(), + viewportHeight: getWinDimensions().visualViewport.height, + viewportWidth: getWinDimensions().visualViewport.width, + prebid_version: '$prebid.version$', + }; + + const firstBidRequest = validBidRequests[0]; + + if (firstBidRequest.schain) { + payload.schain = firstBidRequest.schain; + } + + hydratePayloadWithGppConsentData(payload, bidderRequest.gppConsent); + hydratePayloadWithGdprConsentData(payload, bidderRequest.gdprConsent); + hydratePayloadWithUspConsentData(payload, bidderRequest.uspConsent); + + const userAgentClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua'); + if (userAgentClientHints) { + payload.userAgentClientHints = userAgentClientHints; + } + + const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); + if (dsa) { + payload.dsa = dsa; + } + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server response. + */ + interpretResponse: function (serverResponse) { + serverResponse = serverResponse.body; + if (!serverResponse.responses) { + return []; + } + return serverResponse.responses.map((bid) => { + const bidResponse = { + cpm: bid.cpm, + width: bid.width, + height: bid.height, + currency: bid.currency, + netRevenue: true, + size: bid.size, + ttl: bid.ttl, + meta: { + advertiserDomains: bid && bid.adomain ? bid.adomain : [], + }, + ad: bid.ad, + requestId: bid.bidId, + creativeId: bid.creativeId, + placementId: bid.placementId, + }; + if (bid.dealId) { + bidResponse.dealId = bid.dealId + } + if (bid?.ext?.dsa) { + bidResponse.meta.dsa = bid.ext.dsa; + } + return bidResponse; + }); + } +}; + +registerBidder(spec); + +/** + * Converts the sizes from the bid object to the required format. + * + * @param {Object} bid - The bid object containing size information. + * @param {Array} bid.sizes - The sizes array from the bid object. + * @returns {Array} - The parsed sizes in the required format. + */ +function getSizes(bid) { + return parseSizesInput(bid.sizes); +} + +// Privacy handling + +/** + * Hydrates the given payload with GPP consent data if available. + * + * @param {Object} payload - The payload object to be hydrated. + * @param {Object} gppData - The GPP consent data object. + * @param {string} gppData.gppString - The GPP consent string. + * @param {number[]} gppData.applicableSections - An array of applicable section IDs. + */ +function hydratePayloadWithGppConsentData(payload, gppData) { + if (!gppData) { return; } + let isValidConsentString = typeof gppData.gppString === 'string'; + let validateApplicableSections = + Array.isArray(gppData.applicableSections) && + gppData.applicableSections.every((section) => typeof (section) === 'number') + payload.gpp = { + consentString: isValidConsentString ? gppData.gppString : '', + applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [], + }; +} + +/** + * Hydrates the given payload with GDPR consent data if available. + * + * @param {Object} payload - The payload object to be hydrated with GDPR consent data. + * @param {Object} gdprData - The GDPR data object containing consent information. + * @param {boolean} gdprData.gdprApplies - Indicates if GDPR applies. + * @param {string} gdprData.consentString - The GDPR consent string. + * @param {number} gdprData.apiVersion - The version of the GDPR API being used. + * @param {Object} gdprData.vendorData - Additional vendor data related to GDPR. + */ +function hydratePayloadWithGdprConsentData(payload, gdprData) { + if (!gdprData) { return; } + let isCmp = typeof gdprData.gdprApplies === 'boolean'; + let isConsentString = typeof gdprData.consentString === 'string'; + let status = isCmp + ? findGdprStatus(gdprData.gdprApplies, gdprData.vendorData) + : gdprStatus.CMP_NOT_FOUND_OR_ERROR; + payload.gdpr_iab = { + consent: isConsentString ? gdprData.consentString : '', + status: status, + apiVersion: gdprData.apiVersion + }; +} + +/** + * Adds USP (CCPA) consent data to the payload if available. + * + * @param {Object} payload - The payload object to be hydrated with USP consent data. + * @param {string} uspConsentData - The USP consent string to be added to the payload. + */ +function hydratePayloadWithUspConsentData(payload, uspConsentData) { + if (!uspConsentData) { return; } + payload.us_privacy = uspConsentData; +} + +const gdprStatus = { + GDPR_APPLIES_PUBLISHER: 12, + GDPR_APPLIES_GLOBAL: 11, + GDPR_DOESNT_APPLY: 0, + CMP_NOT_FOUND_OR_ERROR: 22 +}; + +/** + * Determines the GDPR status based on whether GDPR applies and the provided GDPR data. + * + * @param {boolean} gdprApplies - Indicates if GDPR applies. + * @param {Object} gdprData - The GDPR data object. + * @param {boolean} gdprData.isServiceSpecific - Indicates if the GDPR data is service-specific. + * @returns {string} The GDPR status. + */ +function findGdprStatus(gdprApplies, gdprData) { + let status = gdprStatus.GDPR_APPLIES_PUBLISHER; + if (gdprApplies) { + if (gdprData && !gdprData.isServiceSpecific) { + status = gdprStatus.GDPR_APPLIES_GLOBAL; + } + } else { + status = gdprStatus.GDPR_DOESNT_APPLY; + } + return status; +} diff --git a/modules/greenbidsBidAdapter.md b/modules/greenbidsBidAdapter.md new file mode 100644 index 00000000000..df536294f47 --- /dev/null +++ b/modules/greenbidsBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +**Module Name**: Greenbids Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: tech@greenbids.ai + +# Description + +Use `greenbids` as bidder. + +## AdUnits configuration example +``` + var adUnits = [{ + code: 'your-slot_1-div', //use exactly the same code as your slot div id. + sizes: [[300, 250]], + bids: [{ + bidder: 'greenbids', + params: { + placementId: 12345, + } + }] + },{ + code: 'your-slot_2-div', //use exactly the same code as your slot div id. + sizes: [[600, 800]], + bids: [{ + bidder: 'greenbids', + params: { + placementId: 12345, + } + }] + }]; +``` diff --git a/modules/greenbidsRtdProvider.js b/modules/greenbidsRtdProvider.js index 5496fc71c4e..e350cebb33e 100644 --- a/modules/greenbidsRtdProvider.js +++ b/modules/greenbidsRtdProvider.js @@ -1,11 +1,11 @@ -import { logError, deepClone, generateUUID, deepSetValue, deepAccess } from '../src/utils.js'; +import { logError, logInfo, logWarn, logMessage, deepClone, generateUUID, deepSetValue, deepAccess, getParameterByName } from '../src/utils.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'; const MODULE_NAME = 'greenbidsRtdProvider'; -const MODULE_VERSION = '2.0.0'; +const MODULE_VERSION = '2.0.1'; const ENDPOINT = 'https://t.greenbids.ai'; const rtdOptions = {}; @@ -46,6 +46,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { function createPromise(reqBidsConfigObj, greenbidsId) { return new Promise((resolve) => { const timeoutId = setTimeout(() => { + logWarn('GreenbidsRtdProvider: Greenbids API timeout, skipping shaping'); resolve(reqBidsConfigObj); }, rtdOptions.timeout); ajax( @@ -57,6 +58,7 @@ function createPromise(reqBidsConfigObj, greenbidsId) { }, error: () => { clearTimeout(timeoutId); + logWarn('GreenbidsRtdProvider: Greenbids API response error, skipping shaping'); resolve(reqBidsConfigObj); }, }, @@ -73,11 +75,17 @@ function createPromise(reqBidsConfigObj, greenbidsId) { function processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId) { clearTimeout(timeoutId); - const responseAdUnits = JSON.parse(response); - updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId); + try { + const responseAdUnits = JSON.parse(response); + updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId); + } catch (e) { + logWarn('GreenbidsRtdProvider: Greenbids API response parsing error, skipping shaping'); + } } function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) { + const isFilteringForced = getParameterByName('greenbids_force_filtering'); + const isFilteringDisabled = getParameterByName('greenbids_disable_filtering'); adUnits.forEach((adUnit) => { const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code); if (matchingAdUnit) { @@ -86,7 +94,12 @@ function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) { keptInAuction: matchingAdUnit.bidders, isExploration: matchingAdUnit.isExploration }); - if (!matchingAdUnit.isExploration) { + if (matchingAdUnit.isExploration || isFilteringDisabled) { + logMessage('Greenbids Rtd: either exploration traffic, or disabled filtering flag detected'); + } else if (isFilteringForced) { + adUnit.bids = []; + logInfo('Greenbids Rtd: filtering flag detected, forcing filtering of Rtd module.'); + } else { removeFalseBidders(adUnit, matchingAdUnit); } } diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index f7db6d878f1..4f3dfb94747 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -7,7 +7,8 @@ import { mergeDeep, logWarn, isNumber, - isStr + isStr, + isPlainObject } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -15,6 +16,7 @@ import { Renderer } from '../src/Renderer.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +import { getBidFromResponse } from '../libraries/processResponse/index.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -274,6 +276,11 @@ export const spec = { userExt.device = { ...ortb2UserExtDevice }; } + // if present, add device data object from ortb2 to the request + if (bidderRequest?.ortb2?.device) { + request.device = bidderRequest.ortb2.device; + } + if (userIdAsEids && userIdAsEids.length) { userExt = userExt || {}; userExt.eids = [...userIdAsEids]; @@ -441,7 +448,7 @@ export const spec = { if (!errorMessage && serverResponse.seatbid) { serverResponse.seatbid.forEach(respItem => { - _addBidResponse(_getBidFromResponse(respItem), bidRequest, bidResponses, RendererConst, bidderCode); + _addBidResponse(getBidFromResponse(respItem, LOG_ERROR_MESS), bidRequest, bidResponses, RendererConst, bidderCode); }); } if (errorMessage) logError(errorMessage); @@ -502,7 +509,7 @@ function _getFloor (mediaTypes, bid) { size: bid.sizes.map(([w, h]) => ({w, h})) }); - if (typeof floorInfo === 'object' && + if (isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = Math.max(floor, parseFloat(floorInfo.floor)); @@ -512,17 +519,6 @@ function _getFloor (mediaTypes, bid) { return floor; } -function _getBidFromResponse(respItem) { - if (!respItem) { - logError(LOG_ERROR_MESS.emptySeatbid); - } else if (!respItem.bid) { - logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); - } else if (!respItem.bid[0]) { - logError(LOG_ERROR_MESS.noBid); - } - return respItem && respItem.bid && respItem.bid[0]; -} - function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst, bidderCode) { if (!serverBid) return; let errorMessage; diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index d2cd160a364..0b1f343e4dc 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -31,7 +31,7 @@ let analyticsType = 'endpoint'; let growthCodeAnalyticsAdapter = Object.assign(adapter({url: url, analyticsType}), { track({eventType, args}) { - let eventData = args ? JSON.parse(JSON.stringify(args)) : {}; + let eventData = args ? utils.deepClone(args) : {}; let data = {}; if (!trackEvents.includes(eventType)) return; switch (eventType) { @@ -140,7 +140,7 @@ function logToServer() { if (pid === DEFAULT_PID) return; if (eventQueue.length >= 1) { // Get the correct GCID - let gcid = localStorage.getItem('gcid') + let gcid = storage.getDataFromLocalStorage('gcid'); let data = { session: sessionId, diff --git a/modules/growthCodeRtdProvider.js b/modules/growthCodeRtdProvider.js index b12b25a0951..a8893b9648e 100644 --- a/modules/growthCodeRtdProvider.js +++ b/modules/growthCodeRtdProvider.js @@ -75,7 +75,7 @@ function callServer(configParams, items, expiresAt, userConsent) { storage.removeDataFromLocalStorage(RTD_EXPIRE_KEY, null) } if ((items === null) && (isNaN(expiresAt))) { - let gcid = localStorage.getItem('gcid') + let gcid = storage.getDataFromLocalStorage('gcid') let url = configParams.url ? configParams.url : ENDPOINT_URL; url = tryAppendQueryString(url, 'pid', configParams.pid); diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 4b12431302f..35fc95d5d6d 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -1,5 +1,5 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {_each, deepAccess, logError, logWarn, parseSizesInput} from '../src/utils.js'; +import {_each, deepAccess, getWinDimensions, logError, logWarn, parseSizesInput} from '../src/utils.js'; import {config} from '../src/config.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -29,13 +29,14 @@ let invalidRequestIds = {}; let pageViewId = null; // TODO: potential 0 values for browserParams sent to ad server -function _getBrowserParams(topWindowUrl) { +function _getBrowserParams(topWindowUrl, mosttopLocation) { const paramRegex = paramName => new RegExp(`[?#&](${paramName}=(.*?))($|&)`, 'i'); let browserParams = {}; let topWindow; let topScreen; let topUrl; + let mosttopURL let ggad; let ggdeal; let ns; @@ -74,17 +75,19 @@ function _getBrowserParams(topWindowUrl) { topWindow = global.top; topScreen = topWindow.screen; topUrl = topWindowUrl || ''; + mosttopURL = mosttopLocation || ''; } catch (error) { logError(error); return browserParams; } browserParams = { - vw: topWindow.innerWidth, - vh: topWindow.innerHeight, + vw: getWinDimensions().innerWidth, + vh: getWinDimensions().innerHeight, sw: topScreen.width, sh: topScreen.height, pu: stripGGParams(topUrl), + tpl: mosttopURL, ce: storage.cookiesAreEnabled(), dpr: topWindow.devicePixelRatio || 1, jcsi: JSON.stringify(JCSI), @@ -187,7 +190,12 @@ function _getVidParams(attributes) { placement: pt, plcmt, protocols = [], - playerSize = [] + playerSize = [], + skip, + api, + mimes, + playbackmethod, + playbackend: pbe } = attributes; const sizes = parseSizesInput(playerSize); const [viw, vih] = sizes[0] && sizes[0].split('x'); @@ -205,21 +213,34 @@ function _getVidParams(attributes) { pt, pr, viw, - vih + vih, + skip, + pbe }; - // Add vplcmt property to the result object if plcmt is available + if (plcmt !== undefined && plcmt !== null) { result.vplcmt = plcmt; } + if (api && api.length) { + result.api = api.join(','); + } + if (mimes && mimes.length) { + result.mimes = mimes.join(','); + } + if (playbackmethod && playbackmethod.length) { + result.pbm = playbackmethod.join(','); + } + return result; } /** - * Gets bidfloor - * @param {Object} mediaTypes - * @param {Number} bidfloor - * @param {Object} bid - * @returns {Number} floor + * Retrieves the bid floor value, which is the minimum acceptable bid for an ad unit. + * This function calculates the bid floor based on the given media types and other bidding parameters. + * @param {Object} mediaTypes - The media types specified for the bid, which might influence floor calculations. + * @param {number} staticBidFloor - The default or static bid floor set for the bid. + * @param {Object} bid - The bid object which may contain a method to get dynamic floor values. + * @returns {Object} An object containing the calculated bid floor and its currency. */ function _getFloor(mediaTypes, staticBidFloor, bid) { const curMediaType = Object.keys(mediaTypes)[0] || 'banner'; @@ -229,7 +250,7 @@ function _getFloor(mediaTypes, staticBidFloor, bid) { const { currency, floor } = bid.getFloor({ mediaType: curMediaType, size: '*' - }); + }) || {}; floor && (bidFloor.floor = floor); currency && (bidFloor.currency = currency); @@ -243,6 +264,41 @@ function _getFloor(mediaTypes, staticBidFloor, bid) { return bidFloor; } +/** + * Retrieves the device data from the ORTB2 object + * @param {Object} ortb2Data ORTB2 object + * @returns {Object} Device data + */ +function _getDeviceData(ortb2Data) { + const _device = deepAccess(ortb2Data, 'device') || {}; + + // set device data params from ortb2 + const _deviceRequestParams = { + ip: _device.ip, + ipv6: _device.ipv6, + ua: _device.ua, + dnt: _device.dnt, + os: _device.os, + osv: _device.osv, + dt: _device.devicetype, + lang: _device.language, + make: _device.make, + model: _device.model, + ppi: _device.ppi, + pxratio: _device.pxratio, + foddid: _device?.ext?.fiftyonedegrees_deviceId, + }; + + // return device data params with only non-empty values + return Object.keys(_deviceRequestParams) + .reduce((r, key) => { + if (_deviceRequestParams[key] !== undefined) { + r[key] = _deviceRequestParams[key]; + } + return r; + }, {}); +} + /** * loops through bannerSizes array to get greatest slot dimensions * @param {number[][]} sizes @@ -290,10 +346,10 @@ function getEids(userId) { } /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. + * Builds requests for bids. + * @param {validBidRequests[]} validBidRequests - An array of valid bid requests. + * @param {Object} bidderRequest - The bidder's request information. + * @returns {Object[]} An array of server requests. */ function buildRequests(validBidRequests, bidderRequest) { const bids = []; @@ -303,6 +359,7 @@ function buildRequests(validBidRequests, bidderRequest) { const timeout = bidderRequest && bidderRequest.timeout const coppa = config.getConfig('coppa') === true ? 1 : 0; const topWindowUrl = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page; + const mosttopLocation = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.topmostLocation _each(validBidRequests, bidRequest => { const { bidId, @@ -315,7 +372,8 @@ function buildRequests(validBidRequests, bidderRequest) { } = bidRequest; const { currency, floor } = _getFloor(mediaTypes, params.bidfloor, bidRequest); const eids = getEids(userId); - const gpid = deepAccess(ortb2Imp, 'ext.data.pbadslot') || deepAccess(ortb2Imp, 'ext.data.adserver.adslot'); + const gpid = deepAccess(ortb2Imp, 'ext.gpid') || deepAccess(ortb2Imp, 'ext.data.pbadslot'); + const paapiEligible = deepAccess(ortb2Imp, 'ext.ae') === 1 let sizes = [1, 1]; let data = {}; data.displaymanager = 'Prebid.js - gumgum'; @@ -373,15 +431,14 @@ function buildRequests(validBidRequests, bidderRequest) { data.fp = floor; data.fpc = currency; } - + if (bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.site) { + setIrisId(data, bidderRequest.ortb2.site, params); + const curl = bidderRequest.ortb2.site.content?.url; + if (curl) data.curl = curl; + } if (params.iriscat && typeof params.iriscat === 'string') { data.iriscat = params.iriscat; } - - if (params.irisid && typeof params.irisid === 'string') { - data.irisid = params.irisid; - } - if (params.zone || params.pubId) { params.zone ? (data.t = params.zone) : (data.pubId = params.pubId); @@ -405,7 +462,9 @@ function buildRequests(validBidRequests, bidderRequest) { } else { // legacy params data = { ...data, ...handleLegacyParams(params, sizes) }; } - + if (paapiEligible) { + data.ae = paapiEligible + } if (gdprConsent) { data.gdprApplies = gdprConsent.gdprApplies ? 1 : 0; } @@ -432,21 +491,49 @@ function buildRequests(validBidRequests, bidderRequest) { if (schain && schain.nodes) { data.schain = _serializeSupplyChainObj(schain); } + const tId = deepAccess(ortb2Imp, 'ext.tid') || deepAccess(bidderRequest, 'ortb2.source.tid') || ''; + data.tId = tId + Object.assign( + data, + _getBrowserParams(topWindowUrl, mosttopLocation), + _getDeviceData(bidderRequest?.ortb2), + ); bids.push({ id: bidId, tmax: timeout, - tId: ortb2Imp?.ext?.tid, + tId: tId, pi: data.pi, selector: params.selector, sizes, url: BID_ENDPOINT, method: 'GET', - data: Object.assign(data, _getBrowserParams(topWindowUrl)) + data }); }); return bids; } +export function getCids(site) { + if (site.content && Array.isArray(site.content.data)) { + for (const dataItem of site.content.data) { + if (typeof dataItem?.name === 'string' && (dataItem.name.includes('iris.com') || dataItem.name.includes('iris.tv'))) { + return Array.isArray(dataItem.ext?.cids) ? dataItem.ext.cids.join(',') : ''; + } + } + } + return null; +} +export function setIrisId(data, site, params) { + let irisID = getCids(site); + if (irisID) { + data.irisid = irisID; + } else { + // Just adding this chechk for safty and if needed we can remove + if (params.irisid && typeof params.irisid === 'string') { + data.irisid = params.irisid; + } + } +} function handleLegacyParams(params, sizes) { const data = {}; diff --git a/modules/h12mediaBidAdapter.js b/modules/h12mediaBidAdapter.js index 7b1ba9ee286..9b9414a9ec6 100644 --- a/modules/h12mediaBidAdapter.js +++ b/modules/h12mediaBidAdapter.js @@ -1,5 +1,7 @@ import { inIframe, logError, logMessage, deepAccess } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; const BIDDER_CODE = 'h12media'; const DEFAULT_URL = 'https://bidder.h12-media.com/prebid/'; const DEFAULT_CURRENCY = 'USD'; @@ -35,8 +37,8 @@ export const spec = { x: framePos[0], y: framePos[1], } : { - x: adUnitElement && adUnitElement.getBoundingClientRect().x, - y: adUnitElement && adUnitElement.getBoundingClientRect().y, + x: adUnitElement && getBoundingClientRect(adUnitElement).x, + y: adUnitElement && getBoundingClientRect(adUnitElement).y, }; const bidrequest = { @@ -208,8 +210,7 @@ function isVisible(element) { function getClientDimensions() { try { - const t = window.top.innerWidth || window.top.document.documentElement.clientWidth || window.top.document.body.clientWidth; - const e = window.top.innerHeight || window.top.document.documentElement.clientHeight || window.top.document.body.clientHeight; + const { width: t, height: e } = getViewportSize(); return [Math.round(t), Math.round(e)]; } catch (i) { return [0, 0]; @@ -242,8 +243,8 @@ function getFramePos() { if (m > 1) { t = t.parent } - frmLeft = frmLeft + t.frameElement.getBoundingClientRect().left; - frmTop = frmTop + t.frameElement.getBoundingClientRect().top; + frmLeft = frmLeft + getBoundingClientRect(t.frameElement).left; + frmTop = frmTop + getBoundingClientRect(t.frameElement).top; } catch (o) { /* keep looping */ } } while ((m < 100) && (t.parent !== t.self)) diff --git a/modules/hadronAnalyticsAdapter.js b/modules/hadronAnalyticsAdapter.js index d9fd4fa6f19..b5f8a7baa33 100644 --- a/modules/hadronAnalyticsAdapter.js +++ b/modules/hadronAnalyticsAdapter.js @@ -6,6 +6,7 @@ import { EVENTS } from '../src/constants.js'; import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; /** * hadronAnalyticsAdapter.js - Audigent Hadron Analytics Adapter @@ -24,12 +25,7 @@ var viewId = utils.generateUUID(); var partnerId = DEFAULT_PARTNER_ID; var eventsToTrack = []; -var w = window; -var d = document; -var e = d.documentElement; -var g = d.getElementsByTagName('body')[0]; -var x = w.innerWidth || e.clientWidth || g.clientWidth; -var y = w.innerHeight || e.clientHeight || g.clientHeight; +const { width: x, height: y } = getViewportSize(); var pageView = { eventType: 'pageView', @@ -53,7 +49,7 @@ let analyticsType = 'endpoint'; let hadronAnalyticsAdapter = Object.assign(adapter({url: HADRON_ANALYTICS_URL, analyticsType}), { track({eventType, args}) { - args = args ? JSON.parse(JSON.stringify(args)) : {}; + args = args ? utils.deepClone(args) : {}; var data = {}; if (!eventsToTrack.includes(eventType)) return; switch (eventType) { diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index 66cb5624a38..ccd63bc0184 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -19,9 +19,9 @@ import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterM * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse */ -const LOG_PREFIX = '[hadronIdSystem]'; -const HADRONID_LOCAL_NAME = 'auHadronId'; -const MODULE_NAME = 'hadronId'; +export const MODULE_NAME = 'hadronId'; +const LOG_PREFIX = `[${MODULE_NAME}System]`; +export const LS_TAM_KEY = 'auHadronId'; const AU_GVLID = 561; const DEFAULT_HADRON_URL_ENDPOINT = 'https://id.hadron.ad.gt/api/v1/pbhid'; @@ -68,11 +68,9 @@ export const hadronIdSubmodule = { * @returns {Object} */ decode(value) { - const hadronId = storage.getDataFromLocalStorage(HADRONID_LOCAL_NAME); - if (isStr(hadronId)) { - return {hadronId: hadronId}; + return { + hadronId: isStr(value) ? value : value.hasOwnProperty('id') ? value.id[MODULE_NAME] : value[MODULE_NAME] } - return (value && typeof value['hadronId'] === 'string') ? {'hadronId': value['hadronId']} : undefined; }, /** * performs action to obtain id and return a value in the callback's response argument @@ -81,14 +79,19 @@ export const hadronIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { + logInfo(LOG_PREFIX, `getId is called`, config); if (!isPlainObject(config.params)) { config.params = {}; } - const partnerId = config.params.partnerId | 0; - let hadronId = storage.getDataFromLocalStorage(HADRONID_LOCAL_NAME); - if (isStr(hadronId)) { - return {id: {hadronId}}; + let hadronId = ''; + // at this point hadronId was not found by prebid, let check if it is in the webpage by other ways + hadronId = storage.getDataFromLocalStorage(LS_TAM_KEY); + if (isStr(hadronId) && hadronId.length > 0) { + logInfo(LOG_PREFIX, `${LS_TAM_KEY} found in localStorage = ${hadronId}`) + // return {callback: function(cb) { cb(hadronId) }}; + return {id: hadronId} } + const partnerId = config.params.partnerId | 0; const resp = function (callback) { let responseObj = {}; const callbacks = { @@ -98,11 +101,13 @@ export const hadronIdSubmodule = { responseObj = JSON.parse(response); } catch (error) { logError(error); + callback(); } logInfo(LOG_PREFIX, `Response from backend is ${response}`, responseObj); - hadronId = responseObj['hadronId']; - storage.setDataInLocalStorage(HADRONID_LOCAL_NAME, hadronId); - responseObj = {id: {hadronId}}; + if (isPlainObject(responseObj) && responseObj.hasOwnProperty(MODULE_NAME)) { + hadronId = responseObj[MODULE_NAME]; + } + responseObj = hadronId; // {id: {hadronId: hadronId}}; } callback(responseObj); }, @@ -115,7 +120,7 @@ export const hadronIdSubmodule = { // config.params.url and config.params.urlArg are not documented // since their use is for debugging purposes only paramOrDefault(config.params.url, DEFAULT_HADRON_URL_ENDPOINT, config.params.urlArg), - `partner_id=${partnerId}&_it=prebid&t=1&src=id` // src=id => the backend was called from getId + `partner_id=${partnerId}&_it=prebid&t=1&src=id&domain=${document.location.hostname}` // src=id => the backend was called from getId ); if (isDebug) { url += '&debug=1' @@ -137,7 +142,7 @@ export const hadronIdSubmodule = { url += `${gppConsent.applicableSections ? '&gpp_sid=' + encodeURIComponent(gppConsent.applicableSections) : ''}`; } - logInfo(LOG_PREFIX, `hadronId not found in storage, calling home (${url})`); + logInfo(LOG_PREFIX, `${MODULE_NAME} not found, calling home (${url})`); ajax(url, callbacks, undefined, {method: 'GET'}); }; diff --git a/modules/hadronIdSystem.md b/modules/hadronIdSystem.md index 212030cbcd9..f58cd46ef61 100644 --- a/modules/hadronIdSystem.md +++ b/modules/hadronIdSystem.md @@ -12,7 +12,7 @@ pbjs.setConfig({ userIds: [{ name: 'hadronId', params: { - partnerId: 1234 // change it to the Partner ID you'll get from Audigent + partnerId: 1234 // change it to the Partner ID you got from Audigent }, storage: { name: 'hadronId', @@ -25,14 +25,13 @@ pbjs.setConfig({ ## Parameter Descriptions for the `usersync` Configuration Section The below parameters apply only to the HadronID User ID Module integration. -| Param under usersync.userIds[] | Scope | Type | Description | Example | -|--------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| -| name | Required | String | ID value for the HadronID module - `"hadronId"` | `"hadronId"` | -| storage | Required | Object | The publisher must specify the local storage in which to store the results of the call to get the user ID. This can be either cookie or HTML5 storage. | | -| storage.type | Required | String | This is where the results of the user ID will be stored. The recommended method is `localStorage` by specifying `html5`. | `"html5"` | -| storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. | `"hadronid"` | -| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. | `365` | -| value | Optional | Object | Used only if the page has a separate mechanism for storing the Hadron ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"hadronId": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | +| Param under usersync.userIds[] | Scope | Type | Description | Example | +|--------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| +| name | Required | String | ID value for the HadronID module - `"hadronId"` | `"hadronId"` | +| storage | Required | Object | The publisher must specify the local storage in which to store the results of the call to get the user ID. This can be either cookie or HTML5 storage. | | +| storage.type | Required | String | This is where the the user ID will be stored. The recommended method is `localStorage` by specifying `html5`. | `"html5"` | +| storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. The recommended value is `hadronId`. | `"auHadronId"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. The recommended value is 14 days. | `14` | +| value | Optional | Object | Used only if the page has a separate mechanism for storing the Hadron ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"hadronId": "0aRSTUAackg79ijgd8e8j6kah9ed9j6hdfgb6cl00volopxo00npzjmmb"}` | | params | Optional | Object | Used to store params for the id system | -| params.partnerId | Required | Number | This is the Audigent Partner ID obtained from Audigent. | `1234` | - | +| params.partnerId | Required | Number | This is the Audigent Partner ID obtained from Audigent. | `1234` | diff --git a/modules/hadronRtdProvider.js b/modules/hadronRtdProvider.js index 5c604709b4b..eae85db3c34 100644 --- a/modules/hadronRtdProvider.js +++ b/modules/hadronRtdProvider.js @@ -5,12 +5,11 @@ * @module modules/hadronRtdProvider * @requires module:modules/realTimeData */ -import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import {isFn, isStr, isArray, deepEqual, isPlainObject, logError, logInfo} from '../src/utils.js'; +import {isFn, isStr, isArray, deepEqual, isPlainObject, logInfo} from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; @@ -18,14 +17,13 @@ import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule */ -const LOG_PREFIX = 'User ID - HadronRtdProvider submodule: '; +const LOG_PREFIX = '[HadronRtdProvider] '; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'hadron'; const AU_GVLID = 561; -const HADRON_ID_DEFAULT_URL = 'https://id.hadron.ad.gt/api/v1/hadronid?_it=prebid'; -const HADRON_SEGMENT_URL = 'https://id.hadron.ad.gt/api/v1/rtd'; -export const HALOID_LOCAL_NAME = 'auHadronId'; -export const RTD_LOCAL_NAME = 'auHadronRtd'; +const HADRON_JS_URL = 'https://cdn.hadronid.net/hadron.js'; +const LS_TAM_KEY = 'auHadronId'; +const RTD_LOCAL_NAME = 'auHadronRtd'; export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** @@ -37,19 +35,6 @@ const urlAddParams = (url, params) => { return url + (url.indexOf('?') > -1 ? '&' : '?') + params }; -/** - * Deep set an object unless value present. - * @param {Object} obj - * @param {String} path - * @param {Object} val - */ -function set(obj, path, val) { - const keys = path.split('.'); - const lastKey = keys.pop(); - const lastObj = keys.reduce((obj, key) => obj[key] = obj[key] || {}, obj); - lastObj[lastKey] = lastObj[lastKey] || val; -} - /** * Deep object merging with array deduplication. * @param {Object} target @@ -62,11 +47,11 @@ function mergeDeep(target, ...sources) { if (isPlainObject(target) && isPlainObject(source)) { for (const key in source) { if (isPlainObject(source[key])) { - if (!target[key]) Object.assign(target, { [key]: {} }); + if (!target[key]) Object.assign(target, {[key]: {}}); mergeDeep(target[key], source[key]); } else if (isArray(source[key])) { if (!target[key]) { - Object.assign(target, { [key]: source[key] }); + Object.assign(target, {[key]: source[key]}); } else if (isArray(target[key])) { source[key].forEach(obj => { let e = 1; @@ -82,7 +67,7 @@ function mergeDeep(target, ...sources) { }); } } else { - Object.assign(target, { [key]: source[key] }); + Object.assign(target, {[key]: source[key]}); } } } @@ -132,7 +117,6 @@ export function addRealTimeData(bidConfig, rtd, rtdConfig) { if (rtdConfig.params && rtdConfig.params.handleRtd) { rtdConfig.params.handleRtd(bidConfig, rtd, rtdConfig, config); } else { - // TODO: this and haloRtdProvider are a copy-paste of each other if (isPlainObject(rtd.ortb2)) { mergeLazy(bidConfig.ortb2Fragments?.global, rtd.ortb2); } @@ -151,6 +135,17 @@ export function addRealTimeData(bidConfig, rtd, rtdConfig) { * @param {Object} userConsent */ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { + if (!storage.getDataFromLocalStorage(LS_TAM_KEY)) { + const partnerId = rtdConfig.params.partnerId | 0; + const hadronIdUrl = rtdConfig.params && rtdConfig.params.hadronIdUrl; + const scriptUrl = urlAddParams( + paramOrDefault(hadronIdUrl, HADRON_JS_URL, {}), + `partner_id=${partnerId}&_it=prebid` + ); + loadExternalScript(scriptUrl, MODULE_TYPE_RTD, SUBMODULE_NAME, () => { + logInfo(LOG_PREFIX, 'hadronId JS snippet loaded', scriptUrl); + }) + } if (rtdConfig && isPlainObject(rtdConfig.params) && rtdConfig.params.segmentCache) { let jsonData = storage.getDataFromLocalStorage(RTD_LOCAL_NAME); @@ -165,83 +160,21 @@ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { } } - const userIds = typeof getGlobal().getUserIds === 'function' ? (getGlobal()).getUserIds() : {}; + const userIds = {}; - let hadronId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); - if (isStr(hadronId)) { - if (typeof getGlobal().refreshUserIds === 'function') { - (getGlobal()).refreshUserIds({submoduleNames: 'hadronId'}); - } - userIds.hadronId = hadronId; - getRealTimeDataAsync(bidConfig, onDone, rtdConfig, userConsent, userIds); + const allUserIds = getGlobal().getUserIds(); + if (allUserIds.hasOwnProperty('hadronId')) { + userIds['hadronId'] = allUserIds.hadronId; + logInfo(LOG_PREFIX, 'hadronId user module found', allUserIds.hadronId); } else { - window.pubHadronCb = (hadronId) => { - userIds.hadronId = hadronId; - getRealTimeDataAsync(bidConfig, onDone, rtdConfig, userConsent, userIds); + let hadronId = storage.getDataFromLocalStorage(LS_TAM_KEY); + if (isStr(hadronId) && hadronId.length > 0) { + userIds['hadronId'] = hadronId; + logInfo(LOG_PREFIX, 'hadronId TAM found', hadronId); } - const partnerId = rtdConfig.params.partnerId | 0; - const hadronIdUrl = rtdConfig.params && rtdConfig.params.hadronIdUrl; - const scriptUrl = urlAddParams( - paramOrDefault(hadronIdUrl, HADRON_ID_DEFAULT_URL, userIds), - `partner_id=${partnerId}&_it=prebid` - ); - loadExternalScript(scriptUrl, 'hadron', () => { - logInfo(LOG_PREFIX, 'hadronIdTag loaded', scriptUrl); - }) } } -/** - * Async rtd retrieval from Audigent - * @param {Object} bidConfig - * @param {function} onDone - * @param {Object} rtdConfig - * @param {Object} userConsent - * @param {Object} userIds - */ -export function getRealTimeDataAsync(bidConfig, onDone, rtdConfig, userConsent, userIds) { - let reqParams = {}; - - if (isPlainObject(rtdConfig)) { - set(rtdConfig, 'params.requestParams.ortb2', bidConfig.ortb2Fragments.global); - reqParams = rtdConfig.params.requestParams; - } - - if (isPlainObject(window.pubHadronPm)) { - reqParams.pubHadronPm = window.pubHadronPm; - } - - ajax(HADRON_SEGMENT_URL, { - success: function (response, req) { - if (req.status === 200) { - try { - const data = JSON.parse(response); - if (data && data.rtd) { - addRealTimeData(bidConfig, data.rtd, rtdConfig); - onDone(); - storage.setDataInLocalStorage(RTD_LOCAL_NAME, JSON.stringify(data)); - } else { - onDone(); - } - } catch (err) { - logError('unable to parse audigent segment data'); - onDone(); - } - } else if (req.status === 204) { - // unrecognized partner config - onDone(); - } - }, - error: function () { - onDone(); - logError('unable to get audigent segment data'); - } - }, - JSON.stringify({'userIds': userIds, 'config': reqParams}), - {contentType: 'application/json'} - ); -} - /** * Module init * @param {Object} provider diff --git a/modules/holidBidAdapter.js b/modules/holidBidAdapter.js index f046c860562..abc8e0b403c 100644 --- a/modules/holidBidAdapter.js +++ b/modules/holidBidAdapter.js @@ -1,48 +1,74 @@ import { deepAccess, - deepSetValue, getBidIdParameter, + deepSetValue, + getBidIdParameter, isStr, logMessage, triggerPixel, } from '../src/utils.js'; -import * as events from '../src/events.js'; -import { EVENTS } from '../src/constants.js'; -import {BANNER} from '../src/mediaTypes.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; - -const BIDDER_CODE = 'holid' -const GVLID = 1177 -const ENDPOINT = 'https://helloworld.holid.io/openrtb2/auction' -const COOKIE_SYNC_ENDPOINT = 'https://null.holid.io/sync.html' -const TIME_TO_LIVE = 300 -const TMAX = 500 -let wurlMap = {} - -events.on(EVENTS.BID_WON, bidWonHandler) +const BIDDER_CODE = 'holid'; +const GVLID = 1177; +const ENDPOINT = 'https://helloworld.holid.io/openrtb2/auction'; +const COOKIE_SYNC_ENDPOINT = 'https://null.holid.io/sync.html'; +const TIME_TO_LIVE = 300; +const TMAX = 500; +let wurlMap = {}; export const spec = { code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER], + // Validate bid: requires adUnitID parameter isBidRequestValid: function (bid) { - return !!bid.params.adUnitID + return !!bid.params.adUnitID; }, + // Build request payload including GDPR, GPP, and US Privacy data if available buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map((bid) => { const requestData = { ...bid.ortb2, - source: {schain: bid.schain}, + source: { schain: bid.schain }, id: bidderRequest.bidderRequestId, imp: [getImp(bid)], tmax: TMAX, - ...buildStoredRequest(bid) + ...buildStoredRequest(bid), + }; + + // GDPR: If available, include GDPR signals in the request + if (bidderRequest && bidderRequest.gdprConsent) { + deepSetValue( + requestData, + 'regs.ext.gdpr', + bidderRequest.gdprConsent.gdprApplies ? 1 : 0 + ); + deepSetValue( + requestData, + 'user.ext.consent', + bidderRequest.gdprConsent.consentString + ); + } + + // GPP: If available, include GPP data in regs.ext + if (bidderRequest && bidderRequest.gpp) { + deepSetValue(requestData, 'regs.ext.gpp', bidderRequest.gpp); + } + if (bidderRequest && bidderRequest.gppSids) { + deepSetValue(requestData, 'regs.ext.gpp_sid', bidderRequest.gppSids); + } + + // US Privacy: If available, include US Privacy signal in regs.ext + if (bidderRequest && bidderRequest.usPrivacy) { + deepSetValue(requestData, 'regs.ext.us_privacy', bidderRequest.usPrivacy); } + // If user IDs are available, add them under user.ext.eids if (bid.userIdAsEids) { - deepSetValue(requestData, 'user.ext.eids', bid.userIdAsEids) + deepSetValue(requestData, 'user.ext.eids', bid.userIdAsEids); } return { @@ -50,23 +76,35 @@ export const spec = { url: ENDPOINT, data: JSON.stringify(requestData), bidId: bid.bidId, - } - }) + }; + }); }, + // Interpret response: group bids by unique impid and select the highest CPM bid per imp interpretResponse: function (serverResponse, bidRequest) { - const bidResponses = [] - - if (!serverResponse.body.seatbid) { - return [] + const bidResponsesMap = {}; // Maps impid -> highest bid object + if (!serverResponse.body || !serverResponse.body.seatbid) { + return []; } - serverResponse.body.seatbid.map((response) => { - response.bid.map((bid) => { - const requestId = bidRequest.bidId - const wurl = deepAccess(bid, 'ext.prebid.events.win') - const bidResponse = { - requestId, + serverResponse.body.seatbid.forEach((seatbid) => { + 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 + let meta = deepAccess(bid, 'ext.prebid.meta', {}) || {}; + const adomain = deepAccess(bid, 'adomain', []); + if (adomain.length > 0) { + meta.adomain = adomain; + } + const networkId = deepAccess(bid, 'ext.prebid.meta.networkId'); + if (networkId) { + meta.networkId = networkId; + } + deepSetValue(bid, 'ext.prebid.meta', meta); + + const currentBidResponse = { + requestId: impId, // Using imp.id as the unique request identifier cpm: bid.price, width: bid.w, height: bid.h, @@ -75,69 +113,111 @@ export const spec = { currency: serverResponse.body.cur, netRevenue: true, ttl: TIME_TO_LIVE, + meta: meta, + }; + + // For each imp, only keep the bid with the highest CPM + if ( + !bidResponsesMap[impId] || + currentBidResponse.cpm > bidResponsesMap[impId].cpm + ) { + bidResponsesMap[impId] = currentBidResponse; } - addWurl(requestId, wurl) - - bidResponses.push(bidResponse) - }) - }) + // Store win notification URL (if provided) using the impid as key + const wurl = deepAccess(bid, 'ext.prebid.events.win'); + if (wurl) { + addWurl(impId, wurl); + } + }); + }); - return bidResponses + return Object.values(bidResponsesMap); }, + // User syncs: supports both image and iframe syncing with privacy parameters if available getUserSyncs(optionsType, serverResponse, gdprConsent, uspConsent) { - const syncs = [{ - type: 'image', - url: 'https://track.adform.net/Serving/TrackPoint/?pm=2992097&lid=132720821' - }] + const syncs = [ + { + type: 'image', + url: 'https://track.adform.net/Serving/TrackPoint/?pm=2992097&lid=132720821', + }, + ]; - if (!serverResponse || serverResponse.length === 0) { - return syncs + if (!serverResponse || (Array.isArray(serverResponse) && serverResponse.length === 0)) { + return syncs; } - const bidders = getBidders(serverResponse) + const responses = Array.isArray(serverResponse) + ? serverResponse + : [serverResponse]; + const bidders = getBidders(responses); if (optionsType.iframeEnabled && bidders) { - const queryParams = [] - - queryParams.push('bidders=' + bidders) - queryParams.push('gdpr=' + +gdprConsent.gdprApplies) - queryParams.push('gdpr_consent=' + gdprConsent.consentString) - queryParams.push('usp_consent=' + (uspConsent || '')) - - let strQueryParams = queryParams.join('&') + const queryParams = []; + queryParams.push('bidders=' + bidders); + + if (gdprConsent) { + queryParams.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + queryParams.push( + 'gdpr_consent=' + + encodeURIComponent(gdprConsent.consentString || '') + ); + } else { + queryParams.push('gdpr=0'); + } - if (strQueryParams.length > 0) { - strQueryParams = '?' + strQueryParams + if (typeof uspConsent !== 'undefined') { + queryParams.push('us_privacy=' + encodeURIComponent(uspConsent)); } + queryParams.push('type=iframe'); + const strQueryParams = '?' + queryParams.join('&'); + syncs.push({ type: 'iframe', - url: COOKIE_SYNC_ENDPOINT + strQueryParams + '&type=iframe', - }) + url: COOKIE_SYNC_ENDPOINT + strQueryParams, + }); } - return syncs + return syncs; }, -} + // On bid win, trigger win notification via an image pixel if available + onBidWon(bid) { + const wurl = getWurl(bid.requestId); + if (wurl) { + logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`); + triggerPixel(wurl); + removeWurl(bid.requestId); + } + }, +}; + +// Create a unique impression object with bid id as the identifier function getImp(bid) { - const imp = buildStoredRequest(bid) + const imp = buildStoredRequest(bid); + imp.id = bid.bidId; // Ensure imp.id is unique to match the bid response correctly const sizes = - bid.sizes && !Array.isArray(bid.sizes[0]) ? [bid.sizes] : bid.sizes + bid.sizes && !Array.isArray(bid.sizes[0]) ? [bid.sizes] : bid.sizes; if (deepAccess(bid, 'mediaTypes.banner')) { imp.banner = { format: sizes.map((size) => { - return { w: size[0], h: size[1] } + return { w: size[0], h: size[1] }; }), - } + }; + } + + // Include bid floor if defined in bid.params + if (bid.params.floor) { + imp.bidfloor = bid.params.floor; } - return imp + return imp; } +// Build stored request object using bid parameters function buildStoredRequest(bid) { return { ext: { @@ -147,42 +227,35 @@ function buildStoredRequest(bid) { }, }, }, - } + }; } -function getBidders(serverResponse) { - const bidders = serverResponse - .map((res) => Object.keys(res.body.ext.responsetimemillis || [])) - .flat(1) +// Helper: Extract unique bidders from responses for user syncs +function getBidders(responses) { + const bidders = responses + .map((res) => Object.keys(res.body.ext?.responsetimemillis || {})) + .flat(); if (bidders.length) { - return encodeURIComponent(JSON.stringify([...new Set(bidders)])) + return encodeURIComponent(JSON.stringify([...new Set(bidders)])); } } +// Win URL helper functions function addWurl(requestId, wurl) { if (isStr(requestId)) { - wurlMap[requestId] = wurl + wurlMap[requestId] = wurl; } } function removeWurl(requestId) { - delete wurlMap[requestId] + delete wurlMap[requestId]; } function getWurl(requestId) { if (isStr(requestId)) { - return wurlMap[requestId] - } -} - -function bidWonHandler(bid) { - const wurl = getWurl(bid.requestId) - if (wurl) { - logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`) - triggerPixel(wurl) - removeWurl(bid.requestId) + return wurlMap[requestId]; } } -registerBidder(spec) +registerBidder(spec); diff --git a/modules/holidBidAdapter.md b/modules/holidBidAdapter.md index 1d83918c00a..db6396a203e 100644 --- a/modules/holidBidAdapter.md +++ b/modules/holidBidAdapter.md @@ -8,9 +8,43 @@ Maintainer: richard@holid.se # Description -Currently module supports only banner mediaType. +The Holid Bid Adapter is a Prebid.js bidder adapter designed for display (banner) ads. It fully supports TCF (GDPR) and GPP frameworks, along with US Privacy signals. Key features include: -# Test Parameters +``` +Supply Chain (schain) support ensuring transparency. +Safeframes to securely display ads. +Floors Module Support to enforce bid floors. +Comprehensive User ID integrations. +Compliance with COPPA and support for first-party data. +The adapter selects the highest bid for optimal revenue. +Additional support for Demand Chain and ORTB Blocking should be confirmed with the bidder. +``` + +# Specifications + +``` +Bidder Code: holid +Prebid.js Adapter: yes +Media Types: display (banner) +TCF-EU Support: yes +GPP Support: yes +Supply Chain Support: yes +Safeframes OK: yes +Floors Module Support: yes +User IDs: all +Privacy Sandbox: check with bidder +Prebid.org Member: no +Prebid Server Adapter: no +Multi Format Support: no +IAB GVL ID: 1177 +DSA Support: no +COPPA Support: yes +Demand Chain Support: check with bidder +Supports Deals: yes +First Party Data Support: yes +ORTB Blocking Support: check with bidder +Prebid Server App Support: no +``` ## Sample Banner Ad Unit @@ -28,9 +62,11 @@ var adUnits = [ bidder: 'holid', params: { adUnitID: '12345', + // Optional: set a bid floor if needed + floor: 0.5, }, }, ], }, -] +]; ``` diff --git a/modules/humansecurityRtdProvider.js b/modules/humansecurityRtdProvider.js new file mode 100644 index 00000000000..aeb872beb8d --- /dev/null +++ b/modules/humansecurityRtdProvider.js @@ -0,0 +1,180 @@ +/** + * This module adds humansecurity provider to the real time data module + * + * The {@link module:modules/realTimeData} module is required + * The module will inject the HUMAN Security script into the context where Prebid.js is initialized, enriching bid requests with specific data to provide advanced protection against ad fraud and spoofing. + * @module modules/humansecurityRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { + prefixLog, + mergeDeep, + generateUUID, + getWindowSelf, +} from '../src/utils.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('../modules/rtdModule/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/rtdModule/index.js').UserConsentData} UserConsentData + */ + +const SUBMODULE_NAME = 'humansecurity'; +const SCRIPT_URL = 'https://sonar.script.ac/prebid/rtd.js'; + +const { logInfo, logWarn, logError } = prefixLog(`[${SUBMODULE_NAME}]:`); + +/** @type {string} */ +let clientId = ''; + +/** @type {boolean} */ +let verbose = false; + +/** @type {string} */ +let sessionId = ''; + +/** @type {Object} */ +let hmnsData = { }; + +/** + * Submodule registration + */ +function main() { + submodule('realTimeData', /** @type {RtdSubmodule} */ ({ + name: SUBMODULE_NAME, + + // + init: (config, userConsent) => { + try { + load(config); + return true; + } catch (err) { + logError('init', err.message); + return false; + } + }, + + getBidRequestData: onGetBidRequestData + })); +} + +/** + * Injects HUMAN Security script on the page to facilitate pre-bid signal collection. + * @param {SubmoduleConfig} config + */ +function load(config) { + // By default, this submodule loads the generic implementation script + // only identified by the referrer information. In the future, if publishers + // want to have analytics where their websites are grouped, they can request + // Client ID from HUMAN, pass it here, and it will enable advanced reporting + clientId = config?.params?.clientId || ''; + if (clientId && (typeof clientId !== 'string' || !/^\w{3,16}$/.test(clientId))) { + throw new Error(`The 'clientId' parameter must be a short alphanumeric string`); + } + + // Load/reset the state + verbose = !!config?.params?.verbose; + sessionId = generateUUID(); + hmnsData = {}; + + // We rely on prebid implementation to get the best domain possible here + // In some cases, it still might be null, though + const refDomain = getRefererInfo().domain || ''; + + // Once loaded, the implementation script will publish an API using + // the session ID value it was given in data attributes + const scriptAttrs = { 'data-sid': sessionId }; + const scriptUrl = `${SCRIPT_URL}?r=${refDomain}${clientId ? `&c=${clientId}` : ''}`; + + loadExternalScript(scriptUrl, MODULE_TYPE_RTD, SUBMODULE_NAME, onImplLoaded, null, scriptAttrs); +} + +/** + * The callback to loadExternalScript + * Establishes the bridge between this RTD submodule and the loaded implementation + */ +function onImplLoaded() { + // We then get a hold on this script using the knowledge of this session ID + const wnd = getWindowSelf(); + const impl = wnd[`sonar_${sessionId}`]; + if (typeof impl !== 'object' || typeof impl.connect !== 'function') { + verbose && logWarn('onload', 'Unable to access the implementation script'); + return; + } + + // And set up a bridge between the RTD submodule and the implementation. + // The first argument is used to identify the caller. + // The callback might be called multiple times to update the signals + // once more precise information is available. + impl.connect(getGlobal(), onImplMessage); +} + +/** + * The bridge function will be called by the implementation script + * to update the token information or report errors + * @param {Object} msg + */ +function onImplMessage(msg) { + if (typeof msg !== 'object') { + return; + } + + switch (msg.type) { + case 'hmns': { + hmnsData = mergeDeep({}, msg.data || {}); + break; + } + case 'error': { + logError('impl', msg.data || ''); + break; + } + case 'warn': { + verbose && logWarn('impl', msg.data || ''); + break; + } + case 'info': { + verbose && logInfo('impl', msg.data || ''); + break; + } + } +} + +/** + * onGetBidRequestData is called once per auction. + * Update the `ortb2Fragments` object with the data from the injected script. + * + * @param {Object} reqBidsConfigObj + * @param {function} callback + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ +function onGetBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + // Add device.ext.hmns to the global ORTB data for all vendors to use + // At the time of writing this submodule, "hmns" is an object defined + // internally by humansecurity, and it currently contains "v1" field + // with a token that contains collected signals about this session. + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, { device: { ext: { hmns: hmnsData } } }); + callback(); +} + +/** + * Exporting local (and otherwise encapsulated to this module) functions + * for testing purposes + */ +export const __TEST__ = { + SUBMODULE_NAME, + SCRIPT_URL, + main, + load, + onImplLoaded, + onImplMessage, + onGetBidRequestData +}; + +main(); diff --git a/modules/humansecurityRtdProvider.md b/modules/humansecurityRtdProvider.md new file mode 100644 index 00000000000..6722319cbb5 --- /dev/null +++ b/modules/humansecurityRtdProvider.md @@ -0,0 +1,223 @@ +# Overview + +``` +Module Name: HUMAN Security Rtd provider +Module Type: Rtd Provider +Maintainer: alexey@humansecurity.com +``` + +## What is it? + +The HUMAN Security RTD submodule offers publishers a mechanism to integrate pre-bid signal collection +for the purpose of providing real-time protection against all sorts of invalid traffic, +such as bot-generated ad interactions or sophisticated ad fraud schemes. + +## How does it work? + +HUMAN Security RTD submodule generates a HUMAN Security token, which then can be consumed by adapters, +sent within bid requests, and used for bot detection on the backend. + +## Key Facts about the HUMAN Security RTD Submodule + +* Enriches bid requests with IVT signal, historically done post-bid +* No incremental signals collected beyond existing HUMAN post-bid solution +* Offsets negative impact from loss of granularity in IP and User Agent at bid time +* Does not expose collected IVT signal to any party who doesn’t otherwise already have access to the same signal collected post-bid +* Does not introduce meaningful latency, as demonstrated in the Latency section +* Comes at no additional cost to collect IVT signal and make it available at bid time +* Leveraged to differentiate the invalid bid requests at device level, and cannot be used to identify a user or a device, thus preserving privacy. + +# Build + +First, make sure to add the HUMAN Security submodule to your Prebid.js package with: + +```bash +gulp build --modules="rtdModule,humansecurityRtdProvider,..." +``` + +> `rtdModule` is a required module to use HUMAN Security RTD module. + +# Configuration + +This module is configured as part of the `realTimeData.dataProviders` object. +Please refer to [Prebid Documentation](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-realTimeData) +on RTD module configuration for details on required and optional parameters of `realTimeData`. + +By default, using this submodule *does not require any prior communication with HUMAN, nor any special configuration*, +besides just indicating that it should be loaded: + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'humansecurity' + }] + } +}); +``` + +It can be optionally parameterized, for example, to include client ID obtained from HUMAN, +should any advanced reporting be needed, or to have verbose output for troubleshooting: + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'humansecurity', + params: { + clientId: 'ABC123', + verbose: true + } + }] + } +}); +``` + +## Supported parameters + +| Name |Type | Description | Required | +| :--------------- | :------------ | :------------------------------------------------------------------ |:---------| +| `clientId` | String | Should you need advanced reporting, contact [prebid@humansecurity.com](prebid@humansecurity.com) to receive client ID. | No | +| `verbose` | Boolean | Only set to `true` if troubleshooting issues. | No | + +## Logging, latency and troubleshooting + +The optional `verbose` parameter can be especially helpful to troubleshoot any issues and/or monitor latency. + +By default, the submodule may, in case of unexpected issues, invoke `logError`, emitting `auctionDebug` events +of type `ERROR`. With `verbose` parameter set to `true`, it may additionally: + +* Call `logWarning`, resulting in `auctionDebug` events of type `WARNING`, +* Call `logInfo` with latency information. + * To observe these messages in console, Prebid.js must be run in + [debug mode](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#debugging) - + either by adding `?pbjs_debug=true` to your page's URL, or by configuring with `pbjs.setConfig({ debug: true });` + +Example output of the latency information: + +``` +INFO: [humansecurity]: impl JS time to init (ms): 6. +INFO: [humansecurity]: impl JS time to collect (ms): 13. +``` + +Here, the two reported metrics are how much time the signal collection script spent blocking on initialization, +and the total time required to obtain the signals, respectively. Note that "time to collect" metric accounts +for all the time spent since the script has started initializing until the signals were made available to the bidders, +therefore it includes "time to init", and typically some non-blocking time spent waiting for signals. Only “time to init” is blocking. + +# How can I contribute? + +Prebid has launched a Measurement Taskforce to address signal deprecation and measurement in the current environment, +which has become a publisher-level issue. Without a solution, granularity of measurement disappears. +If you would like to participate to help identify and develop solutions to these problems such as the one tackled +by this submodule, please consider joining the [Measurement Taskforce](https://prebid.org/project-management-committees/). + +# Notes + +## Operation model + +Following is the expected data flow: + +* Prebid.js gets initialized, including the HUMAN RTD submodule. +* The submodule loads the signal collection implementation script from a high-performance, low latency endpoint. +* This script starts collecting the signals, and makes them available to the RTD submodule as soon as possible. +* The RTD submodule places the collected signals into the ORTB structure for bid adapters to pick up. +* Bid adapters are expected to retrieve the `ortb2.device.ext.hmns` object and incorporate it into their bid requests. +* Bid requests having the `ortb2.device.ext.hmns` data allow their backend to make more informative requests to HUMAN Ad Fraud Defense. + * Should bid requests be passed to other platforms during the bidding process, adapter developers are + encouraged to keep `ortb2.device.ext.hmns` so that, for example, a downstream DSP can also have this data passed to HUMAN. + +## Remarks on the collected signals + +There are a few points that are worth being mentioned separately, to avoid confusion and unnecessary suspicion: + +* The nature of the collected signals is exactly the same as those already collected in analytics scripts + that arrive in the ads via existing post-bid processes. +* The signals themselves are even less verbose than those HUMAN normally collects post-bid, because of timing / performance requirements. +* No signals attempt to identify users. Their only purpose is to classify traffic into valid / invalid. +* The signal collection script is external to Prebid.js. This ensures that it can be constantly kept up to date with + the ever-evolving nature of the threat landscape without the publishers having to rebuild their Prebid.js frequently. + * The signal collection script is also obfuscated, as a defense-in-depth measure in order to complicate tampering by + bad actors, as are all similar scripts in the industry, which is something that cannot be accommodated by Prebid.js itself. + +## Why is this approach an innovation? + +Historically, IVT protection is achieved via dropping analytics scripts and/or pixels in the ads, which enriches impression data with collected signals. +Those signals, when analyzed by IVT protection vendors, allow distinguishing valid from invalid traffic, but only retroactively - +after the impression was served, and all the participant infrastructures have already participated in serving the request. + +This not only leads to unnecessary infrastructure costs, but to uncomfortable and often difficult processes of reconciliation +and reimbursement, or claw-back. When handled only at the post-bid stage, the true bad actors have already achieved their objectives, +and legitimate advertisers, platforms, and publishers are left holding the bag. + +HUMAN’s Ad Fraud Defense solves this problem by making predictions at the pre-bid stage about whether the traffic is fraudulent, +allowing the platforms to knowingly not participate in the IVT-generated auctions. + +However, the challenge in making those predictions is that even these prebid predictions rely primarily on historical data, +which not only introduces lag, but typically might be less accurate than direct decision making (were it possible) using +the high-quality signals obtained from the pixels and/or JS analytics scripts delivered in the ads. + +The HUMAN Security RTD submodule bridges the gap by introducing a new innovation: it **facilitates the very same signal +collection that is typically performed post-bid, but at the pre-bid stage, and makes the signals available during bidding.** +This not only permits for accurate invalid traffic detection at the earliest stages of the auction process, but diminishes +the impacts of signal deprecation such as the loss of IP and User Agent on effective fraud mitigation. + +## Why is this good for publishers? + +In the process of Invalid Traffic reconciliation, publishers are often the last to know, as they are informed by their downstream +partners that the inventory they had provided in good faith has been detected as invalid traffic. This is most painful when it +happens via post-bid detection when publishers are often the last party in the chain from whom the others can collect clawbacks, +and the publishers themselves are left with little recourse. And when invalid traffic is blocked by platforms prebid, it is after +the fact of publishers having sent out bid requests, thus harming fill rates, revenue opportunities, and overall auction and bidding +efficiencies. And of course, invalid traffic whether detected post-bid or pre-bid is damaging to the publisher’s reputation +with its demand partners. + +The HUMAN Security RTD submodule creates a more efficient integration for the process of invalid traffic mitigation. +Invalid traffic detection and filtration is being done already with or without the participation of publishers, and measurement +will be done on the ad inventory because advertisers need it to manage their own ad spend. The HUMAN Security RTD submodule gives +publishers a direct seat, and in fact the first seat, in the invalid traffic detection process, allowing it to be done effectively, +directly, and in a way that provides the publisher with more direct insight. + +Existing models of signal deprecation suggest that IP protection is going to be 100 times or more less granular. +This would normally be expected to significantly reduce the ability to do prebid publisher-side predictions. This in turn would prevent +the ability to see if specific impressions are bad and instead potentially result in the whole publisher being identified as being +invalid traffic by a buyer. It is important to note that the purpose of data collection by the HUMAN Security RTD submodule is +specifically for invalid traffic detection and filtration. It will not be used for unrelated and unauthorized purposes +like targeting audiences, etc. + +The HUMAN Security RTD submodule makes sure to have the best integration possible to avoid revenue loss. +It will help publishers avoid painful clawbacks. Currently, clawbacks are based on opaque measurement processes downstream from +publishers where the information is controlled and withheld. The HUMAN Security RTD submodule will make publishers a more direct +party to the measurement and verification process and help make sure the origin and the recipient match. + +Importantly, the effective use of the HUMAN Security RTD submodule signifies to SSPs and buyers that the publisher +is a joint partner in ensuring quality ad delivery, and demonstrates that the publisher is a premium supply source. + +Finally, the HUMAN Security RTD submodule sets the ecosystem up for a future where publisher level reporting is facilitated. +This will allow for increased transparency about what is happening with publisher inventory, further enhancing and +ensuring the value of the inventory. + +## FAQ + +### Is latency an issue? + +The HUMAN Security RTD submodule is designed to minimize any latency in the auction within normal SLAs. + +### Do publishers get any insight into how the measurement is judged? + +Having the The HUMAN Security RTD submodule be part of the prebid process will allow the publisher to have insight +into the invalid traffic metrics as they are determined and provide confidence that they are delivering quality +inventory to the buyer. + +### How are privacy concerns addressed? + +The HUMAN Security RTD submodule seeks to reduce the impacts from signal deprecation that are inevitable without +compromising privacy by avoiding re-identification. Each bid request is enriched with just enough signal +to identify if the traffic is invalid or not. + +By having the The HUMAN Security RTD submodule operate at the Prebid level, data can be controlled +and not as freely passed through the bidstream where it may be accessible to various unknown parties. + +Note: anti-fraud use cases typically have carve outs in laws and regulations to permit data collection +essential for effective fraud mitigation, but this does not constitute legal advice and you should +consult your attorney when making data access decisions. diff --git a/modules/iasRtdProvider.js b/modules/iasRtdProvider.js index b9de7ef4e46..5e72c416cdb 100644 --- a/modules/iasRtdProvider.js +++ b/modules/iasRtdProvider.js @@ -101,7 +101,8 @@ function stringifySlot(bidRequest, adUnitPath) { } function stringifyWindowSize() { - return [window.innerWidth || -1, window.innerHeight || -1].join('.'); + const { innerWidth, innerHeight } = utils.getWinDimensions(); + return [innerWidth || -1, innerHeight || -1].join('.'); } function stringifyScreenSize() { diff --git a/modules/id5AnalyticsAdapter.js b/modules/id5AnalyticsAdapter.js index 70da467ff7c..10fe8d82ef4 100644 --- a/modules/id5AnalyticsAdapter.js +++ b/modules/id5AnalyticsAdapter.js @@ -4,7 +4,6 @@ import adapterManager from '../src/adapterManager.js'; import { ajax } from '../src/ajax.js'; import { logInfo, logError } from '../src/utils.js'; import * as events from '../src/events.js'; -import {getGlobal} from '../src/prebidGlobal.js'; const { AUCTION_END, @@ -33,7 +32,7 @@ const FLUSH_EVENTS = [ const CONFIG_URL_PREFIX = 'https://api.id5-sync.com/analytics' const TZ = new Date().getTimezoneOffset(); -const PBJS_VERSION = getGlobal().version; +const PBJS_VERSION = 'v' + '$prebid.version$'; const ID5_REDACTED = '__ID5_REDACTED__'; const isArray = Array.isArray; diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 42f0044edc3..98d070a4615 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -13,16 +13,14 @@ import { isPlainObject, logError, logInfo, - logWarn, - safeJSONParse + logWarn } from '../src/utils.js'; import {fetch} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; -import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -import {GreedyPromise} from '../src/utils/promise.js'; +import {PbPromise} from '../src/utils/promise.js'; import {loadExternalScript} from '../src/adloader.js'; /** @@ -34,17 +32,11 @@ import {loadExternalScript} from '../src/adloader.js'; const MODULE_NAME = 'id5Id'; const GVLID = 131; -const NB_EXP_DAYS = 30; export const ID5_STORAGE_NAME = 'id5id'; -export const ID5_PRIVACY_STORAGE_NAME = `${ID5_STORAGE_NAME}_privacy`; -const LOCAL_STORAGE = 'html5'; const LOG_PREFIX = 'User ID - ID5 submodule: '; const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid'; const ID5_DOMAIN = 'id5-sync.com'; - -// order the legacy cookie names in reverse priority order so the last -// cookie in the array is the most preferred to use -const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id']; +const TRUE_LINK_SOURCE = 'true-link-id5-sync.com'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); @@ -112,6 +104,49 @@ export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleNam * @property {boolean} [disableUaHints] - When true, look up of high entropy values through user agent hints is disabled. */ +const DEFAULT_EIDS = { + 'id5id': { + getValue: function (data) { + return data.uid; + }, + source: ID5_DOMAIN, + atype: 1, + getUidExt: function (data) { + if (data.ext) { + return data.ext; + } + } + }, + 'euid': { + getValue: function (data) { + return data.uid; + }, + getSource: function (data) { + return data.source; + }, + atype: 3, + getUidExt: function (data) { + if (data.ext) { + return data.ext; + } + } + }, + 'trueLinkId': { + getValue: function (data) { + return data.uid; + }, + getSource: function (data) { + return TRUE_LINK_SOURCE; + }, + atype: 1, + getUidExt: function (data) { + if (data.ext) { + return data.ext; + } + } + } +}; + /** @type {Submodule} */ export const id5IdSubmodule = { /** @@ -134,21 +169,40 @@ export const id5IdSubmodule = { * @returns {(Object|undefined)} */ decode(value, config) { - let universalUid; + if (value && value.ids !== undefined) { + const responseObj = {}; + const eids = {}; + Object.entries(value.ids).forEach(([key, value]) => { + let eid = value.eid; + let uid = eid?.uids?.[0] + responseObj[key] = { + uid: uid?.id, + ext: uid?.ext + }; + eids[key] = function () { + return eid; + }; // register function to get eid for each id (key) decoded + }); + this.eids = eids; // overwrite global eids + return responseObj; + } + + let universalUid, publisherTrueLinkId; let ext = {}; if (value && typeof value.universal_uid === 'string') { universalUid = value.universal_uid; ext = value.ext || ext; + publisherTrueLinkId = value.publisherTrueLinkId; } else { return undefined; } - + this.eids = DEFAULT_EIDS; let responseObj = { id5id: { uid: universalUid, ext: ext - }, + } }; if (isPlainObject(ext.euid)) { @@ -156,7 +210,13 @@ export const id5IdSubmodule = { uid: ext.euid.uids[0].id, source: ext.euid.source, ext: {provider: ID5_DOMAIN} - } + }; + } + + if (publisherTrueLinkId) { + responseObj.trueLinkId = { + uid: publisherTrueLinkId + }; } const abTestingResult = deepAccess(value, 'ab_testing.result'); @@ -195,16 +255,16 @@ export const id5IdSubmodule = { return undefined; } - if (!hasWriteConsentToLocalStorage(consentData)) { - logInfo(LOG_PREFIX + 'Skipping ID5 local storage write because no consent given.') + if (!hasWriteConsentToLocalStorage(consentData?.gdpr)) { + logInfo(LOG_PREFIX + 'Skipping ID5 local storage write because no consent given.'); return undefined; } const resp = function (cbFunction) { - const fetchFlow = new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData(), gppDataHandler.getConsentData()); + const fetchFlow = new IdFetchFlow(submoduleConfig, consentData?.gdpr, cacheIdObj, consentData?.usp, consentData?.gpp); fetchFlow.execute() .then(response => { - cbFunction(response) + cbFunction(response); }) .catch(error => { logError(LOG_PREFIX + 'getId fetch encountered an error', error); @@ -223,57 +283,34 @@ export const id5IdSubmodule = { * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @param {Object} cacheIdObj - existing id, if any - * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. + * @return {IdResponse} A response object that contains id. */ extendId(config, consentData, cacheIdObj) { - if (!hasWriteConsentToLocalStorage(consentData)) { - logInfo(LOG_PREFIX + 'No consent given for ID5 local storage writing, skipping nb increment.') + if (!hasWriteConsentToLocalStorage(consentData?.gdpr)) { + logInfo(LOG_PREFIX + 'No consent given for ID5 local storage writing, skipping nb increment.'); return cacheIdObj; } - const partnerId = validateConfig(config) ? config.params.partner : 0; - incrementNb(partnerId); - logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj); - return cacheIdObj; - }, - eids: { - 'id5id': { - getValue: function(data) { - return data.uid - }, - source: ID5_DOMAIN, - atype: 1, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'euid': { - getValue: function (data) { - return data.uid; - }, - getSource: function (data) { - return data.source; - }, - atype: 3, - getUidExt: function (data) { - if (data.ext) { - return data.ext; - } - } + if (cacheIdObj) { + cacheIdObj.nbPage = incrementNb(cacheIdObj); } + return cacheIdObj; }, + primaryIds: ['id5id', 'trueLinkId'], + eids: DEFAULT_EIDS, + _reset() { + this.eids = DEFAULT_EIDS; + } }; export class IdFetchFlow { constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData, gppData) { - this.submoduleConfig = submoduleConfig - this.gdprConsentData = gdprConsentData - this.cacheIdObj = cacheIdObj - this.usPrivacyData = usPrivacyData - this.gppData = gppData + this.submoduleConfig = submoduleConfig; + this.gdprConsentData = gdprConsentData; + this.cacheIdObj = cacheIdObj; + this.usPrivacyData = usPrivacyData; + this.gppData = gppData; } /** @@ -298,7 +335,7 @@ export class IdFetchFlow { return typeof this.submoduleConfig.params.externalModuleUrl === 'string'; } - // eslint-disable-next-line no-dupe-class-members + async #externalModuleFlow(configCallPromise) { await loadExternalModule(this.submoduleConfig.params.externalModuleUrl); const fetchFlowConfig = await configCallPromise; @@ -306,12 +343,12 @@ export class IdFetchFlow { return this.#getExternalIntegration().fetchId5Id(fetchFlowConfig, this.submoduleConfig.params, getRefererInfo(), this.gdprConsentData, this.usPrivacyData, this.gppData); } - // eslint-disable-next-line no-dupe-class-members + #getExternalIntegration() { return window.id5Prebid && window.id5Prebid.integration; } - // eslint-disable-next-line no-dupe-class-members + async #regularFlow(configCallPromise) { const fetchFlowConfig = await configCallPromise; const extensionsData = await this.#callForExtensions(fetchFlowConfig.extensionsCall); @@ -319,12 +356,16 @@ export class IdFetchFlow { return this.#processFetchCallResponse(fetchCallResponse); } - // eslint-disable-next-line no-dupe-class-members + async #callForConfig() { let url = this.submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only const response = await fetch(url, { method: 'POST', - body: JSON.stringify(this.submoduleConfig) + body: JSON.stringify({ + ...this.submoduleConfig, + bounce: true + }), + credentials: 'include' }); if (!response.ok) { throw new Error('Error while calling config endpoint: ', response); @@ -334,7 +375,7 @@ export class IdFetchFlow { return dynamicConfig; } - // eslint-disable-next-line no-dupe-class-members + async #callForExtensions(extensionsCallConfig) { if (extensionsCallConfig === undefined) { return undefined; @@ -342,7 +383,7 @@ export class IdFetchFlow { const extensionsUrl = extensionsCallConfig.url; const method = extensionsCallConfig.method || 'GET'; const body = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {}); - const response = await fetch(extensionsUrl, { method, body }); + const response = await fetch(extensionsUrl, {method, body}); if (!response.ok) { throw new Error('Error while calling extensions endpoint: ', response); } @@ -351,7 +392,7 @@ export class IdFetchFlow { return extensions; } - // eslint-disable-next-line no-dupe-class-members + async #callId5Fetch(fetchCallConfig, extensionsData) { const fetchUrl = fetchCallConfig.url; const additionalData = fetchCallConfig.overrides || {}; @@ -360,7 +401,7 @@ export class IdFetchFlow { ...additionalData, extensions: extensionsData }); - const response = await fetch(fetchUrl, { method: 'POST', body, credentials: 'include' }); + const response = await fetch(fetchUrl, {method: 'POST', body, credentials: 'include'}); if (!response.ok) { throw new Error('Error while calling fetch endpoint: ', response); } @@ -369,13 +410,15 @@ export class IdFetchFlow { return fetchResponse; } - // eslint-disable-next-line no-dupe-class-members + #createFetchRequestData() { const params = this.submoduleConfig.params; const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0; const referer = getRefererInfo(); - const signature = (this.cacheIdObj && this.cacheIdObj.signature) ? this.cacheIdObj.signature : getLegacyCookieSignature(); - const nbPage = incrementAndResetNb(params.partner); + const signature = this.cacheIdObj ? this.cacheIdObj.signature : undefined; + const nbPage = incrementNb(this.cacheIdObj); + const trueLinkInfo = window.id5Bootstrap ? window.id5Bootstrap.getTrueLinkInfo() : {booted: false}; + const data = { 'partner': params.partner, 'gdpr': hasGdpr, @@ -388,7 +431,8 @@ export class IdFetchFlow { 'u': referer.stack[0] || window.location.href, 'v': '$prebid.version$', 'storage': this.submoduleConfig.storage, - 'localStorage': storage.localStorageIsEnabled() ? 1 : 0 + 'localStorage': storage.localStorageIsEnabled() ? 1 : 0, + 'true_link': trueLinkInfo }; // pass in optional data, but only if populated @@ -422,11 +466,13 @@ export class IdFetchFlow { return data; } - // eslint-disable-next-line no-dupe-class-members + #processFetchCallResponse(fetchCallResponse) { try { if (fetchCallResponse.privacy) { - storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS); + if (window.id5Bootstrap && window.id5Bootstrap.setPrivacy) { + window.id5Bootstrap.setPrivacy(fetchCallResponse.privacy); + } } } catch (error) { logError(LOG_PREFIX + 'Error while writing privacy info into local storage.', error); @@ -436,13 +482,13 @@ export class IdFetchFlow { } async function loadExternalModule(url) { - return new GreedyPromise((resolve, reject) => { + return new PbPromise((resolve, reject) => { if (window.id5Prebid) { // Already loaded resolve(); } else { try { - loadExternalScript(url, 'id5', resolve); + loadExternalScript(url, MODULE_TYPE_UID, 'id5', resolve); } catch (error) { reject(error); } @@ -456,7 +502,7 @@ function validateConfig(config) { return false; } - const partner = config.params.partner + const partner = config.params.partner; if (typeof partner === 'string' || partner instanceof String) { let parsedPartnerId = parseInt(partner); if (isNaN(parsedPartnerId) || parsedPartnerId < 0) { @@ -474,89 +520,19 @@ function validateConfig(config) { logError(LOG_PREFIX + 'storage required to be set'); return false; } - - // in a future release, we may return false if storage type or name are not set as required - if (config.storage.type !== LOCAL_STORAGE) { - logWarn(LOG_PREFIX + `storage type recommended to be '${LOCAL_STORAGE}'. In a future release this may become a strict requirement`); - } - // in a future release, we may return false if storage type or name are not set as required if (config.storage.name !== ID5_STORAGE_NAME) { - logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'. In a future release this may become a strict requirement`); + logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'.`); } return true; } -export function expDaysStr(expDays) { - return (new Date(Date.now() + (1000 * 60 * 60 * 24 * expDays))).toUTCString(); -} - -export function nbCacheName(partnerId) { - return `${ID5_STORAGE_NAME}_${partnerId}_nb`; -} - -export function storeNbInCache(partnerId, nb) { - storeInLocalStorage(nbCacheName(partnerId), nb, NB_EXP_DAYS); -} - -export function getNbFromCache(partnerId) { - let cacheNb = getFromLocalStorage(nbCacheName(partnerId)); - return (cacheNb) ? parseInt(cacheNb) : 0; -} - -function incrementNb(partnerId) { - const nb = (getNbFromCache(partnerId) + 1); - storeNbInCache(partnerId, nb); - return nb; -} - -function incrementAndResetNb(partnerId) { - const result = incrementNb(partnerId); - storeNbInCache(partnerId, 0); - return result; -} - -function getLegacyCookieSignature() { - let legacyStoredValue; - LEGACY_COOKIE_NAMES.forEach(function (cookie) { - if (storage.getCookie(cookie)) { - legacyStoredValue = safeJSONParse(storage.getCookie(cookie)) || legacyStoredValue; - } - }); - return (legacyStoredValue && legacyStoredValue.signature) || ''; -} - -/** - * This will make sure we check for expiration before accessing local storage - * @param {string} key - */ -export function getFromLocalStorage(key) { - const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`); - // empty string means no expiration set - if (storedValueExp === '') { - return storage.getDataFromLocalStorage(key); - } else if (storedValueExp) { - if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { - return storage.getDataFromLocalStorage(key); - } +function incrementNb(cachedObj) { + if (cachedObj && cachedObj.nbPage !== undefined) { + return cachedObj.nbPage + 1; + } else { + return 1; } - // if we got here, then we have an expired item or we didn't set an - // expiration initially somehow, so we need to remove the item from the - // local storage - storage.removeDataFromLocalStorage(key); - return null; -} - -/** - * Ensure that we always set an expiration in local storage since - * by default it's not required - * @param {string} key - * @param {any} value - * @param {number} expDays - */ -export function storeInLocalStorage(key, value, expDays) { - storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays)); - storage.setDataInLocalStorage(`${key}`, value); } /** @@ -566,8 +542,8 @@ export function storeInLocalStorage(key, value, expDays) { */ function hasWriteConsentToLocalStorage(consentData) { const hasGdpr = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies; - const localstorageConsent = deepAccess(consentData, `vendorData.purpose.consents.1`) - const id5VendorConsent = deepAccess(consentData, `vendorData.vendor.consents.${GVLID.toString()}`) + const localstorageConsent = deepAccess(consentData, `vendorData.purpose.consents.1`); + const id5VendorConsent = deepAccess(consentData, `vendorData.vendor.consents.${GVLID.toString()}`); if (hasGdpr && (!localstorageConsent || !id5VendorConsent)) { return false; } diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index 592c69056fa..68081e4b3be 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -25,19 +25,20 @@ pbjs.setConfig({ name: 'id5Id', params: { partner: 173, // change to the Partner Number you received from ID5 - externalModuleUrl: "https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js" // optional but recommended + externalModuleUrl: "https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js", // optional but recommended pd: 'MT1iNTBjY...', // optional, see table below for a link to how to generate this abTesting: { // optional enabled: true, // false by default controlGroupPct: 0.1 // valid values are 0.0 - 1.0 (inclusive) }, - disableExtensions: false // optional + disableExtensions: false,// optional + canCookieSync: true // optional, has effect only when externalModuleUrl is used }, storage: { type: 'html5', // "html5" is the required storage type name: 'id5id', // "id5id" is the required storage name expires: 90, // storage lasts for 90 days - refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh + refreshInSeconds: 7200 // refresh ID every 2 hours to ensure it's fresh } }], auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules @@ -45,23 +46,24 @@ pbjs.setConfig({ }); ``` -| Param under userSync.userIds[] | Scope | Type | Description | Example | -| --- | --- | --- | --- | --- | -| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | -| params | Required | Object | Details for the ID5 ID. | | -| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | -| params.externalModuleUrl | Optional | String | The URL for the id5-prebid external module. It is recommended to use the latest version at the URL in the example. Source code available [here](https://github.com/id5io/id5-api.js/blob/master/src/id5PrebidModule.js). | https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js -| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | -| params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | -| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | -| params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | -| params.abTesting.controlGroupPct | Optional | Number | Must be a number between `0.0` and `1.0` (inclusive) and is used to determine the percentage of requests that fall into the control group (and thus not exposing the ID5 ID). For example, a value of `0.20` will result in 20% of requests without an ID5 ID and 80% with an ID. | `0.1` | -| params.disableExtensions | Optional | Boolean | Set this to `true` to force turn off extensions call. Default `false` | `true` or `false` | -| storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | -| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | -| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | -| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` | -| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` | +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | +| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | +| params | Required | Object | Details for the ID5 ID. | | +| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | +| params.externalModuleUrl | Optional | String | The URL for the id5-prebid external module. It is recommended to use the latest version at the URL in the example. Source code available [here](https://github.com/id5io/id5-api.js/blob/master/src/id5PrebidModule.js). | https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js +| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | +| params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | +| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | +| params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | +| params.abTesting.controlGroupPct | Optional | Number | Must be a number between `0.0` and `1.0` (inclusive) and is used to determine the percentage of requests that fall into the control group (and thus not exposing the ID5 ID). For example, a value of `0.20` will result in 20% of requests without an ID5 ID and 80% with an ID. | `0.1` | +| params.disableExtensions | Optional | Boolean | Set this to `true` to force turn off extensions call. Default `false` | `true` or `false` | +| params.canCookieSync | Optional | Boolean | Set this to `true` to enable cookie syncing with other ID5 partners. See [our documentation](https://wiki.id5.io/docs/initiate-cookie-sync-to-id5) for details. Default `false` | `true` or `false` | +| storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | +| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | +| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` | +| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 2 hours between refreshes | `7200` | **ATTENTION:** As of Prebid.js v4.14.0, ID5 requires `storage.type` to be `"html5"` and `storage.name` to be `"id5id"`. Using other values will display a warning today, but in an upcoming release, it will prevent the ID5 module from loading. This change is to ensure the ID5 module in Prebid.js interoperates properly with the [ID5 API](https://github.com/id5io/id5-api.js) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [prebid@id5.io](mailto:prebid@id5.io). @@ -73,3 +75,68 @@ To turn on A/B Testing, simply edit the configuration (see above table) to enabl ### A Note on Using Multiple Wrappers If you or your monetization partners are deploying multiple Prebid wrappers on your websites, you should make sure you add the ID5 ID User ID module to *every* wrapper. Only the bidders configured in the Prebid wrapper where the ID5 ID User ID module is installed and configured will be able to pick up the ID5 ID. Bidders from other Prebid instances will not be able to pick up the ID5 ID. + +### Provided eids +The module provides following eids: + +``` +[ + { + source: 'id5-sync.com', + uids: [ + { + id: 'some-random-id-value', + atype: 1, + ext: { + linkType: 2, + abTestingControlGroup: false + } + } + ] + }, + { + source: 'true-link-id5-sync.com', + uids: [ + { + id: 'some-publisher-true-link-id', + atype: 1 + } + ] + }, + { + source: 'uidapi.com', + uids: [ + { + id: 'some-uid2', + atype: 3, + ext: { + provider: 'id5-sync.com' + } + } + ] + } +] +``` + +The id from `id5-sync.com` should be always present (though the id provided will be '0' in case of no consent or optout) + +The id from `true-link-id5-sync.com` will be available if the page is integrated with TrueLink (if you are an ID5 partner you can learn more at https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/true-link-integration) + +The id from `uidapi.com` will be available if the partner that is used in ID5 user module has the EUID2 integration enabled (it has to be enabled on the ID5 side) + + +### Providing TrueLinkId as a Google PPID + +TrueLinkId can be provided as a PPID - to use, it the `true-link-id5-sync.com` needs to be provided as a ppid source in prebid userSync configuration: + +```javascript +pbjs.setConfig({ + userSync: { + ppid: 'true-link-id5-sync.com', + userIds: [], //userIds modules should be configured here + } +}); +``` + + + diff --git a/modules/idImportLibrary.js b/modules/idImportLibrary.js index e1847edfce4..8cfa7e47c7c 100644 --- a/modules/idImportLibrary.js +++ b/modules/idImportLibrary.js @@ -1,15 +1,19 @@ -import { logInfo, logError } from '../src/utils.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import {ajax} from '../src/ajax.js'; -import {config} from '../src/config.js'; import MD5 from 'crypto-js/md5.js'; +import { ACTIVITY_ENRICH_UFPD } from '../src/activities/activities.js'; +import { activityParams } from '../src/activities/activityParams.js'; +import { MODULE_TYPE_PREBID } from '../src/activities/modules.js'; +import { isActivityAllowed } from '../src/activities/rules.js'; +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { logError, logInfo } from '../src/utils.js'; let email; let conf; const LOG_PRE_FIX = 'ID-Library: '; const CONF_DEFAULT_OBSERVER_DEBOUNCE_MS = 250; -const CONF_DEFAULT_FULL_BODY_SCAN = false; -const CONF_DEFAULT_INPUT_SCAN = false; +export const CONF_DEFAULT_FULL_BODY_SCAN = false; +export const CONF_DEFAULT_INPUT_SCAN = false; const OBSERVER_CONFIG = { subtree: true, attributes: true, @@ -155,7 +159,7 @@ function handleTargetElement() { const targetElement = document.getElementById(conf.target); if (targetElement) { - email = targetElement.innerText; + email = targetElement.textContent; if (!email) { _logInfo('Finding the email with observer'); @@ -257,6 +261,10 @@ export function setConfig(config) { _logError('The required url is not configured'); return; } + if (!isActivityAllowed(ACTIVITY_ENRICH_UFPD, activityParams(MODULE_TYPE_PREBID, 'idImportLibrary'))) { + _logError('Permission for id import was denied by CMP'); + return; + } if (typeof config.debounce !== 'number') { config.debounce = CONF_DEFAULT_OBSERVER_DEBOUNCE_MS; _logInfo('Set default observer debounce to ' + CONF_DEFAULT_OBSERVER_DEBOUNCE_MS); diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js deleted file mode 100644 index 8e6e3c20a64..00000000000 --- a/modules/idWardRtdProvider.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * This module adds the ID Ward RTD provider to the real time data module - * The {@link module:modules/realTimeData} module is required - * The module will populate real-time data from ID Ward - * @module modules/idWardRtdProvider - * @requires module:modules/realTimeData - */ -import { createRtdProvider } from './anonymisedRtdProvider.js';/* eslint prebid/validate-imports: "off" */ - -export const { getRealTimeData, rtdSubmodule: idWardRtdSubmodule, storage } = createRtdProvider('idWard'); diff --git a/modules/idWardRtdProvider.md b/modules/idWardRtdProvider.md deleted file mode 100644 index 1c9f0654de6..00000000000 --- a/modules/idWardRtdProvider.md +++ /dev/null @@ -1,51 +0,0 @@ -> **Warning!** -> -> The **idWardRtdProvider** module has been renamed to [anonymisedRtdProvider](anonymisedRtdProvider.md) in light of the company's rebranding. -> **idWardRtdProvider** module is maintained for backward compatibility until the next major Prebid release. -> -> Please use anonymisedRtdProvider instead of idWardRtdProvider in your Prebid integration. - -### Overview - -ID Ward is a data anonymization technology for privacy-preserving advertising. Publishers and advertisers are able to target and retarget custom audience segments covering 100% of consented audiences. -ID Ward’s Real-time Data Provider automatically obtains segment IDs from the ID Ward on-domain script (via localStorage) and passes them to the bid-stream. - -### Integration - - 1) Build the idWardRtd module into the Prebid.js package with: - - ``` - gulp build --modules=idWardRtdProvider,... - ``` - - 2) Use `setConfig` to instruct Prebid.js to initilaize the idWardRtdProvider module, as specified below. - -### Configuration - -``` - pbjs.setConfig({ - realTimeData: { - dataProviders: [ - { - name: "idWard", - waitForIt: true, - params: { - cohortStorageKey: "cohort_ids", - segtax: , - } - } - ] - } - }); - ``` - -Please note that idWardRtdProvider should be integrated into the publisher website along with the [ID Ward Pixel](https://publishers-web.id-ward.com/pixel-integration). -Please reach out to Id Ward representative(support@id-ward.com) if you have any questions or need further help to integrate Prebid, idWardRtdProvider, and Id Ward Pixel - -### Testing -To view an example of available segments returned by Id Ward: -``` -‘gulp serve --modules=rtdModule,idWardRtdProvider,pubmaticBidAdapter -``` -and then point your browser at: -"http://localhost:9999/integrationExamples/gpt/idward_segments_example.html" \ No newline at end of file diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index 82aa2303e1c..45bdd4a51c7 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -10,7 +10,6 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -import { gppDataHandler } from '../src/adapterManager.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -59,14 +58,14 @@ export const identityLinkSubmodule = { utils.logError('identityLink: requires partner id to be defined'); return; } - const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - const gdprConsentString = hasGdpr ? consentData.consentString : ''; + const {gdpr, gpp: gppData} = consentData ?? {}; + const hasGdpr = (gdpr && typeof gdpr.gdprApplies === 'boolean' && gdpr.gdprApplies) ? 1 : 0; + const gdprConsentString = hasGdpr ? gdpr.consentString : ''; // use protocol relative urls for http or https if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { utils.logInfo('identityLink: Consent string is required to call envelope API.'); return; } - const gppData = gppDataHandler.getConsentData(); const gppString = gppData && gppData.gppString ? gppData.gppString : false; const gppSectionId = gppData && gppData.gppString && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== -1 ? gppData.applicableSections[0] : false; const hasGpp = gppString && gppSectionId; @@ -132,6 +131,8 @@ function getEnvelope(url, callback, configParams) { utils.logInfo('identityLink: A 3P retrieval is attempted!'); setEnvelopeSource(false); ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); + } else { + callback() } } diff --git a/modules/idxBidAdapter.js b/modules/idxBidAdapter.js index 48739275788..12adb4058ae 100644 --- a/modules/idxBidAdapter.js +++ b/modules/idxBidAdapter.js @@ -1,6 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' import { isArray, isNumber } from '../src/utils.js' +import { interpretResponse } from '../libraries/precisoUtils/bidUtils.js'; const BIDDER_CODE = 'idx' const ENDPOINT_URL = 'https://dev-event.dxmdp.com/rest/api/v1/bid' @@ -49,32 +50,7 @@ export const spec = { } } }, - interpretResponse: function (serverResponse) { - const response = serverResponse.body - - const bids = [] - - response.seatbid.forEach(seat => { - seat.bid.forEach(bid => { - bids.push({ - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - creativeId: bid.crid, - ad: bid.adm, - currency: 'USD', - netRevenue: true, - ttl: 300, - meta: { - advertiserDomains: bid.adomain || [], - }, - }) - }) - }) - - return bids - }, + interpretResponse, } registerBidder(spec) diff --git a/modules/illuminBidAdapter.js b/modules/illuminBidAdapter.js index 45b74bec5c9..0e76e471f4b 100644 --- a/modules/illuminBidAdapter.js +++ b/modules/illuminBidAdapter.js @@ -1,328 +1,27 @@ -import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; -import {config} from '../src/config.js'; +import { + isBidRequestValid, createUserSyncGetter, createInterpretResponseFn, createBuildRequestsFn +} from '../libraries/vidazooUtils/bidderUtils.js'; const DEFAULT_SUB_DOMAIN = 'exchange'; const BIDDER_CODE = 'illumin'; const BIDDER_VERSION = '1.0.0'; const GVLID = 149; -const CURRENCY = 'USD'; -const TTL_SECONDS = 60 * 5; -const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); - -function getTopWindowQueryParams() { - try { - const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); - return parsedUrl.search; - } catch (e) { - return ''; - } -} +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.illumin.com`; } -export function extractCID(params) { - return params.cId; -} - -export function extractPID(params) { - return params.pId; -} - -export function extractSubDomain(params) { - return params.subDomain; -} - -function isBidRequestValid(bid) { - const params = bid.params || {}; - return !!(extractCID(params) && extractPID(params)); -} - -function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { - const { - params, - bidId, - userId, - adUnitCode, - schain, - mediaTypes, - ortb2Imp, - bidderRequestId, - bidRequestsCount, - bidderRequestsCount, - bidderWinsCount - } = bid; - let {bidFloor, ext} = params; - const hashUrl = hashCode(topWindowUrl); - const uniqueDealId = getUniqueDealId(hashUrl); - const cId = extractCID(params); - const pId = extractPID(params); - const subDomain = extractSubDomain(params); - - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); - - if (isFn(bid.getFloor)) { - const floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - - if (floorInfo.currency === 'USD') { - bidFloor = floorInfo.floor; - } - } +const buildRequests = createBuildRequestsFn(createDomain, null, storage, BIDDER_CODE, BIDDER_VERSION, false); - let data = { - url: encodeURIComponent(topWindowUrl), - uqs: getTopWindowQueryParams(), - cb: Date.now(), - bidFloor: bidFloor, - bidId: bidId, - referrer: bidderRequest.refererInfo.ref, - adUnitCode: adUnitCode, - publisherId: pId, - sizes: sizes, - uniqueDealId: uniqueDealId, - bidderVersion: BIDDER_VERSION, - prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}`, - schain: schain, - mediaTypes: mediaTypes, - gpid: gpid, - transactionId: ortb2Imp?.ext?.tid, - bidderRequestId: bidderRequestId, - bidRequestsCount: bidRequestsCount, - bidderRequestsCount: bidderRequestsCount, - bidderWinsCount: bidderWinsCount, - bidderTimeout: bidderTimeout - }; - - appendUserIdsToRequestPayload(data, userId); - - const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); - - if (sua) { - data.sua = sua; - } - - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.consentString) { - data.gdprConsent = bidderRequest.gdprConsent.consentString; - } - if (bidderRequest.gdprConsent.gdprApplies !== undefined) { - data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - } - if (bidderRequest.uspConsent) { - data.usPrivacy = bidderRequest.uspConsent; - } - - if (bidderRequest.gppConsent) { - data.gppString = bidderRequest.gppConsent.gppString; - data.gppSid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - data.gppString = bidderRequest.ortb2.regs.gpp; - data.gppSid = bidderRequest.ortb2.regs.gpp_sid; - } - - const dto = { - method: 'POST', - url: `${createDomain(subDomain)}/prebid/multi/${cId}`, - data: data - }; - - _each(ext, (value, key) => { - dto.data['ext.' + key] = value; - }); - - return dto; -} - -function appendUserIdsToRequestPayload(payloadRef, userIds) { - let key; - _each(userIds, (userId, idSystemProviderName) => { - key = `uid.${idSystemProviderName}`; - - switch (idSystemProviderName) { - case 'digitrustid': - payloadRef[key] = deepAccess(userId, 'data.id'); - break; - case 'lipb': - payloadRef[key] = userId.lipbid; - break; - case 'parrableId': - payloadRef[key] = userId.eid; - break; - case 'id5id': - payloadRef[key] = userId.uid; - break; - default: - payloadRef[key] = userId; - } - }); -} +const interpretResponse = createInterpretResponseFn(BIDDER_CODE, false); -function buildRequests(validBidRequests, bidderRequest) { - const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - const bidderTimeout = config.getConfig('bidderTimeout'); - const requests = []; - validBidRequests.forEach(validBidRequest => { - const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); - requests.push(request); - }); - return requests; -} - -function interpretResponse(serverResponse, request) { - if (!serverResponse || !serverResponse.body) { - return []; - } - const {bidId} = request.data; - const {results} = serverResponse.body; - - let output = []; - - try { - results.forEach(result => { - const { - creativeId, - ad, - price, - exp, - width, - height, - currency, - metaData, - advertiserDomains, - mediaType = BANNER - } = result; - if (!ad || !price) { - return; - } - - const response = { - requestId: bidId, - cpm: price, - width: width, - height: height, - creativeId: creativeId, - currency: currency || CURRENCY, - netRevenue: true, - ttl: exp || TTL_SECONDS, - }; - - if (metaData) { - Object.assign(response, { - meta: metaData - }) - } else { - Object.assign(response, { - meta: { - advertiserDomains: advertiserDomains || [] - } - }) - } - - if (mediaType === BANNER) { - Object.assign(response, { - ad: ad, - }); - } else { - Object.assign(response, { - vastXml: ad, - mediaType: VIDEO - }); - } - output.push(response); - }); - return output; - } catch (e) { - return []; - } -} - -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { - let syncs = []; - const {iframeEnabled, pixelEnabled} = syncOptions; - const {gdprApplies, consentString = ''} = gdprConsent; - - const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` - if (iframeEnabled) { - syncs.push({ - type: 'iframe', - url: `https://sync.illumin.com/api/sync/iframe/${params}` - }); - } - if (pixelEnabled) { - syncs.push({ - type: 'image', - url: `https://sync.illumin.com/api/sync/image/${params}` - }); - } - return syncs; -} - -export function hashCode(s, prefix = '_') { - const l = s.length; - let h = 0 - let i = 0; - if (l > 0) { - while (i < l) { - h = (h << 5) - h + s.charCodeAt(i++) | 0; - } - } - return prefix + h; -} - -export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { - const storageKey = `u_${key}`; - const now = Date.now(); - const data = getStorageItem(storageKey); - let uniqueId; - - if (!data || !data.value || now - data.created > expiry) { - uniqueId = `${key}_${now.toString()}`; - setStorageItem(storageKey, uniqueId); - } else { - uniqueId = data.value; - } - - return uniqueId; -} - -export function getStorageItem(key) { - try { - return tryParseJSON(storage.getDataFromLocalStorage(key)); - } catch (e) { - } - - return null; -} - -export function setStorageItem(key, value, timestamp) { - try { - const created = timestamp || Date.now(); - const data = JSON.stringify({value, created}); - storage.setDataInLocalStorage(key, data); - } catch (e) { - } -} - -export function tryParseJSON(value) { - try { - return JSON.parse(value); - } catch (e) { - return value; - } -} +const getUserSyncs = createUserSyncGetter({ + iframeSyncUrl: 'https://sync.illumin.com/api/sync/iframe', imageSyncUrl: 'https://sync.illumin.com/api/sync/image' +}); export const spec = { code: BIDDER_CODE, diff --git a/modules/imRtdProvider.js b/modules/imRtdProvider.js index 78681c2beda..46573a81c15 100644 --- a/modules/imRtdProvider.js +++ b/modules/imRtdProvider.js @@ -109,6 +109,7 @@ export function setRealTimeData(bidConfig, moduleConfig, data) { const segments = getSegments(data.im_segments, moduleConfig); const ortb2 = bidConfig.ortb2Fragments?.global || {}; deepSetValue(ortb2, 'user.ext.data.im_segments', segments); + deepSetValue(ortb2, 'user.ext.data.im_uid', data.im_uid); if (moduleConfig.params.setGptKeyValues || !moduleConfig.params.hasOwnProperty('setGptKeyValues')) { window.googletag = window.googletag || {cmd: []}; @@ -145,6 +146,7 @@ export function getRealTimeData(reqBidsConfigObj, onDone, moduleConfig) { onDone(); return; } + const uid = storage.getDataFromLocalStorage(imUidLocalName); const sids = storage.getDataFromLocalStorage(imRtdLocalName); const parsedSids = sids ? sids.split(',') : []; const mt = storage.getDataFromLocalStorage(`${imRtdLocalName}_mt`); @@ -163,7 +165,7 @@ export function getRealTimeData(reqBidsConfigObj, onDone, moduleConfig) { } if (sids !== null) { - setRealTimeData(reqBidsConfigObj, moduleConfig, {im_segments: parsedSids}); + setRealTimeData(reqBidsConfigObj, moduleConfig, {im_uid: uid, im_segments: parsedSids}); onDone(); alreadyDone = true; } @@ -210,7 +212,7 @@ export function getApiCallback(reqBidsConfigObj, onDone, moduleConfig) { } if (parsedResponse.segments) { - setRealTimeData(reqBidsConfigObj, moduleConfig, {im_segments: parsedResponse.segments}); + setRealTimeData(reqBidsConfigObj, moduleConfig, {im_uid: parsedResponse.uid, im_segments: parsedResponse.segments}); storage.setDataInLocalStorage(imRtdLocalName, parsedResponse.segments); storage.setDataInLocalStorage(`${imRtdLocalName}_mt`, new Date(timestamp()).toUTCString()); } diff --git a/modules/imdsBidAdapter.js b/modules/imdsBidAdapter.js index 4cad1d614c5..0a0514df205 100644 --- a/modules/imdsBidAdapter.js +++ b/modules/imdsBidAdapter.js @@ -11,7 +11,7 @@ const BID_SCHEME = 'https://'; const BID_DOMAIN = 'technoratimedia.com'; const USER_SYNC_IFRAME_URL = 'https://ad-cdn.technoratimedia.com/html/usersync.html'; const USER_SYNC_PIXEL_URL = 'https://sync.technoratimedia.com/services'; -const VIDEO_PARAMS = [ 'minduration', 'maxduration', 'startdelay', 'placement', 'linearity', 'mimes', 'protocols', 'api' ]; +const VIDEO_PARAMS = [ 'minduration', 'maxduration', 'startdelay', 'placement', 'plcmt', 'linearity', 'mimes', 'protocols', 'api' ]; const BLOCKED_AD_SIZES = [ '1x1', '1x2' diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index ea446bd150d..2853bd95fd3 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -1,6 +1,6 @@ 'use strict'; -import { deepAccess, deepSetValue, generateUUID } from '../src/utils.js'; +import { deepAccess, deepSetValue, generateUUID, getWinDimensions, isPlainObject } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { ajax } from '../src/ajax.js'; @@ -21,6 +21,7 @@ const DEFAULT_VIDEO_WIDTH = 640; const DEFAULT_VIDEO_HEIGHT = 360; const ORIGIN = 'https://sonic.impactify.media'; const LOGGER_URI = 'https://logger.impactify.media'; +const LOGGER_JS_URI = 'https://log.impactify.it' const AUCTION_URI = '/bidder'; const COOKIE_SYNC_URI = '/static/cookie_sync.html'; const GVL_ID = 606; @@ -97,7 +98,7 @@ const helpers = { mediaType: '*', size: '*' }); - if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + if (isPlainObject(floorInfo) && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { return parseFloat(floorInfo.floor); } return null; @@ -149,8 +150,8 @@ function createOpenRtbRequest(validBidRequests, bidderRequest) { if (!request.device) request.device = {}; if (!request.site) request.site = {}; request.device = { - w: window.innerWidth, - h: window.innerHeight, + w: getWinDimensions().innerWidth, + h: getWinDimensions().innerHeight, devicetype: helpers.getDeviceType(), ua: navigator.userAgent, js: 1, @@ -167,11 +168,6 @@ function createOpenRtbRequest(validBidRequests, bidderRequest) { } deepSetValue(request, 'regs.ext.gdpr', gdprApplies); - if (bidderRequest.uspConsent) { - deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); - this.syncStore.uspConsent = bidderRequest.uspConsent; - } - if (GET_CONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); if (bidderRequest.uspConsent) { @@ -389,6 +385,19 @@ export const spec = { }); return true; - } + }, + + /** + * Register bidder specific code, which will execute if the bid request failed + * @param {*} param0 + */ + onBidderError: function ({ error, bidderRequest }) { + ajax(`${LOGGER_JS_URI}/logger`, null, JSON.stringify({ error, bidderRequest }), { + method: 'POST', + contentType: 'application/json' + }); + + return true; + }, }; registerBidder(spec); diff --git a/modules/impactifyBidAdapter.md b/modules/impactifyBidAdapter.md index de3373395dc..e08f2f3ce01 100644 --- a/modules/impactifyBidAdapter.md +++ b/modules/impactifyBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Impactify Bidder Adapter Module Type: Bidder Adapter -Maintainer: thomas.destefano@impactify.io +Maintainer: programmatic@impactify.io ``` # Description diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index fbceeee6d90..ed6278de6eb 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -3,9 +3,9 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; -import {loadExternalScript} from '../src/adloader.js'; +import {convertCurrency} from '../libraries/currencyUtils/currency.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -23,13 +23,10 @@ const BASIC_ADS_BASE_URL = 'https://ad.360yield-basic.com'; const PB_ENDPOINT = 'pb'; const EXTEND_URL = 'https://pbs.360yield.com/openrtb2/auction'; const IFRAME_SYNC_URL = 'https://hb.360yield.com/prebid-universal-creative/load-cookie.html'; +const DEFAULT_CURRENCY = 'USD'; const VIDEO_PARAMS = { - DEFAULT_MIMES: ['video/mp4'], - PLACEMENT_TYPE: { - INSTREAM: 1, - OUTSTREAM: 3, - } + DEFAULT_MIMES: ['video/mp4'] }; export const spec = { @@ -46,7 +43,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid(bid) { - return !!(bid && bid.params && (bid.params.placementId || (bid.params.placementKey && bid.params.publisherId))); + return !!(bid && bid.params && bid.params.placementId && bid.params.publisherId); }, /** @@ -122,6 +119,21 @@ export const spec = { registerBidder(spec); +const convertBidFloorCurrency = (imp) => { + try { + const bidFloor = convertCurrency( + imp.bidfloor, + imp.bidfloorcur, + DEFAULT_CURRENCY, + false, + ); + imp.bidfloor = bidFloor; + imp.bidfloorcur = DEFAULT_CURRENCY; + } catch (err) { + logWarn(`Failed to convert bid floor to ${DEFAULT_CURRENCY}. Passing floor price in its original currency.`, err); + } +}; + export const CONVERTER = ortbConverter({ context: { ttl: CREATIVE_TTL, @@ -133,24 +145,27 @@ export const CONVERTER = ortbConverter({ }, imp(buildImp, bidRequest, context) { const imp = buildImp(bidRequest, context); - imp.secure = Number(window.location.protocol === 'https:'); + imp.secure = bidRequest.ortb2Imp?.secure ?? 1; if (!imp.bidfloor && bidRequest.params.bidFloor) { imp.bidfloor = bidRequest.params.bidFloor; - imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || DEFAULT_CURRENCY; + } + + if (imp.bidfloor && imp.bidfloorcur && imp.bidfloorcur !== DEFAULT_CURRENCY) { + convertBidFloorCurrency(imp); } const bidderParamsPath = context.extendMode ? 'ext.prebid.bidder.improvedigital' : 'ext.bidder'; const placementId = bidRequest.params.placementId; - if (placementId) { - deepSetValue(imp, `${bidderParamsPath}.placementId`, placementId); - if (context.extendMode) { - deepSetValue(imp, 'ext.prebid.storedrequest.id', '' + placementId); - } - } else { - deepSetValue(imp, `${bidderParamsPath}.publisherId`, getBidIdParameter('publisherId', bidRequest.params)); - deepSetValue(imp, `${bidderParamsPath}.placementKey`, getBidIdParameter('placementKey', bidRequest.params)); + const publisherId = bidRequest.params.publisherId; + deepSetValue(imp, `${bidderParamsPath}.placementId`, placementId); + deepSetValue(imp, `${bidderParamsPath}.publisherId`, publisherId); + if (context.extendMode) { + deepSetValue(imp, 'ext.prebid.storedrequest.id', '' + placementId); } deepSetValue(imp, `${bidderParamsPath}.keyValues`, getBidIdParameter('keyValues', bidRequest.params) || undefined); + context.bidderRequest.bidLimit && deepSetValue(imp, 'ext.max_bids', context.bidderRequest.bidLimit); + return imp; }, request(buildRequest, imps, bidderRequest, context) { @@ -204,15 +219,14 @@ export const CONVERTER = ortbConverter({ renderer: ID_OUTSTREAM.createRenderer(bidRequest) }) } - ID_RAZR.forwardBid({bidRequest, bid: bidResponse}); return bidResponse; }, overrides: { imp: { banner(fillImpBanner, imp, bidRequest, context) { - // override to disregard banner.sizes if usePrebidSizes is not set + // override to disregard banner.sizes if usePrebidSizes is false if (!bidRequest.mediaTypes[BANNER]) return; - if (config.getConfig('improvedigital.usePrebidSizes') !== true) { + if (config.getConfig('improvedigital.usePrebidSizes') === false) { const banner = Object.assign({}, bidRequest.mediaTypes[BANNER], {sizes: null}); bidRequest = {...bidRequest, mediaTypes: {[BANNER]: banner}} } @@ -232,33 +246,6 @@ export const CONVERTER = ortbConverter({ context ); deepSetValue(imp, 'ext.is_rewarded_inventory', (video.rewarded === 1 || deepAccess(video, 'ext.rewarded') === 1) || undefined); - if (!imp.video.placement && ID_REQUEST.isOutstreamVideo(bidRequest)) { - // fillImpVideo will have already set placement = 1 for instream - imp.video.placement = VIDEO_PARAMS.PLACEMENT_TYPE.OUTSTREAM; - } - } - }, - request: { - gdprAddtlConsent(setAddtlConsent, ortbRequest, bidderRequest) { - const additionalConsent = bidderRequest?.gdprConsent?.addtlConsent; - if (!additionalConsent) { - return; - } - if (spec.syncStore.extendMode) { - setAddtlConsent(ortbRequest, bidderRequest); - return; - } - if (additionalConsent && additionalConsent.indexOf('~') !== -1) { - // Google Ad Tech Provider IDs - const atpIds = additionalConsent.substring(additionalConsent.indexOf('~') + 1); - if (atpIds) { - deepSetValue( - ortbRequest, - 'user.ext.consented_providers_settings.consented_providers', - atpIds.split('.').map(id => parseInt(id, 10)) - ); - } - } } } } @@ -373,61 +360,3 @@ const ID_OUTSTREAM = { bid.renderer.handleVideoEvent({ id, eventName }); }, }; - -const ID_RAZR = { - RENDERER_URL: 'https://cdn.360yield.com/razr/tag.js', - - forwardBid({bidRequest, bid}) { - if (bid.mediaType !== BANNER) { - return; - } - - const cfg = { - prebid: { - bidRequest, - bid - } - }; - - const cfgStr = JSON.stringify(cfg).replace(/<\/script>/ig, '\\x3C/script>'); - const s = ``; - // prepend RAZR config to ad markup: - bid.ad = s + bid.ad; - - this.installListener(); - }, - - installListener() { - if (this._listenerInstalled) { - return; - } - - window.addEventListener('message', function(e) { - const data = e.data?.razr?.load; - if (!data) { - return; - } - - if (e.source) { - data.source = e.source; - if (data.id) { - e.source.postMessage({ - razr: { - id: data.id - } - }, '*'); - } - } - - const ns = window.razr = window.razr || {}; - ns.q = ns.q || []; - ns.q.push(data); - - if (!ns.loaded) { - loadExternalScript(ID_RAZR.RENDERER_URL, BIDDER_CODE); - } - }); - - this._listenerInstalled = true; - } -}; diff --git a/modules/improvedigitalBidAdapter.md b/modules/improvedigitalBidAdapter.md index 15602d11038..4614ea550b4 100644 --- a/modules/improvedigitalBidAdapter.md +++ b/modules/improvedigitalBidAdapter.md @@ -1,50 +1,52 @@ # Overview -**Module Name**: Improve Digital Bidder Adapter -**Module Type**: Bidder Adapter -**Maintainer**: hb@azerion.com +```text +Module Name: Improve Digital Bidder Adapter +Module Type: Bidder Adapter +Maintainer: hb@azerion.com +``` # Description -Module that connects to Improve Digital's demand sources +This module connects publishers to Improve Digital's demand sources through Prebid.js. # Test Parameters + +```javascript +const adUnits = [{ + code: 'banner-ad-unit', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'improvedigital', + params: { + publisherId: 950, + placementId: 22135702, + } + } + ] +}, { + code: 'video-ad-unit', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + plcmt: 2, + } + }, + bids: [{ + bidder: 'improvedigital', + params: { + publisherId: 950, + placementId: 22137694, + keyValues: { + testKey: ["testValue"], + }, + }, + }], +}]; ``` - var adUnits = [{ - code: 'div-gpt-ad-1499748733608-0', - sizes: [[600, 290]], - bids: [ - { - bidder: 'improvedigital', - params: { - placementId:1053688 - } - } - ] - }, { - code: 'div-gpt-ad-1499748833901-0', - sizes: [[250, 250]], - bids: [{ - bidder: 'improvedigital', - params: { - placementId:1053689, - keyValues: { - testKey: ["testValue"] - } - } - }] - }, { - code: 'div-gpt-ad-1499748913322-0', - sizes: [[300, 300]], - bids: [{ - bidder: 'improvedigital', - params: { - placementId:1053687, - size: { - w:300, - h:300 - } - } - }] - }]; -``` \ No newline at end of file diff --git a/modules/imuIdSystem.js b/modules/imuIdSystem.js index 1242ca183ea..3e9904c526f 100644 --- a/modules/imuIdSystem.js +++ b/modules/imuIdSystem.js @@ -166,6 +166,7 @@ export const imuIdSubmodule = { } }; }, + primaryIds: ['imppid', 'imuid'], eids: { 'imppid': { source: 'ppid.intimatemerger.com', diff --git a/modules/incrxBidAdapter.js b/modules/incrxBidAdapter.js index 9b939aff11b..57faff63ccf 100644 --- a/modules/incrxBidAdapter.js +++ b/modules/incrxBidAdapter.js @@ -1,7 +1,8 @@ import { parseSizesInput, isEmpty } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js' - +import { BANNER, VIDEO } from '../src/mediaTypes.js' +import { INSTREAM, OUTSTREAM } from '../src/video.js'; +import { Renderer } from '../src/Renderer.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid @@ -15,7 +16,7 @@ const CREATIVE_TTL = 300; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. @@ -26,7 +27,9 @@ export const spec = { isBidRequestValid: function (bid) { return !!(bid.params.placementId); }, - + hasTypeVideo(bid) { + return typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.video !== 'undefined'; + }, /** * Make a server request from the list of BidRequests. * @@ -37,17 +40,29 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); - + let mdType = 0; + if (bidRequest.mediaTypes[BANNER]) { + mdType = 1; + } else { + mdType = 2; + } const requestParams = { _vzPlacementId: bidRequest.params.placementId, sizes: sizes, _slotBidId: bidRequest.bidId, - // TODO: is 'page' the right value here? _rqsrc: bidderRequest.refererInfo.page, + mChannel: mdType }; - - const payload = { - q: encodeURI(JSON.stringify(requestParams)) + let payload; + if (mdType === 1) { // BANNER + payload = { + q: encodeURI(JSON.stringify(requestParams)) + }; + } else { // VIDEO or other types + payload = { + q: encodeURI(JSON.stringify(requestParams)), + bidderRequestData: encodeURI(JSON.stringify(bidderRequest)) + }; } return { @@ -64,30 +79,83 @@ export const spec = { * @param {ServerResponse} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidderRequest) { const response = serverResponse.body; const bids = []; if (isEmpty(response)) { return bids; } + let decodedBidderRequestData; + if (typeof bidderRequest.data.bidderRequestData === 'string') { + decodedBidderRequestData = JSON.parse(decodeURI(bidderRequest.data.bidderRequestData)); + } else { + decodedBidderRequestData = bidderRequest.data.bidderRequestData; + } const responseBid = { requestId: response.slotBidId, - cpm: response.cpm, + cpm: response.cpm > 0 ? response.cpm : 0, currency: response.currency || DEFAULT_CURRENCY, adType: response.adType || '1', settings: response.settings, - width: response.adWidth, - height: response.adHeight, + width: response.adWidth || 300, + height: response.adHeight || 250, ttl: CREATIVE_TTL, creativeId: response.creativeId || 0, netRevenue: response.netRevenue || false, + mediaType: response.mediaType || BANNER, meta: { - mediaType: response.mediaType || BANNER, + mediaType: response.mediaType, advertiserDomains: response.advertiserDomains || [] }, - ad: response.ad + }; + if (response.mediaType === BANNER) { + responseBid.ad = response.ad || ''; + } else if (response.mediaType === VIDEO) { + let context, adUnitCode; + for (let i = 0; i < decodedBidderRequestData.bids.length; i++) { + const item = decodedBidderRequestData.bids[i]; + if (item.bidId === response.slotBidId) { + context = item.mediaTypes.video.context; + adUnitCode = item.adUnitCode; + break; + } + } + if (context === INSTREAM) { + responseBid.vastUrl = response.ad || ''; + } else if (context === OUTSTREAM) { + responseBid.vastXml = response.ad || ''; + if (response.rUrl) { + responseBid.renderer = createRenderer({ ...response, adUnitCode }); + } + } + } bids.push(responseBid); + function createRenderer(bid, rendererOptions = {}) { + const renderer = Renderer.install({ + id: bid.slotBidId, + url: bid.rUrl, + config: rendererOptions, + adUnitCode: bid.adUnitCode, + loaded: false + }); + try { + renderer.setRender(({ renderer, width, height, vastXml, adUnitCode }) => { + renderer.push(() => { + window.onetag.Player.init({ + ...bid, + width, + height, + vastXml, + nodeId: adUnitCode, + config: renderer.getConfig() + }); + }); + }); + } catch (e) { + } + return renderer; + } return bids; } diff --git a/modules/incrxBidAdapter.md b/modules/incrxBidAdapter.md new file mode 100644 index 00000000000..3dca572cd47 --- /dev/null +++ b/modules/incrxBidAdapter.md @@ -0,0 +1,65 @@ +# Overview + +``` +Module Name: IncrementX Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid-team@vertoz.com +``` + +# Description + +IncrementX Bid Adapter supports banner and video at present. + +# Test Parameters +``` + var adUnits = [ + { + code: "banner-space", + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: "incrementx", + params: { + placementId: "your_placementId" // required, + } + }] + }, + { + code: 'video-instream-space', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1 + } + }, + bids: [{ + bidder: "incrementx", + params: { + placementId: "your_placement_id" // required, + } + }] + }, + { + code: 'video-outstream-space', + mediaTypes: { + video: { + context: "outstream", + playerSize: [640,480] + } + }, + bids: [{ + bidder: "incrementx", + params: { + placementId: "your_placement_id" // required, + } + }] + }]; + +``` diff --git a/modules/inmobiBidAdapter.js b/modules/inmobiBidAdapter.js new file mode 100644 index 00000000000..910c53bf838 --- /dev/null +++ b/modules/inmobiBidAdapter.js @@ -0,0 +1,347 @@ +import { deepAccess, deepSetValue, isFn } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { ortb25Translator } from '../libraries/ortb2.5Translator/translator.js'; +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').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid + */ + +const GVLID = 333; +export const ADAPTER_VERSION = 1.0; +const BIDDER_CODE = 'inmobi'; +const BID_ENDPOINT = 'https://api.w.inmobi.com/openrtb/bidder/prebidjs'; +export const EVENT_ENDPOINT = 'https://sync.inmobi.com'; +export const SYNC_ENDPOINT = 'https://sync.inmobi.com/prebidjs?'; +const TRANSLATOR = ortb25Translator(); +const CURRENCY = 'USD'; +const POST_METHOD = 'POST'; +const PLAIN_CONTENT_TYPE = 'text/plain'; + +/** + * Defines the core oRTB converter inherited from converter library and all customization functions. + */ +const CONVERTER = ortbConverter({ + context: { + netRevenue: true, + ttl: 3600, + currency: CURRENCY + }, + imp, + request, + bidResponse, + response +}); + +/** + * Builds an impression object for oRTB 2.5 requests based on the bid request. + * + * @param {function} buildImp - Function to build the imp object. + * @param {Object} bidRequest - The request containing bid details. + * @param {Object} context - Context for the impression. + * @returns {Object} The constructed impression object. + */ +function imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + const params = bidRequest.params; + + imp.tagid = bidRequest.adUnitCode; + let floorInfo = {}; + + if (isFn(bidRequest.getFloor)) { + floorInfo = bidRequest.getFloor({ + currency: CURRENCY, + size: '*', + mediaType: '*' + }); + } + + // if floor price module is not set reading from bidRequest.params + if (!imp.bidfloor && bidRequest.params.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor; + imp.bidfloorcur = CURRENCY; + } + + deepSetValue(imp, 'ext', { + ...imp.ext, + params: bidRequest.params, + bidder: { + plc: params?.plc, + }, + moduleFloors: floorInfo + }); + imp.secure = Number(window.location.protocol === 'https:'); + + return imp; +} + +/** + * Constructs the oRTB 2.5 request object. + * + * @param {function} buildRequest - Function to build the request. + * @param {Array} imps - Array of impression objects. + * @param {Object} bidderRequest - Object containing bidder request information. + * @param {Object} context - Additional context. + * @returns {Object} The complete oRTB request object. + */ +function request(buildRequest, imps, bidderRequest, context) { + let request = buildRequest(imps, bidderRequest, context); + + deepSetValue(request, 'ext.prebid.channel.name', 'pbjs_InMobi'); + deepSetValue(request, 'ext.prebid.channel.pbjsversion', '$prebid.version$'); + deepSetValue(request, 'ext.prebid.channel.adapterversion', ADAPTER_VERSION); + + request = TRANSLATOR(request); + return request; +} + +/** + * Transforms an oRTB 2.5 bid into a bid response format for Prebid.js. + * + * @param {function} buildBidResponse - Function to build a bid response. + * @param {Object} bid - The bid to be transformed. + * @param {Object} context - Context for the bid. + * @returns {Object} Formatted bid response. + */ +function bidResponse(buildBidResponse, bid, context) { + context.mtype = deepAccess(bid, 'mtype'); + if (context.mtype === 4) { + const admJson = JSON.parse(bid.adm); + bid.adm = JSON.stringify(admJson.native); + } + let bidResponse = buildBidResponse(bid, context); + + if (typeof deepAccess(bid, 'ext') !== 'undefined') { + deepSetValue(bidResponse, 'meta', { + ...bidResponse.meta, + ...bid.ext, + }); + } + + return bidResponse; +} + +/** + * Converts the oRTB 2.5 bid response into the format required by Prebid.js. + * + * @param {function} buildResponse - Function to build the response. + * @param {Array} bidResponses - List of bid responses. + * @param {Object} ortbResponse - Original oRTB response data. + * @param {Object} context - Additional context. + * @returns {Object} Prebid.js compatible bid response. + */ +function response(buildResponse, bidResponses, ortbResponse, context) { + let response = buildResponse(bidResponses, ortbResponse, context); + + return response; +} + +/** @type {BidderSpec} */ +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + /** + * Determines user sync options based on consent and supported sync types. + * + * @param {Object} syncOptions - Options for user syncing (iframe, pixel). + * @param {Array} responses - List of bid responses. + * @param {Object} gdprConsent - GDPR consent details. + * @param {Object} uspConsent - CCPA consent details. + * @param {Object} gppConsent - GPP consent details. + * @returns {Array} List of user sync URLs. + */ + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { + const urls = []; + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return urls; + } + const pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + + let query = ''; + if (gdprConsent) { + query = tryAppendQueryString(query, 'gdpr', (gdprConsent.gdprApplies ? 1 : 0)); + } + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + query = tryAppendQueryString(query, 'gdpr_consent', gdprConsent.consentString); + } + if (uspConsent) { + query = tryAppendQueryString(query, 'us_privacy', uspConsent); + } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + query = tryAppendQueryString(query, 'gpp', gppConsent.gppString); + query = tryAppendQueryString(query, 'gpp_sid', gppConsent?.applicableSections?.join(',')); + } + if (query.slice(-1) === '&') { + query = query.slice(0, -1); + } + + if (pixelType === 'iframe' || (!responses || responses.length === 0)) { + return [{ + type: pixelType, + url: SYNC_ENDPOINT + query + }]; + } else { + responses.forEach(resp => { + const userSyncs = deepAccess(resp, 'body.ext.prebidjs.urls'); + if (!userSyncs) { + return; + } + + userSyncs.forEach(us => { + let url = us.url; + if (query) { + url = url + (url.indexOf('?') === -1 ? '?' : '&') + query; + } + + urls.push({ + type: pixelType, + url: url + }); + }); + }); + return urls; + } + }, + + /** + * Validates if a bid request contains the required parameters for InMobi. + * + * @param {Object} bid - Bid request to validate. + * @returns {boolean} True if the bid request is valid, otherwise false. + */ + isBidRequestValid: (bid) => { + if (!(bid && bid.params && bid.params.plc)) { + return false; + } + + return true; + }, + + /** + * Builds the server request from bid requests for InMobi. + * + * @param {BidRequest[]} bidRequests - Array of bid requests. + * @param {Object} bidderRequest - Additional request details. + * @returns {ServerRequest} The server request for bidding. + */ + buildRequests: (bidRequests, bidderRequest) => { + const data = CONVERTER.toORTB({ bidderRequest, bidRequests }); + + if (data) { + const requestPayload = { + method: POST_METHOD, + url: BID_ENDPOINT, + data: data, + options: { + contentType: PLAIN_CONTENT_TYPE, + crossOrigin: true, + withCredentials: true + } + }; + return requestPayload; + } + }, + + /** + * Interprets the server response and formats it into bids. + * + * @param {Object} response - Response from the server. + * @param {ServerRequest} request - Original bid request. + * @returns {Bid[]} Parsed bids or configurations. + */ + interpretResponse: (response, request) => { + if (typeof response?.body == 'undefined') { + return []; + } + + const interpretedResponse = CONVERTER.fromORTB({ response: response.body, request: request.data }); + const bids = interpretedResponse.bids || []; + + return bids; + }, + + /** + * Callback to report timeout event. + * + * @param {TimedOutBid[]} timeoutData - Array of timeout details. + */ + onTimeout: (timeoutData) => { + report('onTimeout', timeoutData); + }, + + /** + * Callback to report targeting event. + * + * @param {Bid} bid - The bid object + */ + onSetTargeting: (bid) => { + report('onSetTargeting', bid?.meta); + }, + + /** + * Callback to report successful ad render event. + * + * @param {Bid} bid - The bid that successfully rendered. + */ + onAdRenderSucceeded: (bid) => { + report('onAdRenderSucceeded', bid?.meta); + }, + + /** + * Callback to report bidder error event. + * + * @param {Object} errorData - Details about the error. + */ + onBidderError: (errorData) => { + report('onBidderError', errorData); + }, + + /** + * Callback to report bid won event. + * + * @param {Bid} bid - The bid that won the auction. + */ + onBidWon: (bid) => { + report('onBidWon', bid?.meta); + } + +}; + +function isReportingAllowed(loggingPercentage) { + return loggingPercentage != 0; +} + +function report(type, data) { + if (!data) { + return; + } + if (['onBidWon', 'onAdRenderSucceeded', 'onSetTargeting'].includes(type) && !isReportingAllowed(data.loggingPercentage)) { + return; + } + const payload = JSON.stringify({ + domain: location.hostname, + eventPayload: data + }); + + fetch(`${EVENT_ENDPOINT}/report/${type}`, { + body: payload, + keepalive: true, + credentials: 'include', + method: POST_METHOD, + headers: { + 'Content-Type': PLAIN_CONTENT_TYPE + } + }).catch((_e) => { + // do nothing; ignore errors + }); +} + +registerBidder(spec); diff --git a/modules/inmobiBidAdapter.md b/modules/inmobiBidAdapter.md new file mode 100644 index 00000000000..bf309fcb01d --- /dev/null +++ b/modules/inmobiBidAdapter.md @@ -0,0 +1,87 @@ +# Overview + +Module Name: InMobi Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid-support@inmobi.com + +# Description + +Module that connects to InMobi's demand sources. + +# Bid Params + +| Name | Scope | Description | Example | Type | +|------------|----------|--------------|-----------|-------- | +| `plc` | required | Placement ID | `'1234'` | `string` | +| `bidfloor` | optional | Bid Floor | `1.2` | `float` | + + +# Test Parameters + +## Banner +``` + var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + + bids: [{ + bidder: 'inmobi', + params: { + plc: '1719108420057' // Mandatory + } + }] + + }]; +``` + +## Video +``` + var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + video: { + playerSize : [300,250], + mimes : ["video/x-ms-wmv", "video/mp4"], + minduration : 0, + maxduration: 30, + protocols : [1,2], + api: [1, 2, 4, 6], + protocols: [3, 4, 7, 8, 10], + placement: 1, + plcmt: 1 + } + }, + + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'inmobi', + params: { + plc: '1443164204446401' //Mandatory + } + }] + }]; +``` + +## Native +``` + var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + native: { + type: 'image' + } + }, + + bids: [{ + bidder: 'inmobi', + params: { + plc: '10000033152', + bidfloor: 0.9 + } + }] + }]; +``` \ No newline at end of file diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index 617ce49f171..ba4e099dbc5 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums, deepSetValue} from '../src/utils.js'; +import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums, deepSetValue, isFn, logWarn, getWinDimensions} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {find} from '../src/polyfill.js'; @@ -102,24 +102,13 @@ function buildVideo(bidRequest) { let w = deepAccess(bidRequest, 'mediaTypes.video.w'); let h = deepAccess(bidRequest, 'mediaTypes.video.h'); const mimes = deepAccess(bidRequest, 'mediaTypes.video.mimes'); - const placement = deepAccess(bidRequest, 'mediaTypes.video.placement') || 3; + const placement = deepAccess(bidRequest, 'mediaTypes.video.placement'); const plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt') || undefined; const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); const context = deepAccess(bidRequest, 'mediaTypes.video.context'); - if (!w && playerSize) { - if (Array.isArray(playerSize[0])) { - w = parseInt(playerSize[0][0], 10); - } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { - w = parseInt(playerSize[0], 10); - } - } - if (!h && playerSize) { - if (Array.isArray(playerSize[0])) { - h = parseInt(playerSize[0][1], 10); - } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { - h = parseInt(playerSize[1], 10); - } + if (!h && !w && playerSize) { + ({w, h} = parsePlayerSizeToWidthHeight(playerSize, w, h)); } const bidRequestVideo = deepAccess(bidRequest, 'mediaTypes.video'); @@ -136,6 +125,10 @@ function buildVideo(bidRequest) { } } + if (placement && typeof placement !== 'undefined' && typeof placement === 'number') { + optionalParams['placement'] = placement; + } + if (plcmt) { optionalParams['plcmt'] = plcmt; } @@ -145,7 +138,6 @@ function buildVideo(bidRequest) { } let videoObj = { - placement, mimes, w, h, @@ -170,6 +162,27 @@ function buildImpression(bidRequest) { }, } + if (bidRequest?.params?.adUnitId) { + deepSetValue(imp, 'ext.prebid.bidder.insticator.adUnitId', bidRequest.params.adUnitId); + } + + if (bidRequest?.params?.publisherId) { + deepSetValue(imp, 'ext.prebid.bidder.insticator.publisherId', bidRequest.params.publisherId); + } + + let bidFloor = parseFloat(deepAccess(bidRequest, 'params.floor')); + + if (!isNaN(bidFloor)) { + imp.bidfloor = deepAccess(bidRequest, 'params.floor'); + imp.bidfloorcur = 'USD'; + const bidfloorcur = deepAccess(bidRequest, 'params.bidfloorcur') + if (bidfloorcur && bidfloorcur !== 'USD') { + delete imp.bidfloor; + delete imp.bidfloorcur; + logWarn('insticator: bidfloorcur supported by insticator is USD only. ignoring bidfloor and bidfloorcur params'); + } + } + if (deepAccess(bidRequest, 'mediaTypes.banner')) { imp.banner = buildBanner(bidRequest); } @@ -178,6 +191,49 @@ function buildImpression(bidRequest) { imp.video = buildVideo(bidRequest); } + if (isFn(bidRequest.getFloor)) { + let moduleBidFloor; + + const mediaType = deepAccess(bidRequest, 'mediaTypes.banner') ? 'banner' : deepAccess(bidRequest, 'mediaTypes.video') ? 'video' : undefined; + + let _mediaType = mediaType; + let _size = '*'; + + if (mediaType && ['banner', 'video'].includes(mediaType)) { + if (mediaType === 'banner') { + const { w: width, h: height } = imp[mediaType]; + if (width && height) { + _size = [width, height]; + } else { + const sizes = deepAccess(bidRequest, 'mediaTypes.banner.format'); + if (sizes && sizes.length > 0) { + const {w: width, h: height} = sizes[0]; + _size = [width, height]; + } + } + } else if (mediaType === 'video') { + const { w: width, h: height } = imp[mediaType]; + _mediaType = mediaType; + _size = [width, height]; + } + } + try { + moduleBidFloor = bidRequest.getFloor({ + currency: 'USD', + mediaType: _mediaType, + size: _size + }); + } catch (err) { + // continue with no module floors + logWarn('priceFloors module call getFloor failed, error : ', err); + } + + if (moduleBidFloor) { + imp.bidfloor = moduleBidFloor.floor; + imp.bidfloorcur = moduleBidFloor.currency; + } + } + return imp; } @@ -186,9 +242,9 @@ function buildDevice(bidRequest) { const deviceConfig = ortb2Data?.device || {} const device = { - w: window.innerWidth, - h: window.innerHeight, - js: true, + w: getWinDimensions().innerWidth, + h: getWinDimensions().innerHeight, + js: 1, ext: { localStorage: storage.localStorageIsEnabled(), cookies: storage.cookiesAreEnabled(), @@ -380,6 +436,10 @@ function buildRequest(validBidRequests, bidderRequest) { deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString); } + if (validBidRequests[0]?.params?.publisherId) { + deepSetValue(req, 'site.publisher.id', validBidRequests[0].params.publisherId); + } + return req; } @@ -506,24 +566,12 @@ function validateVideo(bid) { let w = deepAccess(bid, 'mediaTypes.video.w'); let h = deepAccess(bid, 'mediaTypes.video.h'); const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); - if (!w && playerSize) { - if (Array.isArray(playerSize[0])) { - w = parseInt(playerSize[0][0], 10); - } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { - w = parseInt(playerSize[0], 10); - } - } - if (!h && playerSize) { - if (Array.isArray(playerSize[0])) { - h = parseInt(playerSize[0][1], 10); - } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { - h = parseInt(playerSize[1], 10); - } + + if (!h && !w && playerSize) { + ({w, h} = parsePlayerSizeToWidthHeight(playerSize, w, h)); } - const videoSize = [ - w, - h, - ]; + + const videoSize = [w, h]; if ( !validateSize(videoSize) @@ -539,13 +587,6 @@ function validateVideo(bid) { return false; } - const placement = deepAccess(bid, 'mediaTypes.video.placement'); - - if (typeof placement !== 'undefined' && typeof placement !== 'number') { - logError('insticator: video placement is not a number'); - return false; - } - const plcmt = deepAccess(bid, 'mediaTypes.video.plcmt'); if (typeof plcmt !== 'undefined' && typeof plcmt !== 'number') { @@ -569,6 +610,25 @@ function validateVideo(bid) { return true; } +function parsePlayerSizeToWidthHeight(playerSize, w, h) { + if (!w && playerSize) { + if (Array.isArray(playerSize[0])) { + w = parseInt(playerSize[0][0], 10); + } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { + w = parseInt(playerSize[0], 10); + } + } + if (!h && playerSize) { + if (Array.isArray(playerSize[0])) { + h = parseInt(playerSize[0][1], 10); + } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { + h = parseInt(playerSize[1], 10); + } + } + + return { w, h }; +} + export const spec = { code: BIDDER_CODE, gvlid: GVLID, diff --git a/modules/instreamTracking.js b/modules/instreamTracking.js index 2686feab679..909c21b29bd 100644 --- a/modules/instreamTracking.js +++ b/modules/instreamTracking.js @@ -41,9 +41,10 @@ const whitelistedResources = /video|fetch|xmlhttprequest|other/; * * Note: this is a workaround till a better approach is engineered. * - * @param {Array} adUnits - * @param {Array} bidsReceived - * @param {Array} bidderRequests + * @param {object} config + * @param {Array} config.adUnits + * @param {Array} config.bidsReceived + * @param {Array} config.bidderRequests * * @return {boolean} returns TRUE if tracking started */ diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js new file mode 100644 index 00000000000..3cc0efbdb57 --- /dev/null +++ b/modules/intentIqAnalyticsAdapter.js @@ -0,0 +1,316 @@ +import {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 {appendVrrefAndFui, getReferrer} from '../libraries/intentIqUtils/getRefferer.js'; +import {getCmpData} from '../libraries/intentIqUtils/getCmpData.js' +import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, VERSION} from '../libraries/intentIqConstants/intentIqConstants.js'; +import {readData, defineStorageType} from '../libraries/intentIqUtils/storageUtils.js'; + +const MODULE_NAME = 'iiqAnalytics' +const analyticsType = 'endpoint'; +const REPORT_ENDPOINT = 'https://reports.intentiq.com/report'; +const REPORT_ENDPOINT_GDPR = 'https://reports-gdpr.intentiq.com/report'; +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); + +const PARAMS_NAMES = { + abTestGroup: 'abGroup', + pbPauseUntil: 'pbPauseUntil', + pbMonitoringEnabled: 'pbMonitoringEnabled', + isInTestGroup: 'isInTestGroup', + enhanceRequests: 'enhanceRequests', + wasSubscribedForPrebid: 'wasSubscribedForPrebid', + hadEids: 'hadEids', + ABTestingConfigurationSource: 'ABTestingConfigurationSource', + lateConfiguration: 'lateConfiguration', + jsversion: 'jsversion', + eidsNames: 'eidsNames', + requestRtt: 'rtt', + clientType: 'clientType', + adserverDeviceType: 'AdserverDeviceType', + terminationCause: 'terminationCause', + callCount: 'callCount', + manualCallCount: 'mcc', + pubprovidedidsFailedToregister: 'ppcc', + noDataCount: 'noDataCount', + profile: 'profile', + isProfileDeterministic: 'pidDeterministic', + siteId: 'sid', + hadEidsInLocalStorage: 'idls', + auctionStartTime: 'ast', + eidsReadTime: 'eidt', + agentId: 'aid', + auctionEidsLength: 'aeidln', + wasServerCalled: 'wsrvcll', + referrer: 'vrref', + isInBrowserBlacklist: 'inbbl', + prebidVersion: 'pbjsver', + partnerId: 'partnerId', + firstPartyId: 'pcid', + placementId: 'placementId' +}; + +let iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({defaultUrl: REPORT_ENDPOINT, analyticsType}), { + initOptions: { + lsValueInitialized: false, + partner: null, + fpid: null, + currentGroup: null, + dataInLs: null, + eidl: null, + lsIdsInitialized: false, + manualWinReportEnabled: false, + domainName: null + }, + track({eventType, args}) { + switch (eventType) { + case BID_WON: + bidWon(args); + break; + case BID_REQUESTED: + defineGlobalVariableName(); + break; + default: + break; + } + } +}); + +// Events needed +const { + BID_WON, + BID_REQUESTED +} = EVENTS; + +function getIntentIqConfig() { + return config.getConfig('userSync.userIds')?.find(m => m.name === 'intentIqId'); +} + +function initLsValues() { + if (iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) return; + let iiqConfig = getIntentIqConfig() + + if (iiqConfig) { + iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = true; + iiqAnalyticsAnalyticsAdapter.initOptions.partner = + iiqConfig.params?.partner && !isNaN(iiqConfig.params.partner) ? iiqConfig.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 || ''; + } else { + iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = false; + iiqAnalyticsAnalyticsAdapter.initOptions.partner = -1; + } +} + +function initReadLsIds() { + try { + iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = null; + iiqAnalyticsAnalyticsAdapter.initOptions.fpid = JSON.parse(readData(FIRST_PARTY_KEY, 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 clientsHints = readData(CLIENT_HINTS_KEY, allowedStorage, storage) || ''; + + if (partnerData) { + iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized = true; + let pData = JSON.parse(partnerData); + iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause = pData.terminationCause + iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = pData.data; + iiqAnalyticsAnalyticsAdapter.initOptions.eidl = pData.eidl || -1; + iiqAnalyticsAnalyticsAdapter.initOptions.ct = pData.ct || null; + iiqAnalyticsAnalyticsAdapter.initOptions.siteId = pData.siteId || null; + iiqAnalyticsAnalyticsAdapter.initOptions.wsrvcll = pData.wsrvcll || false; + iiqAnalyticsAnalyticsAdapter.initOptions.rrtt = pData.rrtt || null; + } + + iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints = clientsHints + } catch (e) { + logError(e) + } +} + +function bidWon(args, isReportExternal) { + if (!iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) { + initLsValues(); + } + + 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) { + initReadLsIds(); + } + if ((isReportExternal && iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled) || (!isReportExternal && !iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled)) { + ajax(constructFullUrl(preparePayload(args, true)), undefined, null, {method: 'GET'}); + logInfo('IIQ ANALYTICS -> BID WON') + return true; + } + return false; +} + +function defineGlobalVariableName() { + function reportExternalWin(args) { + return bidWon(args, true); + } + + const iiqConfig = getIntentIqConfig(); + const partnerId = iiqConfig?.params?.partner || 0; + + window[`intentIqAnalyticsAdapter_${partnerId}`] = { reportExternalWin }; +} + +function getRandom(start, end) { + return Math.floor((Math.random() * (end - start + 1)) + start); +} + +export function preparePayload(data) { + let 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(); + result[PARAMS_NAMES.terminationCause] = iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause; + result[PARAMS_NAMES.abTestGroup] = iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup; + result[PARAMS_NAMES.clientType] = iiqAnalyticsAnalyticsAdapter.initOptions.ct; + result[PARAMS_NAMES.siteId] = iiqAnalyticsAnalyticsAdapter.initOptions.siteId; + result[PARAMS_NAMES.wasServerCalled] = iiqAnalyticsAnalyticsAdapter.initOptions.wsrvcll; + result[PARAMS_NAMES.requestRtt] = iiqAnalyticsAnalyticsAdapter.initOptions.rrtt; + + 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) + + prepareData(data, result); + + fillEidsData(result); + + return result; +} + +function fillEidsData(result) { + if (iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized) { + result[PARAMS_NAMES.hadEidsInLocalStorage] = iiqAnalyticsAnalyticsAdapter.initOptions.eidl && iiqAnalyticsAnalyticsAdapter.initOptions.eidl > 0; + result[PARAMS_NAMES.auctionEidsLength] = iiqAnalyticsAnalyticsAdapter.initOptions.eidl || -1; + } +} + +function prepareData (data, result) { + if (data.bidderCode) { + result.bidderCode = data.bidderCode; + } + if (data.cpm) { + result.cpm = data.cpm; + } + if (data.currency) { + result.currency = data.currency; + } + if (data.originalCpm) { + result.originalCpm = data.originalCpm; + } + if (data.originalCurrency) { + result.originalCurrency = data.originalCurrency; + } + if (data.status) { + result.status = data.status; + } + if (data.auctionId) { + result.prebidAuctionId = data.auctionId; + } + if (data.placementId) { + result.placementId = data.placementId; + } else { + // Simplified placementId determination + let placeIdFound = false; + if (data.params && Array.isArray(data.params)) { + for (let i = 0; i < data.params.length; i++) { + const param = data.params[i]; + if (param.placementId) { + result.placementId = param.placementId; + placeIdFound = true; + break; + } + } + } + if (!placeIdFound && data.adUnitCode) { + result.placementId = data.adUnitCode; + } + } + + result.biddingPlatformId = 1; + result.partnerAuctionId = 'BW'; +} + +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 + } +} + +function constructFullUrl(data) { + let report = []; + data = btoa(JSON.stringify(data)); + report.push(data); + + const cmpData = getCmpData(); + const gdprDetected = cmpData.gdprString; + const baseUrl = gdprDetected ? REPORT_ENDPOINT_GDPR : REPORT_ENDPOINT; + + let url = baseUrl + '?pid=' + iiqAnalyticsAnalyticsAdapter.initOptions.partner + + '&mct=1' + + ((iiqAnalyticsAnalyticsAdapter.initOptions?.fpid) + ? '&iiqid=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid) : '') + + '&agid=' + REPORTER_ID + + '&jsver=' + VERSION + + '&source=pbjs' + + '&payload=' + JSON.stringify(report) + + '&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 = appendVrrefAndFui(url, iiqAnalyticsAnalyticsAdapter.initOptions.domainName); + return url; +} + +iiqAnalyticsAnalyticsAdapter.originEnableAnalytics = iiqAnalyticsAnalyticsAdapter.enableAnalytics; + +iiqAnalyticsAnalyticsAdapter.enableAnalytics = function (myConfig) { + iiqAnalyticsAnalyticsAdapter.originEnableAnalytics(myConfig); // call the base class function +}; +adapterManager.registerAnalyticsAdapter({ + adapter: iiqAnalyticsAnalyticsAdapter, + code: MODULE_NAME +}); + +export default iiqAnalyticsAnalyticsAdapter; diff --git a/modules/intentIqAnalyticsAdapter.md b/modules/intentIqAnalyticsAdapter.md new file mode 100644 index 00000000000..2a3eece0576 --- /dev/null +++ b/modules/intentIqAnalyticsAdapter.md @@ -0,0 +1,85 @@ +# Overview + +Module Name: iiqAnalytics +Module Type: Analytics Adapter +Maintainer: julian@intentiq.com + +# 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 + +No registration for this module is required. + +## Intent IQ Universal IDConfiguration + +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) + +#### Example Configuration + +```js +pbjs.enableAnalytics({ + provider: 'iiqAnalytics' +}); +``` + + +### 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() +``` +Example use with Partner ID = 123455 + +```js +window.intentIqAnalyticsAdapter_123455.reportExternalWin() +``` + +### Function Parameters + +The reportExternalWin function takes an object containing auction win data. Below is an example of the object: + +```js +var reportData = { +biddingPlatformId: 1, // Platform ID. The value 1 corresponds to PreBid. +partnerAuctionId: '[YOUR_AUCTION_ID_IF_EXISTS]', // Auction ID, if available. +bidderCode: 'xxxxxxxx', // Bidder code. +prebidAuctionId: '3d4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8e', // PreBid auction ID. +cpm: 1.5, // Cost per thousand impressions (CPM). +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. +} +``` + +| 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 | +| partnerAuctionId | String | Use this when you are running multiple auction solutions across your assets and have a unified identifier for auctions | 3d44542d-xx-4662-xxxx-4xxxx3d8e | No | +| bidderCode | String | Specifies the name of the bidder that won the auction as reported by Prebid and all other bidding platforms | newAppnexus | Yes | +| prebidAuctionId | String | Specifies the identifier of the Prebid auction. Leave empty or undefined if Prebid is not the bidding platform | | | +| cpm | Decimal | Cost per mille of the impression as received from the demand-side auction (without modifications or reductions) | 5.62 | Yes | +| currency | String | Currency of the auction | USD | Yes | +| originalCpm | Decimal | Leave empty or undefined if Prebid is not the bidding platform | 5.5 | No | +| originalCurrency | String | Currency of the original auction | USD | No | +| status | String | Status of the impression. Leave empty or undefined if Prebid is not the bidding platform | rendered | No | +| placementId | String | Unique identifier of the ad unit on the webpage that showed this ad | div-1 | No | + + +To report the auction win, call the function as follows: + +```js +window.intentIqAnalyticsAdapter_[partner_id].reportExternalWin(reportData) +``` diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 109ea5c39c6..548af86d772 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -5,11 +5,25 @@ * @requires module:modules/userId */ -import { logError, logInfo } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { submodule } from '../src/hook.js' -import { getStorageManager } from '../src/storageManager.js'; -import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import {logError, isPlainObject} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js' +import AES from 'crypto-js/aes.js'; +import Utf8 from 'crypto-js/enc-utf8.js'; +import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; +import {appendVrrefAndFui} from '../libraries/intentIqUtils/getRefferer.js'; +import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; +import {readData, storeData, defineStorageType, removeDataByKey} from '../libraries/intentIqUtils/storageUtils.js'; +import { + FIRST_PARTY_KEY, + WITH_IIQ, WITHOUT_IIQ, + NOT_YET_DEFINED, + BLACK_LIST, + CLIENT_HINTS_KEY, + EMPTY, + GVLID, + VERSION, +} from '../libraries/intentIqConstants/intentIqConstants.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -17,15 +31,23 @@ import { MODULE_TYPE_UID } from '../src/activities/modules.js'; * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse */ -const PCID_EXPIRY = 365; - const MODULE_NAME = 'intentIqId'; -export const FIRST_PARTY_KEY = '_iiq_fdata'; -export var FIRST_PARTY_DATA_KEY = '_iiq_fdata'; - -export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); +const encoderCH = { + brands: 0, + mobile: 1, + platform: 2, + architecture: 3, + bitness: 4, + model: 5, + platformVersion: 6, + wow64: 7, + fullVersionList: 8 +}; const INVALID_ID = 'INVALID_ID'; +const ENDPOINT = 'https://api.intentiq.com'; +const GDPR_ENDPOINT = 'https://api-gdpr.intentiq.com'; +export let firstPartyData; /** * Generate standard UUID string @@ -42,51 +64,27 @@ function generateGUID() { } /** - * Read Intent IQ data from cookie or local storage - * @param key - * @return {string} + * Encrypts plaintext. + * @param {string} plainText The plaintext to encrypt. + * @returns {string} The encrypted text as a base64 string. */ -export function readData(key) { - try { - if (storage.hasLocalStorage()) { - return storage.getDataFromLocalStorage(key); - } - if (storage.cookiesAreEnabled()) { - return storage.getCookie(key); - } - } catch (error) { - logError(error); - } +export function encryptData(plainText) { + return AES.encrypt(plainText, MODULE_NAME).toString(); } /** - * Store Intent IQ data in either cookie or local storage - * expiration date: 365 days - * @param key - * @param {string} value IntentIQ ID value to sintentIqIdSystem_spec.jstore + * Decrypts ciphertext. + * @param {string} encryptedText The encrypted text as a base64 string. + * @returns {string} The decrypted plaintext. */ -function storeData(key, value, cookieStorageEnabled = false) { - try { - logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value); - - if (value) { - if (storage.hasLocalStorage()) { - storage.setDataInLocalStorage(key, value); - } - const expiresStr = (new Date(Date.now() + (PCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); - if (storage.cookiesAreEnabled() && cookieStorageEnabled) { - storage.setCookie(key, value, expiresStr, 'LAX'); - } - } - } catch (error) { - logError(error); - } +export function decryptData(encryptedText) { + const bytes = AES.decrypt(encryptedText, MODULE_NAME); + return bytes.toString(Utf8); } /** * Parse json if possible, else return null * @param data - * @param {object|null} */ function tryParse(data) { try { @@ -97,6 +95,55 @@ function tryParse(data) { } } +/** + * Configures and updates A/B testing group in Google Ad Manager (GAM). + * + * @param {object} gamObjectReference - Reference to the GAM object, expected to have a `cmd` queue and `pubads()` API. + * @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) { + if (isPlainObject(gamObjectReference) && gamObjectReference.cmd) { + gamObjectReference.cmd.push(() => { + gamObjectReference + .pubads() + .setTargeting(gamParameterName, userGroup || NOT_YET_DEFINED); + }); + } +} + +/** + * Processes raw client hints data into a structured format. + * @param {object} clientHints - Raw client hints data + * @return {string} A JSON string of processed client hints or an empty string if no hints + */ +export function handleClientHints(clientHints) { + const chParams = {}; + for (const key in clientHints) { + if (clientHints.hasOwnProperty(key) && clientHints[key] !== '') { + if (['brands', 'fullVersionList'].includes(key)) { + let handledParam = ''; + clientHints[key].forEach((element, index) => { + const isNotLast = index < clientHints[key].length - 1; + handledParam += `"${element.brand}";v="${element.version}"${isNotLast ? ', ' : ''}`; + }); + chParams[encoderCH[key]] = handledParam; + } else if (typeof clientHints[key] === 'boolean') { + chParams[encoderCH[key]] = `?${clientHints[key] ? 1 : 0}`; + } else { + chParams[encoderCH[key]] = `"${clientHints[key]}"`; + } + } + } + return Object.keys(chParams).length ? JSON.stringify(chParams) : ''; +} + +export function isCMPStringTheSame(fpData, cmpData) { + const firstPartyDataCPString = `${fpData.gdprString}${fpData.gppString}${fpData.uspString}`; + const cmpDataString = `${cmpData.gdprString}${cmpData.gppString}${cmpData.uspString}`; + return firstPartyDataCPString === cmpDataString; +} + /** @type {Submodule} */ export const intentIqIdSubmodule = { /** @@ -104,6 +151,7 @@ export const intentIqIdSubmodule = { * @type {string} */ name: MODULE_NAME, + gvlid: GVLID, /** * decode the stored id value for passing to bid requests * @function @@ -111,7 +159,7 @@ export const intentIqIdSubmodule = { * @returns {{intentIqId: {string}}|undefined} */ decode(value) { - return value && value != '' && INVALID_ID != value ? { 'intentIqId': value } : undefined; + return value && value != '' && INVALID_ID != value ? {'intentIqId': value} : undefined; }, /** * performs action to obtain id and return a value in the callback's response argument @@ -120,28 +168,143 @@ export const intentIqIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { - const configParams = (config && config.params) || {}; - if (!configParams || typeof configParams.partner !== 'number') { + const configParams = (config?.params) || {}; + let decryptedData, callbackTimeoutID; + let callbackFired = false; + let runtimeEids = { eids: [] }; + + let gamObjectReference = isPlainObject(configParams.gamObjectReference) ? configParams.gamObjectReference : undefined; + let gamParameterName = configParams.gamParameterName ? configParams.gamParameterName : 'intent_iq_group'; + + const allowedStorage = defineStorageType(config.enabledStorageTypes); + + let rrttStrtTime = 0; + let partnerData = {}; + let shouldCallServer = false; + const FIRST_PARTY_DATA_KEY = `${FIRST_PARTY_KEY}_${configParams.partner}`; + const cmpData = getCmpData(); + const gdprDetected = cmpData.gdprString; + firstPartyData = tryParse(readData(FIRST_PARTY_KEY, allowedStorage)); + const isGroupB = firstPartyData?.group === WITHOUT_IIQ; + setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group) + + const firePartnerCallback = () => { + if (configParams.callback && !callbackFired) { + callbackFired = true; + if (callbackTimeoutID) clearTimeout(callbackTimeoutID); + if (isGroupB) runtimeEids = { eids: [] }; + configParams.callback(runtimeEids, firstPartyData?.group || NOT_YET_DEFINED); + } + } + + callbackTimeoutID = setTimeout(() => { + firePartnerCallback(); + }, configParams.timeoutInMillis || 500 + ); + + if (typeof configParams.partner !== 'number') { logError('User ID - intentIqId submodule requires a valid partner to be defined'); + firePartnerCallback() return; } - const cookieStorageEnabled = typeof configParams.enableCookieStorage === 'boolean' ? configParams.enableCookieStorage : false - if (!FIRST_PARTY_DATA_KEY.includes(configParams.partner)) { FIRST_PARTY_DATA_KEY += '_' + configParams.partner; } - let rrttStrtTime = 0; - // Read Intent IQ 1st party id or generate it if none exists - let firstPartyData = tryParse(readData(FIRST_PARTY_KEY)); - if (!firstPartyData || !firstPartyData.pcid || firstPartyData.pcidDate) { + const currentBrowserLowerCase = detectBrowser(); + const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; + + // Check if current browser is in blacklist + if (browserBlackList?.includes(currentBrowserLowerCase)) { + logError('User ID - intentIqId submodule: browser is in blacklist!'); + if (configParams.callback) configParams.callback('', BLACK_LIST); + return; + } + + if (!firstPartyData?.pcid) { const firstPartyId = generateGUID(); - firstPartyData = { 'pcid': firstPartyId, 'pcidDate': Date.now() }; - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), cookieStorageEnabled); + firstPartyData = { + pcid: firstPartyId, + pcidDate: Date.now(), + group: NOT_YET_DEFINED, + cttl: 0, + uspString: EMPTY, + gppString: EMPTY, + gdprString: EMPTY, + date: Date.now() + }; + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + } else if (!firstPartyData.pcidDate) { + firstPartyData.pcidDate = Date.now(); + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); } - let partnerData = tryParse(readData(FIRST_PARTY_DATA_KEY)); - if (!partnerData) partnerData = {}; + if (gdprDetected && !('isOptedOut' in firstPartyData)) { + firstPartyData.isOptedOut = true; + } + + // Read client hints from storage + let clientHints = readData(CLIENT_HINTS_KEY, allowedStorage); + + // Get client hints and save to storage + if (navigator?.userAgentData?.getHighEntropyValues) { + navigator.userAgentData + .getHighEntropyValues([ + 'brands', + 'mobile', + 'bitness', + 'wow64', + 'architecture', + 'model', + 'platform', + 'platformVersion', + 'fullVersionList' + ]) + .then(ch => { + clientHints = handleClientHints(ch); + storeData(CLIENT_HINTS_KEY, clientHints, allowedStorage, firstPartyData) + }); + } + + const savedData = tryParse(readData(FIRST_PARTY_DATA_KEY, allowedStorage)) + if (savedData) { + partnerData = savedData; + + if (partnerData.wsrvcll) { + partnerData.wsrvcll = false; + storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + } + } + + if (partnerData.data) { + if (partnerData.data.length) { // encrypted data + decryptedData = tryParse(decryptData(partnerData.data)); + runtimeEids = decryptedData; + } + } + + if (!firstPartyData.cttl || Date.now() - firstPartyData.date > firstPartyData.cttl || !isCMPStringTheSame(firstPartyData, cmpData)) { + firstPartyData.uspString = cmpData.uspString; + firstPartyData.gppString = cmpData.gppString; + firstPartyData.gdprString = cmpData.gdprString; + firstPartyData.date = Date.now(); + shouldCallServer = true; + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + } else if (firstPartyData.isOptedOut) { + partnerData.data = runtimeEids = { eids: [] }; + firePartnerCallback() + } + + if (firstPartyData.group === WITHOUT_IIQ || (firstPartyData.group !== WITHOUT_IIQ && runtimeEids?.eids?.length)) { + firePartnerCallback() + } + + if (!shouldCallServer) { + if (isGroupB) runtimeEids = { eids: [] }; + firePartnerCallback(); + return { id: runtimeEids.eids }; + } // use protocol relative urls for http or https - let url = `https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; + let url = `${gdprDetected ? GDPR_ENDPOINT : ENDPOINT}/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; url += configParams.pcid ? '&pcid=' + encodeURIComponent(configParams.pcid) : ''; url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : ''; url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : ''; @@ -149,61 +312,159 @@ export const intentIqIdSubmodule = { url += (partnerData.cttl) ? '&cttl=' + encodeURIComponent(partnerData.cttl) : ''; url += (partnerData.rrtt) ? '&rrtt=' + encodeURIComponent(partnerData.rrtt) : ''; url += firstPartyData.pcidDate ? '&iiqpciddate=' + encodeURIComponent(firstPartyData.pcidDate) : ''; + url += cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : ''; + url += cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : ''; + url += cmpData.gdprApplies + ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' + : '&gdpr=0'; + url += clientHints ? '&uh=' + encodeURIComponent(clientHints) : ''; + url += VERSION ? '&jsver=' + VERSION : ''; + url += firstPartyData?.group ? '&testGroup=' + encodeURIComponent(firstPartyData.group) : ''; + + // Add vrref and fui to the URL + url = appendVrrefAndFui(url, configParams.domainName); + + const storeFirstPartyData = () => { + partnerData.eidl = runtimeEids?.eids?.length || -1 + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + } const resp = function (callback) { const callbacks = { success: response => { let respJson = tryParse(response); // If response is a valid json and should save is true - if (respJson && respJson.ls) { - // Store pid field if found in response json - let shouldUpdateLs = false; - if ('pid' in respJson) { - firstPartyData.pid = respJson.pid; - shouldUpdateLs = true; + if (respJson) { + partnerData.date = Date.now(); + firstPartyData.date = Date.now(); + const defineEmptyDataAndFireCallback = () => { + respJson.data = partnerData.data = runtimeEids = { eids: [] }; + storeFirstPartyData() + firePartnerCallback() + callback(runtimeEids) } + if (callbackTimeoutID) clearTimeout(callbackTimeoutID) if ('cttl' in respJson) { - partnerData.cttl = respJson.cttl; - shouldUpdateLs = true; + firstPartyData.cttl = respJson.cttl; + } else firstPartyData.cttl = 86400000; + + if ('tc' in respJson) { + partnerData.terminationCause = respJson.tc; + if (respJson.tc == 41) { + firstPartyData.group = WITHOUT_IIQ; + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + defineEmptyDataAndFireCallback(); + if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group); + return + } else { + firstPartyData.group = WITH_IIQ; + if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group); + } } - // If should save and data is empty, means we should save as INVALID_ID - if (respJson.data == '') { - respJson.data = INVALID_ID; - } else { + if ('isOptedOut' in respJson) { + if (respJson.isOptedOut !== firstPartyData.isOptedOut) { + firstPartyData.isOptedOut = respJson.isOptedOut; + } + if (respJson.isOptedOut === true) { + respJson.data = partnerData.data = runtimeEids = { eids: [] }; + + const keysToRemove = [ + FIRST_PARTY_DATA_KEY, + CLIENT_HINTS_KEY + ]; + + keysToRemove.forEach(key => removeDataByKey(key, allowedStorage)); + + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + firePartnerCallback(); + callback(runtimeEids); + return + } + } + if ('pid' in respJson) { + firstPartyData.pid = respJson.pid; + } + if ('ls' in respJson) { + if (respJson.ls === false) { + defineEmptyDataAndFireCallback() + return + } + // If data is empty, means we should save as INVALID_ID + if (respJson.data == '') { + respJson.data = INVALID_ID; + } else { + // If data is a single string, assume it is an id with source intentiq.com + if (respJson.data && typeof respJson.data === 'string') { + respJson.data = {eids: [respJson.data]} + } + } partnerData.data = respJson.data; - shouldUpdateLs = true; } + + if ('ct' in respJson) { + partnerData.ct = respJson.ct; + } + + if ('sid' in respJson) { + partnerData.siteId = respJson.sid; + } + if (rrttStrtTime && rrttStrtTime > 0) { partnerData.rrtt = Date.now() - rrttStrtTime; - shouldUpdateLs = true; } - if (shouldUpdateLs === true) { - partnerData.date = Date.now(); - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), cookieStorageEnabled); - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), cookieStorageEnabled); + + if (respJson.data?.eids) { + runtimeEids = respJson.data + callback(respJson.data.eids); + firePartnerCallback() + const encryptedData = encryptData(JSON.stringify(respJson.data)) + partnerData.data = encryptedData; + } else { + callback(runtimeEids); + firePartnerCallback() } - callback(respJson.data); + storeFirstPartyData(); } else { - callback(); + callback(runtimeEids); + firePartnerCallback() } }, error: error => { logError(MODULE_NAME + ': ID fetch encountered an error', error); - callback(); + callback(runtimeEids); } }; - if (partnerData.date && partnerData.cttl && partnerData.data && - Date.now() - partnerData.date < partnerData.cttl) { callback(partnerData.data); } else { - rrttStrtTime = Date.now(); - ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); - } + rrttStrtTime = Date.now(); + + partnerData.wsrvcll = true; + storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); }; - return { callback: resp }; + const respObj = {callback: resp}; + + if (runtimeEids?.eids?.length) respObj.id = runtimeEids.eids; + return respObj }, eids: { 'intentIqId': { source: 'intentiq.com', - atype: 1 + atype: 1, + getSource: function (data) { + return data.source; + }, + getValue: function (data) { + if (data?.uids?.length) { + return data.uids[0].id + } + return null + }, + getUidExt: function (data) { + if (data?.uids?.length) { + return data.uids[0].ext; + } + return null + } }, } }; diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md new file mode 100644 index 00000000000..44e4032e23d --- /dev/null +++ b/modules/intentIqIdSystem.md @@ -0,0 +1,73 @@ +``` +Module Name: IntentIQ Id System +Module Type: Id System +Maintainer: julian@intentiq.com, dmytro.piskun@intentiq.com +usp_supported: true +gpp_sids: usnat +``` + +# Intent IQ Universal ID module + +By leveraging the Intent IQ identity graph, our module helps publishers, SSPs, and DSPs overcome the challenges of monetizing cookie-less inventory and preparing for a future without 3rd-party cookies. Our solution implements 1st-party data clustering and provides Intent IQ person IDs with over 90% coverage and unmatched accuracy in supported countries while remaining privacy-friendly and CCPA compliant. This results in increased CPMs, higher fill rates, and, ultimately, lifting overall revenue + +# All you need is a few basic steps to start using our solution. + +## 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 + +## Integration + +``` +gulp build –modules=intentIqIdSystem +``` + +We recommend including the Intent IQ Analytics adapter module for improved visibility + +## Configuration + +### Parameters + +Please find below list of paramters that could be used in configuring Intent IQ Universal ID module + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| ------------------------------ | -------- |----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------| +| name | Required | String | The name of this module: "intentIqId" | `"intentIqId"` | +| params | Required | Object | Details for IntentIqId initialization. | | +| params.partner | Required | Number | This is the partner ID value obtained from registering with IntentIQ. | `1177538` | +| params.pcid | Optional | String | This is the partner cookie ID, it is a dynamic value attached to the request. | `"g3hC52b"` | +| params.pai | Optional | String | This is the partner customer ID / advertiser ID, it is a dynamic value attached to the request. | `"advertiser1"` | +| params.callback | Required | Function | This is a callback which is trigered with data and AB group | `(data, group) => console.log({ data, group })` | +| 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"` | + +### Configuration example + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: "intentIqId", + params: { + partner: 123456, // valid partner id + timeoutInMillis: 500, + browserBlackList: "chrome", + callback: (data, group) => window.pbjs.requestBids(), + manualWinReportEnabled: true, + domainName: "currentDomain.com", + }, + storage: { + type: "html5", + name: "intentIqId", // set localstorage with this name + expires: 0, + refreshInSeconds: 0 + } + }] + } +}); +``` diff --git a/modules/interactiveOffersBidAdapter.js b/modules/interactiveOffersBidAdapter.js index feb576fbb02..bb27239fef4 100644 --- a/modules/interactiveOffersBidAdapter.js +++ b/modules/interactiveOffersBidAdapter.js @@ -1,4 +1,4 @@ -import {isNumber, logWarn} from '../src/utils.js'; +import {deepClone, isNumber, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; @@ -79,8 +79,7 @@ function parseRequestPrebidjsToOpenRTB(prebidRequest, bidderRequest) { // TODO: these should probably look at refererInfo let pageURL = window.location.href; let domain = window.location.hostname; - let secure = (window.location.protocol == 'https:' ? 1 : 0); - let openRTBRequest = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequest'])); + let openRTBRequest = deepClone(DEFAULT['OpenRTBBidRequest']); openRTBRequest.id = bidderRequest.bidderRequestId; openRTBRequest.ext = { // TODO: please do not send internal data structures over the network @@ -89,38 +88,38 @@ function parseRequestPrebidjsToOpenRTB(prebidRequest, bidderRequest) { auctionId: prebidRequest.auctionId }; - openRTBRequest.site = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSite'])); + openRTBRequest.site = deepClone(DEFAULT['OpenRTBBidRequestSite']); openRTBRequest.site.id = domain; openRTBRequest.site.name = domain; openRTBRequest.site.domain = domain; openRTBRequest.site.page = pageURL; openRTBRequest.site.ref = prebidRequest.refererInfo.ref; - openRTBRequest.site.publisher = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSitePublisher'])); + openRTBRequest.site.publisher = deepClone(DEFAULT['OpenRTBBidRequestSitePublisher']); openRTBRequest.site.publisher.id = 0; openRTBRequest.site.publisher.name = prebidRequest.refererInfo.domain; openRTBRequest.site.publisher.domain = domain; openRTBRequest.site.publisher.domain = domain; - openRTBRequest.site.content = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSiteContent'])); + openRTBRequest.site.content = deepClone(DEFAULT['OpenRTBBidRequestSiteContent']); - openRTBRequest.source = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSource'])); + openRTBRequest.source = deepClone(DEFAULT['OpenRTBBidRequestSource']); openRTBRequest.source.fd = 0; openRTBRequest.source.tid = prebidRequest.ortb2?.source?.tid; openRTBRequest.source.pchain = ''; - openRTBRequest.device = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestDevice'])); + openRTBRequest.device = deepClone(DEFAULT['OpenRTBBidRequestDevice']); - openRTBRequest.user = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestUser'])); + openRTBRequest.user = deepClone(DEFAULT['OpenRTBBidRequestUser']); openRTBRequest.imp = []; prebidRequest.bids.forEach(function(bid) { if (!ret.partnerId) { ret.partnerId = bid.params.partnerId; } - let imp = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestImp'])); + let imp = deepClone(DEFAULT['OpenRTBBidRequestImp']); imp.id = bid.bidId; - imp.secure = secure; + imp.secure = bid.ortb2Imp?.secure ?? 1; imp.tagid = bid.adUnitCode; imp.ext = { rawdata: bid @@ -131,7 +130,7 @@ function parseRequestPrebidjsToOpenRTB(prebidRequest, bidderRequest) { Object.keys(bid.mediaTypes).forEach(function(mediaType) { if (mediaType == 'banner') { - imp.banner = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestImpBanner'])); + imp.banner = deepClone(DEFAULT['OpenRTBBidRequestImpBanner']); imp.banner.w = 0; imp.banner.h = 0; imp.banner.format = []; @@ -156,7 +155,7 @@ function parseResponseOpenRTBToPrebidjs(openRTBResponse) { response.seatbid.forEach(function(seatbid) { if (seatbid.bid && seatbid.bid.forEach) { seatbid.bid.forEach(function(bid) { - let prebid = JSON.parse(JSON.stringify(DEFAULT['PrebidBid'])); + let prebid = deepClone(DEFAULT['PrebidBid']); prebid.requestId = bid.impid; prebid.ad = bid.adm; prebid.creativeId = bid.crid; diff --git a/modules/invamiaBidAdapter.js b/modules/invamiaBidAdapter.js index 96af163ca4f..7f9a40e1473 100644 --- a/modules/invamiaBidAdapter.js +++ b/modules/invamiaBidAdapter.js @@ -1,5 +1,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +import { buildBannerRequests, interpretBannerResponse } from '../libraries/biddoInvamiaUtils/index.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -12,86 +13,15 @@ const ENDPOINT_URL = 'https://ad.invamia.com/delivery/impress'; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], - /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bidRequest The bid request params to validate. - * @return boolean True if this is a valid bid request, and false otherwise. - */ - isBidRequestValid: function(bidRequest) { + isBidRequestValid: function (bidRequest) { return !!bidRequest.params.zoneId; }, - /** - * Make a server request from the list of BidRequests. - * - * @param {Array} validBidRequests an array of bid requests - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: function(validBidRequests) { - let serverRequests = []; - - validBidRequests.forEach(bidRequest => { - const sizes = bidRequest.mediaTypes.banner.sizes; - - sizes.forEach(([width, height]) => { - bidRequest.params.requestedSizes = [width, height]; - - const payload = { - ctype: 'div', - pzoneid: bidRequest.params.zoneId, - width, - height, - }; - - const payloadString = Object.keys(payload).map(k => k + '=' + encodeURIComponent(payload[k])).join('&'); - - serverRequests.push({ - method: 'GET', - url: ENDPOINT_URL, - data: payloadString, - bidderRequest: bidRequest, - }); - }); - }); - - return serverRequests; + buildRequests: function (validBidRequests) { + return validBidRequests.flatMap((bidRequest) => buildBannerRequests(bidRequest, ENDPOINT_URL)); }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @param {BidRequest} bidderRequest A matched bid request for this response. - * @return Array An array of bids which were nested inside the server. - */ - interpretResponse: function(serverResponse, {bidderRequest}) { - const response = serverResponse.body; - const bidResponses = []; - - if (response && response.template && response.template.html) { - const {bidId} = bidderRequest; - const [width, height] = bidderRequest.params.requestedSizes; - - const bidResponse = { - requestId: bidId, - cpm: response.hb.cpm, - creativeId: response.banner.hash, - currency: 'USD', - netRevenue: response.hb.netRevenue, - ttl: 600, - ad: response.template.html, - mediaType: 'banner', - meta: { - advertiserDomains: response.hb.adomains || [], - }, - width, - height, - }; - - bidResponses.push(bidResponse); - } - - return bidResponses; + interpretResponse: function (serverResponse, { bidderRequest }) { + return interpretBannerResponse(serverResponse, bidderRequest); }, -} +}; registerBidder(spec); diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 2c37c0edad9..8bb5f5aaac6 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -1,4 +1,4 @@ -import {logInfo} from '../src/utils.js'; +import {getWinDimensions, logInfo} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -14,7 +14,7 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 11, + PREBID_VERSION: 13, METHOD: 'GET', INVIBES_VENDOR_ID: 436, USERID_PROVIDERS: ['pubcid', 'pubProvidedId', 'uid2', 'zeotapIdPlus', 'id5id'], @@ -22,7 +22,7 @@ const CONSTANTS = { DISABLE_USER_SYNC: true }; -const storage = getStorageManager({bidderCode: CONSTANTS.BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: CONSTANTS.BIDDER_CODE}); export const spec = { code: CONSTANTS.BIDDER_CODE, @@ -40,7 +40,7 @@ export const spec = { buildRequests: buildRequest, /** * @param {*} responseObj - * @param {requestParams} bidRequests + * @param {*} requestParams * @return {Bid[]} An array of bids which */ interpretResponse: function (responseObj, requestParams) { @@ -131,7 +131,6 @@ function buildRequest(bidRequests, bidderRequest) { window.invibes.placementIds.push(bidRequest.params.placementId); - _placementIds.push(bidRequest.params.placementId); _placementIds.push(bidRequest.params.placementId); _adUnitCodes.push(bidRequest.adUnitCode); _domainId = _domainId || bidRequest.params.domainId; @@ -141,7 +140,7 @@ function buildRequest(bidRequests, bidderRequest) { _userId = _userId || bidRequest.userId; }); - invibes.optIn = invibes.optIn || readGdprConsent(bidderRequest.gdprConsent); + invibes.optIn = invibes.optIn || readGdprConsent(bidderRequest.gdprConsent, bidderRequest.uspConsent); invibes.visitId = invibes.visitId || generateRandomId(); @@ -167,8 +166,8 @@ function buildRequest(bidRequests, bidderRequest) { pcids: Object.keys(invibes.pushedCids).join(','), vId: invibes.visitId, - width: topWin.innerWidth, - height: topWin.innerHeight, + width: getWinDimensions().innerWidth, + height: getWinDimensions().innerHeight, oi: invibes.optIn, @@ -177,12 +176,22 @@ function buildRequest(bidRequests, bidderRequest) { li: invibes.legitimateInterests.toString(), tc: invibes.gdpr_consent, + uspc: bidderRequest.uspConsent, isLocalStorageEnabled: storage.hasLocalStorage(), preventPageViewEvent: preventPageViewEvent, isPlacementRefresh: isPlacementRefresh, - isInfiniteScrollPage: isInfiniteScrollPage, + isInfiniteScrollPage: isInfiniteScrollPage }; + if (bidderRequest.refererInfo && bidderRequest.refererInfo.ref) { + data.pageReferrer = bidderRequest.refererInfo.ref.substring(0, 300); + } + + let hid = invibes.getCookie('handIid'); + if (hid) { + data.handIid = hid; + } + let lid = readFromLocalStorage('ivbsdid'); if (!lid) { let str = invibes.getCookie('ivbsdid'); @@ -490,7 +499,7 @@ function renderCreative(bidModel) { } function readFromLocalStorage(key) { - if (invibes.GdprModuleInstalled && (!invibes.optIn || !invibes.purposes[0])) { + if ((invibes.GdprModuleInstalled || invibes.UspModuleInstalled) && (!invibes.optIn || !invibes.purposes[0])) { return; } @@ -584,20 +593,15 @@ function buildSyncUrl() { return syncUrl; } -function readGdprConsent(gdprConsent) { +function readGdprConsent(gdprConsent, usConsent) { + invibes.GdprModuleInstalled = false; + invibes.UspModuleInstalled = false; if (gdprConsent && gdprConsent.vendorData) { invibes.GdprModuleInstalled = true; invibes.gdpr_consent = getVendorConsentData(gdprConsent.vendorData); if (!gdprConsent.vendorData.gdprApplies || gdprConsent.vendorData.hasGlobalConsent) { - var index; - for (index = 0; index < invibes.purposes.length; ++index) { - invibes.purposes[index] = true; - } - - for (index = 0; index < invibes.legitimateInterests.length; ++index) { - invibes.legitimateInterests[index] = true; - } + setAllPurposesAndLegitimateInterests(true); return 2; } @@ -627,12 +631,29 @@ function readGdprConsent(gdprConsent) { } return 2; + } else if (usConsent && usConsent.length > 2) { + invibes.UspModuleInstalled = true; + if (usConsent[2] == 'N') { + setAllPurposesAndLegitimateInterests(true); + return 2; + } } - invibes.GdprModuleInstalled = false; + setAllPurposesAndLegitimateInterests(false); return 0; } +function setAllPurposesAndLegitimateInterests(value) { + var index; + for (index = 0; index < invibes.purposes.length; ++index) { + invibes.purposes[index] = value; + } + + for (index = 0; index < invibes.legitimateInterests.length; ++index) { + invibes.legitimateInterests[index] = value; + } +} + function tryCopyValueToArray(value, target, length) { if (value instanceof Array) { for (let i = 0; i < length && i < value.length; i++) { @@ -743,7 +764,7 @@ invibes.getCookie = function (name) { return; } - if (invibes.GdprModuleInstalled && (!invibes.optIn || !invibes.purposes[0])) { + if ((invibes.GdprModuleInstalled || invibes.UspModuleInstalled) && (!invibes.optIn || !invibes.purposes[0])) { return; } diff --git a/modules/invisiblyAnalyticsAdapter.js b/modules/invisiblyAnalyticsAdapter.js index 24c2c452402..a2305cc5154 100644 --- a/modules/invisiblyAnalyticsAdapter.js +++ b/modules/invisiblyAnalyticsAdapter.js @@ -5,8 +5,9 @@ import { ajaxBuilder } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import { generateUUID, logInfo } from '../src/utils.js'; +import { deepClone, hasNonSerializableProperty, generateUUID, logInfo } from '../src/utils.js'; import { EVENTS } from '../src/constants.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; const DEFAULT_EVENT_URL = 'https://api.pymx5.com/v1/' + 'sites/events'; const analyticsType = 'endpoint'; @@ -38,12 +39,7 @@ let _bidRequestTimeout = 0; let flushInterval; let invisiblyAnalyticsEnabled = false; -const w = window; -const d = document; -let e = d.documentElement; -let g = d.getElementsByTagName('body')[0]; -let x = w.innerWidth || e.clientWidth || g.clientWidth; -let y = w.innerHeight || e.clientHeight || g.clientHeight; +const { width: x, height: y } = getViewportSize(); let _pageView = { eventType: 'pageView', @@ -133,7 +129,12 @@ function flush() { } function handleEvent(eventType, eventArgs) { - eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {}; + if (eventArgs) { + eventArgs = hasNonSerializableProperty(eventArgs) ? eventArgs : deepClone(eventArgs) + } else { + eventArgs = {} + } + let invisiblyEvent = {}; switch (eventType) { diff --git a/modules/iqmBidAdapter.js b/modules/iqmBidAdapter.js deleted file mode 100644 index c94a88748a7..00000000000 --- a/modules/iqmBidAdapter.js +++ /dev/null @@ -1,277 +0,0 @@ -import {_each, deepAccess, getBidIdParameter, isArray} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {INSTREAM} from '../src/video.js'; - -/** - * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest - */ - -const BIDDER_CODE = 'iqm'; -const VERSION = 'v.1.0.0'; -const VIDEO_ORTB_PARAMS = [ - 'mimes', - 'minduration', - 'maxduration', - 'placement', - 'protocols', - 'startdelay' -]; -var ENDPOINT_URL = 'https://pbd.bids.iqm.com'; - -export const spec = { - supportedMediaTypes: [BANNER, VIDEO], - code: BIDDER_CODE, - aliases: ['iqm'], - - /** - * 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. - */ - isBidRequestValid: function (bid) { - const banner = deepAccess(bid, 'mediaTypes.banner'); - const videoMediaType = deepAccess(bid, 'mediaTypes.video'); - const context = deepAccess(bid, 'mediaTypes.video.context'); - if ((videoMediaType && context === INSTREAM)) { - const videoBidderParams = deepAccess(bid, 'params.video', {}); - - if (!Array.isArray(videoMediaType.playerSize)) { - return false; - } - - if (!videoMediaType.context) { - return false; - } - - const videoParams = { - ...videoMediaType, - ...videoBidderParams - }; - - if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { - return false; - } - - if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { - return false; - } - - if ( - typeof videoParams.placement !== 'undefined' && - typeof videoParams.placement !== 'number' - ) { - return false; - } - if ( - videoMediaType.context === INSTREAM && - typeof videoParams.startdelay !== 'undefined' && - typeof videoParams.startdelay !== 'number' - ) { - return false; - } - - return !!(bid && bid.params && bid.params.publisherId && bid.params.placementId); - } else { - if (banner === 'undefined') { - return false; - } - return !!(bid && bid.params && bid.params.publisherId && bid.params.placementId); - } - }, - /** - * Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test. - *It prepares a bid request with the required information for the DSP side and sends this request to alloted endpoint - * parameter{validBidRequests, bidderRequest} bidderRequest object is useful because it carries a couple of bid parameters that are global to all the bids. - */ - buildRequests: function (validBidRequests, bidderRequest) { - return validBidRequests.map(bid => { - var finalRequest = {}; - let bidfloor = getBidIdParameter('bidfloor', bid.params); - - const imp = { - id: bid.bidId, - secure: 1, - bidfloor: bidfloor || 0, - displaymanager: 'Prebid.js', - displaymanagerver: VERSION, - - } - if (deepAccess(bid, 'mediaTypes.banner')) { - imp.banner = getSize(bid.sizes); - imp.mediatype = 'banner'; - } else if (deepAccess(bid, 'mediaTypes.video')) { - imp.video = _buildVideoORTB(bid); - imp.mediatype = 'video'; - } - const site = getSite(bidderRequest); - let device = getDevice(bid.params); - finalRequest = { - sizes: bid.sizes, - id: bid.bidId, - publisherId: getBidIdParameter('publisherId', bid.params), - placementId: getBidIdParameter('placementId', bid.params), - device: device, - site: site, - imp: imp, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, - adUnitCode: bid.adUnitCode, - bidderRequestId: bid.bidderRequestId, - uuid: bid.bidId, - // TODO: please do not send internal data structures over the network - // I am not going to attempt to accommodate this, no way this is usable on their end, it changes way too frequently - bidderRequest - } - const request = { - method: 'POST', - url: ENDPOINT_URL, - data: finalRequest, - options: { - withCredentials: false - }, - - } - return request; - }); - }, - /** - * Takes Response from server as input and request. - *It parses the response from server side and generates bidresponses for with required rendering paramteres - * parameter{serverResponse, bidRequest} serverReponse: Response from the server side with ad creative. - */ - interpretResponse: function (serverResponse, bidRequest) { - const bidResponses = []; - serverResponse = serverResponse.body; - if (serverResponse && isArray(serverResponse.seatbid)) { - _each(serverResponse.seatbid, function (bidList) { - _each(bidList.bid, function (bid) { - const responseCPM = parseFloat(bid.price); - if (responseCPM > 0.0 && bid.impid) { - const bidResponse = { - requestId: bidRequest.data.id, - currency: serverResponse.cur || 'USD', - cpm: responseCPM, - netRevenue: true, - creativeId: bid.crid || '', - adUnitCode: bidRequest.data.adUnitCode, - auctionId: bidRequest.data.auctionId, - mediaType: bidRequest.data.imp.mediatype, - - ttl: bid.ttl || 60 - }; - - if (bidRequest.data.imp.mediatype === VIDEO) { - bidResponse.width = bid.w || bidRequest.data.imp.video.w; - bidResponse.height = bid.h || bidRequest.data.imp.video.h; - bidResponse.adResponse = { - content: bid.adm, - height: bidRequest.data.imp.video.h, - width: bidRequest.data.imp.video.w - }; - - if (bidRequest.data.imp.video.context === INSTREAM) { - bidResponse.vastUrl = bid.adm; - } - } else if (bidRequest.data.imp.mediatype === BANNER) { - bidResponse.ad = bid.adm; - bidResponse.width = bid.w || bidRequest.data.imp.banner.w; - bidResponse.height = bid.h || bidRequest.data.imp.banner.h; - } - bidResponses.push(bidResponse); - } - }) - }); - } - return bidResponses; - }, - -}; - -let getDevice = function (bidparams) { - const language = navigator.language ? 'language' : 'userLanguage'; - return { - geo: bidparams.geo, - h: screen.height, - w: screen.width, - dnt: _getDNT() ? 1 : 0, - language: navigator[language].split('-')[0], - make: navigator.vendor ? navigator.vendor : '', - ua: navigator.userAgent, - devicetype: _isMobile() ? 1 : _isConnectedTV() ? 3 : 2 - }; -}; - -let _getDNT = function () { - return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1' || navigator.doNotTrack === 'yes'; -}; - -let getSize = function (sizes) { - let sizeMap; - if (sizes.length === 2 && typeof sizes[0] === 'number' && typeof sizes[1] === 'number') { - sizeMap = {w: sizes[0], h: sizes[1]}; - } else { - sizeMap = {w: sizes[0][0], h: sizes[0][1]}; - } - return sizeMap; -}; - -function _isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(global.navigator.userAgent); -} - -function _isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(global.navigator.userAgent); -} - -function getSite(bidderRequest) { - let domain = ''; - let page = ''; - let referrer = ''; - const Id = 1; - - const {refererInfo} = bidderRequest; - - // TODO: are these the right refererInfo values? - domain = refererInfo.domain; - page = refererInfo.page; - referrer = refererInfo.ref; - - return { - domain, - page, - Id, - referrer - }; -}; - -function _buildVideoORTB(bidRequest) { - const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video'); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - const video = {}; - - const videoParams = { - ...videoAdUnit, - ...videoBidderParams // Bidder Specific overrides - }; - video.context = 1; - const {w, h} = getSize(videoParams.playerSize[0]); - video.w = w; - video.h = h; - - VIDEO_ORTB_PARAMS.forEach((param) => { - if (videoParams.hasOwnProperty(param)) { - video[param] = videoParams[param]; - } - }); - - video.placement = video.placement || 2; - - video.startdelay = video.startdelay || 0; - video.placement = 1; - video.context = INSTREAM; - - return video; -} -registerBidder(spec); diff --git a/modules/iqxBidAdapter.js b/modules/iqxBidAdapter.js index 1bef158c4a2..e859dfd2c01 100644 --- a/modules/iqxBidAdapter.js +++ b/modules/iqxBidAdapter.js @@ -1,205 +1,16 @@ -import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; -import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {buildRequests, getUserSyncs, interpretResponse, isBidRequestValid} from '../libraries/xeUtils/bidderUtils.js'; -const CUR = 'USD'; const BIDDER_CODE = 'iqx'; const ENDPOINT = 'https://pbjs.iqzonertb.live'; -/** - * 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(req) { - if (req && typeof req.params !== 'object') { - logError('Params is not defined or is incorrect in the bidder settings'); - return false; - } - - if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { - logError('Env or pid is not present in bidder params'); - return false; - } - - if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { - logError('mediaTypes.video.playerSize is required for video'); - return false; - } - - return true; -} - -/** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ -function buildRequests(validBidRequests, bidderRequest) { - const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; - const requests = validBidRequests.map(req => { - const request = {}; - request.bidId = req.bidId; - request.banner = deepAccess(req, 'mediaTypes.banner'); - request.auctionId = req.ortb2?.source?.tid; - request.transactionId = req.ortb2Imp?.ext?.tid; - request.sizes = parseSizesInput(getAdUnitSizes(req)); - request.schain = req.schain; - request.location = { - page: refererInfo.page, - location: refererInfo.location, - domain: refererInfo.domain, - whost: window.location.host, - ref: refererInfo.ref, - isAmp: refererInfo.isAmp - }; - request.device = { - ua: navigator.userAgent, - lang: navigator.language - }; - request.env = { - env: req.params.env, - pid: req.params.pid - }; - request.ortb2 = req.ortb2; - request.ortb2Imp = req.ortb2Imp; - request.tz = new Date().getTimezoneOffset(); - request.ext = req.params.ext; - request.bc = req.bidRequestsCount; - request.floor = getBidFloor(req); - - if (req.userIdAsEids && req.userIdAsEids.length !== 0) { - request.userEids = req.userIdAsEids; - } else { - request.userEids = []; - } - if (gdprConsent.gdprApplies) { - request.gdprApplies = Number(gdprConsent.gdprApplies); - request.consentString = gdprConsent.consentString; - } else { - request.gdprApplies = 0; - request.consentString = ''; - } - if (uspConsent) { - request.usPrivacy = uspConsent; - } else { - request.usPrivacy = ''; - } - if (config.getConfig('coppa')) { - request.coppa = 1; - } else { - request.coppa = 0; - } - - const video = deepAccess(req, 'mediaTypes.video'); - if (video) { - request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); - request.video = video; - } - - return request; - }); - - return { - method: 'POST', - url: ENDPOINT + '/bid', - data: JSON.stringify(requests), - withCredentials: true, - bidderRequest, - options: { - contentType: 'application/json', - } - }; -} - -/** - * 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, {bidderRequest}) { - const response = []; - if (!isArray(deepAccess(serverResponse, 'body.data'))) { - return response; - } - - serverResponse.body.data.forEach(serverBid => { - const bid = { - requestId: bidderRequest.bidId, - dealId: bidderRequest.dealId || null, - ...serverBid - }; - response.push(bid); - }); - - return response; -} - -/** - * 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. - */ -function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { - const syncs = []; - const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); - - if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { - const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; - const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; - const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; - - pixels.forEach(pixel => { - const [type, url] = pixel; - const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; - if (type === 'iframe' && syncOptions.iframeEnabled) { - syncs.push(sync) - } else if (type === 'image' && syncOptions.pixelEnabled) { - syncs.push(sync) - } - }); - } - - return syncs; -} - -/** - * Get valid floor value from getFloor fuction. - * - * @param {Object} bid Current bid request. - * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. - */ -export function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return null; - } - - let floor = bid.getFloor({ - currency: CUR, - mediaType: '*', - size: '*' - }); - - if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { - return floor.floor; - } - - return null; -} - export const spec = { code: BIDDER_CODE, aliases: ['iqx'], supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, - buildRequests, + buildRequests: (validBidRequests, bidderRequest) => buildRequests(validBidRequests, bidderRequest, ENDPOINT), interpretResponse, getUserSyncs } diff --git a/modules/iqzoneBidAdapter.js b/modules/iqzoneBidAdapter.js index 52f3be7e4b4..9603a509ac5 100644 --- a/modules/iqzoneBidAdapter.js +++ b/modules/iqzoneBidAdapter.js @@ -1,211 +1,19 @@ -import { isFn, deepAccess, logMessage } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'iqzone'; const AD_URL = 'https://smartssp-us-east.iqzone.com/pbjs'; -const SYNC_URL = 'https://cs.smartssp.iqzone.com'; - -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} +const SYNC_URL = 'https://cs.iqzone.com'; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 248dbea0046..a96b0da132b 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -18,18 +18,14 @@ import { } from '../src/utils.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; -import { EVENTS } from '../src/constants.js'; import { getStorageManager } from '../src/storageManager.js'; -import * as events from '../src/events.js'; import { find } from '../src/polyfill.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { INSTREAM, OUTSTREAM } from '../src/video.js'; import { Renderer } from '../src/Renderer.js'; import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'ix'; -const ALIAS_BIDDER_CODE = 'roundel'; const GLOBAL_VENDOR_ID = 10; const SECURE_BID_URL = 'https://htlb.casalemedia.com/openrtb/pbjs'; const SUPPORTED_AD_TYPES = [BANNER, VIDEO, NATIVE]; @@ -49,17 +45,6 @@ const PRICE_TO_DOLLAR_FACTOR = { const IFRAME_USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; const FLOOR_SOURCE = { PBJS: 'p', IX: 'x' }; const IMG_USER_SYNC_URL = 'https://dsum.casalemedia.com/pbusermatch?origin=prebid'; -export const ERROR_CODES = { - BID_SIZE_INVALID_FORMAT: 1, - BID_SIZE_NOT_INCLUDED: 2, - PROPERTY_NOT_INCLUDED: 3, - SITE_ID_INVALID_VALUE: 4, - BID_FLOOR_INVALID_FORMAT: 5, - IX_FPD_EXCEEDS_MAX_SIZE: 6, - EXCEEDS_MAX_SIZE: 7, - PB_FPD_EXCEEDS_MAX_SIZE: 8, - VIDEO_DURATION_INVALID: 9 -}; const FIRST_PARTY_DATA = { SITE: [ 'id', 'name', 'domain', 'cat', 'sectioncat', 'pagecat', 'page', 'ref', 'search', 'mobile', @@ -73,22 +58,9 @@ const SOURCE_RTI_MAPPING = { 'neustar.biz': 'fabrickId', 'zeotap.com': 'zeotapIdPlus', 'uidapi.com': 'UID2', - 'adserver.org': 'TDID', - 'id5-sync.com': '', // ID5 Universal ID, configured as id5Id - 'crwdcntrl.net': '', // Lotame Panorama ID, lotamePanoramaId - 'epsilon.com': '', // Publisher Link, publinkId - 'audigent.com': '', // Hadron ID from Audigent, hadronId - 'pubcid.org': '', // SharedID, pubcid - 'utiq.com': '', // Utiq - 'criteo.com': '', // Criteo - 'euid.eu': '', // EUID - 'intimatemerger.com': '', - '33across.com': '', - 'liveintent.indexexchange.com': '', - 'google.com': '' + 'adserver.org': 'TDID' }; const PROVIDERS = [ - 'britepoolid', 'lipbid', 'criteoId', 'merkleId', @@ -110,7 +82,6 @@ const VIDEO_PARAMS_ALLOW_LIST = [ ]; const LOCAL_STORAGE_KEY = 'ixdiag'; export const LOCAL_STORAGE_FEATURE_TOGGLES_KEY = `${BIDDER_CODE}_features`; -let hasRegisteredHandler = false; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const FEATURE_TOGGLES = { // Update with list of CFTs to be requested from Exchange @@ -262,8 +233,7 @@ export function bidToVideoImp(bid) { if (imp.video.minduration > imp.video.maxduration) { logError( - `IX Bid Adapter: video minduration [${imp.video.minduration}] cannot be greater than video maxduration [${imp.video.maxduration}]`, - { bidder: BIDDER_CODE, code: ERROR_CODES.VIDEO_DURATION_INVALID } + `IX Bid Adapter: video minduration [${imp.video.minduration}] cannot be greater than video maxduration [${imp.video.maxduration}]` ); return {}; } @@ -526,6 +496,11 @@ function parseBid(rawBid, currency, bidRequest) { if (rawBid.ext?.dsa) { bid.meta.dsa = rawBid.ext.dsa } + + if (rawBid.ext?.ibv) { + bid.ext = bid.ext || {} + bid.ext.ibv = rawBid.ext.ibv + } return bid; } @@ -666,10 +641,9 @@ function getEidInfo(allEids) { if (isArray(allEids)) { for (const eid of allEids) { const isSourceMapped = SOURCE_RTI_MAPPING.hasOwnProperty(eid.source); - const allowAllEidsFeatureEnabled = FEATURE_TOGGLES.isFeatureEnabled('pbjs_allow_all_eids'); const hasUids = deepAccess(eid, 'uids.0'); - if ((isSourceMapped || allowAllEidsFeatureEnabled) && hasUids) { + if (hasUids) { seenSources[eid.source] = true; if (isSourceMapped && SOURCE_RTI_MAPPING[eid.source] !== '') { @@ -677,7 +651,6 @@ function getEidInfo(allEids) { rtiPartner: SOURCE_RTI_MAPPING[eid.source] }; } - delete eid.uids[0].atype; toSend.push(eid); if (toSend.length >= MAX_EID_SOURCES) { break; @@ -712,11 +685,6 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { addRTI(userEids, eidInfo); } - // If `roundel` alias bidder, only send requests if liveramp ids exist. - if (bidderRequest && bidderRequest.bidderCode === ALIAS_BIDDER_CODE && !eidInfo.seenSources['liveramp.com']) { - return []; - } - const requests = []; let r = createRequest(validBidRequests); @@ -724,7 +692,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = addRequestedFeatureToggles(r, FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES) // getting ixdiags for adunits of the video, outstream & multi format (MF) style - const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + const fledgeEnabled = deepAccess(bidderRequest, 'paapi.enabled') let ixdiag = buildIXDiag(validBidRequests, fledgeEnabled); for (let key in ixdiag) { r.ext.ixdiag[key] = ixdiag[key]; @@ -788,8 +756,9 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { method: 'POST', url: exchangeUrl, data: deepClone(r), - option: { + options: { contentType: 'text/plain', + withCredentials: true }, validBidRequests }); @@ -883,13 +852,6 @@ function enrichRequest(r, bidderRequest, impressions, validBidRequests, userEids r.ext.ixdiag.syncsPerBidder = config.getConfig('userSync').syncsPerBidder; } - // Get cached errors stored in LocalStorage - const cachedErrors = getCachedErrors(); - - if (!isEmpty(cachedErrors)) { - r.ext.ixdiag.err = cachedErrors; - } - // Add number of available imps to ixDiag. r.ext.ixdiag.imps = Object.keys(impressions).length; @@ -997,6 +959,7 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { const tid = impressions[impKeys[adUnitIndex]].tid; const sid = impressions[impKeys[adUnitIndex]].sid; const auctionEnvironment = impressions[impKeys[adUnitIndex]].ae; + const paapi = impressions[impKeys[adUnitIndex]].paapi; const bannerImpressions = impressionObjects.filter(impression => BANNER in impression); const otherImpressions = impressionObjects.filter(impression => !(BANNER in impression)); @@ -1046,7 +1009,7 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { _bannerImpression.banner.pos = position; } - if (dfpAdUnitCode || gpid || tid || sid || auctionEnvironment || externalID) { + if (dfpAdUnitCode || gpid || tid || sid || auctionEnvironment || externalID || paapi) { _bannerImpression.ext = {}; _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; @@ -1058,6 +1021,7 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { // enable fledge auction if (auctionEnvironment == 1) { _bannerImpression.ext.ae = 1; + _bannerImpression.ext.paapi = paapi; } } @@ -1460,17 +1424,19 @@ function createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidde bannerImps[validBidRequest.adUnitCode].pos = deepAccess(validBidRequest, 'mediaTypes.banner.pos'); // Add Fledge flag if enabled - const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + const fledgeEnabled = deepAccess(bidderRequest, 'paapi.enabled') if (fledgeEnabled) { const auctionEnvironment = deepAccess(validBidRequest, 'ortb2Imp.ext.ae') + const paapi = deepAccess(validBidRequest, 'ortb2Imp.ext.paapi') + if (paapi) { + bannerImps[validBidRequest.adUnitCode].paapi = paapi + } if (auctionEnvironment) { if (isInteger(auctionEnvironment)) { bannerImps[validBidRequest.adUnitCode].ae = auctionEnvironment; } else { logWarn('error setting auction environment flag - must be an integer') } - } else if (deepAccess(bidderRequest, 'defaultForSlots') == 1) { - bannerImps[validBidRequest.adUnitCode].ae = 1 } } @@ -1546,104 +1512,6 @@ function createMissingBannerImp(bid, imp, newSize) { return newImp; } -/** - * @typedef {Array[message: string, err: Object]} ErrorData - * @property {string} message - The error message. - * @property {object} err - The error object. - * @property {string} err.bidder - The bidder of the error. - * @property {string} err.code - The error code. - */ - -/** - * Error Event handler that receives type and arguments in a data object. - * - * @param {ErrorData} data - */ -function storeErrorEventData(data) { - if (!storage.localStorageIsEnabled()) { - return; - } - - let currentStorage; - - try { - currentStorage = JSON.parse(storage.getDataFromLocalStorage(LOCAL_STORAGE_KEY) || '{}'); - } catch (e) { - logWarn('ix can not read ixdiag from localStorage.'); - } - - const todayDate = new Date(); - - Object.keys(currentStorage).map((errorDate) => { - const date = new Date(errorDate); - - if (date.setDate(date.getDate() + 7) - todayDate < 0) { - delete currentStorage[errorDate]; - } - }); - - if (data.type === 'ERROR' && data.arguments && data.arguments[1] && data.arguments[1].bidder === BIDDER_CODE) { - const todayString = todayDate.toISOString().slice(0, 10); - - const errorCode = data.arguments[1].code; - - if (errorCode) { - currentStorage[todayString] = currentStorage[todayString] || {}; - - if (!Number(currentStorage[todayString][errorCode])) { - currentStorage[todayString][errorCode] = 0; - } - - currentStorage[todayString][errorCode]++; - }; - } - - storage.setDataInLocalStorage(LOCAL_STORAGE_KEY, JSON.stringify(currentStorage)); -} - -/** - * Event handler for storing data into local storage. It will only store data if - * local storage premissions are avaliable - */ -function localStorageHandler(data) { - if (data.type === 'ERROR' && data.arguments && data.arguments[1] && data.arguments[1].bidder === BIDDER_CODE) { - storeErrorEventData(data); - } -} - -/** - * Get ixdiag stored in LocalStorage and format to be added to request payload - * - * @returns {Object} Object with error codes and counts - */ -function getCachedErrors() { - if (!storage.localStorageIsEnabled()) { - return; - } - - const errors = {}; - let currentStorage; - - try { - currentStorage = JSON.parse(storage.getDataFromLocalStorage(LOCAL_STORAGE_KEY) || '{}'); - } catch (e) { - logError('ix can not read ixdiag from localStorage.'); - return null; - } - - Object.keys(currentStorage).forEach((date) => { - Object.keys(currentStorage[date]).forEach((code) => { - if (typeof currentStorage[date][code] === 'number') { - errors[code] = errors[code] - ? errors[code] + currentStorage[date][code] - : currentStorage[date][code]; - } - }); - }); - - return errors; -} - /** * * Initialize IX Outstream Renderer @@ -1724,11 +1592,6 @@ export const spec = { code: BIDDER_CODE, gvlid: GLOBAL_VENDOR_ID, - aliases: [{ - code: ALIAS_BIDDER_CODE, - gvlid: GLOBAL_VENDOR_ID, - skipPbsAliasing: false - }], supportedMediaTypes: SUPPORTED_AD_TYPES, /** @@ -1738,12 +1601,6 @@ export const spec = { * @return {boolean} True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - if (!hasRegisteredHandler) { - events.on(EVENTS.AUCTION_DEBUG, localStorageHandler); - events.on(EVENTS.AD_RENDER_FAILED, localStorageHandler); - hasRegisteredHandler = true; - } - const paramsVideoRef = deepAccess(bid, 'params.video'); const paramsSize = deepAccess(bid, 'params.size'); const mediaTypeBannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); @@ -1765,14 +1622,14 @@ export const spec = { // since there is an ix bidder level size, make sure its valid const ixSize = getFirstSize(paramsSize); if (!ixSize) { - logError('IX Bid Adapter: size has invalid format.', { bidder: BIDDER_CODE, code: ERROR_CODES.BID_SIZE_INVALID_FORMAT }); + logError('IX Bid Adapter: size has invalid format.'); return false; } // check if the ix bidder level size, is present in ad unit level if (!includesSize(bid.sizes, ixSize) && !(includesSize(mediaTypeVideoPlayerSize, ixSize)) && !(includesSize(mediaTypeBannerSizes, ixSize))) { - logError('IX Bid Adapter: bid size is not included in ad unit sizes or player size.', { bidder: BIDDER_CODE, code: ERROR_CODES.BID_SIZE_NOT_INCLUDED }); + logError('IX Bid Adapter: bid size is not included in ad unit sizes or player size.'); return false; } } @@ -1784,19 +1641,19 @@ export const spec = { if (bid.params.siteId !== undefined) { if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { - logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + logError('IX Bid Adapter: siteId must be string or number type.'); return false; } if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { - logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + logError('IX Bid Adapter: siteId must valid value'); return false; } } if (hasBidFloor || hasBidFloorCur) { if (!(hasBidFloor && hasBidFloorCur && isValidBidFloorParams(bid.params.bidFloor, bid.params.bidFloorCur))) { - logError('IX Bid Adapter: bidFloor / bidFloorCur parameter has invalid format.', { bidder: BIDDER_CODE, code: ERROR_CODES.BID_FLOOR_INVALID_FORMAT }); + logError('IX Bid Adapter: bidFloor / bidFloorCur parameter has invalid format.'); return false; } } @@ -1815,7 +1672,7 @@ export const spec = { if (errorList.length) { errorList.forEach((err) => { - logError(err, { bidder: BIDDER_CODE, code: ERROR_CODES.PROPERTY_NOT_INCLUDED }); + logError(err); }); return false; } @@ -1986,7 +1843,7 @@ export const spec = { try { return { bids, - fledgeAuctionConfigs, + paapi: fledgeAuctionConfigs, }; } catch (error) { logWarn('Error attaching AuctionConfigs', error); @@ -1997,18 +1854,6 @@ export const spec = { } }, - /** - * Covert bid param types for S2S - * @param {Object} params bid params - * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol - * @return {Object} params bid params - */ - transformBidParams: function (params, isOpenRtb) { - return convertTypes({ - 'siteID': 'number' - }, params); - }, - /** * Determine which user syncs should occur * @param {object} syncOptions diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 0705c5932cf..f2f6d97daf9 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -472,7 +472,7 @@ The timeout value must be a positive whole number in milliseconds. Protected Audience API (FLEDGE) =========================== -In order to enable receiving [Protected Audience API](https://developer.chrome.com/en/docs/privacy-sandbox/fledge/) traffic, follow Prebid's documentation on [fledgeForGpt](https://docs.prebid.org/dev-docs/modules/fledgeForGpt.html) module to build and enable Fledge. +In order to enable receiving [Protected Audience API](https://developer.chrome.com/en/docs/privacy-sandbox/fledge/) traffic, follow Prebid's documentation on [paapiForGpt](https://docs.prebid.org/dev-docs/modules/paapiForGpt.html) module to build and enable Fledge. Additional Information ====================== diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index 1e07ce6b5d8..420f3434da7 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, getDNT, isArray, logWarn, isFn, isPlainObject, logError, logInfo} from '../src/utils.js'; +import {deepAccess, getDNT, isArray, logWarn, isFn, isPlainObject, logError, logInfo, getWinDimensions} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -104,8 +104,8 @@ function fetchIds_(cfg) { // Now changed to an object. yes the backend is able to handle it. function getDevice_() { const device = config.getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; + device.w = device.w || getWinDimensions().innerWidth; + device.h = device.h || getWinDimensions().innerHeight; device.ua = device.ua || navigator.userAgent; device.dnt = getDNT() ? 1 : 0; device.language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; diff --git a/modules/justIdSystem.js b/modules/justIdSystem.js index a5698023020..c71654a845b 100644 --- a/modules/justIdSystem.js +++ b/modules/justIdSystem.js @@ -9,6 +9,7 @@ import * as utils from '../src/utils.js' import { submodule } from '../src/hook.js' import { loadExternalScript } from '../src/adloader.js' import {includes} from '../src/polyfill.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -79,7 +80,7 @@ export const justIdSubmodule = { utils.logInfo(LOG_PREFIX, 'fetching uid...'); var uidProvider = configWrapper.isCombinedMode() - ? new CombinedUidProvider(configWrapper, consentData, cacheIdObj) + ? new CombinedUidProvider(configWrapper, consentData?.gdpr, cacheIdObj) : new BasicUidProvider(configWrapper); uidProvider.getUid(justId => { @@ -154,7 +155,7 @@ const CombinedUidProvider = function(configWrapper, consentData, cacheIdObj) { const url = configWrapper.getUrl(); this.getUid = function(idCallback, errCallback) { - const scriptTag = loadExternalScript(url, EXTERNAL_SCRIPT_MODULE_CODE, () => { + const scriptTag = loadExternalScript(url, MODULE_TYPE_UID, EXTERNAL_SCRIPT_MODULE_CODE, () => { utils.logInfo(LOG_PREFIX, 'script loaded', url); const eventDetails = { diff --git a/modules/justpremiumBidAdapter.js b/modules/justpremiumBidAdapter.js index 7f154614e4d..2ed2d544a34 100644 --- a/modules/justpremiumBidAdapter.js +++ b/modules/justpremiumBidAdapter.js @@ -1,5 +1,5 @@ import { registerBidder } from '../src/adapters/bidderFactory.js' -import { deepAccess } from '../src/utils.js'; +import { deepAccess, getWinDimensions } from '../src/utils.js'; const BIDDER_CODE = 'justpremium' const GVLID = 62 @@ -17,7 +17,9 @@ export const spec = { buildRequests: (validBidRequests, bidderRequest) => { const c = preparePubCond(validBidRequests) - const dim = getWebsiteDim() + const { + screen + } = getWinDimensions(); const ggExt = getGumGumParams() const payload = { zone: validBidRequests.map(b => { @@ -27,10 +29,10 @@ export const spec = { }), // TODO: is 'page' the right value here? referer: bidderRequest.refererInfo.page, - sw: dim.screenWidth, - sh: dim.screenHeight, - ww: dim.innerWidth, - wh: dim.innerHeight, + sw: screen.width, + sh: screen.height, + ww: getWinDimensions().innerWidth, + wh: getWinDimensions().innerHeight, c: c, id: validBidRequests[0].params.zone, sizes: {}, @@ -245,22 +247,6 @@ function arrayUnique (array) { return a } -function getWebsiteDim () { - let top - try { - top = window.top - } catch (e) { - top = window - } - - return { - screenWidth: top.screen.width, - screenHeight: top.screen.height, - innerWidth: top.innerWidth, - innerHeight: top.innerHeight - } -} - function getGumGumParams () { if (!window.top) return null diff --git a/modules/jwplayerBidAdapter.js b/modules/jwplayerBidAdapter.js index 151d08bf3a6..c58eed8ffb8 100644 --- a/modules/jwplayerBidAdapter.js +++ b/modules/jwplayerBidAdapter.js @@ -2,7 +2,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; import { isArray, isFn, deepAccess, deepSetValue, getDNT, logError, logWarn } from '../src/utils.js'; import { config } from '../src/config.js'; -import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { hasPurpose1Consent } from '../src/utils/gdpr.js'; const BIDDER_CODE = 'jwplayer'; const BASE_URL = 'https://vpb-server.jwplayer.com/'; diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 29ce0da5317..78c26cbda6a 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -12,7 +12,7 @@ import {submodule} from '../src/hook.js'; import {config} from '../src/config.js'; import {ajaxBuilder} from '../src/ajax.js'; -import {deepAccess, logError} from '../src/utils.js'; +import { deepAccess, logError, logWarn } from '../src/utils.js' import {find} from '../src/polyfill.js'; import {getGlobal} from '../src/prebidGlobal.js'; @@ -31,9 +31,7 @@ const playlistItemCache = {}; const pendingRequests = {}; let activeRequestCount = 0; let resumeBidRequest; -// defaults to 'always' for backwards compatibility -// TODO: Prebid 9 - replace with ENRICH_WHEN_EMPTY -let overrideContentId = ENRICH_ALWAYS; +let overrideContentId = ENRICH_WHEN_EMPTY; let overrideContentUrl = ENRICH_WHEN_EMPTY; let overrideContentTitle = ENRICH_WHEN_EMPTY; let overrideContentDescription = ENRICH_WHEN_EMPTY; @@ -83,9 +81,7 @@ export function fetchTargetingInformation(jwTargeting) { } export function setOverrides(params) { - // For backwards compatibility, default to always unless overridden by Publisher. - // TODO: Prebid 9 - replace with ENRICH_WHEN_EMPTY - overrideContentId = sanitizeOverrideParam(params.overrideContentId, ENRICH_ALWAYS); + overrideContentId = sanitizeOverrideParam(params.overrideContentId, ENRICH_WHEN_EMPTY); overrideContentUrl = sanitizeOverrideParam(params.overrideContentUrl, ENRICH_WHEN_EMPTY); overrideContentTitle = sanitizeOverrideParam(params.overrideContentTitle, ENRICH_WHEN_EMPTY); overrideContentDescription = sanitizeOverrideParam(params.overrideContentDescription, ENRICH_WHEN_EMPTY); @@ -433,17 +429,37 @@ export function addTargetingToBid(bid, targeting) { bid.rtd = Object.assign({}, rtd, jwRtd); } -function getPlayer(playerDivId) { +export function getPlayer(playerDivId) { const jwplayer = window.jwplayer; if (!jwplayer) { logError(SUBMODULE_NAME + '.js was not found on page'); return; } - const player = jwplayer(playerDivId); - if (!player || !player.getPlaylist) { - logError('player ID did not match any players'); + let player = jwplayer(playerDivId); + if (player && player.getPlaylist) { + return player; + } + + const playerOnPageCount = document.getElementsByClassName('jwplayer').length; + if (playerOnPageCount === 0) { + logError('No JWPlayer instances have been detected on the page'); return; } - return player; + + let errorMessage = `player Div ID ${playerDivId} did not match any players.`; + + // If there are multiple instances on the page, we cannot guess which one should be targeted. + if (playerOnPageCount > 1) { + logError(errorMessage); + return; + } + + player = jwplayer(); + if (player && player.getPlaylist) { + logWarn(`${errorMessage} Targeting player Div ID ${player.id} instead`); + return player; + } + + logError(errorMessage); } diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md index 936cd1d10a2..44d696eea6d 100644 --- a/modules/jwplayerRtdProvider.md +++ b/modules/jwplayerRtdProvider.md @@ -12,16 +12,20 @@ Publishers must register JW Player as a real time data provider by setting up a following structure: ```javascript -const jwplayerDataProvider = { - name: "jwplayer" -}; - pbjs.setConfig({ ..., realTimeData: { - dataProviders: [ - jwplayerDataProvider - ] + dataProviders: [{ + name: 'jwplayer', + waitForIt: true, + params: { + mediaIDs: ['abc', 'def', 'ghi', 'jkl'], + overrideContentId: 'always', + overrideContentUrl: 'always', + overrideContentTitle: 'always', + overrideContentDescription: 'always' + } + }] } }); ``` @@ -86,7 +90,7 @@ realTimeData = { | waitForIt | Boolean | Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false | | params | Object | | | | params.mediaIDs | Array of Strings | Media Ids for prefetching | Optional | -| params.overrideContentId | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.id | Defaults to 'always' | +| params.overrideContentId | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.id | Defaults to 'whenEmpty' | | params.overrideContentUrl | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.url | Defaults to 'whenEmpty' | | params.overrideContentTitle | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.title | Defaults to 'whenEmpty' | | params.overrideContentDescription | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.ext.description | Defaults to 'whenEmpty' | @@ -155,7 +159,7 @@ To view an example: - in your browser, navigate to: -`http://localhost:9999/integrationExamples/gpt/jwplayerRtdProvider_example.html` +`http://localhost:9999/integrationExamples/realTimeData/jwplayerRtdProvider_example.html` **Note:** the mediaIds in the example are placeholder values; replace them with your existing IDs. diff --git a/modules/jwplayerVideoProvider.js b/modules/jwplayerVideoProvider.js index ed6b69d756a..8b956ab1850 100644 --- a/modules/jwplayerVideoProvider.js +++ b/modules/jwplayerVideoProvider.js @@ -12,9 +12,15 @@ import stateFactory from '../libraries/video/shared/state.js'; import { JWPLAYER_VENDOR } from '../libraries/video/constants/vendorCodes.js'; import { getEventHandler } from '../libraries/video/shared/eventHandler.js'; import { submodule } from '../src/hook.js'; +/** + * @typedef {import('../libraries/video/constants/ortb.js').OrtbVideoParams} OrtbVideoParams + * @typedef {import('../libraries/video/shared/state.js').State} State + * @typedef {import('../modules/videoModule/coreVideo.js').VideoProvider} VideoProvider + * @typedef {import('../modules/videoModule/coreVideo.js').videoProviderConfig} videoProviderConfig + */ /** - * @constructor + * @class * @param {videoProviderConfig} config * @param {Object} jwplayer_ - JW Player global factory * @param {State} adState_ @@ -44,6 +50,8 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba VIDEO_MIME_TYPE.AAC, VIDEO_MIME_TYPE.HLS ]; + let height = null; + let width = null; function init() { if (!jwplayer) { @@ -86,6 +94,20 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba const adConfig = config.advertising || {}; supportedMediaTypes = supportedMediaTypes || utils.getSupportedMediaTypes(MEDIA_TYPES); + if (height === null) { + height = utils.getPlayerHeight(player, config); + } + + if (width === null) { + width = utils.getPlayerWidth(player, config); + } + + if (config.aspectratio && !height && !width) { + const size = utils.getPlayerSizeFromAspectRatio(player, config); + height = size.height; + width = size.width; + } + const video = { mimes: supportedMediaTypes, protocols: [ @@ -96,8 +118,8 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba PROTOCOLS.VAST_3_0_WRAPPER, PROTOCOLS.VAST_4_0_WRAPPER ], - h: player.getHeight(), // TODO does player call need optimization ? - w: player.getWidth(), // TODO does player call need optimization ? + h: height, + w: width, startdelay: utils.getStartDelay(), placement: utils.getPlacement(adConfig, player), // linearity is omitted because both forms are supported. @@ -187,6 +209,14 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba player.playAd(adTagUrl || options.adXml, options); } + function setAdXml(vastXml, options) { + if (!player || !vastXml) { + return; + } + + player.loadAdXml(vastXml, options); + } + function onEvent(externalEventName, callback, basePayload) { if (externalEventName === SETUP_COMPLETE) { setupCompleteCallbacks.push(callback); @@ -408,10 +438,14 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba break; case PLAYER_RESIZE: - getEventPayload = e => ({ - height: e.height, - width: e.width, - }); + getEventPayload = e => { + height = e.height; + width = e.width; + return { + height, + width + }; + }; break; case VIEWABLE: @@ -470,6 +504,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba getOrtbVideo, getOrtbContent, setAdTagUrl, + setAdXml, onEvent, offEvent, destroy @@ -579,6 +614,79 @@ export const utils = { return jwConfig; }, + getPlayerHeight: function(player, config) { + let height; + + if (player.getHeight) { + height = player.getHeight(); + } + + // Height is undefined when player has not yet rendered + if (height !== undefined) { + return height; + } + + return config.height; + }, + + getPlayerWidth: function(player, config) { + let width; + + if (player.getWidth) { + width = player.getWidth(); + } + + // Width is undefined when player has not yet rendered + if (width !== undefined) { + return width; + } + + // Width can be a string when aspectratio is set + if (typeof config.width === 'number') { + return config.width; + } + }, + + getPlayerSizeFromAspectRatio: function(player, config) { + const aspectRatio = config.aspectratio; + let percentageWidth = config.width; + + if (typeof aspectRatio !== 'string' || typeof percentageWidth !== 'string') { + return {}; + } + + const ratios = aspectRatio.split(':'); + + if (ratios.length !== 2) { + return {}; + } + + const containerElement = player.getContainer && player.getContainer(); + if (!containerElement) { + return {}; + } + + const containerWidth = containerElement.clientWidth; + const containerHeight = containerElement.clientHeight; + + const xRatio = parseInt(ratios[0], 10); + const yRatio = parseInt(ratios[1], 10); + + if (isNaN(xRatio) || isNaN(yRatio)) { + return {}; + } + + const numericWidthPercentage = parseInt(percentageWidth, 10); + + const desiredWidth = containerWidth * numericWidthPercentage / 100; + const desiredHeight = Math.min(desiredWidth * yRatio / xRatio, containerHeight); + + return { + height: desiredHeight, + width: desiredWidth + }; + }, + getJwEvent: function(eventName) { switch (eventName) { case SETUP_COMPLETE: diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index f3b3166ccad..9416e6a0411 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -1,4 +1,4 @@ -import { _each, isEmpty, buildUrl, deepAccess, pick, triggerPixel, logError } from '../src/utils.js'; +import { _each, isEmpty, buildUrl, deepAccess, pick, logError, isPlainObject } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -48,7 +48,7 @@ const SUA_ATTRIBUTES = [ const CERBERUS = Object.freeze({ KEY: 'krg_crb', - SYNC_URL: 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&idx={INDEX}&gdpr={GDPR}&gdpr_consent={GDPR_CONSENT}&us_privacy={US_PRIVACY}&gpp={GPP_STRING}&gpp_sid={GPP_SID}', + SYNC_URL: 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&gdpr={GDPR}&gdpr_consent={GDPR_CONSENT}&us_privacy={US_PRIVACY}&gpp={GPP_STRING}&gpp_sid={GPP_SID}', SYNC_COUNT: 5, PAGE_VIEW_ID: 'pageViewId', PAGE_VIEW_TIMESTAMP: 'pageViewTimestamp', @@ -95,16 +95,13 @@ function buildRequests(validBidRequests, bidderRequest) { ] }, imp: impressions, - user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids, bidderRequest.gppConsent) + user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids, bidderRequest.gppConsent), + ext: getExtensions(firstBidRequest.ortb2, bidderRequest?.refererInfo) }); - // Add full ortb2 object as backup - if (firstBidRequest.ortb2) { - const siteCat = firstBidRequest.ortb2.site?.cat; - if (siteCat != null) { - krakenParams.site = { cat: siteCat }; - } - krakenParams.ext = { ortb2: firstBidRequest.ortb2 }; + // Add site.cat if it exists + if (firstBidRequest.ortb2?.site?.cat != null) { + krakenParams.site = { cat: firstBidRequest.ortb2.site.cat }; } // Add schain @@ -186,6 +183,10 @@ function buildRequests(validBidRequests, bidderRequest) { krakenParams.page = page; } + if (krakenParams.ext && Object.keys(krakenParams.ext).length === 0) { + delete krakenParams.ext; + } + return Object.assign({}, bidderRequest, { method: BIDDER.REQUEST_METHOD, url: `https://${BIDDER.HOST}${BIDDER.REQUEST_ENDPOINT}`, @@ -196,6 +197,7 @@ function buildRequests(validBidRequests, bidderRequest) { function interpretResponse(response, bidRequest) { const bids = response.body; + const fledgeAuctionConfigs = []; const bidResponses = []; if (isEmpty(bids) || typeof bids !== 'object') { @@ -237,9 +239,23 @@ function interpretResponse(response, bidRequest) { } bidResponses.push(bidResponse); + + if (adUnit.auctionConfig) { + fledgeAuctionConfigs.push({ + bidId: bidID, + config: adUnit.auctionConfig + }) + } } - return bidResponses; + if (fledgeAuctionConfigs.length > 0) { + return { + bids: bidResponses, + paapi: fledgeAuctionConfigs + } + } else { + return bidResponses; + } } function getUserSyncs(syncOptions, _, gdprConsent, usPrivacy, gppConsent) { @@ -258,19 +274,16 @@ function getUserSyncs(syncOptions, _, gdprConsent, usPrivacy, gppConsent) { return syncs; } if (syncOptions.iframeEnabled && seed && clientId) { - for (let i = 0; i < CERBERUS.SYNC_COUNT; i++) { - syncs.push({ - type: 'iframe', - url: CERBERUS.SYNC_URL.replace('{UUID}', clientId) - .replace('{SEED}', seed) - .replace('{INDEX}', i) - .replace('{GDPR}', gdpr) - .replace('{GDPR_CONSENT}', gdprConsentString) - .replace('{US_PRIVACY}', usPrivacy || '') - .replace('{GPP_STRING}', gppString) - .replace('{GPP_SID}', gppApplicableSections) - }); - } + syncs.push({ + type: 'iframe', + url: CERBERUS.SYNC_URL.replace('{UUID}', clientId) + .replace('{SEED}', seed) + .replace('{GDPR}', gdpr) + .replace('{GDPR_CONSENT}', gdprConsentString) + .replace('{US_PRIVACY}', usPrivacy || '') + .replace('{GPP_STRING}', gppString) + .replace('{GPP_SID}', gppApplicableSections) + }) } return syncs; } @@ -285,6 +298,13 @@ function onTimeout(timeoutData) { }); } +function getExtensions(ortb2, refererInfo) { + const ext = {}; + if (ortb2) ext.ortb2 = ortb2; + if (refererInfo) ext.refererInfo = refererInfo; + return ext; +} + function _generateRandomUUID() { try { // crypto.getRandomValues is supported everywhere but Opera Mini for years @@ -432,21 +452,20 @@ function getRequestCount() { } function sendTimeoutData(auctionId, auctionTimeout) { - let params = { - aid: auctionId, - ato: auctionTimeout - }; - - try { - let timeoutRequestUrl = buildUrl({ - protocol: 'https', - hostname: BIDDER.HOST, - pathname: BIDDER.TIMEOUT_ENDPOINT, - search: params - }); + const params = { aid: auctionId, ato: auctionTimeout }; + const timeoutRequestUrl = buildUrl({ + protocol: 'https', + hostname: BIDDER.HOST, + pathname: BIDDER.TIMEOUT_ENDPOINT, + search: params, + }); - triggerPixel(timeoutRequestUrl); - } catch (e) {} + fetch(timeoutRequestUrl, { + method: 'GET', + keepalive: true, + }).catch((e) => { + logError('Kargo: sendTimeoutData/fetch threw an error: ', e); + }); } function getImpression(bid) { @@ -507,7 +526,7 @@ function getImpression(bid) { } catch (e) { logError('Kargo: getFloor threw an error: ', e); } - imp.floor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; + imp.floor = isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } } diff --git a/modules/kimberliteBidAdapter.js b/modules/kimberliteBidAdapter.js index 72df921e18f..fbb9974d52d 100644 --- a/modules/kimberliteBidAdapter.js +++ b/modules/kimberliteBidAdapter.js @@ -1,13 +1,14 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js' -import { deepSetValue } from '../src/utils.js'; +import { deepSetValue, replaceMacros } from '../src/utils.js'; +import {ORTB_MTYPES} from '../libraries/ortbConverter/processors/mediaType.js'; -const VERSION = '1.0.0'; +const VERSION = '1.1.0'; const BIDDER_CODE = 'kimberlite'; const METHOD = 'POST'; -const ENDPOINT_URL = 'https://kimberlite.io/rtb/bid/pbjs'; +export const ENDPOINT_URL = 'https://kimberlite.io/rtb/bid/pbjs'; const VERSION_INFO = { ver: '$prebid.version$', @@ -16,7 +17,6 @@ const VERSION_INFO = { const converter = ortbConverter({ context: { - mediaType: BANNER, netRevenue: true, ttl: 300 }, @@ -35,18 +35,38 @@ const converter = ortbConverter({ const imp = buildImp(bidRequest, context); imp.tagid = bidRequest.params.placementId; return imp; - } + }, + + bidResponse: function (buildBidResponse, bid, context) { + if (!bid.price) return; + + const [type] = Object.keys(context.bidRequest.mediaTypes); + if (Object.values(ORTB_MTYPES).includes(type)) { + context.mediaType = type; + } + + bid.adm = expandAuctionMacros(bid.adm, bid.price, context.ortbResponse.cur); + + if (bid.nurl && bid.nurl != '') { + bid.nurl = expandAuctionMacros(bid.nurl, bid.price, context.ortbResponse.cur); + } + + const bidResponse = buildBidResponse(bid, context); + return bidResponse; + }, }); export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: (bidRequest = {}) => { const { params, mediaTypes } = bidRequest; let isValid = Boolean(params && params.placementId); if (mediaTypes && mediaTypes[BANNER]) { isValid = isValid && Boolean(mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + isValid = isValid && Boolean(mediaTypes[VIDEO].mimes); } else { isValid = false; } @@ -58,7 +78,10 @@ export const spec = { return { method: METHOD, url: ENDPOINT_URL, - data: converter.toORTB({ bidderRequest, bidRequests }) + data: converter.toORTB({ + bidRequests, + bidderRequest + }) } }, @@ -68,4 +91,12 @@ export const spec = { } }; +export function expandAuctionMacros(str, price, currency) { + if (!str) return; + + const defaultCurrency = 'RUB'; + + return replaceMacros(str, {AUCTION_PRICE: price, AUCTION_CURRENCY: currency || defaultCurrency}); +}; + registerBidder(spec); diff --git a/modules/kimberliteBidAdapter.md b/modules/kimberliteBidAdapter.md index c165f1073aa..06749f2c8e0 100644 --- a/modules/kimberliteBidAdapter.md +++ b/modules/kimberliteBidAdapter.md @@ -20,7 +20,7 @@ var adUnits = [ code: 'test-div', mediaTypes: { banner: { - sizes: [[320, 250], [640, 480]], + sizes: [[320, 250], [640, 480]], // Required. } }, bids: [ @@ -34,3 +34,32 @@ var adUnits = [ } ] ``` + +## Video AdUnit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + // ORTB 2.5 options. + mimes: ['video/mp4'], // Required. + // Other options are optional. + placement: 1, + protocols: [3, 6], + linearity: 1, + startdelay: 0 + } + }, + bids: [ + { + bidder: "kimberlite", + params: { + placementId: 'testVideo' + } + } + ] + } +] +``` diff --git a/modules/kinessoIdSystem.js b/modules/kinessoIdSystem.js index 35b8dcc182d..56cf7353b71 100644 --- a/modules/kinessoIdSystem.js +++ b/modules/kinessoIdSystem.js @@ -8,7 +8,6 @@ import { logError, logInfo } from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; -import {coppaDataHandler, uspDataHandler} from '../src/adapterManager.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -183,14 +182,14 @@ function encodeId(value) { * @return {string} */ function kinessoSyncUrl(accountId, consentData) { - const usPrivacyString = uspDataHandler.getConsentData(); + const {gdpr, usp: usPrivacyString} = consentData ?? {}; let kinessoSyncUrl = `${ID_SVC}?accountid=${accountId}`; if (usPrivacyString) { kinessoSyncUrl = `${kinessoSyncUrl}&us_privacy=${usPrivacyString}`; } - if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return kinessoSyncUrl; + if (!gdpr || typeof gdpr.gdprApplies !== 'boolean' || !gdpr.gdprApplies) return kinessoSyncUrl; - kinessoSyncUrl = `${kinessoSyncUrl}&gdpr=1&gdpr_consent=${consentData.consentString}`; + kinessoSyncUrl = `${kinessoSyncUrl}&gdpr=1&gdpr_consent=${gdpr.consentString}`; return kinessoSyncUrl } @@ -226,8 +225,7 @@ export const kinessoIdSubmodule = { logError('User ID - KinessoId submodule requires a valid accountid to be defined'); return; } - const coppa = coppaDataHandler.getCoppa(); - if (coppa) { + if (consentData?.coppa) { logInfo('KinessoId: IDs not provided for coppa requests, exiting KinessoId'); return; } diff --git a/modules/kiviadsBidAdapter.js b/modules/kiviadsBidAdapter.js index 13739d57cb2..161ddad470f 100644 --- a/modules/kiviadsBidAdapter.js +++ b/modules/kiviadsBidAdapter.js @@ -1,212 +1,19 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'kiviads'; const AD_URL = 'https://lb.kiviads.com/pbjs'; const SYNC_URL = 'https://sync.kiviads.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: bidderRequest.coppa === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - gpp: bidderRequest.gppConsent || undefined, - tmax: bidderRequest.bidderTimeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/koblerBidAdapter.js b/modules/koblerBidAdapter.js index 596e5b2695f..6331ed9bbbb 100644 --- a/modules/koblerBidAdapter.js +++ b/modules/koblerBidAdapter.js @@ -1,5 +1,6 @@ import { deepAccess, + generateUUID, getWindowSelf, isArray, isStr, @@ -7,10 +8,24 @@ import { replaceAuctionPrice, triggerPixel } from '../src/utils.js'; -import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {getRefererInfo} from '../src/refererDetection.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; + +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 }); +} + +export function getAdditionalData(obj, key) { + const data = additionalData.get(obj) || {}; + return data[key]; +} const BIDDER_CODE = 'kobler'; const BIDDER_ENDPOINT = 'https://bid.essrtb.com/bid/prebid_rtb_call'; @@ -36,17 +51,18 @@ export const buildRequests = function (validBidRequests, bidderRequest) { data: buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest), options: { contentType: 'application/json' - } + }, + bidderRequest }; }; -export const interpretResponse = function (serverResponse) { +export const interpretResponse = function (serverResponse, request) { const res = serverResponse.body; const bids = [] if (res) { res.seatbid.forEach(sb => { sb.bid.forEach(b => { - bids.push({ + const bid = { requestId: b.impid, cpm: b.price, currency: res.cur, @@ -58,23 +74,28 @@ export const interpretResponse = function (serverResponse) { ttl: TIME_TO_LIVE_IN_SECONDS, ad: b.adm, nurl: b.nurl, + cid: b.cid, meta: { advertiserDomains: b.adomain } - }) + } + setAdditionalData(bid, 'adServerCurrency', getCurrencyFromBidderRequest(request.bidderRequest)); + bids.push(bid); }) }); } + return bids; }; export const onBidWon = function (bid) { + const adServerCurrency = getAdditionalData(bid, 'adServerCurrency'); // We intentionally use the price set by the publisher to replace the ${AUCTION_PRICE} macro // instead of the `originalCpm` here. This notification is not used for billing, only for extra logging. const publisherPrice = bid.cpm || 0; - const publisherCurrency = bid.currency || config.getConfig('currency.adServerCurrency') || SUPPORTED_CURRENCY; + const publisherCurrency = bid.currency || adServerCurrency || SUPPORTED_CURRENCY; const adServerPrice = deepAccess(bid, 'adserverTargeting.hb_pb', 0); - const adServerPriceCurrency = config.getConfig('currency.adServerCurrency') || SUPPORTED_CURRENCY; + const adServerPriceCurrency = adServerCurrency || SUPPORTED_CURRENCY; if (isStr(bid.nurl) && bid.nurl !== '') { const winNotificationUrl = replaceAuctionPrice(bid.nurl, publisherPrice) .replace(/\${AUCTION_PRICE_CURRENCY}/g, publisherCurrency) @@ -90,8 +111,6 @@ export const onTimeout = function (timeoutDataArray) { timeoutDataArray.forEach(timeoutData => { const query = parseQueryStringParameters({ ad_unit_code: timeoutData.adUnitCode, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auction_id: timeoutData.auctionId, bid_id: timeoutData.bidId, timeout: timeoutData.timeout, page_url: pageUrl, @@ -103,13 +122,6 @@ export const onTimeout = function (timeoutDataArray) { }; function getPageUrlFromRequest(validBidRequest, bidderRequest) { - // pageUrl is considered only when testing to ensure that non-test requests always contain the correct URL - if (isTest(validBidRequest) && config.getConfig('pageUrl')) { - // TODO: it's not clear what the intent is here - but all adapters should always respect pageUrl. - // With prebid 7, using `refererInfo.page` will do that automatically. - return config.getConfig('pageUrl'); - } - return (bidderRequest.refererInfo && bidderRequest.refererInfo.page) ? bidderRequest.refererInfo.page : window.location.href; @@ -125,7 +137,26 @@ function getPageUrlFromRefererInfo() { function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { const imps = validBidRequests.map(buildOpenRtbImpObject); const timeout = bidderRequest.timeout; - const pageUrl = getPageUrlFromRequest(validBidRequests[0], bidderRequest) + const pageUrl = getPageUrlFromRequest(validBidRequests[0], bidderRequest); + // Kobler, a contextual advertising provider, does not process any personal data itself, so it is not part of TCF/GVL. + // However, it supports using select third-party creatives in its platform, some of which require certain permissions + // in order to be shown. Kobler's bidder checks if necessary permissions are present to avoid bidding + // with ineligible creatives. + let purpose2Given; + let purpose3Given; + if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.vendorData) { + const vendorData = bidderRequest.gdprConsent.vendorData + const purposeData = vendorData.purpose; + const restrictions = vendorData.publisher ? vendorData.publisher.restrictions : null; + const restrictionForPurpose2 = restrictions ? (restrictions[2] ? Object.values(restrictions[2])[0] : null) : null; + purpose2Given = restrictionForPurpose2 === 1 ? ( + purposeData && purposeData.consents && purposeData.consents[2] + ) : ( + restrictionForPurpose2 === 0 + ? false : (purposeData && purposeData.legitimateInterests && purposeData.legitimateInterests[2]) + ); + purpose3Given = purposeData && purposeData.consents && purposeData.consents[3]; + } const request = { id: bidderRequest.bidderRequestId, at: 1, @@ -133,12 +164,21 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { cur: [SUPPORTED_CURRENCY], imp: imps, device: { - devicetype: getDevice() + devicetype: getDevice(), + ua: navigator.userAgent, + sua: validBidRequests[0]?.ortb2?.device?.sua }, site: { page: pageUrl, }, - test: getTestAsNumber(validBidRequests[0]) + test: getTestAsNumber(validBidRequests[0]), + ext: { + kobler: { + tcf_purpose_2_given: purpose2Given, + tcf_purpose_3_given: purpose3Given, + page_view_id: pageViewId + } + } }; return JSON.stringify(request); @@ -147,7 +187,7 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { function buildOpenRtbImpObject(validBidRequest) { const sizes = getSizes(validBidRequest); const mainSize = sizes[0]; - const floorInfo = getFloorInfo(validBidRequest, mainSize); + const floorInfo = getFloorInfo(validBidRequest, mainSize) || {}; return { id: validBidRequest.bidId, diff --git a/modules/krushmediaBidAdapter.js b/modules/krushmediaBidAdapter.js index 876f0ebabc6..255e3670254 100644 --- a/modules/krushmediaBidAdapter.js +++ b/modules/krushmediaBidAdapter.js @@ -1,163 +1,40 @@ -import { isFn, deepAccess, logMessage } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { + isBidRequestValid, + buildRequestsBase, + interpretResponse, + getUserSyncs, + buildPlacementProcessingFunction, +} from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'krushmedia'; const AD_URL = 'https://ads4.krushmedia.com/?c=rtb&m=hb'; -const SYNC_URL = 'https://cs.krushmedia.com/html?src=pbjs' - -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; - } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers); - default: - return false; +const SYNC_URL = 'https://cs.krushmedia.com'; + +const addCustomFieldsToPlacement = (bid, bidderRequest, placement) => { + placement.key = bid.params.key; + placement.traffic = placement.adFormat; + if (placement.adFormat === VIDEO) { + placement.wPlayer = placement.playerSize?.[0]?.[0]; + placement.hPlayer = placement.playerSize?.[0]?.[1]; } -} +}; -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } +const placementProcessingFunction = buildPlacementProcessingFunction({ addCustomFieldsToPlacement }); - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + return buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest, placementProcessingFunction }); +}; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.key))); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let winTop = window; - let location; - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page); - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - - const placements = []; - const request = { - deviceWidth: winTop.screen.width, - deviceHeight: winTop.screen.height, - language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - secure: 1, - host: location.host, - page: location.pathname, - placements: placements - }; - - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent; - } - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - const placement = { - key: bid.params.key, - bidId: bid.bidId, - traffic: bid.params.traffic || BANNER, - schain: bid.schain || {}, - bidFloor: getBidFloor(bid) - }; - - if (bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { - placement.sizes = bid.mediaTypes[BANNER].sizes; - } else if (bid.mediaTypes && bid.mediaTypes[VIDEO] && bid.mediaTypes[VIDEO].playerSize) { - placement.wPlayer = bid.mediaTypes[VIDEO].playerSize[0]; - placement.hPlayer = bid.mediaTypes[VIDEO].playerSize[1]; - placement.minduration = bid.mediaTypes[VIDEO].minduration; - placement.maxduration = bid.mediaTypes[VIDEO].maxduration; - placement.mimes = bid.mediaTypes[VIDEO].mimes; - placement.protocols = bid.mediaTypes[VIDEO].protocols; - placement.startdelay = bid.mediaTypes[VIDEO].startdelay; - placement.placement = bid.mediaTypes[VIDEO].placement; - placement.skip = bid.mediaTypes[VIDEO].skip; - placement.skipafter = bid.mediaTypes[VIDEO].skipafter; - placement.minbitrate = bid.mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = bid.mediaTypes[VIDEO].maxbitrate; - placement.delivery = bid.mediaTypes[VIDEO].delivery; - placement.playbackmethod = bid.mediaTypes[VIDEO].playbackmethod; - placement.api = bid.mediaTypes[VIDEO].api; - placement.linearity = bid.mediaTypes[VIDEO].linearity; - } else if (bid.mediaTypes && bid.mediaTypes[NATIVE]) { - placement.native = bid.mediaTypes[NATIVE]; - } - placements.push(placement); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncUrl = SYNC_URL - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - return [{ - type: 'iframe', - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(['key']), + buildRequests, + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/kubientBidAdapter.js b/modules/kubientBidAdapter.js index 57cbe6acd07..8ccfa4ab059 100644 --- a/modules/kubientBidAdapter.js +++ b/modules/kubientBidAdapter.js @@ -1,4 +1,4 @@ -import { isArray, deepAccess } from '../src/utils.js'; +import { isArray, deepAccess, isPlainObject } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import { config } from '../src/config.js'; @@ -33,7 +33,7 @@ export const spec = { const mediaType = (Object.keys(bid.mediaTypes).length == 1) ? Object.keys(bid.mediaTypes)[0] : '*'; const sizes = bid.sizes || '*'; const floorInfo = bid.getFloor({currency: 'USD', mediaType: mediaType, size: sizes}); - if (typeof floorInfo === 'object' && floorInfo.currency === 'USD') { + if (isPlainObject(floorInfo) && floorInfo.currency === 'USD') { let floor = parseFloat(floorInfo.floor) if (!isNaN(floor) && floor > 0) { adSlot.floor = parseFloat(floorInfo.floor); diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js index 5a5536e0c1a..f11d71f3318 100644 --- a/modules/kueezBidAdapter.js +++ b/modules/kueezBidAdapter.js @@ -155,7 +155,7 @@ function getFloorPrice(bid, mediaType) { currency: MAIN_CURRENCY, mediaType: mediaType, size: '*' - }); + }) || {}; floor = floorResult.currency === MAIN_CURRENCY && floorResult.floor ? floorResult.floor : 0; } @@ -417,6 +417,7 @@ function populateVideoParams(params, bid) { const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); const placement = deepAccess(bid, `mediaTypes.video.placement`); + const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); const playbackMethod = getPlaybackMethod(bid); const skip = deepAccess(bid, `mediaTypes.video.skip`); @@ -435,7 +436,9 @@ function populateVideoParams(params, bid) { if (placement) { params.placement = placement; } - + if (plcmt) { + params.plcmt = plcmt; + } if (playbackMethod) { params.playbackMethod = playbackMethod; } diff --git a/modules/kueezRtbBidAdapter.js b/modules/kueezRtbBidAdapter.js index 264592cd7d6..60eced6a490 100644 --- a/modules/kueezRtbBidAdapter.js +++ b/modules/kueezRtbBidAdapter.js @@ -1,348 +1,77 @@ -import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; -import {config} from '../src/config.js'; +import { + createBuildRequestsFn, + createInterpretResponseFn, + createUserSyncGetter, + isBidRequestValid, + tryParseJSON +} from '../libraries/vidazooUtils/bidderUtils.js'; const GVLID = 1165; const DEFAULT_SUB_DOMAIN = 'exchange'; const BIDDER_CODE = 'kueezrtb'; const BIDDER_VERSION = '1.0.0'; -const CURRENCY = 'USD'; -const TTL_SECONDS = 60 * 5; -const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); -function getTopWindowQueryParams() { - try { - const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); - return parsedUrl.search; - } catch (e) { - return ''; - } -} +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests: createBuildRequestsFn(createDomain, createUniqueRequestData, storage, BIDDER_CODE, BIDDER_VERSION, false), + interpretResponse: createInterpretResponseFn(BIDDER_CODE, false), + getUserSyncs: createUserSyncGetter({ + iframeSyncUrl: 'https://sync.kueezrtb.com/api/sync/iframe', + imageSyncUrl: 'https://sync.kueezrtb.com/api/sync/image' + }), + createFirstPartyData, +}; export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.kueezrtb.com`; } -export function extractCID(params) { - return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; -} - -export function extractPID(params) { - return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; -} - -export function extractSubDomain(params) { - return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; -} - -function isBidRequestValid(bid) { - const params = bid.params || {}; - return !!(extractCID(params) && extractPID(params)); -} - -function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { - const { - params, - bidId, - userId, - adUnitCode, - schain, - mediaTypes, - auctionId, - transactionId, - bidderRequestId, - bidRequestsCount, - bidderRequestsCount, - bidderWinsCount - } = bid; - let {bidFloor, ext} = params; - const hashUrl = hashCode(topWindowUrl); - const uniqueDealId = getUniqueDealId(hashUrl); - const cId = extractCID(params); - const pId = extractPID(params); - const subDomain = extractSubDomain(params); - - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); - - if (isFn(bid.getFloor)) { - const floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - - if (floorInfo.currency === 'USD') { - bidFloor = floorInfo.floor; - } +export function getAndSetFirstPartyData() { + if (!storage.hasLocalStorage()) { + return; } - - let data = { - url: encodeURIComponent(topWindowUrl), - uqs: getTopWindowQueryParams(), - cb: Date.now(), - bidFloor: bidFloor, - bidId: bidId, - referrer: bidderRequest.refererInfo.ref, - adUnitCode: adUnitCode, - publisherId: pId, - sizes: sizes, - uniqueDealId: uniqueDealId, - bidderVersion: BIDDER_VERSION, - prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}`, - schain: schain, - mediaTypes: mediaTypes, - gpid: gpid, - // TODO: fix auctionId/transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: auctionId, - transactionId: transactionId, - bidderRequestId: bidderRequestId, - bidRequestsCount: bidRequestsCount, - bidderRequestsCount: bidderRequestsCount, - bidderWinsCount: bidderWinsCount, - bidderTimeout: bidderTimeout - }; - - appendUserIdsToRequestPayload(data, userId); - - const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); - - if (sua) { - data.sua = sua; - } - - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.consentString) { - data.gdprConsent = bidderRequest.gdprConsent.consentString; - } - if (bidderRequest.gdprConsent.gdprApplies !== undefined) { - data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - } - if (bidderRequest.uspConsent) { - data.usPrivacy = bidderRequest.uspConsent; - } - - if (bidderRequest.gppConsent) { - data.gppString = bidderRequest.gppConsent.gppString; - data.gppSid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - data.gppString = bidderRequest.ortb2.regs.gpp; - data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + let fdata = tryParseJSON(storage.getDataFromLocalStorage('_iiq_fdata')); + if (!fdata) { + fdata = spec.createFirstPartyData(); + storage.setDataInLocalStorage('_iiq_fdata', JSON.stringify(fdata)); } - - const dto = { - method: 'POST', - url: `${createDomain(subDomain)}/prebid/multi/${cId}`, - data: data - }; - - _each(ext, (value, key) => { - dto.data['ext.' + key] = value; - }); - - return dto; + return fdata; } -function appendUserIdsToRequestPayload(payloadRef, userIds) { - let key; - _each(userIds, (userId, idSystemProviderName) => { - key = `uid.${idSystemProviderName}`; - - switch (idSystemProviderName) { - case 'digitrustid': - payloadRef[key] = deepAccess(userId, 'data.id'); - break; - case 'lipb': - payloadRef[key] = userId.lipbid; - break; - case 'parrableId': - payloadRef[key] = userId.eid; - break; - case 'id5id': - payloadRef[key] = userId.uid; - break; - default: - payloadRef[key] = userId; - } - }); +export function createFirstPartyData() { + return { + pcid: getFirstPartyUUID(), pcidDate: Date.now(), + }; } -function buildRequests(validBidRequests, bidderRequest) { - const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - const bidderTimeout = bidderRequest.timeout ?? config.getConfig('bidderTimeout'); - const requests = []; - validBidRequests.forEach(validBidRequest => { - const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); - requests.push(request); +function getFirstPartyUUID() { + let d = new Date().getTime(); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); }); - return requests; -} - -function interpretResponse(serverResponse, request) { - if (!serverResponse || !serverResponse.body) { - return []; - } - const {bidId} = request.data; - const {results} = serverResponse.body; - - let output = []; - - try { - results.forEach(result => { - const { - creativeId, - ad, - price, - exp, - width, - height, - currency, - metaData, - advertiserDomains, - mediaType = BANNER - } = result; - if (!ad || !price) { - return; - } - - const response = { - requestId: bidId, - cpm: price, - width: width, - height: height, - creativeId: creativeId, - currency: currency || CURRENCY, - netRevenue: true, - ttl: exp || TTL_SECONDS, - }; - - if (metaData) { - Object.assign(response, { - meta: metaData - }) - } else { - Object.assign(response, { - meta: { - advertiserDomains: advertiserDomains || [] - } - }) - } - - if (mediaType === BANNER) { - Object.assign(response, { - ad: ad, - }); - } else { - Object.assign(response, { - vastXml: ad, - mediaType: VIDEO - }); - } - output.push(response); - }); - return output; - } catch (e) { - return []; - } -} - -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { - let syncs = []; - const {iframeEnabled, pixelEnabled} = syncOptions; - const {gdprApplies, consentString = ''} = gdprConsent; - const {gppString, applicableSections} = gppConsent; - - const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` - - if (gppString && applicableSections?.length) { - params += '&gpp=' + encodeURIComponent(gppString); - params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); - } - - if (iframeEnabled) { - syncs.push({ - type: 'iframe', - url: `https://sync.kueezrtb.com/api/sync/iframe/${params}` - }); - } - if (pixelEnabled) { - syncs.push({ - type: 'image', - url: `https://sync.kueezrtb.com/api/sync/image/${params}` - }); - } - return syncs; -} - -export function hashCode(s, prefix = '_') { - const l = s.length; - let h = 0 - let i = 0; - if (l > 0) { - while (i < l) { - h = (h << 5) - h + s.charCodeAt(i++) | 0; - } - } - return prefix + h; -} - -export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { - const storageKey = `u_${key}`; - const now = Date.now(); - const data = getStorageItem(storageKey); - let uniqueId; - - if (!data || !data.value || now - data.created > expiry) { - uniqueId = `${key}_${now.toString()}`; - setStorageItem(storageKey, uniqueId); - } else { - uniqueId = data.value; - } - - return uniqueId; -} - -export function getStorageItem(key) { - try { - return tryParseJSON(storage.getDataFromLocalStorage(key)); - } catch (e) { - } - - return null; -} - -export function setStorageItem(key, value, timestamp) { - try { - const created = timestamp || Date.now(); - const data = JSON.stringify({value, created}); - storage.setDataInLocalStorage(key, data); - } catch (e) { - } -} +}; -export function tryParseJSON(value) { - try { - return JSON.parse(value); - } catch (e) { - return value; - } +function createUniqueRequestData(hashUrl, bid) { + const {auctionId, transactionId} = bid; + const fdata = getAndSetFirstPartyData(); + return { + auctionId, + transactionId, + ...(fdata && { + iiqpcid: fdata.pcid, + iiqpcidDate: fdata.pcidDate + }) + }; } -export const spec = { - code: BIDDER_CODE, - version: BIDDER_VERSION, - gvlid: GVLID, - supportedMediaTypes: [BANNER, VIDEO], - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs -}; - registerBidder(spec); diff --git a/modules/lane4BidAdapter.js b/modules/lane4BidAdapter.js new file mode 100644 index 00000000000..243dabb0bef --- /dev/null +++ b/modules/lane4BidAdapter.js @@ -0,0 +1,41 @@ +import { + BANNER, + NATIVE +} from '../src/mediaTypes.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { + getBannerRequest, + getBannerResponse, + getNativeResponse, +} from '../libraries/audUtils/bidderUtils.js'; + +const EP = 'https://rtb.lane4.io/hb'; +// Export const spec +export const spec = { + code: 'lane4', + supportedMediaTypes: [BANNER, NATIVE], + // Determines whether or not the given bid request is valid + isBidRequestValid: (bidRequestParam) => { + return !!(bidRequestParam.params.placement_id); + }, + // Make a server request from the list of BidRequests + buildRequests: (bidR, serverR) => { + // Get Requests based on media types + return getBannerRequest(bidR, serverR, EP); + }, + // Unpack the response from the server into a list of bids. + interpretResponse: (bidRS, bidRQ) => { + let Response = {}; + const mediaType = JSON.parse(bidRQ.data)[0].MediaType; + if (mediaType == BANNER) { + Response = getBannerResponse(bidRS, BANNER); + } else if (mediaType == NATIVE) { + Response = getNativeResponse(bidRS, bidRQ, NATIVE); + } + return Response; + } +} + +registerBidder(spec); diff --git a/modules/lane4BidAdapter.md b/modules/lane4BidAdapter.md new file mode 100644 index 00000000000..af6ee255ad7 --- /dev/null +++ b/modules/lane4BidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: Lane4 Bidder Adapter +Module Type: Bidder Adapter +Maintainer: adsupport@lane4.io +``` + +# Description + +Lane4 currently supports the BANNER and NATIVE type ads through prebid js + +Module that connects to Lane4's demand sources. + +# Test Request +``` + var adUnits = [ + { + code: 'display-ad', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + } + bids: [ + { + bidder: 'lane4', + params: { + placement_id: 5550, // Required parameter + width: 300, // Optional parameter + height: 250, // Optional parameter + bid_floor: 0.5 // Optional parameter + } + } + ] + }, + { + code: 'native-ad-container', + mediaTypes: { + native: { + title: { required: true, len: 100 }, + image: { required: true, sizes: [300, 250] }, + sponsored: { required: false }, + clickUrl: { required: true }, + desc: { required: true }, + icon: { required: false, sizes: [50, 50] }, + cta: { required: false } + } + }, + bids: [ + { + bidder: 'lane4', + params: { + placement_id: 5551, // Required parameter + bid_floor: 1 // Optional parameter + } + } + ] + } + ]; +``` diff --git a/modules/lassoBidAdapter.js b/modules/lassoBidAdapter.js index 6215af03a97..19dde7c36e2 100644 --- a/modules/lassoBidAdapter.js +++ b/modules/lassoBidAdapter.js @@ -3,6 +3,7 @@ import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; +import { getWinDimensions } from '../src/utils.js'; const BIDDER_CODE = 'lasso'; const ENDPOINT_URL = 'https://trc.lhmos.com/prebid'; @@ -32,6 +33,22 @@ export const spec = { sizes = bidRequest.mediaTypes[BANNER].sizes; } + const { params } = bidRequest; + + let npi = params.npi || ''; + let dgid = params.dgid || ''; + let test = false; + + if (params.testNPI) { + npi = params.testNPI; + test = true; + } + + if (params.testDGID) { + dgid = params.testDGID; + test = true; + } + const payload = { auctionStart: bidderRequest.auctionStart, url: encodeURIComponent(window.location.href), @@ -44,12 +61,16 @@ export const spec = { sizes, aimXR, uid: '$UID', + npi, + dgid, + npi_hash: params.npiHash || '', params: JSON.stringify(bidRequest.params), crumbs: JSON.stringify(bidRequest.crumbs), prebidVersion: '$prebid.version$', version: 4, coppa: config.getConfig('coppa') == true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined + ccpa: bidderRequest.uspConsent || undefined, + test } if ( @@ -128,18 +149,19 @@ function getBidRequestUrl(aimXR, params) { if (params && params.dtc) { path = '/dtc-request'; } - if (!aimXR) { - return GET_IUD_URL + ENDPOINT_URL + path; + if (aimXR || params.npi || params.dgid || params.npiHash || params.testNPI || params.testDGID) { + return ENDPOINT_URL + path; } - return ENDPOINT_URL + path; + return GET_IUD_URL + ENDPOINT_URL + path; } function getDeviceData() { const win = window.top; + const winDimensions = getWinDimensions(); return { ua: navigator.userAgent, - width: win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth, - height: win.innerHeight || win.document.documentElement.clientHeight || win.document.body.clientHeight, + width: winDimensions.innerWidth || winDimensions.document.documentElement.clientWidth || win.document.body.clientWidth, + height: winDimensions.innerHeight || winDimensions.document.documentElement.clientHeight || win.document.body.clientHeight, browserLanguage: navigator.language, } } diff --git a/modules/lemmaDigitalBidAdapter.js b/modules/lemmaDigitalBidAdapter.js index dde7c25d9b9..2daa768eb9c 100644 --- a/modules/lemmaDigitalBidAdapter.js +++ b/modules/lemmaDigitalBidAdapter.js @@ -295,7 +295,7 @@ export var spec = { [BANNER, VIDEO].forEach(mediaType => { if (impObj.hasOwnProperty(mediaType)) { let floorInfo = bid.getFloor({ currency: impObj.bidfloorcur, mediaType: mediaType, size: '*' }); - if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidfloorcur && !isNaN(parseInt(floorInfo.floor))) { + if (utils.isPlainObject(floorInfo) && floorInfo.currency === impObj.bidfloorcur && !isNaN(parseInt(floorInfo.floor))) { let mediaTypeFloor = parseFloat(floorInfo.floor); bidFloor = (bidFloor == -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor)); } @@ -450,8 +450,8 @@ export var spec = { dnt: utils.getDNT() ? 1 : 0, ua: navigator.userAgent, language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), - w: (window.screen.width || window.innerWidth), - h: (window.screen.height || window.innerHeigh), + w: (utils.getWinDimensions().screen.width || utils.getWinDimensions().innerWidth), + h: (utils.getWinDimensions().screen.height || utils.getWinDimensions().innerHeight), geo: { country: params.country, lat: params.latitude, diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index acc6876b822..ffca19f5df8 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -32,7 +32,17 @@ function isBidResponseValid(bid) { export const spec = { code: BIDDER_CODE, - aliases: ['pll', 'iionads', 'apester'], + aliases: [ + { code: 'pll' }, + { code: 'iionads', gvlid: 1358 }, + { code: 'apester' }, + { code: 'adsyield' }, + { code: 'tgm' }, + { code: 'adtg_org' }, + { code: 'velonium' }, + { code: 'orangeclickmedia', gvlid: 1148 }, + { code: 'streamvision' } + ], supportedMediaTypes: [BANNER, VIDEO], /** @@ -128,7 +138,10 @@ function buildRequest(winTop, host, adUnits, bidderRequest) { deviceWidth: winTop.screen.width, deviceHeight: winTop.screen.height, adUnits: adUnits, - sua: bidderRequest?.ortb2?.device?.sua + ortb2: bidderRequest?.ortb2, + refererInfo: bidderRequest?.refererInfo, + sua: bidderRequest?.ortb2?.device?.sua, + page: bidderRequest?.ortb2?.site?.page || bidderRequest?.refererInfo?.page } } } @@ -163,6 +176,7 @@ function buildPlacement(bidRequest) { } }), type: bidRequest.params.adUnitType.toUpperCase(), + ortb2Imp: bidRequest.ortb2Imp, publisherId: bidRequest.params.publisherId, userIdAsEids: bidRequest.userIdAsEids, supplyChain: bidRequest.schain, @@ -170,8 +184,7 @@ function buildPlacement(bidRequest) { custom2: bidRequest.params.custom2, custom3: bidRequest.params.custom3, custom4: bidRequest.params.custom4, - custom5: bidRequest.params.custom5, - page: bidRequest.refererInfo.page + custom5: bidRequest.params.custom5 } } } diff --git a/modules/liveIntentAnalyticsAdapter.js b/modules/liveIntentAnalyticsAdapter.js index 04b9e333e8a..a86b6412f8d 100644 --- a/modules/liveIntentAnalyticsAdapter.js +++ b/modules/liveIntentAnalyticsAdapter.js @@ -1,134 +1,121 @@ -import {ajax} from '../src/ajax.js'; -import { generateUUID, logInfo, logWarn } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { generateUUID, isNumber } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import { EVENTS } from '../src/constants.js'; import adapterManager from '../src/adapterManager.js'; -import { auctionManager } from '../src/auctionManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import { config as prebidConfig } from '../src/config.js'; +import { auctionManager } from '../src/auctionManager.js'; const ANALYTICS_TYPE = 'endpoint'; const URL = 'https://wba.liadm.com/analytic-events'; const GVL_ID = 148; const ADAPTER_CODE = 'liveintent'; -const DEFAULT_BID_WON_TIMEOUT = 2000; -const { AUCTION_END } = EVENTS; -let bidWonTimeout; - -function handleAuctionEnd(args) { - setTimeout(() => { - const auction = auctionManager.index.getAuction(args.auctionId); - const winningBids = (auction) ? auction.getWinningBids() : []; - const data = createAnalyticsEvent(args, winningBids); - sendAnalyticsEvent(data); - }, bidWonTimeout); -} +const { AUCTION_INIT, BID_WON } = EVENTS; +const INTEGRATION_ID = '$$PREBID_GLOBAL$$'; -function getAnalyticsEventBids(bidsReceived) { - return bidsReceived.map(bid => { - return { - adUnitCode: bid.adUnitCode, - timeToRespond: bid.timeToRespond, - cpm: bid.cpm, - currency: bid.currency, - ttl: bid.ttl, - bidder: bid.bidder - }; - }); -} +let partnerIdFromUserIdConfig; +let sendAuctionInitEvents; + +let liAnalytics = Object.assign(adapter({URL, ANALYTICS_TYPE}), { + track({ eventType, args }) { + switch (eventType) { + case AUCTION_INIT: + if (sendAuctionInitEvents) { + handleAuctionInitEvent(args); + } + break; + case BID_WON: + handleBidWonEvent(args); + break; + } + } +}); + +function handleAuctionInitEvent(auctionInitEvent) { + const liveIntentIdsPresent = checkLiveIntentIdsPresent(auctionInitEvent.bidderRequests) + + // This is for old integration that enable or disable the user id module + // dependeing on the result of rolling the dice outside of Prebid. + const partnerIdFromAnalyticsLabels = auctionInitEvent.analyticsLabels?.partnerId; -function getBannerSizes(banner) { - if (banner && banner.sizes) { - return banner.sizes.map(size => { - const [width, height] = size; - return {w: width, h: height}; - }); - } else return []; + const data = { + id: generateUUID(), + aid: auctionInitEvent.auctionId, + u: getRefererInfo().page, + ats: auctionInitEvent.timestamp, + pid: partnerIdFromUserIdConfig || partnerIdFromAnalyticsLabels, + iid: INTEGRATION_ID, + tr: window.liTreatmentRate, + me: encodeBoolean(window.liModuleEnabled), + liip: encodeBoolean(liveIntentIdsPresent), + aun: auctionInitEvent?.adUnits?.length || 0 + }; + const filteredData = ignoreUndefined(data); + sendData('auction-init', filteredData); } -function getUniqueBy(arr, key) { - return [...new Map(arr.map(item => [item[key], item])).values()] +function handleBidWonEvent(bidWonEvent) { + const auction = auctionManager.index.getAuction({auctionId: bidWonEvent.auctionId}); + const liveIntentIdsPresent = checkLiveIntentIdsPresent(auction?.getBidRequests()) + + // This is for old integration that enable or disable the user id module + // depending on the result of rolling the dice outside of Prebid. + const partnerIdFromAnalyticsLabels = bidWonEvent.analyticsLabels?.partnerId; + + const data = { + id: generateUUID(), + aid: bidWonEvent.auctionId, + u: getRefererInfo().page, + ats: auction?.getAuctionStart(), + auc: bidWonEvent.adUnitCode, + auid: bidWonEvent.adUnitId, + cpm: bidWonEvent.cpm, + c: bidWonEvent.currency, + b: bidWonEvent.bidder, + bc: bidWonEvent.bidderCode, + pid: partnerIdFromUserIdConfig || partnerIdFromAnalyticsLabels, + iid: INTEGRATION_ID, + sts: bidWonEvent.requestTimestamp, + rts: bidWonEvent.responseTimestamp, + tr: window.liTreatmentRate, + me: encodeBoolean(window.liModuleEnabled), + liip: encodeBoolean(liveIntentIdsPresent) + }; + + const filteredData = ignoreUndefined(data); + sendData('bid-won', filteredData); } -function createAnalyticsEvent(args, winningBids) { - let payload = { - instanceId: generateUUID(), - url: getRefererInfo().page, - bidsReceived: getAnalyticsEventBids(args.bidsReceived), - auctionStart: args.timestamp, - auctionEnd: args.auctionEnd, - adUnits: [], - userIds: [], - bidders: [] - } - let allUserIds = []; - - if (args.adUnits) { - args.adUnits.forEach(unit => { - if (unit.mediaTypes && unit.mediaTypes.banner) { - payload['adUnits'].push({ - code: unit.code, - mediaType: 'banner', - sizes: getBannerSizes(unit.mediaTypes.banner), - ortb2Imp: unit.ortb2Imp - }); - } - if (unit.bids) { - let userIds = unit.bids.flatMap(getAnalyticsEventUserIds); - allUserIds.push(...userIds); - let bidders = unit.bids.map(({bidder, params}) => { - return { bidder, params } - }); - - payload['bidders'].push(...bidders); - } - }) - let uniqueUserIds = getUniqueBy(allUserIds, 'source'); - payload['userIds'] = uniqueUserIds; - } - payload['winningBids'] = getAnalyticsEventBids(winningBids); - payload['auctionId'] = args.auctionId; - return payload; +function encodeBoolean(value) { + return value === undefined ? undefined : value ? 'y' : 'n' } -function getAnalyticsEventUserIds(bid) { - if (bid && bid.userIdAsEids) { - return bid.userIdAsEids.map(({source, uids, ext}) => { - let analyticsEventUserId = {source, uids, ext}; - return ignoreUndefined(analyticsEventUserId) - }); - } else { return []; } +function checkLiveIntentIdsPresent(bidRequests) { + const eids = bidRequests?.flatMap(r => r?.bids).flatMap(b => b?.userIdAsEids); + return !!eids.find(eid => eid?.source === 'liveintent.com') || !!eids.flatMap(e => e?.uids).find(u => u?.ext?.provider === 'liveintent.com') } -function sendAnalyticsEvent(data) { - ajax(URL, { - success: function () { - logInfo('LiveIntent Prebid Analytics: send data success'); - }, - error: function (e) { - logWarn('LiveIntent Prebid Analytics: send data error' + e); - } - }, JSON.stringify(data), { - contentType: 'application/json', - method: 'POST' - }) +function sendData(path, data) { + const fields = Object.entries(data); + if (fields.length > 0) { + const params = fields.map(([key, value]) => key + '=' + encodeURIComponent(value)).join('&'); + ajax(URL + '/' + path + '?' + params, undefined, null, { method: 'GET' }); + } } function ignoreUndefined(data) { - const filteredData = Object.entries(data).filter(([key, value]) => value) - return Object.fromEntries(filteredData) + const filteredData = Object.entries(data).filter(([key, value]) => isNumber(value) || value); + return Object.fromEntries(filteredData); } -let liAnalytics = Object.assign(adapter({URL, ANALYTICS_TYPE}), { - track({ eventType, args }) { - if (eventType == AUCTION_END && args) { handleAuctionEnd(args); } - } -}); - // save the base class function liAnalytics.originEnableAnalytics = liAnalytics.enableAnalytics; // override enableAnalytics so we can get access to the config passed in from the page liAnalytics.enableAnalytics = function (config) { - bidWonTimeout = config?.options?.bidWonTimeout ?? DEFAULT_BID_WON_TIMEOUT; + const userIdModuleConfig = prebidConfig.getConfig('userSync.userIds').filter(m => m.name == 'liveIntentId')?.at(0)?.params + partnerIdFromUserIdConfig = userIdModuleConfig?.liCollectConfig?.appId || userIdModuleConfig?.distributorId; + sendAuctionInitEvents = config?.options.sendAuctionInitEvents; liAnalytics.originEnableAnalytics(config); // call the base class function }; diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 786feeb8052..2ccbb911478 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -1,387 +1,14 @@ -/** - * This module adds LiveIntentId to the User ID module - * The {@link module:modules/userId} module is required - * @module modules/liveIntentIdSystem - * @requires module:modules/userId - */ -import { triggerPixel, logError } from '../src/utils.js'; -import { ajaxBuilder } from '../src/ajax.js'; -import { submodule } from '../src/hook.js'; -import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports -import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -import {UID1_EIDS} from '../libraries/uid1Eids/uid1Eids.js'; -import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; -import { getRefererInfo } from '../src/refererDetection.js'; - -/** - * @typedef {import('../modules/userId/index.js').Submodule} Submodule - * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig - * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse - */ - -const DEFAULT_AJAX_TIMEOUT = 5000 -const EVENTS_TOPIC = 'pre_lips' -const MODULE_NAME = 'liveIntentId'; -const LI_PROVIDER_DOMAIN = 'liveintent.com'; -export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); -const defaultRequestedAttributes = {'nonId': true} -const calls = { - ajaxGet: (url, onSuccess, onError, timeout) => { - ajaxBuilder(timeout)( - url, - { - success: onSuccess, - error: onError - }, - undefined, - { - method: 'GET', - withCredentials: true - } - ) - }, - pixelGet: (url, onload) => triggerPixel(url, onload) -} - -let eventFired = false; -let liveConnect = null; - -/** - * This function is used in tests - */ -export function reset() { - if (window && window.liQ_instances) { - window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)) - window.liQ_instances = []; - } - liveIntentIdSubmodule.setModuleMode(null) - eventFired = false; - liveConnect = null; -} - -/** - * This function is also used in tests - */ -export function setEventFiredFlag() { - eventFired = true; -} - -function parseLiveIntentCollectorConfig(collectConfig) { - const config = {}; - collectConfig = collectConfig || {} - collectConfig.appId && (config.appId = collectConfig.appId); - collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); - collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); - collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); - config.ajaxTimeout = collectConfig.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; - return config; -} - -/** - * Create requestedAttributes array to pass to liveconnect - * @function - * @param {Object} overrides - object with boolean values that will override defaults { 'foo': true, 'bar': false } - * @returns {Array} - */ -function parseRequestedAttributes(overrides) { - function createParameterArray(config) { - return Object.entries(config).flatMap(([k, v]) => (typeof v === 'boolean' && v) ? [k] : []); - } - if (typeof overrides === 'object') { - return createParameterArray({...defaultRequestedAttributes, ...overrides}) +function loadModule() { + // Load appropriate module based on the build flag. Constant folding ensures + // that the other one will not be included in the bundle. + // eslint-disable-next-line no-constant-condition + if ('$$LIVE_INTENT_MODULE_MODE$$' === 'external') { + // eslint-disable-next-line no-restricted-globals + return require('../libraries/liveIntentId/externalIdSystem.js') } else { - return createParameterArray(defaultRequestedAttributes); - } -} - -function initializeLiveConnect(configParams) { - configParams = configParams || {}; - if (liveConnect) { - return liveConnect; - } - - const publisherId = configParams.publisherId || 'any'; - const identityResolutionConfig = { - publisherId: publisherId, - requestedAttributes: parseRequestedAttributes(configParams.requestedAttributesOverrides) - }; - if (configParams.url) { - identityResolutionConfig.url = configParams.url - } - - identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; - - const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); - - if (!liveConnectConfig.appId && configParams.distributorId) { - liveConnectConfig.distributorId = configParams.distributorId; - identityResolutionConfig.source = configParams.distributorId; - } else { - identityResolutionConfig.source = configParams.partner || 'prebid' - } - - liveConnectConfig.wrapperName = 'prebid'; - liveConnectConfig.trackerVersion = '$prebid.version$'; - liveConnectConfig.identityResolutionConfig = identityResolutionConfig; - liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; - liveConnectConfig.fireEventDelay = configParams.fireEventDelay; - const usPrivacyString = uspDataHandler.getConsentData(); - if (usPrivacyString) { - liveConnectConfig.usPrivacyString = usPrivacyString; + // eslint-disable-next-line no-restricted-globals + return require('../libraries/liveIntentId/idSystem.js') } - const gdprConsent = gdprDataHandler.getConsentData() - if (gdprConsent) { - liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; - liveConnectConfig.gdprConsent = gdprConsent.consentString; - } - const gppConsent = gppDataHandler.getConsentData(); - if (gppConsent) { - liveConnectConfig.gppString = gppConsent.gppString; - liveConnectConfig.gppApplicableSections = gppConsent.applicableSections; - } - // The second param is the storage object, LS & Cookie manipulation uses PBJS - // The third param is the ajax and pixel object, the ajax and pixel use PBJS - liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); - if (configParams.emailHash) { - liveConnect.push({ hash: configParams.emailHash }) - } - return liveConnect; } -function tryFireEvent() { - if (!eventFired && liveConnect) { - const eventDelay = liveConnect.config.fireEventDelay || 500 - setTimeout(() => { - const instances = window.liQ_instances - instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)) - if (!eventFired && liveConnect) { - liveConnect.fire(); - } - }, eventDelay) - } -} - -/** @type {Submodule} */ -export const liveIntentIdSubmodule = { - moduleMode: '$$LIVE_INTENT_MODULE_MODE$$', - /** - * used to link submodule with config - * @type {string} - */ - name: MODULE_NAME, - - setModuleMode(mode) { - this.moduleMode = mode - }, - getInitializer() { - return (liveConnectConfig, storage, calls) => LiveConnect(liveConnectConfig, storage, calls, this.moduleMode) - }, - - /** - * decode the stored id value for passing to bid requests. Note that lipb object is a wrapper for everything, and - * internally it could contain more data other than `lipbid`(e.g. `segments`) depending on the `partner` and - * `publisherId` params. - * @function - * @param {{unifiedId:string}} value - * @param {SubmoduleConfig|undefined} config - * @returns {{lipb:Object}} - */ - decode(value, config) { - const configParams = (config && config.params) || {}; - function composeIdObject(value) { - const result = {}; - - // old versions stored lipbid in unifiedId. Ensure that we can still read the data. - const lipbid = value.nonId || value.unifiedId - if (lipbid) { - value.lipbid = lipbid - delete value.unifiedId - result.lipb = value - } - - // Lift usage of uid2 by exposing uid2 if we were asked to resolve it. - // As adapters are applied in lexicographical order, we will always - // be overwritten by the 'proper' uid2 module if it is present. - if (value.uid2) { - result.uid2 = { 'id': value.uid2, ext: { provider: LI_PROVIDER_DOMAIN } } - } - - if (value.bidswitch) { - result.bidswitch = { 'id': value.bidswitch, ext: { provider: LI_PROVIDER_DOMAIN } } - } - - if (value.medianet) { - result.medianet = { 'id': value.medianet, ext: { provider: LI_PROVIDER_DOMAIN } } - } - - if (value.magnite) { - result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } } - } - - if (value.index) { - result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } - } - - if (value.openx) { - result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } } - } - - if (value.pubmatic) { - result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } } - } - - if (value.sovrn) { - result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } } - } - - if (value.thetradedesk) { - result.lipb = {...result.lipb, tdid: value.thetradedesk} - result.tdid = { 'id': value.thetradedesk, ext: { rtiPartner: 'TDID', provider: getRefererInfo().domain || LI_PROVIDER_DOMAIN } } - delete result.lipb.thetradedesk - } - - return result - } - - if (!liveConnect) { - initializeLiveConnect(configParams); - } - tryFireEvent(); - - return composeIdObject(value); - }, - - /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @returns {IdResponse|undefined} - */ - getId(config) { - const configParams = (config && config.params) || {}; - const liveConnect = initializeLiveConnect(configParams); - if (!liveConnect) { - return; - } - tryFireEvent(); - const result = function(callback) { - liveConnect.resolve( - response => { - callback(response); - }, - error => { - logError(`${MODULE_NAME}: ID fetch encountered an error: `, error); - callback(); - } - ) - } - - return { callback: result }; - }, - eids: { - ...UID1_EIDS, - ...UID2_EIDS, - 'lipb': { - getValue: function(data) { - return data.lipbid; - }, - source: 'liveintent.com', - atype: 3, - getEidExt: function(data) { - if (Array.isArray(data.segments) && data.segments.length) { - return { - segments: data.segments - }; - } - } - }, - 'bidswitch': { - source: 'bidswitch.net', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'medianet': { - source: 'media.net', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'magnite': { - source: 'rubiconproject.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'index': { - source: 'liveintent.indexexchange.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'openx': { - source: 'openx.net', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'pubmatic': { - source: 'pubmatic.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - 'sovrn': { - source: 'liveintent.sovrn.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - } - } -}; - -submodule('userId', liveIntentIdSubmodule); +export const liveIntentIdSubmodule = loadModule() diff --git a/modules/liveIntentRtdProvider.js b/modules/liveIntentRtdProvider.js new file mode 100644 index 00000000000..92cd09ae346 --- /dev/null +++ b/modules/liveIntentRtdProvider.js @@ -0,0 +1,52 @@ +/** + * This module adds the LiveIntent provider to the Real Time Data module (rtdModule). + */ +import { submodule } from '../src/hook.js'; +import {deepAccess, deepSetValue} from '../src/utils.js' + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('../modules/rtdModule/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/rtdModule/index.js').UserConsentData} UserConsentData + */ + +const SUBMODULE_NAME = 'liveintent'; +const GVLID = 148; + +/** + * Init + * @param {Object} config Module configuration + * @param {UserConsentData} userConsent User consent + * @returns true + */ +const init = (config, userConsent) => { + return true; +} + +/** + * onBidRequest is called for each bidder during an auction and contains the bids for that bidder. + * + * @param {Object} bidRequest + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + +function onBidRequest(bidRequest, config, userConsent) { + bidRequest.bids.forEach(bid => { + const providedSegmentsFromUserId = deepAccess(bid, 'userId.lipb.segments', []) + if (providedSegmentsFromUserId.length > 0) { + const providedSegments = { name: 'liveintent.com', segment: providedSegmentsFromUserId.map(id => ({ id })) } + const existingData = deepAccess(bid, 'ortb2.user.data', []) + deepSetValue(bid, 'ortb2.user.data', existingData.concat(providedSegments)) + } + }) +} + +export const liveIntentRtdSubmodule = { + name: SUBMODULE_NAME, + gvlid: GVLID, + init: init, + onBidRequestEvent: onBidRequest +}; + +submodule('realTimeData', liveIntentRtdSubmodule); diff --git a/modules/liveIntentRtdProvider.md b/modules/liveIntentRtdProvider.md new file mode 100644 index 00000000000..e742fb74bee --- /dev/null +++ b/modules/liveIntentRtdProvider.md @@ -0,0 +1,45 @@ +# Overview + +Module Name: LiveIntent Provider +Module Type: Rtd Provider +Maintainer: product@liveIntent.com + +# Description + +This module extracts segments from `bidRequest.userId.lipb.segments` enriched by the userID module and +injects them in `ortb2.user.data` array entry. + +Please visit [LiveIntent](https://www.liveIntent.com/) for more information. + +# Testing + +To run the example and test the Rtd provider: + +```sh +gulp serve --modules=appnexusBidAdapter,rtdModule,liveIntentRtdProvider,userId,liveIntentIdSystem +``` + +Open chrome with this URL: +`http://localhost:9999/integrationExamples/gpt/liveIntentRtdProviderExample.html` + +To run the unit test: +```sh +gulp test --file "test/spec/modules/liveIntentRtdProvider_spec.js" +``` + +# Integration + +```bash +gulp build --modules=userId,liveIntentIdSystem,rtdModule,liveIntentRtdProvider +``` + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders:[{ + name: 'liveintent', + waitForIt: true + }] + } +}); +``` diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index 2797664e954..ec8fea42bac 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { timestamp, logInfo, getWindowTop } from '../src/utils.js'; +import { timestamp, logInfo } from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import { EVENTS, STATUS } from '../src/constants.js'; @@ -77,6 +77,7 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE logInfo('LIVEWRAPPED_BID_RESPONSE:', args); let bidResponse = cache.auctions[args.auctionId].bids[args.requestId]; + if (bidResponse.cpm > args.cpm) break; // For now we only store the highest bid bidResponse.isBid = args.getStatusCode() === STATUS.GOOD; bidResponse.width = args.width; bidResponse.height = args.height; @@ -84,7 +85,7 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE bidResponse.originalCpm = prebidGlobal.convertCurrency(args.originalCpm, args.originalCurrency, args.currency); bidResponse.ttr = args.timeToRespond; bidResponse.readyToSend = 1; - bidResponse.mediaType = args.mediaType == 'native' ? 2 : (args.mediaType == 'video' ? 4 : 1); + bidResponse.mediaType = getMediaTypeEnum(args.mediaType); bidResponse.floorData = args.floorData; bidResponse.meta = args.meta; @@ -115,6 +116,11 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE logInfo('LIVEWRAPPED_BID_WON:', args); let wonBid = cache.auctions[args.auctionId].bids[args.requestId]; wonBid.won = true; + wonBid.width = args.width; + wonBid.height = args.height; + wonBid.cpm = args.cpm; + wonBid.originalCpm = prebidGlobal.convertCurrency(args.originalCpm, args.originalCurrency, args.currency); + wonBid.mediaType = getMediaTypeEnum(args.mediaType); wonBid.floorData = args.floorData; wonBid.rUp = args.rUp; wonBid.meta = args.meta; @@ -171,7 +177,7 @@ livewrappedAnalyticsAdapter.sendEvents = function() { timeouts: getTimeouts(sentRequests.gdpr, sentRequests.auctionIds), bidAdUnits: getbidAdUnits(), rf: getAdRenderFailed(sentRequests.auctionIds), - rcv: getAdblockerRecovered() + ext: initOptions.ext }; if (events.requests.length == 0 && @@ -185,10 +191,8 @@ livewrappedAnalyticsAdapter.sendEvents = function() { ajax(initOptions.endpoint || URL, undefined, JSON.stringify(events), {method: 'POST'}); }; -function getAdblockerRecovered() { - try { - return getWindowTop().I12C && getWindowTop().I12C.Morph === 1; - } catch (e) {} +function getMediaTypeEnum(mediaType) { + return mediaType == 'native' ? 2 : (mediaType == 'video' ? 4 : 1); } function getSentRequests() { diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index cfbd2b5b3b5..aa520afcda6 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -1,9 +1,10 @@ -import {deepAccess, getWindowTop, isSafariBrowser, mergeDeep, isFn, isPlainObject} from '../src/utils.js'; +import {deepAccess, getWindowTop, isSafariBrowser, mergeDeep, isFn, isPlainObject, getWinDimensions} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -69,7 +70,7 @@ export const spec = { bidUrl = bidUrl ? bidUrl.params.bidUrl : URL; url = url ? url.params.url : (getAppDomain() || getTopWindowLocation(bidderRequest)); test = test ? test.params.test : undefined; - const currency = config.getConfig('currency.adServerCurrency') || 'USD'; + const currency = getCurrencyFromBidderRequest(bidderRequest) || 'USD'; var adRequests = bidRequests.map(b => bidToAdRequest(b, currency)); const adRequestsContainFloors = adRequests.some(r => r.flr !== undefined); @@ -324,12 +325,12 @@ function getDeviceIfa() { function getDeviceWidth() { const device = config.getConfig('device') || {}; - return device.w || window.innerWidth; + return device.w || getWinDimensions().innerWidth; } function getDeviceHeight() { const device = config.getConfig('device') || {}; - return device.h || window.innerHeight; + return device.h || getWinDimensions().innerHeight; } function getCoppa() { diff --git a/modules/lkqdBidAdapter.js b/modules/lkqdBidAdapter.js index 1dbe89f5a49..6c97f64e6a8 100644 --- a/modules/lkqdBidAdapter.js +++ b/modules/lkqdBidAdapter.js @@ -47,7 +47,7 @@ export const spec = { const GDPR = BIDDER_GDPR || bid.params.gdpr || null; const GDPRS = BIDDER_GDPRS || bid.params.gdprs || null; const DNT = bid.params.dnt || null; - const BID_FLOOR = bid.params.flrd > bid.params.flrmp ? bid.params.flrd : bid.params.flrmp; + const BID_FLOOR = 0; const VIDEO_BID = bid.video ? bid.video : {}; const requestData = { @@ -67,7 +67,7 @@ export const spec = { }, test: 0, at: 2, - tmax: bid.params.timeout || config.getConfig('bidderTimeout') || 100, + tmax: bidderRequest.timeout, cur: ['USD'], regs: { ext: { @@ -157,7 +157,6 @@ export const spec = { h: sizes[1], skip: VIDEO_BID.skip || 0, playbackmethod: VIDEO_BID.playbackmethod || [1], - placement: (bid.params.execution === 'outstream' || VIDEO_BID.context === 'outstream') ? 5 : 1, ext: { lkqdcustomparameters: {} }, diff --git a/modules/lm_kiviadsBidAdapter.js b/modules/lm_kiviadsBidAdapter.js index 7c3085047c4..7295eb33258 100644 --- a/modules/lm_kiviadsBidAdapter.js +++ b/modules/lm_kiviadsBidAdapter.js @@ -1,213 +1,16 @@ -import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; -import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {buildRequests, getUserSyncs, interpretResponse, isBidRequestValid} from '../libraries/xeUtils/bidderUtils.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 - */ - -const CUR = 'USD'; const BIDDER_CODE = 'lm_kiviads'; const ENDPOINT = 'https://pbjs.kiviads.live'; -/** - * 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(req) { - if (req && typeof req.params !== 'object') { - logError('Params is not defined or is incorrect in the bidder settings'); - return false; - } - - if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { - logError('Env or pid is not present in bidder params'); - return false; - } - - if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { - logError('mediaTypes.video.playerSize is required for video'); - return false; - } - - return true; -} - -/** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ -function buildRequests(validBidRequests, bidderRequest) { - const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; - const requests = validBidRequests.map(req => { - const request = {}; - request.bidId = req.bidId; - request.banner = deepAccess(req, 'mediaTypes.banner'); - request.auctionId = req.ortb2?.source?.tid; - request.transactionId = req.ortb2Imp?.ext?.tid; - request.sizes = parseSizesInput(getAdUnitSizes(req)); - request.schain = req.schain; - request.location = { - page: refererInfo.page, - location: refererInfo.location, - domain: refererInfo.domain, - whost: window.location.host, - ref: refererInfo.ref, - isAmp: refererInfo.isAmp - }; - request.device = { - ua: navigator.userAgent, - lang: navigator.language - }; - request.env = { - env: req.params.env, - pid: req.params.pid - }; - request.ortb2 = req.ortb2; - request.ortb2Imp = req.ortb2Imp; - request.tz = new Date().getTimezoneOffset(); - request.ext = req.params.ext; - request.bc = req.bidRequestsCount; - request.floor = getBidFloor(req); - - if (req.userIdAsEids && req.userIdAsEids.length !== 0) { - request.userEids = req.userIdAsEids; - } else { - request.userEids = []; - } - if (gdprConsent.gdprApplies) { - request.gdprApplies = Number(gdprConsent.gdprApplies); - request.consentString = gdprConsent.consentString; - } else { - request.gdprApplies = 0; - request.consentString = ''; - } - if (uspConsent) { - request.usPrivacy = uspConsent; - } else { - request.usPrivacy = ''; - } - if (config.getConfig('coppa')) { - request.coppa = 1; - } else { - request.coppa = 0; - } - - const video = deepAccess(req, 'mediaTypes.video'); - if (video) { - request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); - request.video = video; - } - - return request; - }); - - return { - method: 'POST', - url: ENDPOINT + '/bid', - data: JSON.stringify(requests), - withCredentials: true, - bidderRequest, - options: { - contentType: 'application/json', - } - }; -} - -/** - * 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, {bidderRequest}) { - const response = []; - if (!isArray(deepAccess(serverResponse, 'body.data'))) { - return response; - } - - serverResponse.body.data.forEach(serverBid => { - const bid = { - requestId: bidderRequest.bidId, - dealId: bidderRequest.dealId || null, - ...serverBid - }; - response.push(bid); - }); - - return response; -} - -/** - * 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. - */ -function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { - const syncs = []; - const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); - - if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { - const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; - const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; - const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; - - pixels.forEach(pixel => { - const [type, url] = pixel; - const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; - if (type === 'iframe' && syncOptions.iframeEnabled) { - syncs.push(sync) - } else if (type === 'image' && syncOptions.pixelEnabled) { - syncs.push(sync) - } - }); - } - - return syncs; -} - -/** - * Get valid floor value from getFloor fuction. - * - * @param {Object} bid Current bid request. - * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. - */ -export function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return null; - } - - let floor = bid.getFloor({ - currency: CUR, - mediaType: '*', - size: '*' - }); - - if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { - return floor.floor; - } - - return null; -} - export const spec = { code: BIDDER_CODE, aliases: ['kivi'], supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, - buildRequests, + buildRequests: (validBidRequests, bidderRequest) => buildRequests(validBidRequests, bidderRequest, ENDPOINT), interpretResponse, getUserSyncs } diff --git a/modules/lmpIdSystem.js b/modules/lmpIdSystem.js new file mode 100644 index 00000000000..b6dcae3118b --- /dev/null +++ b/modules/lmpIdSystem.js @@ -0,0 +1,61 @@ +/** + * This module adds lmpId support to the User ID module + * The {@link module:modules/userId} module is required. + * @module modules/lmpIdSystem + * @requires module:modules/userId + */ + +import { submodule } from '../src/hook.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const MODULE_NAME = 'lmpid'; +const STORAGE_KEY = '__lmpid'; +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); + +function readFromLocalStorage() { + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(STORAGE_KEY) : null; +} + +function getLmpid() { + return window[STORAGE_KEY] || readFromLocalStorage(); +} + +/** @type {Submodule} */ +export const lmpIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param { string | undefined } value + * @return { {lmpid: string} | undefined } + */ + decode(value) { + return value ? { lmpid: value } : undefined; + }, + + /** + * Retrieve the LMPID + * @function + * @param {SubmoduleConfig} config + * @return {{id: string | undefined} | undefined} + */ + getId(config) { + const id = getLmpid(); + return id ? { id } : undefined; + }, + + eids: { + 'lmpid': { + source: 'loblawmedia.ca', + atype: 3 + }, + } +}; + +submodule('userId', lmpIdSubmodule); diff --git a/modules/lmpIdSystem.md b/modules/lmpIdSystem.md new file mode 100644 index 00000000000..a56c9dbb3d6 --- /dev/null +++ b/modules/lmpIdSystem.md @@ -0,0 +1,27 @@ +# LMPID + +The Loblaw Media Private ID (LMPID) is the Loblaw Advance identity solution deployed by its media partners. LMPID leverages encrypted user registration information to provide a privacy-conscious, secure, and reliable identifier to power Loblaw Advance's digital advertising ecosystem. + +## LMPID Registration + +If you're a media company looking to partner with Loblaw Advance, please reach out to us through our [Contact page](https://www.loblawadvance.ca/contact-us) + +## LMPID Configuration + +First, make sure to add the LMPID submodule to your Prebid.js package with: + +``` +gulp build --modules=lmpIdSystem,userId +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'lmpid' + }] + } +}); +``` diff --git a/modules/lockrAIMIdSystem.js b/modules/lockrAIMIdSystem.js index de0435a2255..28b6698c5c5 100644 --- a/modules/lockrAIMIdSystem.js +++ b/modules/lockrAIMIdSystem.js @@ -10,7 +10,6 @@ import { ajax } from '../src/ajax.js'; import { logInfo, logWarn } from '../src/utils.js'; import { getStorageManager } from '../src/storageManager.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; -import { gppDataHandler } from '../src/adapterManager.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -57,12 +56,12 @@ export const lockrAIMSubmodule = { * @returns {lockrAIMId} */ getId(config, consentData) { - if (consentData?.gdprApplies === true) { + if (consentData?.gdpr?.gdprApplies === true) { _logWarn('lockrAIM is not intended for use where GDPR applies. The lockrAIM module will not run'); return undefined; } - const gppConsent = gppDataHandler.getConsentData(); + const gppConsent = consentData?.gpp; let gppString = ''; if (gppConsent) { gppString = gppConsent.gppString; @@ -137,7 +136,6 @@ class LockrAIMApiClient { } }); LockrAIMApiClient.canRefreshToken = true; - return; } catch (_err) { this._logWarn(_err); rejectPromise(responseText); diff --git a/modules/loganBidAdapter.js b/modules/loganBidAdapter.js index 7aa82e3046c..4e2652e452f 100644 --- a/modules/loganBidAdapter.js +++ b/modules/loganBidAdapter.js @@ -1,54 +1,18 @@ -import { isFn, deepAccess, getWindowTop } from '../src/utils.js'; +import { getWindowTop } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { buildUserSyncs, interpretResponse, isBidRequestValid, getBidFloor, consentCheck } from '../libraries/precisoUtils/bidUtilsCommon.js'; const BIDDER_CODE = 'logan'; const AD_URL = 'https://USeast2.logan.ai/pbjs'; -const SYNC_URL = 'https://ssp-cookie.logan.ai' - -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency || !bid.meta) { - return false; - } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastXml || bid.vastUrl); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers); - default: - return false; - } -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} +const SYNC_URL = 'https://ssp-cookie.logan.ai'; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && bid.params.placementId); - }, + isBidRequestValid: isBidRequestValid, buildRequests: (validBidRequests = [], bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition @@ -67,14 +31,7 @@ export const spec = { placements: placements }; - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent; - } - } + consentCheck(bidderRequest, request); const len = validBidRequests.length; for (let i = 0; i < len; i++) { @@ -99,6 +56,7 @@ export const spec = { placement.protocols = mediaType[VIDEO].protocols; placement.startdelay = mediaType[VIDEO].startdelay; placement.placement = mediaType[VIDEO].placement; + placement.plcmt = mediaType[VIDEO].plcmt; placement.skip = mediaType[VIDEO].skip; placement.skipafter = mediaType[VIDEO].skipafter; placement.minbitrate = mediaType[VIDEO].minbitrate; @@ -122,42 +80,11 @@ export const spec = { }; }, - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - + interpretResponse: interpretResponse, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; + return buildUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, SYNC_URL); } + }; registerBidder(spec); diff --git a/modules/logicadBidAdapter.js b/modules/logicadBidAdapter.js index fe4dd83c9e2..e7c5300d072 100644 --- a/modules/logicadBidAdapter.js +++ b/modules/logicadBidAdapter.js @@ -46,7 +46,7 @@ export const spec = { if (fledgeAuctionConfigs.length) { return { bids, - fledgeAuctionConfigs, + paapi: fledgeAuctionConfigs, }; } @@ -74,7 +74,7 @@ function newBidRequest(bidRequest, bidderRequest) { mediaTypes: bidRequest.mediaTypes, } - const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + const fledgeEnabled = deepAccess(bidderRequest, 'paapi.enabled') if (fledgeEnabled) { const ae = deepAccess(bidRequest, 'ortb2Imp.ext.ae'); if (ae) { diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 64d631c2469..13cb19d9d93 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -11,13 +11,11 @@ import { isBoolean, buildUrl, isEmpty, - isArray, - isEmptyStr + isArray } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; -import { uspDataHandler } from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; /** @@ -38,16 +36,24 @@ const MISSING_CORE_CONSENT = 111; const GVLID = 95; const ID_HOST = 'id.crwdcntrl.net'; const ID_HOST_COOKIELESS = 'c.ltmsphrcl.net'; +const DO_NOT_HONOR_CONFIG = false; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); let cookieDomain; +let appliedConfig = { + name: 'lotamePanoramaId', + storage: { + type: 'cookie&html5', + name: 'panoramaId' + } +}; /** * Set the Lotame First Party Profile ID in the first party namespace * @param {String} profileId */ function setProfileId(profileId) { - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled()) { let expirationDate = new Date(timestamp() + NINE_MONTHS_MS).toUTCString(); storage.setCookie( KEY_PROFILE, @@ -58,7 +64,7 @@ function setProfileId(profileId) { undefined ); } - if (storage.hasLocalStorage()) { + if (localStorageIsEnabled()) { storage.setDataInLocalStorage(KEY_PROFILE, profileId, undefined); } } @@ -68,10 +74,10 @@ function setProfileId(profileId) { */ function getProfileId() { let profileId; - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) { profileId = storage.getCookie(KEY_PROFILE, undefined); } - if (!profileId && storage.hasLocalStorage()) { + if (!profileId && localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) { profileId = storage.getDataFromLocalStorage(KEY_PROFILE, undefined); } return profileId; @@ -83,21 +89,11 @@ function getProfileId() { */ function getFromStorage(key) { let value = null; - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) { value = storage.getCookie(key, undefined); } - if (storage.hasLocalStorage() && value === null) { - const storedValueExp = storage.getDataFromLocalStorage( - `${key}_exp`, undefined - ); - - if (storedValueExp === '' || storedValueExp === null) { - value = storage.getDataFromLocalStorage(key, undefined); - } else if (storedValueExp) { - if ((new Date(parseInt(storedValueExp, 10))).getTime() - Date.now() > 0) { - value = storage.getDataFromLocalStorage(key, undefined); - } - } + if (value === null && localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) { + value = storage.getDataFromLocalStorage(key, undefined); } return value; } @@ -115,7 +111,7 @@ function saveLotameCache( ) { if (key && value) { let expirationDate = new Date(expirationTimestamp).toUTCString(); - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled()) { storage.setCookie( key, value, @@ -125,12 +121,7 @@ function saveLotameCache( undefined ); } - if (storage.hasLocalStorage()) { - storage.setDataInLocalStorage( - `${key}_exp`, - String(expirationTimestamp), - undefined - ); + if (localStorageIsEnabled()) { storage.setDataInLocalStorage(key, value, undefined); } } @@ -172,7 +163,7 @@ function getLotameLocalCache(clientId = undefined) { */ function clearLotameCache(key) { if (key) { - if (storage.cookiesAreEnabled()) { + if (cookiesAreEnabled(DO_NOT_HONOR_CONFIG)) { let expirationDate = new Date(0).toUTCString(); storage.setCookie( key, @@ -183,11 +174,50 @@ function clearLotameCache(key) { undefined ); } - if (storage.hasLocalStorage()) { + if (localStorageIsEnabled(DO_NOT_HONOR_CONFIG)) { storage.removeDataFromLocalStorage(key, undefined); } } } +/** + * @param {boolean} honorConfig - false to override for reading or deleting old cookies + * @returns {boolean} for whether we can write the cookie + */ +function cookiesAreEnabled(honorConfig = true) { + if (honorConfig) { + return storage.cookiesAreEnabled() && appliedConfig.storage.type.includes('cookie'); + } + return storage.cookiesAreEnabled(); +} +/** + * @param {boolean} honorConfig - false to override for reading or deleting old stored items + * @returns {boolean} for whether we can write the cookie + */ +function localStorageIsEnabled(honorConfig = true) { + if (honorConfig) { + return storage.hasLocalStorage() && appliedConfig.storage.type.includes('html5'); + } + return storage.hasLocalStorage(); +} +/** + * @param {SubmoduleConfig} config + * @returns {null|string} - string error if it finds one, null otherwise. + */ +function checkConfigHasErrorsAndReport(config) { + let error = null; + if (typeof config.storage !== 'undefined') { + Object.assign(appliedConfig.storage, appliedConfig.storage, config.storage); + const READABLE_MODULE_NAME = 'Lotame ID module'; + const PERMITTED_STORAGE_TYPES = ['cookie', 'html5', 'cookie&html5']; + if (typeof config.storage.name !== 'undefined' && config.storage.name !== KEY_ID) { + logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.name" is expected to be "${KEY_ID}", actual is "${config.storage.name}"`); + error = true; + } else if (config.storage.type !== 'undefined' && !PERMITTED_STORAGE_TYPES.includes(config.storage.type)) { + logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.type" is expected to be one of "${PERMITTED_STORAGE_TYPES.join(', ')}", actual is "${config.storage.type}"`); + } + } + return error; +} /** @type {Submodule} */ export const lotamePanoramaIdSubmodule = { /** @@ -222,6 +252,9 @@ export const lotamePanoramaIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config, consentData, cacheIdObj) { + if (checkConfigHasErrorsAndReport(config)) { + return; + } cookieDomain = lotamePanoramaIdSubmodule.findRootDomain(); const configParams = (config && config.params) || {}; const clientId = configParams.clientId; @@ -249,18 +282,6 @@ export const lotamePanoramaIdSubmodule = { const storedUserId = getProfileId(); - // Add CCPA Consent data handling - const usp = uspDataHandler.getConsentData(); - - let usPrivacy; - if (typeof usp !== 'undefined' && !isEmpty(usp) && !isEmptyStr(usp)) { - usPrivacy = usp; - } - if (!usPrivacy) { - // fallback to 1st party cookie - usPrivacy = getFromStorage('us_privacy'); - } - const getRequestHost = function() { if (navigator.userAgent && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { return ID_HOST_COOKIELESS; @@ -276,27 +297,15 @@ export const lotamePanoramaIdSubmodule = { let consentString; if (consentData) { - if (isBoolean(consentData.gdprApplies)) { - queryParams.gdpr_applies = consentData.gdprApplies; + if (isBoolean(consentData.gdpr?.gdprApplies)) { + queryParams.gdpr_applies = consentData.gdpr.gdprApplies; } - consentString = consentData.consentString; - } - // If no consent string, try to read it from 1st party cookies - if (!consentString) { - consentString = getFromStorage('eupubconsent-v2'); - } - if (!consentString) { - consentString = getFromStorage('euconsent-v2'); + consentString = consentData.gdpr?.consentString; } if (consentString) { queryParams.gdpr_consent = consentString; } - // Add usPrivacy to the url - if (usPrivacy) { - queryParams.us_privacy = usPrivacy; - } - // Add clientId to the url if (hasCustomClientId) { queryParams.c = clientId; diff --git a/modules/lotamePanoramaIdSystem.md b/modules/lotamePanoramaIdSystem.md index e960f4b5695..1fbc3f561c7 100644 --- a/modules/lotamePanoramaIdSystem.md +++ b/modules/lotamePanoramaIdSystem.md @@ -17,9 +17,32 @@ Retrieve the Lotame Panorama Id pbjs.setConfig({ usersync: { userIds: [ - { - name: 'lotamePanoramaId' // The only parameter that is needed - }], + { + name: 'lotamePanoramaId', + storage: { + name: 'panoramaId', + type: 'cookie&html5', + expires: 7 + } + } + ], } }); -``` \ No newline at end of file +``` + +| Parameters under `userSync.userIds[]` | Scope | Type | Description | Example | +| ---| --- | --- | --- | --- | +| name | Required | String | Name for the Lotame ID submodule | `"lotamePanoramaId"` | +| storage | Optional | Object | Configures how to cache User IDs locally in the browser | See [storage settings](#storage-settings) | + + +### Storage Settings + +The following settings are available for the `storage` property in the `userSync.userIds[]` object. Please note that inclusion of the `storage` property is optional, but if provided, all three attributes listed below *must* be specified: + +| Param name | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String| Name of the cookie or localStorage where the user ID will be stored; *must* be `"panoramaId"` | `"panoramaId"` | +| type | Required | String | `"cookie&html5"` (preferred) or `"cookie"` or `"html5"` | `"cookie&html5"` | +| expires | Required | Number | How long (in days) the user ID information will be stored. Lotame recommends `7`. | `7` | + diff --git a/modules/loyalBidAdapter.js b/modules/loyalBidAdapter.js index 30fdeb44233..e34ec89cf35 100644 --- a/modules/loyalBidAdapter.js +++ b/modules/loyalBidAdapter.js @@ -1,190 +1,17 @@ -import { logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'loyal'; const AD_URL = 'https://us-east-1.loyal.app/pbjs'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor, - eids: [] - }; - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - tmax: bidderRequest.timeout - }; - - if (bidderRequest.gdprConsent?.consentString) { - request.gdpr = { - consentString: bidderRequest.gdprConsent.consentString - }; - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse }; registerBidder(spec); diff --git a/modules/luceadBidAdapter.js b/modules/luceadBidAdapter.js old mode 100644 new mode 100755 index ab7f96c4e60..ffc2307bcb8 --- a/modules/luceadBidAdapter.js +++ b/modules/luceadBidAdapter.js @@ -1,40 +1,42 @@ +/** + * @module modules/luceadBidAdapter + */ + import {ortbConverter} from '../libraries/ortbConverter/converter.js'; -import {loadExternalScript} from '../src/adloader.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getUniqueIdentifierStr, logInfo, deepSetValue} from '../src/utils.js'; +import {getUniqueIdentifierStr, deepSetValue, logInfo} from '../src/utils.js'; import {fetch} from '../src/ajax.js'; +const gvlid = 1309; const bidderCode = 'lucead'; -const bidderName = 'Lucead'; -let baseUrl = 'https://lucead.com'; -let staticUrl = 'https://s.lucead.com'; -let companionUrl = 'https://cdn.jsdelivr.net/gh/lucead/prebid-js-external-js-lucead@master/dist/prod.min.js'; -let endpointUrl = 'https://prebid.lucead.com/go'; const defaultCurrency = 'EUR'; const defaultTtl = 500; const aliases = ['adliveplus']; +const defaultRegion = 'eu'; +const domain = 'lucead.com' +let baseUrl = `https://${domain}`; +let staticUrl = `https://s.${domain}`; +let endpointUrl = baseUrl; function isDevEnv() { - return location.hash.includes('prebid-dev') || location.href.startsWith('https://ayads.io/test'); + return location.hash.includes('prebid-dev'); } function isBidRequestValid(bidRequest) { return !!bidRequest?.params?.placementId; } -export function log(msg, obj) { - logInfo(`${bidderName} - ${msg}`, obj); -} - function buildRequests(bidRequests, bidderRequest) { + const region = bidRequests[0]?.params?.region || defaultRegion; + endpointUrl = `https://${region}.${domain}`; + if (isDevEnv()) { baseUrl = location.origin; staticUrl = baseUrl; - companionUrl = `${staticUrl}/dist/prebid-companion.js`; - endpointUrl = `${baseUrl}/go`; + endpointUrl = `${baseUrl}`; } - log('buildRequests', { + logInfo('buildRequests', { bidRequests, bidderRequest, }); @@ -50,107 +52,134 @@ function buildRequests(bidRequests, bidderRequest) { getUniqueIdentifierStr, ortbConverter, deepSetValue, + is_sra: true, + region, }; - loadExternalScript(companionUrl, bidderCode, () => window.ayads_prebid && window.ayads_prebid(companionData)); + window.lucead_prebid_data = companionData; + const fn = window.lucead_prebid; + + if (fn && typeof fn === 'function') { + fn(companionData); + } - return bidRequests.map(bidRequest => ({ + return { method: 'POST', - url: `${endpointUrl}/prebid/sub`, + url: `${endpointUrl}/go/prebid/sra`, data: JSON.stringify({ request_id: bidderRequest.bidderRequestId, domain: location.hostname, - bid_id: bidRequest.bidId, - sizes: bidRequest.sizes, - media_types: bidRequest.mediaTypes, - fledge_enabled: bidderRequest.fledgeEnabled, - enable_contextual: bidRequest?.params?.enableContextual !== false, - enable_pa: bidRequest?.params?.enablePA !== false, - params: bidRequest.params, + bid_requests: bidRequests.map(bidRequest => { + return { + bid_id: bidRequest.bidId, + sizes: bidRequest.sizes, + media_types: bidRequest.mediaTypes, + placement_id: bidRequest.params.placementId, + schain: bidRequest.schain, + }; + }), }), options: { contentType: 'text/plain', withCredentials: false }, - })); + }; } function interpretResponse(serverResponse, bidRequest) { // @see required fields https://docs.prebid.org/dev-docs/bidder-adaptor.html - const response = serverResponse.body; - const bidRequestData = JSON.parse(bidRequest.data); - - const bids = response.enable_contextual !== false ? [{ - requestId: response?.bid_id || '1', // bid request id, the bid id - cpm: response?.cpm || 0, - width: (response?.size && response?.size?.width) || 300, - height: (response?.size && response?.size?.height) || 250, - currency: response?.currency || defaultCurrency, - ttl: response?.ttl || defaultTtl, - creativeId: response.ssp ? `ssp:${response.ssp}` : (response?.ad_id || '0'), - netRevenue: response?.netRevenue || true, - ad: response?.ad || '', + const response = serverResponse?.body; + const bidRequestData = JSON.parse(bidRequest?.data); + + const bids = (response?.bids || []).map(bid => ({ + requestId: bid?.bid_id || '1', // bid request id, the bid id + cpm: bid?.cpm || 0, + width: (bid?.size && bid?.size?.width) || 300, + height: (bid?.size && bid?.size?.height) || 250, + currency: bid?.currency || defaultCurrency, + ttl: bid?.ttl || defaultTtl, + creativeId: bid?.ssp ? `ssp:${bid.ssp}` : `${bid?.ad_id || 0}:${bid?.ig_id || 0}`, + netRevenue: bid?.net_revenue || true, + ad: bid?.ad || '', meta: { - advertiserDomains: response?.advertiserDomains || [], + advertiserDomains: bid?.advertiser_domains || [], }, - }] : null; - - log('interpretResponse', {serverResponse, bidRequest, bidRequestData, bids}); - - if (response.enable_pa === false) { return bids; } - - const fledgeAuctionConfig = { - seller: baseUrl, - decisionLogicUrl: `${baseUrl}/js/ssp.js`, - interestGroupBuyers: [baseUrl], - perBuyerSignals: {}, - auctionSignals: { - size: bidRequestData.sizes ? {width: bidRequestData?.sizes[0][0] || 300, height: bidRequestData?.sizes[0][1] || 250} : null, - }, - }; + })); - const fledgeAuctionConfigs = [{bidId: response.bid_id, config: fledgeAuctionConfig}]; + logInfo('interpretResponse', {serverResponse, bidRequest, bidRequestData, bids}); + + if (response?.enable_pa === false) { return bids; } + + const fledgeAuctionConfigs = (response.bids || []).map(bid => ({ + bidId: bid?.bid_id, + config: { + seller: baseUrl, + decisionLogicUrl: `${baseUrl}/js/ssp.js`, + interestGroupBuyers: [baseUrl], + requestedSize: bid?.size, + auctionSignals: { + size: bid?.size, + }, + perBuyerSignals: { + [baseUrl]: { + prebid_paapi: true, + prebid_bid_id: bid?.bid_id, + prebid_request_id: bidRequestData.request_id, + placement_id: bid.placement_id, + // floor, + is_sra: true, + endpoint_url: endpointUrl, + }, + } + } + })); - return {bids, fledgeAuctionConfigs}; + return {bids, paapi: fledgeAuctionConfigs}; } -function report(type = 'impression', data = {}) { +function report(type, data) { // noinspection JSCheckFunctionSignatures - return fetch(`${endpointUrl}/report/${type}`, { - body: JSON.stringify(data), + return fetch(`${endpointUrl}/go/report/${type}`, { + body: JSON.stringify({ + ...data, + domain: location.hostname, + }), method: 'POST', - contentType: 'text/plain' + contentType: 'text/plain', }); } function onBidWon(bid) { - log('Bid won', bid); + logInfo('Bid won', bid); let data = { bid_id: bid?.bidId, - placement_id: bid?.params ? bid?.params[0]?.placementId : 0, + placement_id: bid.params ? (bid?.params[0]?.placementId || '0') : '0', spent: bid?.cpm, currency: bid?.currency, }; - if (bid.creativeId) { - if (bid.creativeId.toString().startsWith('ssp:')) { - data.ssp = bid.creativeId.split(':')[1]; + if (bid?.creativeId) { + const parts = bid.creativeId.toString().split(':'); + + if (parts[0] === 'ssp') { + data.ssp = parts[1]; } else { - data.ad_id = bid.creativeId; + data.ad_id = parts[0] + data.ig_id = parts[1] } } - return report(`impression`, data); + return report('impression', data); } function onTimeout(timeoutData) { - log('Timeout from adapter', timeoutData); + logInfo('Timeout from adapter', timeoutData); } export const spec = { code: bidderCode, - // gvlid: BIDDER_GVLID, + gvlid, aliases, isBidRequestValid, buildRequests, diff --git a/modules/luceadBidAdapter.md b/modules/luceadBidAdapter.md old mode 100644 new mode 100755 index 953c911cd2b..41e3730897a --- a/modules/luceadBidAdapter.md +++ b/modules/luceadBidAdapter.md @@ -1,29 +1,41 @@ -# Overview +# Lucead Bid Adapter -Module Name: Lucead Bidder Adapter +- Module Name: Lucead Bidder Adapter +- Module Type: Bidder Adapter +- Maintainer: prebid@lucead.com -Module Type: Bidder Adapter +## Description -Maintainer: prebid@lucead.com +Module that connects to Lucead demand source. -# Description +## Adapter configuration -Module that connects to Lucead demand source to fetch bids. +## Ad units parameters -# Test Parameters +### Type definition + +```typescript +type Params = { + placementId: string; + region?: 'eu' | 'us' | 'ap'; +}; ``` -const adUnits = [ - { - code: 'test-div', - sizes: [[300, 250]], - bids: [ - { - bidder: 'lucead', - params: { - placementId: '2', - } - } - ] - } - ]; + +### Example code +```javascript +const adUnits=[ + { + code:'test-div', + sizes:[[300,250]], + bids:[ + { + bidder: 'lucead', + params:{ + placementId: '1', + region: 'us', // optional: 'eu', 'us', 'ap' + } + } + ] + } +]; ``` diff --git a/modules/lunamediahbBidAdapter.js b/modules/lunamediahbBidAdapter.js index 66838014e18..6ad42a4f3ca 100644 --- a/modules/lunamediahbBidAdapter.js +++ b/modules/lunamediahbBidAdapter.js @@ -1,140 +1,19 @@ -import { logMessage } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'lunamediahb'; const AD_URL = 'https://balancer.lmgssp.com/?c=o&m=multi'; const SYNC_URL = 'https://cookie.lmgssp.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; - } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl) || Boolean(bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers); - default: - return false; - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let winTop = window; - let location; - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - - const placements = []; - const request = { - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - 'secure': 1, - 'host': location.host, - 'page': location.pathname, - 'placements': placements - }; - - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent - } - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - const placement = { - placementId: bid.params.placementId, - bidId: bid.bidId, - schain: bid.schain || {}, - }; - const mediaType = bid.mediaTypes - - if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { - placement.sizes = mediaType[BANNER].sizes; - placement.traffic = BANNER; - } else if (mediaType && mediaType[VIDEO]) { - if (mediaType[VIDEO].playerSize) { - placement.wPlayer = mediaType[VIDEO].playerSize[0]; - placement.hPlayer = mediaType[VIDEO].playerSize[1]; - } - placement.traffic = VIDEO; - placement.videoContext = mediaType[VIDEO].context || 'instream' - } else if (mediaType && mediaType[NATIVE]) { - placement.native = mediaType[NATIVE]; - placement.traffic = NATIVE; - } - placements.push(placement); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/luponmediaBidAdapter.js b/modules/luponmediaBidAdapter.js index 20fa601bade..3dab4524db1 100755 --- a/modules/luponmediaBidAdapter.js +++ b/modules/luponmediaBidAdapter.js @@ -6,15 +6,17 @@ import { isArray, isEmpty, isFn, + isPlainObject, logError, logMessage, logWarn, - parseSizesInput + parseSizesInput, + sizeTupleToRtbSize, + sizesToSizeTuples } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER} from '../src/mediaTypes.js'; -import {ajax} from '../src/ajax.js'; const BIDDER_CODE = 'luponmedia'; const ENDPOINT_URL = 'https://rtb.adxpremium.services/openrtb2/auction'; @@ -170,7 +172,11 @@ export const spec = { netRevenue: false, ttl: 300, referrer: parsedReferrer, - ad: bid.adm + ad: bid.adm, + adomain: bid.adomain || [], + meta: { + advertiserDomains: bid && bid.adomain ? bid.adomain : [] + } }; bidResponses.push(newBid); @@ -220,19 +226,6 @@ export const spec = { hasSynced = true; return allUserSyncs; - }, - onBidWon: bid => { - const bidString = JSON.stringify(bid); - spec.sendWinningsToServer(bidString); - }, - sendWinningsToServer: data => { - let mutation = `mutation {createWin(input: {win: {eventData: "${window.btoa(data)}"}}) {win {createTime } } }`; - let dataToSend = JSON.stringify({ query: mutation }); - - ajax('https://analytics.adxpremium.services/graphql', null, dataToSend, { - contentType: 'application/json', - method: 'POST' - }); } }; @@ -285,16 +278,8 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { let bannerSizes = []; if (bannerParams && bannerParams.sizes) { - const sizes = parseSizesInput(bannerParams.sizes); - // get banner sizes in form [{ w: , h: }, ...] - const format = sizes.map(size => { - const [ width, height ] = size.split('x'); - const w = parseInt(width, 10); - const h = parseInt(height, 10); - return { w, h }; - }); - + const format = sizesToSizeTuples(bannerParams.sizes).map(sizeTupleToRtbSize); bannerSizes = format; } @@ -340,7 +325,7 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { } catch (e) { logError('LuponMedia: getFloor threw an error: ', e); } - bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; + bidFloor = isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; } else { bidFloor = parseFloat(deepAccess(bidRequest, 'params.floor')); } @@ -469,9 +454,7 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); } - // TODO: bidRequest.fpd is not the right place for pbadslot - who's filling that in, if anyone? - // is this meant to be bidRequest.ortb2Imp.ext.data.pbadslot? - const pbAdSlot = deepAccess(bidRequest, 'fpd.context.pbAdSlot'); + const pbAdSlot = deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); if (typeof pbAdSlot === 'string' && pbAdSlot) { deepSetValue(data.imp[0].ext, 'context.data.adslot', pbAdSlot); } diff --git a/modules/mabidderBidAdapter.js b/modules/mabidderBidAdapter.js index 632403c6643..6df68bda269 100644 --- a/modules/mabidderBidAdapter.js +++ b/modules/mabidderBidAdapter.js @@ -1,9 +1,11 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'mabidder'; export const baseUrl = 'https://prebid.ecdrsvc.com/bid'; +const converter = ortbConverter({}) + export const spec = { supportedMediaTypes: [BANNER], code: BIDDER_CODE, @@ -14,7 +16,8 @@ export const spec = { return !!(bid.params.ppid && bid.sizes && Array.isArray(bid.sizes) && Array.isArray(bid.sizes[0])) }, buildRequests: function(validBidRequests, bidderRequest) { - const fpd = bidderRequest.ortb2; + const fpd = converter.toORTB({ bidRequests: validBidRequests, bidderRequest: bidderRequest }); + const bids = []; validBidRequests.forEach(bidRequest => { const sizes = []; @@ -35,7 +38,7 @@ export const spec = { url: baseUrl, method: 'POST', data: { - v: getGlobal().version, + v: 'v' + '$prebid.version$', bids: bids, url: bidderRequest.refererInfo.page || '', referer: bidderRequest.refererInfo.ref || '', diff --git a/modules/madsenseBidAdapter.js b/modules/madsenseBidAdapter.js new file mode 100644 index 00000000000..ba866a9314e --- /dev/null +++ b/modules/madsenseBidAdapter.js @@ -0,0 +1,264 @@ +import { + logError, + logWarn, + logMessage, + deepSetValue, + mergeDeep, +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'madsense'; +const DEFAULT_BID_TTL = 55; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_CURRENCY = 'USD'; +const MS_EXCHANGE_BASE_URL = 'https://ads.madsense.io/pbjs'; + +const buildImpWithDefaults = (buildImp, bidRequest, context) => { + const imp = buildImp(bidRequest, context); + + imp.bidfloor = imp.bidfloor || bidRequest.params.bidfloor || 0; + imp.bidfloorcur = imp.bidfloorcur || bidRequest.params.currency || DEFAULT_CURRENCY; + + return imp; +}; + +const enrichRequestWithConsent = (req, bidderRequest) => { + if (bidderRequest.gdprConsent) { + deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(req, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); + } + + if (bidderRequest.uspConsent) { + deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } +}; + +const determineMediaType = (bid) => { + if (bid.mtype === 2) { + return VIDEO; + } + + if (bid.mtype === 1) { + return BANNER; + } + + logWarn('Unrecognized media type, defaulting to BANNER (madSense)'); + return BANNER; +}; + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + }, + imp: (buildImp, bidRequest, context) => buildImpWithDefaults(buildImp, bidRequest, context), + request: (buildRequest, imps, bidderRequest, context) => { + const req = buildRequest(imps, bidderRequest, context); + + mergeDeep(req, { + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: '1.0.0', + }, + }); + + enrichRequestWithConsent(req, bidderRequest); + return req; + }, + bidResponse: (buildBidResponse, bid, context) => { + const resMediaType = determineMediaType(bid); + + Object.assign(context, { + mediaType: resMediaType, + currency: DEFAULT_CURRENCY, + ...(resMediaType === VIDEO && { vastXml: bid.adm }), + }); + + return buildBidResponse(bid, context); + }, +}); + +export const spec = { + code: BIDDER_CODE, + VERSION: '1.0.0', + supportedMediaTypes: [BANNER, VIDEO], + ENDPOINT: MS_EXCHANGE_BASE_URL, + + isBidRequestValid: function (bid) { + return validateBidRequest(bid); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const contextMediaType = determineMediaType(validBidRequests[0]); + const data = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest, + context: { contextMediaType }, + }); + + const companyId = getCompanyId(validBidRequests); + const madsenseExchangeEndpointUrl = buildEndpointUrl(companyId); + + return { + method: 'POST', + url: madsenseExchangeEndpointUrl, + data: data, + }; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bids = parseServerResponse(serverResponse, bidRequest); + return filterValidBids(bids); + }, +}; + +function validateBidRequest(bid) { + return ( + _validateParams(bid) && + _validateBanner(bid) && + _validateVideo(bid) + ); +} + +function getCompanyId(validBidRequests) { + let companyId = validBidRequests[0].params.company_id; + + if (validBidRequests[0].params.test) { + logMessage('madsense: test mode'); + companyId = 'test'; + } + + return companyId; +} + +function buildEndpointUrl(companyId) { + return `${spec.ENDPOINT}?company_id=${companyId}`; +} + +function parseServerResponse(serverResponse, bidRequest) { + return converter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }).bids; +} + +function filterValidBids(bids) { + return bids + .map((bid) => { + if (bid.mtype === 2 && bid.adm) { + if (!config.getConfig('cache.url')) { + bid.vastXml = bid.adm; + delete bid.adm; + } else { + logError('Prebid Cache is not configured (madSense)'); + return null; + } + } + return bid; + }) + .filter((bid) => bid !== null); +} + +function hasBannerMediaType(bidRequest) { + return !!bidRequest.mediaTypes?.banner; +} + +function hasVideoMediaType(bidRequest) { + return !!bidRequest.mediaTypes?.video; +} + +function _validateParams(bidRequest) { + if (!bidRequest.params) { + return false; + } + + if (bidRequest.params.test) { + return true; + } + + if (!bidRequest.params.company_id) { + logError('company_id not declared (madSense)'); + return false; + } + + const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); + if (!mediaTypesExists) { + return false; + } + + return true; +} + +function _validateBanner(bidRequest) { + if (!hasBannerMediaType(bidRequest)) { + return true; + } + + const bannerSizes = bidRequest.mediaTypes?.banner?.sizes; + + if (!Array.isArray(bannerSizes)) { + return false; + } + + return true; +} + +function _validateVideo(bidRequest) { + if (!hasVideoMediaType(bidRequest)) { + return true; + } + + const videoPlacement = bidRequest.mediaTypes?.video || {}; + const videoBidderParams = bidRequest.params?.video || {}; + const params = bidRequest.params || {}; + + if (params && params.test) { + return true; + } + + const videoParams = { + ...videoPlacement, + ...videoBidderParams, + }; + + if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { + logWarn('Invalid MIME types (madSense)'); + return false; + } + + if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { + logWarn('Invalid protocols (madSense)'); + return false; + } + + if (!videoParams.context) { + logWarn('Context not declared (madSense)'); + return false; + } + + if (videoParams.context !== 'instream') { + if (hasBannerMediaType(bidRequest)) { + logWarn('Context is not instream, preferring banner (madSense)'); + return true; + } else { + logWarn('Only instream context is supported (madSense)'); + } + } + + if ( + typeof videoParams.playerSize === 'undefined' || + !Array.isArray(videoParams.playerSize) || + !Array.isArray(videoParams.playerSize[0]) + ) { + logWarn('Player size not declared or not in [[w,h]] format'); + return false; + } + + return true; +} + +registerBidder(spec); diff --git a/modules/madsenseBidAdapter.md b/modules/madsenseBidAdapter.md new file mode 100644 index 00000000000..aacfbbafacb --- /dev/null +++ b/modules/madsenseBidAdapter.md @@ -0,0 +1,124 @@ +# Overview +``` +Module Name: madSense Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@madsense.io +``` + +# Description + +- A module that integrates with madSense's demand sources. +- The madSense bid adapter supports both Banner and Video formats. + + +### Test Parameters + +#### Banner + +``` +var adUnits = [ + { + code: 'adUnitBanner_div_1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'madsense', + params: { + company_id: '1234567', + bidfloor: 2.7, + } + }] + } +]; +``` + +#### Video + +We support the following OpenRTB parameters, which can be defined in `mediaTypes.video` or `bids[].params.video`. +- `mimes`, `minduration`, `maxduration`, `plcmt`, `protocols`, `startdelay`, `skip`, `skipafter`, `minbitrate`, `maxbitrate`, `delivery`, `playbackmethod`, `api`, `linearity` + + +##### Instream Video Ad Unit with mediaTypes.video +- Note: The adapter, by default, will retrieve the required parameters from mediaTypes.video. +- Note: The Video SSP ad server will return a VAST XML, which can be loaded into your specified player. +``` + var adUnits = [ + { + code: 'adUnitVideo_1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4', 'application/javascript'], + protocols: [2,5], + api: [2], + position: 1, + delivery: [2], + minduration: 10, + maxduration: 30, + plcmt: 1, + playbackmethod: [1,5], + } + }, + bids: [ + { + bidder: 'madsense', + params: { + company_id: '1234567' + } + } + ] + } + ] +``` + +## End To End Testing Mode +By setting `bid.params.test = true`, you can receive a test creative. + +#### Banner +``` +var adUnits = [ + { + code: 'adUnitBanner_div_1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'madsense', + params: { + test: true + } + }] + } +]; +``` + +#### Video +``` +var adUnits = [ + { + code: 'adUnitVideo_1', + mediaTypes: { + video: { + context: "instream", + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [2,5], + } + }, + bids: [ + { + bidder: 'madsense', + params: { + test: true + } + } + ] + } +] +``` diff --git a/modules/madvertiseBidAdapter.js b/modules/madvertiseBidAdapter.js index 3b031623aef..9fc7ceb68aa 100644 --- a/modules/madvertiseBidAdapter.js +++ b/modules/madvertiseBidAdapter.js @@ -1,5 +1,4 @@ import { parseSizesInput, _each } from '../src/utils.js'; -import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; /** @@ -27,9 +26,6 @@ export const spec = { if (sizes.length > 0 && sizes[0] === undefined) { return false; } - if (typeof bid.params.floor == 'undefined' || parseFloat(bid.params.floor) < 0.01) { - bid.params.floor = 0.01; - } return typeof bid.params.s != 'undefined'; }, @@ -58,7 +54,7 @@ export const spec = { } if (bidderRequest && bidderRequest.gdprConsent) { - src = src + '&gdpr=' + (bidderRequest.gdprConsent.gdprApplies ? '1' : '0') + '&consent[0][format]=' + config.getConfig('consentManagement.cmpApi') + '&consent[0][value]=' + bidderRequest.gdprConsent.consentString; + src = src + '&gdpr=' + (bidderRequest.gdprConsent.gdprApplies ? '1' : '0') + '&consent[0][format]=iab&consent[0][value]=' + bidderRequest.gdprConsent.consentString; } return { diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 225607ad6d3..25adf2b8a3c 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -47,13 +47,13 @@ const pbsErrorMap = { 4: 'request-error', 999: 'generic-error' } -let cookieless; let browser; let pageReferer; let auctionIndex = 0; // count of auctions on page let accountId; let endpoint; +let cookieless; let prebidGlobal = getGlobal(); const { @@ -65,7 +65,7 @@ const { BID_TIMEOUT, BID_WON, BILLABLE_EVENT, - SEAT_NON_BID, + PBS_ANALYTICS, BID_REJECTED } = EVENTS; @@ -264,7 +264,8 @@ export const parseBidResponse = (bid, previousBidResponse) => { return pick(bid, [ 'bidPriceUSD', () => responsePrice, 'dealId', dealId => dealId || undefined, - 'mediaType', + 'mediaType', () => bid?.meta?.mediaType ?? bid.mediaType, + 'ogMediaType', () => bid?.meta?.mediaType && bid.mediaType !== bid?.meta?.mediaType ? bid.mediaType : undefined, 'dimensions', () => { const width = bid.width || bid.playerWidth; const height = bid.height || bid.playerHeight; @@ -334,9 +335,9 @@ const getTopLevelDetails = () => { // Add DM wrapper details if (rubiConf.wrapperName) { - let rule; + let rule = rubiConf.rule_name; if (cookieless) { - rule = rubiConf.rule_name ? rubiConf.rule_name.concat('_cookieless') : 'cookieless'; + rule = rule ? rule.concat('_cookieless') : 'cookieless'; } payload.wrapper = { name: rubiConf.wrapperName, @@ -703,6 +704,7 @@ magniteAdapter.disableAnalytics = function () { magniteAdapter._oldEnable = enableMgniAnalytics; endpoint = undefined; accountId = undefined; + cookieless = undefined; auctionIndex = 0; resetConfs(); getHook('callPrebidCache').getHooks({ hook: callPrebidCacheHook }).remove(); @@ -926,8 +928,8 @@ magniteAdapter.track = ({ eventType, args }) => { const bidStatus = args.rejectionReason === REJECTION_REASON.FLOOR_NOT_MET ? BID_REJECTED_IPF : 'rejected'; handleBidResponse(args, bidStatus); break; - case SEAT_NON_BID: - handleNonBidEvent(args); + case PBS_ANALYTICS: + handlePbsAnalytics(args); break; case BIDDER_DONE: const serverError = deepAccess(args, 'serverErrors.0'); @@ -1018,8 +1020,27 @@ magniteAdapter.track = ({ eventType, args }) => { } }; -const handleNonBidEvent = function(args) { - const {seatnonbid, auctionId} = args; +const handlePbsAnalytics = function (args) { + const {seatnonbid, auctionId, atag} = args; + if (seatnonbid) { + handleNonBidEvent(seatnonbid, auctionId); + } + if (atag) { + handleAtagEvent(atag, auctionId); + } +} + +const handleAtagEvent = function (atag, auctionId) { + const tags = findTimeoutOptimization(atag) + tags.forEach(tag => { + tag.activities.forEach(activity => { + if (activity.name === 'optimize-tmax' && activity.status === 'success') { + setAnalyticsTagData(activity.results[0]?.values, deepAccess(cache, `auctions.${auctionId}.auction`)) + } + }) + }); +} +const handleNonBidEvent = function(seatnonbid, auctionId) { const auction = deepAccess(cache, `auctions.${auctionId}.auction`); // if no auction just bail if (!auction) { @@ -1049,6 +1070,28 @@ const handleNonBidEvent = function(args) { }); }; +const findTimeoutOptimization = (atag) => { + let timeoutOpt; + atag.forEach(tag => { + if (tag.module === 'mgni-timeout-optimization') { + timeoutOpt = tag.analyticstags; + } + }) + return timeoutOpt; +} +const setAnalyticsTagData = (values, auction) => { + let data = { + name: values.scenario, + rule: values.rule, + value: values.tmax + } + + const experiments = deepAccess(auction, 'experiments') || []; + experiments.push(data); + + deepSetValue(auction, 'experiments', experiments); +} + const statusMap = { 0: { status: 'no-bid' diff --git a/modules/mantisBidAdapter.js b/modules/mantisBidAdapter.js index 4520bad0f3a..ee18dc73ae5 100644 --- a/modules/mantisBidAdapter.js +++ b/modules/mantisBidAdapter.js @@ -1,6 +1,8 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { ajax } from '../src/ajax.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; +import { getWinDimensions } from '../src/utils.js'; export const storage = getStorageManager({bidderCode: 'mantis'}); @@ -73,9 +75,10 @@ export function onVisible(win, element, doOnVisible, time, pct) { }); } interval = setInterval(function () { - var winHeight = (win.innerHeight || document.documentElement.clientHeight); - var winWidth = (win.innerWidth || document.documentElement.clientWidth); - doCheck(winWidth, winHeight, element.getBoundingClientRect()); + const windowDimensions = getWinDimensions(); + var winHeight = (windowDimensions.innerHeight || windowDimensions.document.documentElement.clientHeight); + var winWidth = (windowDimensions.innerWidth || windowDimensions.document.documentElement.clientWidth); + doCheck(winWidth, winHeight, getBoundingClientRect(element)); }, 100); } function storeUuid(uuid) { @@ -271,9 +274,8 @@ export const spec = { export function sfPostMessage ($sf, width, height, callback) { var viewed = false; - // eslint-disable-next-line no-undef + $sf.ext.register(width, height, function () { - // eslint-disable-next-line no-undef if ($sf.ext.inViewPercentage() < 50 || viewed) { return; } diff --git a/modules/marsmediaAnalyticsAdapter.js b/modules/marsmediaAnalyticsAdapter.js deleted file mode 100644 index f1e53a3c20c..00000000000 --- a/modules/marsmediaAnalyticsAdapter.js +++ /dev/null @@ -1,53 +0,0 @@ -import {ajax} from '../src/ajax.js'; -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; -import adapterManager from '../src/adapterManager.js'; -import {getGlobal} from '../src/prebidGlobal.js'; - -/**** - * Mars Media Analytics - * Contact: prebid@m-m-g.com‏ - * Developer: Chen Saadia - */ - -const MARS_BIDDER_CODE = 'marsmedia'; -const analyticsType = 'endpoint'; -const MARS_VERSION = '1.0.1'; -const MARS_ANALYTICS_URL = 'https://prebid_stats.mars.media/prebidjs/api/analytics.php'; -var events = {}; - -var marsmediaAnalyticsAdapter = Object.assign(adapter( - { - MARS_ANALYTICS_URL, - analyticsType - }), -{ - track({eventType, args}) { - if (typeof args !== 'undefined' && args.bidderCode === MARS_BIDDER_CODE) { - events[eventType] = args; - } - - if (eventType === 'auctionEnd') { - setTimeout(function() { - ajax( - MARS_ANALYTICS_URL, - { - success: function() {}, - error: function() {} - }, - JSON.stringify({act: 'prebid_analytics', params: events, 'pbjs': getGlobal().getBidResponses(), ver: MARS_VERSION}), - { - method: 'POST' - } - ); - }, 3000); - } - } -} -); - -adapterManager.registerAnalyticsAdapter({ - adapter: marsmediaAnalyticsAdapter, - code: 'marsmedia' -}); - -export default marsmediaAnalyticsAdapter; diff --git a/modules/marsmediaBidAdapter.js b/modules/marsmediaBidAdapter.js index 82a25af60d1..81e78ba87b1 100644 --- a/modules/marsmediaBidAdapter.js +++ b/modules/marsmediaBidAdapter.js @@ -1,9 +1,10 @@ 'use strict'; -import { deepAccess, getDNT, parseSizesInput, isArray, getWindowTop, deepSetValue, triggerPixel, getWindowSelf } from '../src/utils.js'; +import { deepAccess, getDNT, 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'; import {config} from '../src/config.js'; +import { percentInView } from '../libraries/percentInView/percentInView.js'; function MarsmediaAdapter() { this.code = 'marsmedia'; @@ -341,7 +342,7 @@ function MarsmediaAdapter() { size: '*' }); - if (typeof floorInfo === 'object' && + if (isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = floorInfo.floor; @@ -369,75 +370,9 @@ function MarsmediaAdapter() { function _getViewability(element, topWin, { w, h } = {}) { return topWin.document.visibilityState === 'visible' - ? _getPercentInView(element, topWin, { w, h }) + ? percentInView(element, { w, h }) : 0; } - - function _getPercentInView(element, topWin, { w, h } = {}) { - const elementBoundingBox = _getBoundingBox(element, { w, h }); - - const elementInViewBoundingBox = _getIntersectionOfRects([ { - left: 0, - top: 0, - right: topWin.innerWidth, - bottom: topWin.innerHeight - }, elementBoundingBox ]); - - let elementInViewArea, elementTotalArea; - - if (elementInViewBoundingBox !== null) { - // Some or all of the element is in view - elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; - elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; - - return ((elementInViewArea / elementTotalArea) * 100); - } - - return 0; - } - - function _getBoundingBox(element, { w, h } = {}) { - let { width, height, left, top, right, bottom } = element.getBoundingClientRect(); - - if ((width === 0 || height === 0) && w && h) { - width = w; - height = h; - right = left + w; - bottom = top + h; - } - - return { width, height, left, top, right, bottom }; - } - - function _getIntersectionOfRects(rects) { - const bbox = { - left: rects[0].left, - right: rects[0].right, - top: rects[0].top, - bottom: rects[0].bottom - }; - - for (let i = 1; i < rects.length; ++i) { - bbox.left = Math.max(bbox.left, rects[i].left); - bbox.right = Math.min(bbox.right, rects[i].right); - - if (bbox.left >= bbox.right) { - return null; - } - - bbox.top = Math.max(bbox.top, rects[i].top); - bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); - - if (bbox.top >= bbox.bottom) { - return null; - } - } - - bbox.width = bbox.right - bbox.left; - bbox.height = bbox.bottom - bbox.top; - - return bbox; - } } export const spec = new MarsmediaAdapter(); diff --git a/modules/mathildeadsBidAdapter.js b/modules/mathildeadsBidAdapter.js index 929cee8f3c0..0ecfe63765b 100644 --- a/modules/mathildeadsBidAdapter.js +++ b/modules/mathildeadsBidAdapter.js @@ -1,212 +1,19 @@ -import { isFn, deepAccess, logMessage } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'mathildeads'; const AD_URL = 'https://endpoint2.mathilde-ads.com/pbjs'; const SYNC_URL = 'https://cs2.mathilde-ads.com'; -function isBidResponseValid (bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency || !bid.meta) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData (bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - placementId, - bidId, - schain, - bidfloor - }; - - if (mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } - - if (mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } - - if (mediaTypes[NATIVE]) { - placement.adFormat = NATIVE; - placement.native = mediaTypes[NATIVE]; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && params.placementId); - - if (mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } - - if (mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } - - if (mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } - - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest?.refererInfo?.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/mediaConsortiumBidAdapter.js b/modules/mediaConsortiumBidAdapter.js new file mode 100644 index 00000000000..a1cd6586735 --- /dev/null +++ b/modules/mediaConsortiumBidAdapter.js @@ -0,0 +1,273 @@ +import {BANNER, VIDEO} from '../src/mediaTypes.js' +import {registerBidder} from '../src/adapters/bidderFactory.js' +import {generateUUID, isPlainObject, isArray, logWarn, deepClone} from '../src/utils.js' +import {Renderer} from '../src/Renderer.js' +import {OUTSTREAM} from '../src/video.js' +import {config} from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const BIDDER_CODE = 'mediaConsortium' + +const PROFILE_API_USAGE_CONFIG_KEY = 'useProfileApi' +const ONE_PLUS_X_ID_USAGE_CONFIG_KEY = 'readOnePlusXId' + +const SYNC_ENDPOINT = 'https://relay.hubvisor.io/v1/sync/big' +const AUCTION_ENDPOINT = 'https://relay.hubvisor.io/v1/auction/big' + +const XANDR_OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +export const OPTIMIZATIONS_STORAGE_KEY = 'media_consortium_optimizations' + +const SYNC_TYPES = { + image: 'image', + redirect: 'image', + iframe: 'iframe' +} + +const storageManager = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + version: '0.0.1', + code: BIDDER_CODE, + gvlid: 1112, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid(bid) { + return true + }, + buildRequests(bidRequests, bidderRequest) { + const useProfileApi = config.getConfig(PROFILE_API_USAGE_CONFIG_KEY) ?? false + const readOnePlusXId = config.getConfig(ONE_PLUS_X_ID_USAGE_CONFIG_KEY) ?? false + + const { + auctionId, + bids, + gdprConsent: {gdprApplies = false, consentString} = {}, + ortb2: {device, site} + } = bidderRequest + + const currentTimestamp = Date.now() + const optimizations = getOptimizationsFromLocalStorage() + + const impressions = bids.reduce((acc, bidRequest) => { + const {bidId, adUnitCode, mediaTypes} = bidRequest + const optimization = optimizations[adUnitCode] + + if (optimization) { + const {expiresAt, isEnabled} = optimization + + if (expiresAt >= currentTimestamp && !isEnabled) { + return acc + } + } + + let finalizedMediatypes = deepClone(mediaTypes) + + if (mediaTypes.video && mediaTypes.video.context !== OUTSTREAM) { + logWarn(`Filtering video request for adUnitCode ${adUnitCode} because context is not ${OUTSTREAM}`) + + if (Object.keys(finalizedMediatypes).length > 1) { + delete finalizedMediatypes.video + } else { + return acc + } + } + + return acc.concat({id: bidId, adUnitCode, mediaTypes: finalizedMediatypes}) + }, []) + + if (!impressions.length) { + return + } + + const request = { + id: auctionId ?? generateUUID(), + impressions, + device, + site, + user: { + ids: {} + }, + regulations: { + gdpr: { + applies: gdprApplies, + consentString + } + }, + timeout: 3600, + options: { + useProfileApi + } + } + + if (readOnePlusXId) { + const fpId = getFpIdFromLocalStorage() + + if (fpId) { + request.user.ids['1plusX'] = fpId + } + } + + const syncData = { + gdpr: gdprApplies, + ad_unit_codes: impressions.map(({adUnitCode}) => adUnitCode).join(',') + } + + if (consentString) { + syncData.gdpr_consent = consentString + } + + return [ + { + method: 'GET', + url: SYNC_ENDPOINT, + data: syncData + }, + { + method: 'POST', + url: AUCTION_ENDPOINT, + data: request + } + ] + }, + interpretResponse(serverResponse, params) { + if (!isValidResponse(serverResponse)) return [] + + const {body: {bids, optimizations}} = serverResponse + + if (optimizations && isArray(optimizations)) { + const currentTimestamp = Date.now() + + const optimizationsToStore = optimizations.reduce((acc, optimization) => { + const {adUnitCode, isEnabled, ttl} = optimization + + return { + ...acc, + [adUnitCode]: {isEnabled, expiresAt: currentTimestamp + ttl} + } + }, getOptimizationsFromLocalStorage()) + + storageManager.setDataInLocalStorage(OPTIMIZATIONS_STORAGE_KEY, JSON.stringify(optimizationsToStore)) + } + + return bids.map((bid) => { + const { + impressionId, + price: {cpm, currency}, + dealId, + ad: { + creative: {id, mediaType, size: {width, height}, markup} + }, + ttl = 360 + } = bid + + const formattedBid = { + requestId: impressionId, + cpm, + currency, + dealId, + ttl, + netRevenue: true, + creativeId: id, + mediaType, + width, + height, + ad: markup, + adUrl: null + } + + if (mediaType === VIDEO) { + const impressionRequest = params.data.impressions.find(({id}) => id === impressionId) + + formattedBid.vastXml = markup + + if (impressionRequest) { + formattedBid.renderer = buildXandrOutstreamRenderer(impressionId, impressionRequest.adUnitCode) + } else { + logWarn(`Could not find adUnitCode matching the impressionId ${impressionId} to setup the renderer`) + } + } + + return formattedBid + }) + }, + getUserSyncs(syncOptions, serverResponses) { + if (serverResponses.length !== 2) { + return + } + + const [sync] = serverResponses + + return sync.body?.bidders?.reduce((acc, {type, url}) => { + const syncType = SYNC_TYPES[type] + + if (!syncType || !url) { + return acc + } + + return acc.concat({type: syncType, url}) + }, []) + } +} + +registerBidder(spec) + +export function getOptimizationsFromLocalStorage() { + try { + const storedOptimizations = storageManager.getDataFromLocalStorage(OPTIMIZATIONS_STORAGE_KEY) + + return storedOptimizations ? JSON.parse(storedOptimizations) : {} + } catch (err) { + return {} + } +} + +function getFpIdFromLocalStorage() { + try { + return storageManager.getDataFromLocalStorage('ope_fpid') + } catch (err) { + return null + } +} + +function isValidResponse(response) { + return isPlainObject(response) && + isPlainObject(response.body) && + isArray(response.body.bids) +} + +function buildXandrOutstreamRenderer(bidId, adUnitCode) { + const renderer = Renderer.install({ + id: bidId, + url: XANDR_OUTSTREAM_RENDERER_URL, + loaded: false, + adUnitCode, + targetId: adUnitCode + }); + + try { + renderer.setRender(xandrOutstreamRenderer); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +function xandrOutstreamRenderer(bid) { + const {width, height, adUnitCode, vastXml} = bid + + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [width, height], + targetId: adUnitCode, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + content: vastXml, + showVolume: false, + allowFullscreen: true, + skippable: false + } + }); + }); +} diff --git a/modules/mediaConsortiumBidAdapter.md b/modules/mediaConsortiumBidAdapter.md new file mode 100644 index 00000000000..b78627077cb --- /dev/null +++ b/modules/mediaConsortiumBidAdapter.md @@ -0,0 +1,79 @@ +# Media Consortium Bid adapter + +## Overview + +``` +- Module Name: Media Consortium Bidder Adapter +- Module Type: Media Consortium Bidder Adapter +- Maintainer: mediaconsortium-develop@bi.garage.co.jp +``` + +## Description + +Module that connects to Media Consortium demand sources and supports the following media types: `banner`, `video`. + +To get access to the full feature set of the adapter you'll need to allow localstorage usage in the `bidderSettings`. + +```javascript + pbjs.bidderSettings = { + mediaConsortium: { + storageAllowed: true + } + } +``` + +## Managing 1plusX profile API usage and FPID retrieval + +You can use the `setBidderConfig` function to enable or disable 1plusX profile API usage and fpid retrieval. + +If the keys found below are not defined, their values will default to `false`. + +```javascript + pbjs.setBidderConfig({ + bidders: ['mediaConsortium'], + config: { + // Controls the 1plusX profile API usage + useProfileApi: true, + // Controls the 1plusX fpid retrieval + readOnePlusXId: true + } + }); +``` + +## Test Parameters + +```javascript + var adUnits = [ + { + code: 'div-prebid-banner', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'mediaConsortium', + params: {} + } + ] + }, + { + code: 'div-prebid-video', + mediaTypes:{ + video: { + playerSize: [ + [300, 250] + ], + context: 'outstream' + } + }, + bids:[ + { + bidder: 'mediaConsortium', + params: {} + } + ] + } + ]; +``` diff --git a/modules/mediabramaBidAdapter.js b/modules/mediabramaBidAdapter.js index caf6854fe03..9722f1672ba 100644 --- a/modules/mediabramaBidAdapter.js +++ b/modules/mediabramaBidAdapter.js @@ -1,155 +1,22 @@ -import { - isFn, - isStr, - deepAccess, - getWindowTop, - triggerPixel -} from '../src/utils.js'; + import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { bidWinReport, buildBidRequests, buildUserSyncs, interpretResponse, isBidRequestValid } from '../libraries/precisoUtils/bidUtilsCommon.js'; const BIDDER_CODE = 'mediabrama'; const AD_URL = 'https://prebid.mediabrama.com/pbjs'; const SYNC_URL = 'https://prebid.mediabrama.com/sync'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency || !bid.meta) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - default: - return false; - } -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidFloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], - - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && bid.params.placementId); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - const winTop = getWindowTop(); - const location = winTop.location; - const placements = []; - - const request = { - deviceWidth: winTop.screen.width, - deviceHeight: winTop.screen.height, - language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - host: location.host, - page: location.pathname, - placements: placements - }; - - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent; - } - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - const placement = { - placementId: bid.params.placementId, - bidId: bid.bidId, - schain: bid.schain || {}, - bidfloor: getBidFloor(bid) - }; - - if (typeof bid.userId !== 'undefined') { - placement.userId = bid.userId; - } - - const mediaType = bid.mediaTypes; - - if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { - placement.sizes = mediaType[BANNER].sizes; - placement.adFormat = BANNER; - } - - placements.push(placement); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - + isBidRequestValid: isBidRequestValid, + buildRequests: buildBidRequests(AD_URL), + interpretResponse: interpretResponse, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; + return buildUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, SYNC_URL); }, - - onBidWon: (bid) => { - const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; - if (isStr(bid.nurl) && bid.nurl !== '') { - bid.nurl = bid.nurl.replace(/\${AUCTION_PRICE}/, cpm); - triggerPixel(bid.nurl); - } - } + onBidWon: bidWinReport }; registerBidder(spec); diff --git a/modules/mediaeyesBidAdapter.js b/modules/mediaeyesBidAdapter.js new file mode 100644 index 00000000000..5c896d87a48 --- /dev/null +++ b/modules/mediaeyesBidAdapter.js @@ -0,0 +1,129 @@ +import { + BANNER +} from '../src/mediaTypes.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { deepAccess, deepSetValue, generateUUID, isArray, isFn, isNumber, isPlainObject, isStr } from '../src/utils.js'; + +const ENDPOINT_URL = 'https://delivery.upremium.asia/ortb/open/auction'; + +export const spec = { + code: 'mediaeyes', + supportedMediaTypes: BANNER, + + isBidRequestValid: (bid) => { + return !!(bid.params.itemId); + }, + + buildRequests: (bidRequests, bidderRequest) => { + let requests = []; + + bidRequests.map(bidRequest => { + let {itemId} = bidRequest.params; + let requestData = { + id: generateUUID(), + imp: [cookingImp(bidRequest)], + device: bidRequest.ortb2?.device, + site: bidRequest.ortb2?.site, + } + requests.push({ + method: 'POST', + url: ENDPOINT_URL + "?item_id=" + itemId, + data: JSON.stringify(requestData), + }); + }) + + return requests + }, + + interpretResponse: (serverResponse, serverRequest) => { + let response = serverResponse.body; + if (!response.seatbid) { + return []; + } + + let rtbBids = response.seatbid + .map(seatbid => seatbid.bid) + .reduce((a, b) => a.concat(b), []); + + let data = rtbBids.map(rtbBid => { + let prBid = { + requestId: rtbBid.impid, + cpm: rtbBid.price, + creativeId: rtbBid.crid, + currency: response.cur || 'USD', + ttl: 360, + netRevenue: true + }; + + prBid.mediaType = BANNER; + prBid.width = rtbBid.w; + prBid.height = rtbBid.h; + prBid.ad = rtbBid.adm; + if (isArray(rtbBid.adomain)) { + deepSetValue(prBid, 'meta.advertiserDomains', rtbBid.adomain); + } + if (isPlainObject(rtbBid.ext)) { + if (isNumber(rtbBid.ext.advertiser_id)) { + deepSetValue(prBid, 'meta.advertiserId', rtbBid.ext.advertiser_id); + } + if (isStr(rtbBid.ext.advertiser_name)) { + deepSetValue(prBid, 'meta.advertiserName', rtbBid.ext.advertiser_name); + } + if (isStr(rtbBid.ext.agency_name)) { + deepSetValue(prBid, 'meta.agencyName', rtbBid.ext.agency_name); + } + } + + return prBid + }); + + return data + } +} + +registerBidder(spec); + +function cookingImp(bidReq) { + let imp = {}; + if (bidReq) { + const bidfloor = getBidFloor(bidReq); + if (bidfloor) { + imp.bidfloor = parseFloat(bidfloor); + imp.bidfloorcur = 'USD'; + } + + imp.id = bidReq.bidId; + imp.bidfloor = bidfloor; + imp.banner = cookImpBanner(bidReq); + } + return imp; +} + +const cookImpBanner = ({ mediaTypes, params }) => { + if (!mediaTypes?.banner) return {}; + + const { sizes } = mediaTypes.banner; + return { + w: sizes[0][0], + h: sizes[0][1] + } +}; + +function getBidFloor(bidRequest) { + let bidfloor = deepAccess(bidRequest, 'params.bidFloor', 0) + + if (!bidfloor && isFn(bidRequest.getFloor)) { + let floor = bidRequest.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor)) { + bidfloor = floor.floor; + } + } + + return bidfloor; +} diff --git a/modules/mediaeyesBidAdapter.md b/modules/mediaeyesBidAdapter.md new file mode 100644 index 00000000000..40e1eb77d67 --- /dev/null +++ b/modules/mediaeyesBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: MediaEyes Bidder Adapter +Module Type: MediaEyes Bidder Adapter +Maintainer: giathinh.ly@urekamedia.vn +``` + +# Description + +Module that connects to MediaEyes Bidder System + +# Test Parameters +``` + var adUnits = [ + { + code: 'div-prebid', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'mediaeyes', + params: { + itemId: '4d27f3cc8bbd5bd153045e' // Item for test + } + } + ] + }, + ]; +``` diff --git a/modules/mediafilterRtdProvider.js b/modules/mediafilterRtdProvider.js index fae5c9e769b..5472c4a6ce0 100644 --- a/modules/mediafilterRtdProvider.js +++ b/modules/mediafilterRtdProvider.js @@ -15,6 +15,7 @@ import { logError, generateUUID } from '../src/utils.js'; import { loadExternalScript } from '../src/adloader.js'; import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; /** The event type for Media Filter. */ export const MEDIAFILTER_EVENT_TYPE = 'com.mediatrust.pbjs.'; @@ -54,7 +55,8 @@ export const MediaFilter = { * @param {string} configurationHash - The configuration hash. */ setupScript: function(configurationHash) { - loadExternalScript(MEDIAFILTER_BASE_URL.concat(configurationHash), 'mediafilter', () => {}); + loadExternalScript(MEDIAFILTER_BASE_URL.concat(configurationHash), MODULE_TYPE_RTD, 'mediafilter', () => { + }); }, /** diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js index 5e31f60d3b5..b70d2bf30a5 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -24,16 +24,14 @@ import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {getStorageManager} from '../src/storageManager.js'; import {bidderSettings} from '../src/bidderSettings.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js'; import { getANKewyordParamFromMaps, - getANKeywordParam, - transformBidderParamKeywords + getANKeywordParam } from '../libraries/appnexusUtils/anKeywords.js'; import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; import {chunk} from '../libraries/chunk/chunk.js'; /** @@ -351,31 +349,6 @@ export const spec = { } }, - transformBidParams: function (params, isOpenRtb) { - params = convertTypes({ - 'member': 'string', - 'invCode': 'string', - 'placementId': 'number', - 'keywords': transformBidderParamKeywords, - 'publisherId': 'number' - }, params); - - if (isOpenRtb) { - params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; - if (params.usePaymentRule) { delete params.usePaymentRule; } - - Object.keys(params).forEach(paramKey => { - let convertedKey = convertCamelToUnderscore(paramKey); - if (convertedKey !== paramKey) { - params[convertedKey] = params[paramKey]; - delete params[paramKey]; - } - }); - } - - return params; - }, - /** * Add element selector to javascript tracker to improve native viewability * @param {Bid} bid @@ -756,7 +729,7 @@ function bidToTag(bid) { if (!isEmpty(bid.params.keywords)) { tag.keywords = getANKewyordParamFromMaps(bid.params.keywords); } - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + let gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { tag.gpid = gpid; } @@ -1063,7 +1036,7 @@ function hideSASIframe(elementId) { function outstreamRender(bid) { hidedfpContainer(bid.adUnitCode); hideSASIframe(bid.adUnitCode); - // push to render queue because ANOutstreamVideo may not be loaded yet + // push to render queue because ANOutstreamVideo may not be loaded bid.renderer.push(() => { window.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index 8f687d30ff3..964ddad84ef 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -5,6 +5,13 @@ import * as utils from '../src/utils.js'; import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink, getReferrer } from '../libraries/fpdUtils/pageInfo.js'; +import { getDevice } from '../libraries/fpdUtils/deviceInfo.js'; +import { getBidFloor } from '../libraries/currencyUtils/floor.js'; +import { transformSizes, normalAdSize } from '../libraries/sizeUtils/tranformSize.js'; +import { getHLen } from '../libraries/navigatorData/navigatorData.js'; +import { cookieSync } from '../libraries/cookieSync/cookieSync.js'; + // import { config } from '../src/config.js'; // import { isPubcidEnabled } from './pubCommonId.js'; @@ -19,7 +26,6 @@ const BIDDER_CODE = 'mediago'; // const PROTOCOL = window.document.location.protocol; const ENDPOINT_URL = 'https://gbid.mediago.io/api/bid?tn='; // const COOKY_SYNC_URL = 'https://gtrace.mediago.io/ju/cs/eplist'; -const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; export const THIRD_PARTY_COOKIE_ORIGIN = 'https://cdn.mediago.io'; const TIME_TO_LIVE = 500; @@ -33,65 +39,8 @@ let itemMaps = {}; export const COOKIE_KEY_MGUID = '__mguid_'; const COOKIE_KEY_PMGUID = '__pmguid_'; const COOKIE_RETENTION_TIME = 365 * 24 * 60 * 60 * 1000; // 1 year +const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; let reqTimes = 0; -/** - * get page title - * @returns {string} - */ - -export function getPageTitle(win = window) { - try { - const ogTitle = win.top.document.querySelector('meta[property="og:title"]') - return win.top.document.title || (ogTitle && ogTitle.content) || ''; - } catch (e) { - const ogTitle = document.querySelector('meta[property="og:title"]') - return document.title || (ogTitle && ogTitle.content) || ''; - } -} - -/** - * get page description - * - * @returns {string} - */ -export function getPageDescription(win = window) { - let element; - - try { - element = win.top.document.querySelector('meta[name="description"]') || - win.top.document.querySelector('meta[property="og:description"]') - } catch (e) { - element = document.querySelector('meta[name="description"]') || - document.querySelector('meta[property="og:description"]') - } - - return (element && element.content) || ''; -} - -/** - * get page keywords - * @returns {string} - */ -export function getPageKeywords(win = window) { - let element; - - try { - element = win.top.document.querySelector('meta[name="keywords"]'); - } catch (e) { - element = document.querySelector('meta[name="keywords"]'); - } - - return (element && element.content) || ''; -} - -/** - * get connection downlink - * @returns {number} - */ -export function getConnectionDownLink(win = window) { - const nav = win.navigator || {}; - return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined; -} /** * get pmg uid @@ -135,54 +84,6 @@ function getProperty(obj, ...keys) { return o; } -/** - * 是不是移动设备或者平板 - * @return {boolean} - */ -function isMobileAndTablet() { - let check = false; - (function (a) { - let reg1 = new RegExp( - [ - '(android|bbd+|meego)', - '.+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', - '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', - '|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap', - '|windows ce|xda|xiino|android|ipad|playbook|silk' - ].join(''), - 'i' - ); - let reg2 = new RegExp( - [ - '1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)', - '|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )', - '|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55/|capi|ccwa|cdm-|cell', - '|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)', - '|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene', - '|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c', - '|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|/)|ibro|idea|ig01|ikom', - '|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |/)|klon|kpt |kwc-|kyo(c|k)', - '|le(no|xi)|lg( g|/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50/|ma(te|ui|xo)|mc(01|21|ca)', - '|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]', - '|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)', - '|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio', - '|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55/|sa(ge|ma|mm|ms', - '|ny|va)|sc(01|h-|oo|p-)|sdk/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al', - '|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)', - '|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|', - 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)', - '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-', - '|your|zeto|zte-' - ].join(''), - 'i' - ); - if (reg1.test(a) || reg2.test(a.substr(0, 4))) { - check = true; - } - })(navigator.userAgent || navigator.vendor || window.opera); - return check; -} - /** * 获取底价 * @param {*} bid @@ -205,62 +106,9 @@ function isMobileAndTablet() { // } // return floor; // } -function getBidFloor(bid) { - if (!utils.isFn(bid.getFloor)) { - return utils.deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - return bidFloor.floor; - } catch (_) { - return 0; - } -} - -/** - * 将尺寸转为RTB识别的尺寸 - * - * @param {Array|Object} requestSizes 配置尺寸 - * @return {Object} - */ -function transformSizes(requestSizes) { - let sizes = []; - let sizeObj = {}; - - if (utils.isArray(requestSizes) && requestSizes.length === 2 && !utils.isArray(requestSizes[0])) { - sizeObj.width = parseInt(requestSizes[0], 10); - sizeObj.height = parseInt(requestSizes[1], 10); - sizes.push(sizeObj); - } else if (typeof requestSizes === 'object') { - for (let i = 0; i < requestSizes.length; i++) { - let size = requestSizes[i]; - sizeObj = {}; - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - sizes.push(sizeObj); - } - } - - return sizes; -} // 支持的广告尺寸 -const mediagoAdSize = [ - { w: 300, h: 250 }, - { w: 300, h: 600 }, - { w: 728, h: 90 }, - { w: 970, h: 250 }, - { w: 320, h: 50 }, - { w: 160, h: 600 }, - { w: 320, h: 180 }, - { w: 320, h: 100 }, - { w: 336, h: 280 } -]; +const mediagoAdSize = normalAdSize; /** * 获取广告位配置 @@ -325,6 +173,7 @@ function getItems(validBidRequests, bidderRequest) { ortb2Imp: utils.deepAccess(req, 'ortb2Imp'), // 传入完整对象,分析日志数据 gpid: gpid, // 加入后无法返回广告 adslot: utils.deepAccess(req, 'ortb2Imp.ext.data.adserver.adslot', '', ''), + publisher: req.params.publisher || '', ...gdprConsent // gdpr }, tagid: req.params && req.params.tagid @@ -340,21 +189,6 @@ function getItems(validBidRequests, bidderRequest) { return items; } -/** - * @param {BidRequest} bidRequest - * @param bidderRequest - * @returns {string} - */ -function getReferrer(bidRequest = {}, bidderRequest = {}) { - let pageUrl; - if (bidRequest.params && bidRequest.params.referrer) { - pageUrl = bidRequest.params.referrer; - } else { - pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); - } - return pageUrl; -} - /** * get current time to UTC string * @returns utc string @@ -386,7 +220,7 @@ function getParam(validBidRequests, bidderRequest) { const cat = utils.deepAccess(bidderRequest, 'ortb2.site.cat'); reqTimes += 1; - let isMobile = isMobileAndTablet() ? 1 : 0; + let isMobile = getDevice() ? 1 : 0; // input test status by Publisher. more frequently for test true req let isTest = validBidRequests[0].params.test || 0; let auctionId = getProperty(bidderRequest, 'auctionId'); @@ -399,7 +233,6 @@ function getParam(validBidRequests, bidderRequest) { const timeout = bidderRequest.timeout || 2000; const firstPartyData = bidderRequest.ortb2; - const topWindow = window.top; const title = getPageTitle(); const desc = getPageDescription(); const keywords = getPageKeywords(); @@ -436,12 +269,10 @@ function getParam(validBidRequests, bidderRequest) { title: title ? title.slice(0, 100) : undefined, desc: desc ? desc.slice(0, 300) : undefined, keywords: keywords ? keywords.slice(0, 100) : undefined, - hLen: topWindow.history?.length || undefined, + hLen: getHLen(), }, device: { nbw: getConnectionDownLink(), - hc: topWindow.navigator?.hardwareConcurrency || undefined, - dm: topWindow.navigator?.deviceMemory || undefined, } }, user: { @@ -457,9 +288,7 @@ function getParam(validBidRequests, bidderRequest) { mobile: isMobile, cat: [], // todo publisher: { - // todo - id: domain, - name: domain + id: globals['publisher'] } }, imp: items, @@ -488,6 +317,9 @@ export const spec = { if (bid.params.token) { globals['token'] = bid.params.token; } + if (bid.params.publisher) { + globals['publisher'] = bid.params.publisher; + } return !!bid.params.token; }, @@ -553,42 +385,7 @@ export const spec = { }, getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) { - const origin = encodeURIComponent(location.origin || `https://${location.host}`); - let syncParamUrl = `dm=${origin}`; - - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - if (syncOptions.iframeEnabled) { - window.addEventListener('message', function handler(event) { - if (!event.data || event.origin != THIRD_PARTY_COOKIE_ORIGIN) { - return; - } - - this.removeEventListener('message', handler); - - event.stopImmediatePropagation(); - - const response = event.data; - if (!response.optout && response.mguid) { - storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); - } - }, true); - return [ - { - type: 'iframe', - url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` - } - ]; - } + return cookieSync(syncOptions, gdprConsent, uspConsent, BIDDER_CODE, THIRD_PARTY_COOKIE_ORIGIN, COOKY_SYNC_IFRAME_URL, getCurrentTimeToUTCString()); }, /** diff --git a/modules/mediaimpactBidAdapter.js b/modules/mediaimpactBidAdapter.js index 4ce11201507..a1f04a44142 100644 --- a/modules/mediaimpactBidAdapter.js +++ b/modules/mediaimpactBidAdapter.js @@ -1,6 +1,5 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { buildUrl } from '../src/utils.js' -import {ajax} from '../src/ajax.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { buildBidRequestsAndParams, postRequest, buildEndpointUrl } from '../libraries/mediaImpactUtils/index.js'; const BIDDER_CODE = 'mediaimpact'; export const ENDPOINT_PROTOCOL = 'https'; @@ -15,79 +14,25 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - // TODO does it make sense to fall back to window.location.href? const referer = bidderRequest?.refererInfo?.page || window.location.href; - let bidRequests = []; - let beaconParams = { - tag: [], - partner: [], - sizes: [], - referer: '' - }; - - validBidRequests.forEach(function(validBidRequest) { - let sizes = validBidRequest.sizes; - if (typeof validBidRequest.params.sizes !== 'undefined') { - sizes = validBidRequest.params.sizes; - } - - let bidRequestObject = { - adUnitCode: validBidRequest.adUnitCode, - sizes: sizes, - bidId: validBidRequest.bidId, - referer: referer - }; - - if (parseInt(validBidRequest.params.unitId)) { - bidRequestObject.unitId = parseInt(validBidRequest.params.unitId); - beaconParams.tag.push(validBidRequest.params.unitId); - } - - if (parseInt(validBidRequest.params.partnerId)) { - bidRequestObject.unitId = 0; - bidRequestObject.partnerId = parseInt(validBidRequest.params.partnerId); - beaconParams.partner.push(validBidRequest.params.partnerId); - } - - bidRequests.push(bidRequestObject); - - beaconParams.sizes.push(spec.joinSizesToString(sizes)); - beaconParams.referer = encodeURIComponent(referer); - }); - - if (beaconParams.partner.length > 0) { - beaconParams.partner = beaconParams.partner.join(','); - } else { - delete beaconParams.partner; - } - - beaconParams.tag = beaconParams.tag.join(','); - beaconParams.sizes = beaconParams.sizes.join(','); + // Use the common function to build bidRequests and beaconParams + const { bidRequests, beaconParams } = buildBidRequestsAndParams(validBidRequests, referer); - let adRequestUrl = buildUrl({ - protocol: ENDPOINT_PROTOCOL, - hostname: ENDPOINT_DOMAIN, - pathname: ENDPOINT_PATH, - search: beaconParams - }); + const adRequestUrl = buildEndpointUrl( + ENDPOINT_PROTOCOL, + ENDPOINT_DOMAIN, + ENDPOINT_PATH, + beaconParams + ); return { method: 'POST', url: adRequestUrl, - data: JSON.stringify(bidRequests) + data: JSON.stringify(bidRequests), }; }, - joinSizesToString: function(sizes) { - let res = []; - sizes.forEach(function(size) { - res.push(size.join('x')); - }); - - return res.join('|'); - }, - interpretResponse: function (serverResponse, bidRequest) { const validBids = JSON.parse(bidRequest.data); @@ -96,16 +41,13 @@ export const spec = { } return validBids - .map(bid => ({ - bid: bid, - ad: serverResponse.body[bid.adUnitCode] - })) + .map(bid => ({ bid: bid, ad: serverResponse.body[bid.adUnitCode] })) .filter(item => item.ad) .map(item => spec.adResponse(item.bid, item.ad)); }, - adResponse: function(bid, ad) { - const bidObject = { + adResponse: function (bid, ad) { + return { requestId: bid.bidId, ad: ad.ad, cpm: ad.cpm, @@ -115,37 +57,27 @@ export const spec = { creativeId: ad.creativeId, netRevenue: ad.netRevenue, currency: ad.currency, - winNotification: ad.winNotification - } - - bidObject.meta = {}; - if (ad.adomain && ad.adomain.length > 0) { - bidObject.meta.advertiserDomains = ad.adomain; - } - - return bidObject; + winNotification: ad.winNotification, + meta: ad.meta || {}, + }; }, - onBidWon: function(data) { - data.winNotification.forEach(function(unitWon) { - let adBidWonUrl = buildUrl({ - protocol: ENDPOINT_PROTOCOL, - hostname: ENDPOINT_DOMAIN, - pathname: unitWon.path - }); + onBidWon: function (data) { + data.winNotification.forEach(function (unitWon) { + const adBidWonUrl = buildEndpointUrl( + ENDPOINT_PROTOCOL, + ENDPOINT_DOMAIN, + unitWon.path + ); if (unitWon.method === 'POST') { - spec.postRequest(adBidWonUrl, JSON.stringify(unitWon.data)); + postRequest(adBidWonUrl, JSON.stringify(unitWon.data)); } }); return true; }, - postRequest(endpoint, data) { - ajax(endpoint, null, data, {method: 'POST'}); - }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { const syncs = []; @@ -210,7 +142,6 @@ export const spec = { return syncs; }, - -} +}; registerBidder(spec); diff --git a/modules/mediakeysBidAdapter.js b/modules/mediakeysBidAdapter.js index f4967fed170..efe9eb90256 100644 --- a/modules/mediakeysBidAdapter.js +++ b/modules/mediakeysBidAdapter.js @@ -11,6 +11,7 @@ import { isFn, isInteger, isNumber, + isPlainObject, isStr, logError, logWarn, @@ -66,6 +67,7 @@ const ORTB_VIDEO_PARAMS = { h: value => isInteger(value), startdelay: value => isInteger(value), placement: value => [1, 2, 3, 4, 5].indexOf(value) !== -1, + plcmt: value => [1, 2, 3, 4].indexOf(value) !== -1, linearity: value => [1, 2].indexOf(value) !== -1, skip: value => [0, 1].indexOf(value) !== -1, skipmin: value => isInteger(value), @@ -138,7 +140,7 @@ function getFloor(bid, mediaType, size = '*') { size }) - return (!isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) ? floor.floor : false + return (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) ? floor.floor : false } /** diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index 68927cc6b13..69dfaab3db5 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -1,887 +1,877 @@ import { - _map, deepAccess, - getWindowTop, + deepSetValue, groupBy, - isEmpty, + isFn, isPlainObject, logError, logInfo, - triggerPixel, - uniques + parseUrl, + safeJSONEncode, } from '../src/utils.js'; -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import {config} from '../src/config.js'; import adapterManager from '../src/adapterManager.js'; -import { BID_STATUS, EVENTS, TARGETING_KEYS } from '../src/constants.js'; -import {ajax} from '../src/ajax.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import {BID_STATUS, EVENTS, REJECTION_REASON, S2S, TARGETING_KEYS} from '../src/constants.js'; import {getRefererInfo} from '../src/refererDetection.js'; -import {AUCTION_COMPLETED, AUCTION_IN_PROGRESS, getPriceGranularity} from '../src/auction.js'; -import {includes} from '../src/polyfill.js'; +import {ajax} from '../src/ajax.js'; +import {getPriceByGranularity} from '../src/auction.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +import {registerVastTrackers} from '../libraries/vastTrackers/vastTrackers.js'; +import { + filterBidsListByFilters, + findBidObj, + formatQS, getBidResponseSize, + getRequestedSizes, + isSampledForLogging, + onHidden, + pick, +} from '../libraries/medianetUtils/utils.js'; +import { + errorLogger, + firePostLog, + getLoggingPayload, + shouldLogAPPR +} from '../libraries/medianetUtils/logger.js'; +import {KeysMap} from '../libraries/medianetUtils/logKeys.js'; +import { + LOGGING_DELAY, + BID_FLOOR_REJECTED, + BID_NOBID, + BID_SUCCESS, + BID_TIMEOUT, + CONFIG_ERROR, + CONFIG_PASS, + CONFIG_PENDING, + CONFIG_URL, + DBF_PRIORITY, + DEFAULT_LOGGING_PERCENT, + DUMMY_BIDDER, + ERROR_CONFIG_FETCH, + ERROR_CONFIG_JSON_PARSE, + GET_ENDPOINT, + GLOBAL_VENDOR_ID, + LOG_APPR, + LOG_RA, + mnetGlobals, + NOBID_AFTER_AUCTION, + PBS_ERROR_STATUS_START, + POST_ENDPOINT, + SEND_ALL_BID_PROP, + SUCCESS_AFTER_AUCTION, + TIMEOUT_AFTER_AUCTION, + VIDEO_CONTEXT, + VIDEO_UUID_PENDING, + WINNING_AUCTION_MISSING_ERROR, + WINNING_BID_ABSENT_ERROR, ERROR_IWB_BID_MISSING +} from '../libraries/medianetUtils/constants.js'; import {getGlobal} from '../src/prebidGlobal.js'; -const analyticsType = 'endpoint'; -const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; -const CONFIG_URL = 'https://prebid.media.net/rtb/prebid/analytics/config'; -const EVENT_PIXEL_URL = 'https://qsearch-a.akamaihd.net/log'; -const DEFAULT_LOGGING_PERCENT = 50; -const ANALYTICS_VERSION = '1.0.0'; - -const PRICE_GRANULARITY = { - 'auto': 'pbAg', - 'custom': 'pbCg', - 'dense': 'pbDg', - 'low': 'pbLg', - 'medium': 'pbMg', - 'high': 'pbHg', -}; +// General Constants +const ADAPTER_CODE = 'medianetAnalytics'; -const MEDIANET_BIDDER_CODE = 'medianet'; -// eslint-disable-next-line no-undef -const PREBID_VERSION = getGlobal().version; -const ERROR_CONFIG_JSON_PARSE = 'analytics_config_parse_fail'; -const ERROR_CONFIG_FETCH = 'analytics_config_ajax_fail'; -const ERROR_WINNING_BID_ABSENT = 'winning_bid_absent'; -const ERROR_WINNING_AUCTION_MISSING = 'winning_auction_missing'; -const BID_SUCCESS = 1; -const BID_NOBID = 2; -const BID_TIMEOUT = 3; -const BID_FLOOR_REJECTED = 12; -const DUMMY_BIDDER = '-2'; - -const CONFIG_PENDING = 0; -const CONFIG_PASS = 1; -const CONFIG_ERROR = 3; - -const VALID_URL_KEY = ['canonical_url', 'og_url', 'twitter_url']; -const DEFAULT_URL_KEY = 'topmostLocation'; - -const LOG_TYPE = { - APPR: 'APPR', - RA: 'RA' +const LoggingEvents = { + SETUP_LISTENERS: 'setupListeners', + CONFIG_INIT: 'loadConfig', + FETCH_CONFIG: 'fetchConfig', + ...EVENTS, }; - -let auctions = {}; -let config; -let pageDetails; -let logsQueue = []; -let errorQueue = []; - -class ErrorLogger { - constructor(event, additionalData) { - this.event = event; - this.logid = 'kfk'; - this.evtid = 'projectevents'; - this.project = 'prebidanalytics'; - this.dn = pageDetails.domain || ''; - this.requrl = pageDetails.topmostLocation || ''; - this.pbversion = PREBID_VERSION; - this.cid = config.cid || ''; - this.rd = additionalData; - } - - send() { - let url = EVENT_PIXEL_URL + '?' + formatQS(this); - errorQueue.push(url); - triggerPixel(url); - } -} - -class Configure { - constructor(cid) { - this.cid = cid; - this.pubLper = -1; - this.ajaxState = CONFIG_PENDING; - this.loggingPercent = DEFAULT_LOGGING_PERCENT; - this.urlToConsume = DEFAULT_URL_KEY; - this.debug = false; - this.gdprConsent = undefined; - this.gdprApplies = undefined; - this.uspConsent = undefined; - this.shouldBeLogged = {}; - this.mnetDebugConfig = ''; - } - - getLoggingData() { - return { - cid: this.cid, - lper: Math.round(100 / this.loggingPercent), - plper: this.pubLper, - gdpr: this.gdprApplies ? '1' : '0', - gdprConsent: this.gdprConsent, - ccpa: this.uspConsent, - ajx: this.ajaxState, - pbv: PREBID_VERSION, - pbav: ANALYTICS_VERSION, - flt: 1, +// =============================[ CONFIGURATION ]================================= +function fetchAnalyticsConfig() { + function updateLoggingPercentage(response) { + if (!isNaN(parseInt(response.percentage, 10))) { + mnetGlobals.configuration.loggingPercent = response.percentage; } } - _configURL() { - return CONFIG_URL + '?cid=' + encodeURIComponent(this.cid) + '&dn=' + encodeURIComponent(pageDetails.domain); + function parseConfig(loggingConfig) { + const domain = deepAccess(loggingConfig, 'domain.' + mnetGlobals.refererInfo.domain); + updateLoggingPercentage(domain || loggingConfig); + + mnetGlobals.configuration.shouldLogAPPR = isSampledForLogging(); + mnetGlobals.configuration.ajaxState = CONFIG_PASS; } - _parseResponse(response) { + function success(response) { try { - response = JSON.parse(response); - this.setDataFromResponse(response); - this.overrideDomainLevelData(response); - this.overrideToDebug(this.mnetDebugConfig); - this.urlToConsume = includes(VALID_URL_KEY, response.urlKey) ? response.urlKey : this.urlToConsume; - this.ajaxState = CONFIG_PASS; + parseConfig(JSON.parse(response)); } catch (e) { - this.ajaxState = CONFIG_ERROR; - /* eslint no-new: "error" */ - new ErrorLogger(ERROR_CONFIG_JSON_PARSE, e).send(); + mnetGlobals.configuration.ajaxState = CONFIG_ERROR; + errorLogger(ERROR_CONFIG_JSON_PARSE, e).send(); } } - setDataFromResponse(response) { - if (!isNaN(parseInt(response.percentage, 10))) { - this.loggingPercent = response.percentage; - } + function error() { + mnetGlobals.configuration.ajaxState = CONFIG_ERROR; + errorLogger(ERROR_CONFIG_FETCH).send(); } - overrideDomainLevelData(response) { - const domain = deepAccess(response, 'domain.' + pageDetails.domain); - if (domain) { - this.setDataFromResponse(domain); - } + function getConfigURL() { + return `${CONFIG_URL}?${(formatQS({ + cid: mnetGlobals.configuration.cid, + dn: mnetGlobals.refererInfo.domain + }))}` } - overrideToDebug(response) { - if (response === '') return; - try { - this.setDataFromResponse(JSON.parse(decodeURIComponent(response))); - } catch (e) { - } + // Debugging and default settings + const urlObj = parseUrl(mnetGlobals.refererInfo.topmostLocation); + if (deepAccess(urlObj, 'search.medianet_test') || urlObj.hostname === 'localhost') { + Object.assign(mnetGlobals.configuration, { + loggingPercent: 100, + shouldLogAPPR: true, + ajaxState: CONFIG_PASS, + debug: true, + }); + return; } - _errorFetch() { - this.ajaxState = CONFIG_ERROR; - /* eslint no-new: "error" */ - new ErrorLogger(ERROR_CONFIG_FETCH).send(); + if (mnetGlobals.configuration.loggingConfig) { + mnetGlobals.configuration.loggingDelay = mnetGlobals.configuration.loggingConfig.loggingDelay || mnetGlobals.configuration.loggingDelay; + parseConfig(mnetGlobals.configuration.loggingConfig); + return; } - init() { - // Forces Logging % to 100% - let urlObj = URL.parseUrl(pageDetails.topmostLocation); - if (deepAccess(urlObj, 'search.medianet_test') || urlObj.hostname === 'localhost') { - this.loggingPercent = 100; - this.ajaxState = CONFIG_PASS; - this.debug = true; - return; + ajax(getConfigURL(), { success, error }); +} + +function initConfiguration(eventType, configuration) { + mnetGlobals.refererInfo = getRefererInfo(); + // Holds configuration details + mnetGlobals.configuration = { + ...mnetGlobals.configuration, + pubLper: configuration.options.sampling || '', + ajaxState: CONFIG_PENDING, + shouldLogAPPR: false, + debug: false, + loggingPercent: DEFAULT_LOGGING_PERCENT, + enabledUids: [], + commonParams: configuration.commonParams, + loggingDelay: LOGGING_DELAY, + ...configuration.options, + }; + mnetGlobals.eventQueue.enqueueEvent(LoggingEvents.SETUP_LISTENERS, mnetGlobals.configuration); + mnetGlobals.eventQueue.enqueueEvent(LoggingEvents.FETCH_CONFIG, mnetGlobals.configuration); +} + +// ======================[ LOGGING AND TRACKING ]=========================== +function doLogging(auctionObj, adUnitCode, logType, bidObj) { + const queryParams = getQueryString(auctionObj, adUnitCode, logType, bidObj); + // Use the generated queryParams for logging + const payload = getLoggingPayload(queryParams); + firePostLog(POST_ENDPOINT, payload); + auctionObj.adSlots[adUnitCode].logged[logType] = true; +} + +function getQueryString(auctionObj, adUnitCode, logType, winningBidObj) { + const commonParams = getCommonParams(auctionObj, adUnitCode, logType); + const bidParams = getBidParams(auctionObj, adUnitCode, winningBidObj); + const queryString = formatQS(commonParams); + let bidStrings = bidParams.map((bid) => `&${formatQS(bid)}`).join(''); + return `${queryString}${bidStrings}`; +} + +function getErrorTracker(bidResponse, error) { + const stack = { + acid: bidResponse.auctionId, + bidId: bidResponse.requestId, + crid: bidResponse.creativeId, + ttl: bidResponse.ttl, + bidder: bidResponse.bidderCode || bidResponse.adapterCode, + context: bidResponse.context, + }; + return [ + { + event: 'impressions', + url: errorLogger('vast_tracker_handler_' + error, stack).getUrl(), + }, + ]; +} + +function vastTrackerHandler(bidResponse, { auction, bidRequest }) { + if (!config.getConfig('cache')?.url) return []; + try { + if (auction) { + mnetGlobals.eventQueue.enqueueEvent(EVENTS.AUCTION_INIT, auction); + } + const bidderRequest = findBidObj(auction.bidderRequests, 'bidderRequestId', bidRequest?.bidderRequestId); + if (bidderRequest) { + mnetGlobals.eventQueue.enqueueEvent(EVENTS.BID_REQUESTED, bidderRequest); + } + const auctionObject = mnetGlobals.auctions[bidResponse.auctionId]; + if (!auctionObject) { + return getErrorTracker(bidResponse, 'missing_auction'); + } + const requestId = bidResponse.originalRequestId || bidResponse.requestId; + const bidRequestObj = findBidObj(auctionObject.bidsRequested, 'bidId', requestId); + if (!bidRequestObj) { + return getErrorTracker(bidResponse, 'missing_bidrequest'); } - if (deepAccess(urlObj, 'search.mnet_setconfig')) { - this.mnetDebugConfig = deepAccess(urlObj, 'search.mnet_setconfig'); + const context = auctionObject.adSlots[bidRequestObj?.adUnitCode]?.context; + if (context !== VIDEO_CONTEXT.INSTREAM) { + return []; } - ajax( - this._configURL(), + bidRequestObj.status = VIDEO_UUID_PENDING; + const { validBidResponseObj } = processBidResponse(auctionObject, bidRequestObj, bidResponse); + const queryParams = getQueryString(auctionObject, bidRequestObj.adUnitCode, LOG_RA, validBidResponseObj); + return [ { - success: this._parseResponse.bind(this), - error: this._errorFetch.bind(this) - } - ); + event: 'impressions', + url: `${GET_ENDPOINT}?${getLoggingPayload(queryParams)}`, + }, + ]; + } catch (e) { + errorLogger('vast_tracker_handler_error', e).send(); + return []; } } -class PageDetail { - constructor () { - const ogUrl = this._getUrlFromSelector('meta[property="og:url"]', 'content'); - const twitterUrl = this._getUrlFromSelector('meta[name="twitter:url"]', 'content'); - const refererInfo = getRefererInfo(); - - // TODO: are these the right refererInfo values? - this.domain = refererInfo.domain; - this.page = refererInfo.page; - this.is_top = refererInfo.reachedTop; - this.referrer = refererInfo.ref || window.document.referrer; - this.canonical_url = refererInfo.canonicalUrl; - this.og_url = ogUrl; - this.twitter_url = twitterUrl; - this.topmostLocation = refererInfo.topmostLocation; - this.screen = this._getWindowSize(); +function processBidResponse(auctionObj, bidRequest, bidResponse) { + bidRequest.bidTs = Date.now(); + bidRequest.responseReceived = true; + // timeout bid can be there for the bidResponse came after auctionEnd + // or it can be from a multi-bid response with a different requestId + let bidObj = findBidObj(auctionObj.bidsReceived, 'bidId', bidResponse.requestId); + let bidIdAlreadyPresent = true; + if (!bidObj || bidObj.status === BID_SUCCESS) { + bidObj = Object.assign({}, bidRequest); + bidIdAlreadyPresent = false; } - _getWindowSize() { - let w = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || -1; - let h = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || -1; - return `${w}x${h}`; - } + Object.assign( + bidObj, + pick(bidResponse, KeysMap.Pick.BidResponse), + getDfpCurrencyInfo(bidResponse), + ); - _getAttributeFromSelector(selector, attribute) { - try { - let doc = getWindowTop().document; - let element = doc.querySelector(selector); - if (element !== null && element[attribute]) { - return element[attribute]; - } - } catch (e) {} + if (bidObj.status === BID_STATUS.BID_REJECTED) { + bidObj.status = BID_FLOOR_REJECTED; + } else { + bidObj.status = auctionObj.hasEnded ? SUCCESS_AFTER_AUCTION : BID_SUCCESS; } + bidRequest.status = bidObj.status; + return { validBidResponseObj: bidObj, bidIdAlreadyPresent }; +} - _getAbsoluteUrl(url) { - let aTag = getWindowTop().document.createElement('a'); - aTag.href = url; +function getLoggingBids(auctionObj, adUnitCode) { + const receivedResponse = buildBidResponseMap(auctionObj, adUnitCode); + const dummyBids = getDummyBids(auctionObj, adUnitCode, receivedResponse); - return aTag.href; - } + return [...auctionObj.psiBids, ...auctionObj.bidsReceived, ...dummyBids].filter( + (bid) => bid.adUnitCode === adUnitCode + ); +} - _getUrlFromSelector(selector, attribute) { - let attr = this._getAttributeFromSelector(selector, attribute); - return attr && this._getAbsoluteUrl(attr); +function getBidParams(auctionObj, adUnitCode, winningBidObj) { + let loggableBids = []; + if (winningBidObj) { + const responseInfoMap = mnetGlobals.infoByAdIdMap[winningBidObj.adId] || {}; + const bidLogData = Object.assign( + {}, + pick(winningBidObj, KeysMap.Log.Bid), + pick(responseInfoMap.srrEvt, ['lineItemId as lid', 'creativeId as crtvid'], true) + ); + loggableBids.push(bidLogData); + } else { + // For logging all bids + loggableBids = setDbf(getLoggingBids(auctionObj, adUnitCode)) + .map((bidObj) => pick(bidObj, KeysMap.Log.Bid)) + .map(({ winner, ...restParams }) => restParams); } + return loggableBids; +} - getLoggingData() { - return { - requrl: this[config.urlToConsume] || this.topmostLocation, - dn: this.domain, - ref: this.referrer, - screen: this.screen - } - } +function getDummyBids(auctionObj, adUnitCode, receivedResponse) { + const emptyBids = []; + + auctionObj.bidsRequested + .forEach((bid) => { + if (bid.adUnitCode !== adUnitCode) return + const emptySizes = bid.sizes.filter( + (size) => !deepAccess(receivedResponse, `${bid.bidId}.${size}`) + ); + + if (emptySizes.length > 0) { + const bidObj = Object.assign({}, bid, { + res_sizes: emptySizes, + status: bid.status === BID_SUCCESS ? BID_NOBID : bid.status, + iwb: 0, + }); + emptyBids.push(bidObj); + } + }); + + return emptyBids; } -class AdSlot { - constructor(tmax, supplyAdCode, context, adext) { - this.tmax = tmax; - this.supplyAdCode = supplyAdCode; - this.context = context; - this.adext = adext; - this.logged = {}; - this.targeting = undefined; - this.medianetPresent = 0; - } +function setDbf(bids) { + const highestBids = {}; - getShouldBeLogged(logType) { - if (!config.shouldBeLogged.hasOwnProperty(logType)) { - config.shouldBeLogged[logType] = isSampled(); + bids.forEach((bid) => { + bid.dbf = 0; // Default all dbf to 0 + if (isHigher(bid, highestBids[bid.bidder])) { + highestBids[bid.bidder] = bid; } - return config.shouldBeLogged[logType]; - } + }); - getLoggingData() { - return Object.assign({ - supcrid: this.supplyAdCode, - tmax: this.tmax, - targ: JSON.stringify(this.targeting), - ismn: this.medianetPresent, - vplcmtt: this.context, - }, - this.adext && {'adext': JSON.stringify(this.adext)}, - ); - } + // Mark the highest-priority bids as dbf = 1 + Object.values(highestBids).forEach((bid) => { + bid.dbf = 1; + }); + + return bids; } -class BidWrapper { - constructor() { - this.bidReqs = []; - this.bidObjs = []; - } +function isHigher(newBid, currentBid = {}) { + const newPriority = DBF_PRIORITY[newBid.status] ?? 0; + const currentPriority = DBF_PRIORITY[currentBid.status] ?? -1; - findReqBid(bidId) { - return this.bidReqs.find(bid => { - return bid['bidId'] === bidId - }); - } + return ( + newPriority > currentPriority || + (newPriority === currentPriority && (newBid.cpm ?? 0) > (currentBid.cpm ?? -1)) + ); +} - findBidObj(key, value) { - return this.bidObjs.find(bid => { - return bid[key] === value - }); - } +function markWinningBidsAndImpressionStatus(auctionObj) { + const sendAllBidsEnabled = config.getConfig(SEND_ALL_BID_PROP) === true; - addBidReq(bidRequest) { - this.bidReqs.push(bidRequest) - } + const updatePsiBid = (winner, adUnitCode, winnersAdIds) => { + const psiBidObj = findBidObj(auctionObj.psiBids, 'adUnitCode', adUnitCode); + if (!psiBidObj) { + return; + } + if (winnersAdIds.length > 0) { + psiBidObj.iwb = 1; + psiBidObj.width = deepAccess(winner, 'width') ?? null; + psiBidObj.height = deepAccess(winner, 'height') ?? null; + psiBidObj.size = getBidResponseSize(psiBidObj.width, psiBidObj.height); + } + const bidsRequested = filterBidsListByFilters(auctionObj.bidsRequested, { adUnitCode }); + const bidsTimeout = filterBidsListByFilters(auctionObj.bidsTimeout, { adUnitCode }); - addBidObj(bidObj) { - if (!(bidObj instanceof Bid)) { - bidObj = Bid.getInstance(bidObj); + if (bidsRequested.length === bidsTimeout.length) { + psiBidObj.status = BID_TIMEOUT; } - const bidReq = this.findReqBid(bidObj.bidId); - if (bidReq instanceof Bid) { - bidReq.used = true; + }; + + const markValidBidsAsWinners = (winnersAdIds) => { + if (!sendAllBidsEnabled) { + return; } - this.bidObjs.push(bidObj); - } + winnersAdIds.forEach((adId) => { + const sendAllWinnerBid = findBidObj(auctionObj.bidsReceived, 'adId', adId); + if (sendAllWinnerBid) { + sendAllWinnerBid.iwb = 1; + } + }); + }; - getAdSlotBids(adSlot) { - const bidResponses = this.getAdSlotBidObjs(adSlot); - return bidResponses.map((bid) => bid.getLoggingData()); + const checkWinnersForIwb = (winner, winningBidObj) => { + // bid-cache can be enabled + const fromSameAuction = (winner?.auctionId === auctionObj.auctionId); + if (fromSameAuction && !winningBidObj) { + errorLogger(ERROR_IWB_BID_MISSING, pick(winner, ['adId', 'auctionId', 'bidder', 'requestId', 'cpm', 'adUnitCode'])).send(); + } } - getAdSlotBidObjs(adSlot) { - const bidResponses = this.bidObjs - .filter((bid) => bid.adUnitCode === adSlot); - const remResponses = this.bidReqs.filter(bid => !bid.used && bid.adUnitCode === adSlot); - return [...bidResponses, ...remResponses]; - } + Object.keys(auctionObj.adSlots).forEach((adUnitCode) => { + const winner = getGlobal().getHighestCpmBids(adUnitCode)[0]; + const winningBid = findBidObj(auctionObj.bidsReceived, 'adId', winner?.adId); + if (winningBid && winningBid.status === BID_SUCCESS) { + winningBid.iwb = 1; + } + checkWinnersForIwb(winner, winningBid); + + const targetingForAdUnitCode = getGlobal().getAdserverTargetingForAdUnitCode(adUnitCode); + auctionObj.adSlots[adUnitCode].targeting = targetingForAdUnitCode; + const winnersAdIds = []; + Object.keys(targetingForAdUnitCode).forEach((key) => { + if (key.includes(TARGETING_KEYS.AD_ID)) { + winnersAdIds.push(targetingForAdUnitCode[key]); + } + }); + markValidBidsAsWinners(winnersAdIds); + updatePsiBid(winner, adUnitCode, winnersAdIds); + }); +} +// =====================[ S2S ]====================== +function addS2sInfo(auctionObj, bidderRequests) { + bidderRequests.forEach((bidderRequest) => { + bidderRequest.bids.forEach((bidRequest) => { + if (bidRequest.src !== S2S.SRC) return; + + const bidObjs = filterBidsListByFilters(auctionObj.bidsReceived, {bidId: bidRequest.bidId}); + + bidObjs.forEach((bidObj) => { + bidObj.serverLatencyMillis = bidderRequest.serverResponseTimeMs; + const serverError = deepAccess(bidderRequest, `serverErrors.0`); + if (serverError && bidObj.status !== BID_SUCCESS) { + bidObj.status = PBS_ERROR_STATUS_START + serverError.code; + } + }); + }); + }); } -class Bid { - constructor(bidId, bidder, src, start, adUnitCode, mediaType, allMediaTypeSizes) { - this.bidId = bidId; - this.bidder = bidder; - this.src = src; - this.start = start; - this.adUnitCode = adUnitCode; - this.allMediaTypeSizes = allMediaTypeSizes; - this.iwb = 0; - this.winner = 0; - this.status = bidder === DUMMY_BIDDER ? BID_SUCCESS : BID_TIMEOUT; - this.ext = {}; - this.originalCpm = undefined; - this.cpm = undefined; - this.dfpbd = undefined; - this.width = undefined; - this.height = undefined; - this.mediaType = mediaType; - this.timeToRespond = undefined; - this.dealId = undefined; - this.creativeId = undefined; - this.adId = undefined; - this.currency = undefined; - this.crid = undefined; - this.pubcrid = undefined; - this.mpvid = undefined; - this.floorPrice = undefined; - this.floorRule = undefined; - this.serverLatencyMillis = undefined; - this.used = false; - this.originalRequestId = bidId; - this.requestId = undefined; - } +// =========================[ HELPERS ]========================== +function getAllMediaTypesAndSizes(adUnits) { + const allMTypes = new Set(); + const allSizes = new Set(); - get size() { - if (!this.width || !this.height) { - return ''; - } - return this.width + 'x' + this.height; - } + adUnits.forEach(({ mediaTypes, sizes }) => { + Object.keys(mediaTypes).forEach((mType) => allMTypes.add(mType)); + getRequestedSizes({ mediaTypes, sizes }).forEach((size) => allSizes.add(size)); + }); - static getInstance(bidProps) { - const bidObj = new Bid(); - return bidProps && Object.assign(bidObj, bidProps); - } + return { allMTypes: [...allMTypes], allSizes: [...allSizes] }; +} - getLoggingData() { - return { - reqId: this.requestId || this.bidId, - ogReqId: this.originalRequestId, - adid: this.adId, - pvnm: this.bidder, - src: this.src, - ogbdp: this.originalCpm, - bdp: this.cpm, - cbdp: this.dfpbd, - dfpbd: this.dfpbd, - szs: this.allMediaTypeSizes.join('|'), - size: this.size, - mtype: this.mediaType, - dId: this.dealId, - winner: this.winner, - curr: this.currency, - rests: this.timeToRespond, - status: this.status, - iwb: this.iwb, - crid: this.crid, - pubcrid: this.pubcrid, - mpvid: this.mpvid, - bidflr: this.floorPrice, - flrrule: this.floorRule, - ext: JSON.stringify(this.ext), - rtime: this.serverLatencyMillis, - } - } +function getAdSlot(adUnits) { + const context = adUnits.find((adUnit) => !!deepAccess(adUnit, 'mediaTypes.video.context'))?.mediaTypes.video.context; + return Object.assign( + {}, + pick(adUnits[0], [...KeysMap.Pick.AdSlot, 'context', () => context]), + getAllMediaTypesAndSizes(adUnits) + ); } -class Auction { - constructor(acid) { - this.acid = acid; - this.status = AUCTION_IN_PROGRESS; - this.bidWrapper = new BidWrapper(); - this.adSlots = {}; - this.auctionInitTime = undefined; - this.auctionStartTime = undefined; - this.setTargetingTime = undefined; - this.auctionEndTime = undefined; - this.bidWonTime = undefined; - this.floorData = {}; - } +function buildBidResponseMap(auctionObj, adUnitCode) { + const responses = [].concat(auctionObj.bidsReceived, auctionObj.psiBids).filter((bid) => bid.adUnitCode === adUnitCode); + const receivedResponse = {}; - hasEnded() { - return this.status === AUCTION_COMPLETED; - } + // Set true in map for success bids + responses + .forEach((bid) => { + if (!bid.size) return; + const sizeKey = `${bid.bidId}.${bid.size}`; + deepSetValue(receivedResponse, sizeKey, true); + }); - getLoggingData() { - return { - sts: this.auctionStartTime - this.auctionInitTime, - ets: this.auctionEndTime - this.auctionInitTime, - tts: this.setTargetingTime - this.auctionInitTime, - wts: this.bidWonTime - this.auctionInitTime, - aucstatus: this.status, - acid: this.acid, - flrdata: this._mergeFieldsToLog({ - ln: this.floorData.location, - skp: this.floorData.skipped, - enfj: deepAccess(this.floorData, 'enforcements.enforceJS'), - enfd: deepAccess(this.floorData, 'enforcements.floorDeals'), - sr: this.floorData.skipRate, - fs: this.floorData.fetchStatus - }), - flrver: this.floorData.modelVersion - } - } + // For non-success bids: + // 1) set bid.res_sizes = (sizes for which no successful bid received) + // 2) set true in map + responses + .forEach((bid) => { + if (bid.size) return; + bid.res_sizes = bid.sizes.filter( + (size) => !deepAccess(receivedResponse, `${bid.bidId}.${size}`) + ); + bid.res_sizes.forEach((size) => + deepSetValue(receivedResponse, `${bid.bidId}.${size}`, true) + ); + }); + + return receivedResponse; +} - addSlot({ adUnitCode, supplyAdCode, mediaTypes, allMediaTypeSizes, tmax, adext, context }) { - if (adUnitCode && this.adSlots[adUnitCode] === undefined) { - this.adSlots[adUnitCode] = new AdSlot(tmax, supplyAdCode, context, adext); - this.addBidObj(new Bid('-1', DUMMY_BIDDER, 'client', Date.now(), adUnitCode, mediaTypes, allMediaTypeSizes)); +function getDfpCurrencyInfo(bidResponse) { + function convertCurrency(price, fromCurrency, toCurrency) { + try { + return getGlobal().convertCurrency?.(price, fromCurrency, toCurrency) || price; + } catch (e) { + logError(`Currency conversion failed: ${fromCurrency} -> ${toCurrency} for price ${price}`); + return price; } } - addBid(bid) { - this.bidWrapper.addBidReq(bid); + let { source, ext, cpm, originalCpm, currency = '', originalCurrency = '', adserverTargeting } = bidResponse; + currency = currency.toUpperCase(); + originalCurrency = (originalCurrency || currency).toUpperCase(); + originalCpm = originalCpm || cpm; + // https://docs.prebid.org/prebid-server/endpoints/openrtb2/pbs-endpoint-auction.html#original-bid-cpm + if (source === S2S.SRC) { + originalCurrency = deepAccess(ext, 'origbidcur') || originalCurrency; + originalCpm = deepAccess(ext, 'origbidcpm') || originalCpm; } - addBidObj(bidObj) { - this.bidWrapper.addBidObj(bidObj) + let currMul = 1; + let inCurrMul = 1; + + if (currency !== 'USD') { + cpm = convertCurrency(cpm, currency, 'USD'); + currMul = convertCurrency(1, 'USD', currency); } - findReqBid(bidId) { - return this.bidWrapper.findReqBid(bidId) + if (originalCurrency !== 'USD') { + originalCpm = convertCurrency(originalCpm, originalCurrency, 'USD'); + inCurrMul = convertCurrency(1, 'USD', originalCurrency); } - findBidObj(key, value) { - return this.bidWrapper.findBidObj(key, value) + let bdp = mnetGlobals.bdpMap[bidResponse.adId]; + if (bdp) { + bdp = convertCurrency(bdp, currency, 'USD'); } - getAdSlotBids(adSlot) { - return this.bidWrapper.getAdSlotBids(adSlot); + // dfpBd + let dfpbd = deepAccess(adserverTargeting, `${TARGETING_KEYS.PRICE_BUCKET}`); + if (!dfpbd) { + const priceGranularityKey = getPriceByGranularity(bidResponse); + dfpbd = bidResponse[priceGranularityKey] || bidResponse.cpm; } - getAdSlotBidObjs(adSlot) { - return this.bidWrapper.getAdSlotBidObjs(adSlot); + if (currency !== 'USD' && dfpbd) { + dfpbd = convertCurrency(dfpbd, currency, 'USD'); } - _mergeFieldsToLog(objParams) { - let logParams = []; - let value; - for (const param of Object.keys(objParams)) { - value = objParams[param]; - logParams.push(param + '=' + (value === undefined ? '' : value)); - } - return logParams.join('||'); - } + return { + originalCpm, + bdp, + cpm, + dfpbd, + currMul, + inCurrMul, + }; } -function auctionInitHandler({auctionId, adUnits, timeout, timestamp, bidderRequests}) { - if (auctionId && auctions[auctionId] === undefined) { - auctions[auctionId] = new Auction(auctionId); - auctions[auctionId].auctionInitTime = timestamp; - } - addAddSlots(auctionId, adUnits, timeout); - const floorData = deepAccess(bidderRequests, '0.bids.0.floorData'); - if (floorData) { - auctions[auctionId].floorData = {...floorData}; +/** + * Generates auction-level, slot-level, and config-level parameters. + */ +function getCommonParams(auctionObj, adUnitCode, logType) { + const adSlotObj = auctionObj.adSlots[adUnitCode] || {}; + let commonParams = Object.assign( + { lgtp: logType }, + pick(mnetGlobals.configuration, KeysMap.Log.Globals), + pick(auctionObj, KeysMap.Log.Auction), + pick(adSlotObj, KeysMap.Log.AdSlot), + mnetGlobals.configuration.commonParams + ); + if (logType === LOG_RA) { + commonParams.lper = 1; } -} - -function addAddSlots(auctionId, adUnits, tmax) { - adUnits = adUnits || []; - const groupedAdUnits = groupBy(adUnits, 'code'); - Object.keys(groupedAdUnits).forEach((adUnitCode) => { - const adUnits = groupedAdUnits[adUnitCode]; - const supplyAdCode = deepAccess(adUnits, '0.adUnitCode') || adUnitCode; - let context = ''; - let adext = {}; - - const mediaTypeMap = {}; - const oSizes = {banner: [], video: []}; - adUnits.forEach(({mediaTypes, sizes, ext}) => { - mediaTypes = mediaTypes || {}; - adext = Object.assign(adext, ext || deepAccess(mediaTypes, 'banner.ext')); - context = deepAccess(mediaTypes, 'video.context') || context; - Object.keys(mediaTypes).forEach((mediaType) => mediaTypeMap[mediaType] = 1); - const sizeObject = _getSizes(mediaTypes, sizes); - sizeObject.banner.forEach(size => oSizes.banner.push(size)); - sizeObject.video.forEach(size => oSizes.video.push(size)); - }); - - adext = isEmpty(adext) ? undefined : adext; - oSizes.banner = oSizes.banner.filter(uniques); - oSizes.video = oSizes.video.filter(uniques); - oSizes.native = mediaTypeMap.native === 1 ? [[1, 1].join('x')] : []; - const allMediaTypeSizes = [].concat(oSizes.banner, oSizes.native, oSizes.video); - const mediaTypes = Object.keys(mediaTypeMap).join('|'); - auctions[auctionId].addSlot({adUnitCode, supplyAdCode, mediaTypes, allMediaTypeSizes, context, tmax, adext}); + Object.keys(commonParams).forEach((key) => { + if (commonParams[key] === undefined) { + delete commonParams[key]; + } }); + return commonParams; } -function bidRequestedHandler({ auctionId, auctionStart, bids, start, uspConsent, gdpr }) { - if (!(auctions[auctionId] instanceof Auction)) { - return; - } +function setupSlotResponseReceivedListener() { + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(() => { + window.googletag.pubads().addEventListener('slotResponseReceived', (slotEvent) => { + if (!slotEvent.slot || !isFn(slotEvent.slot.getResponseInformation)) return; - config.gdprApplies = !!(gdpr && gdpr.gdprApplies); - if (config.gdprApplies) { - config.gdprConsent = gdpr.consentString || ''; - } + const slot = slotEvent.slot; + const slotInf = slot.getResponseInformation(); - config.uspConsent = config.uspConsent || uspConsent; - - auctions[auctionId].auctionStartTime = auctionStart; - bids.forEach(bid => { - const { adUnitCode, bidder, bidId, src, mediaTypes, sizes } = bid; - const sizeObject = _getSizes(mediaTypes, sizes); - const requestSizes = [].concat(sizeObject.banner, sizeObject.native, sizeObject.video); - const bidObj = new Bid(bidId, bidder, src, start, adUnitCode, mediaTypes && Object.keys(mediaTypes).join('|'), requestSizes); - auctions[auctionId].addBid(bidObj); - if (bidder === MEDIANET_BIDDER_CODE) { - bidObj.crid = deepAccess(bid, 'params.crid'); - bidObj.pubcrid = deepAccess(bid, 'params.crid'); - auctions[auctionId].adSlots[adUnitCode].medianetPresent = 1; - } + const setSlotResponseInf = (adId) => { + mnetGlobals.infoByAdIdMap[adId] = mnetGlobals.infoByAdIdMap[adId] || {}; + mnetGlobals.infoByAdIdMap[adId].srrEvt = slotInf; + }; + + slot.getTargetingKeys() + .filter((key) => key.startsWith(TARGETING_KEYS.AD_ID)) + .forEach((key) => setSlotResponseInf(slot.getTargeting(key)[0])); + }); }); } -function _getSizes(mediaTypes, sizes) { - const banner = deepAccess(mediaTypes, 'banner.sizes') || sizes || []; - const native = deepAccess(mediaTypes, 'native') ? [[1, 1]] : []; - const playerSize = deepAccess(mediaTypes, 'video.playerSize') || []; - let video = []; - if (playerSize.length === 2) { - video = [playerSize] - } - return { - banner: banner.map(size => size.join('x')), - native: native.map(size => size.join('x')), - video: video.map(size => size.join('x')) +// ======================[ EVENT QUEUE PROCESSING ]======================= +const eventQueue = () => { + function enqueueEvent(eventType, args) { + if (mnetGlobals.configuration.debug) { + logInfo(eventType, args); + } + processEventQueue(eventType, args); } -} - -function bidResponseHandler(bid) { - const { width, height, mediaType, cpm, requestId, timeToRespond, auctionId, dealId, originalRequestId, bidder } = bid; - const {originalCpm, creativeId, adId, currency} = bid; - if (!(auctions[auctionId] instanceof Auction)) { - return; + function processEventQueue(eventType, args) { + try { + const handler = eventListeners[eventType]; + if (!handler) { + return; + } + handler(eventType, args); + } catch (e) { + errorLogger(`${eventType}_handler_error`, e).send(); + } } - const reqId = originalRequestId || requestId; - const bidReq = auctions[auctionId].findReqBid(reqId); - if (!(bidReq instanceof Bid)) return; + return { + enqueueEvent, + processEventQueue, + }; +}; - let bidObj = auctions[auctionId].findBidObj('bidId', requestId); - let isBidOverridden = true; - if (!bidObj || bidObj.status === BID_SUCCESS) { - bidObj = {}; - isBidOverridden = false; - } - Object.assign(bidObj, bidReq, - { cpm, width, height, mediaType, timeToRespond, dealId, creativeId, originalRequestId, requestId }, - { adId, currency } - ); - bidObj.floorPrice = deepAccess(bid, 'floorData.floorValue'); - bidObj.floorRule = deepAccess(bid, 'floorData.floorRule'); - bidObj.originalCpm = originalCpm || cpm; - let dfpbd = deepAccess(bid, 'adserverTargeting.hb_pb'); - if (!dfpbd) { - let priceGranularity = getPriceGranularity(bid); - let priceGranularityKey = PRICE_GRANULARITY[priceGranularity]; - dfpbd = bid[priceGranularityKey] || cpm; - } - bidObj.dfpbd = dfpbd; - if (bid.status === BID_STATUS.BID_REJECTED) { - bidObj.status = BID_FLOOR_REJECTED; - } else { - bidObj.status = BID_SUCCESS; +// ======================[ AUCTION EVENT HANDLERS ]===================== +function auctionInitHandler(eventType, auction) { + let auctionObj = mnetGlobals.auctions[auction.auctionId]; + if (auctionObj) { + return; } + auctionObj = pick(auction, KeysMap.Pick.Auction); + // addAddSlots + Object + .values(groupBy(auction.adUnits, 'code')) + .map(getAdSlot) + .forEach(adSlot => { + // Assign adSlot to auctionObj.adSlots + auctionObj.adSlots[adSlot.code] = adSlot; + // Push the PSI bid + auctionObj.psiBids.push({ + src: 'client', + bidId: '-1', + originalRequestId: '-1', + bidder: DUMMY_BIDDER, + mediaTypes: adSlot.allMTypes, + sizes: adSlot.allSizes, + size: getBidResponseSize(-1, -1), + width: -1, + height: -1, + iwb: 0, + status: BID_SUCCESS, + adUnitCode: adSlot.code, + }); + }); - if (bidder === MEDIANET_BIDDER_CODE && bid.ext instanceof Object) { - Object.assign( - bidObj, - { 'ext': bid.ext }, - { 'mpvid': bid.ext.pvid }, - bid.ext.crid && { 'crid': bid.ext.crid } - ); - } - if (typeof bid.serverResponseTimeMs !== 'undefined') { - bidObj.serverLatencyMillis = bid.serverResponseTimeMs; - } - !isBidOverridden && auctions[auctionId].addBidObj(bidObj); + // addUidData + const userIds = deepAccess(auction.bidderRequests, '0.bids.0.userId'); + if (isPlainObject(userIds)) { + const enabledUids = mnetGlobals.configuration.enabledUids || []; + auctionObj.availableUids = Object.keys(userIds).sort(); + auctionObj.uidValues = auctionObj.availableUids + .filter((key) => enabledUids.includes(key)) + .map((key) => `${key}##${safeJSONEncode(userIds[key])}`); + } + mnetGlobals.refererInfo = auctionObj.refererInfo; + mnetGlobals.auctions[auction.auctionId] = auctionObj; } -function noBidResponseHandler({ auctionId, bidId }) { - if (!(auctions[auctionId] instanceof Auction)) { - return; - } - if (auctions[auctionId].hasEnded()) { - return; - } - const bidReq = auctions[auctionId].findReqBid(bidId); - if (!(bidReq instanceof Bid) || bidReq.used) { - return; - } - const bidObj = {...bidReq}; - bidObj.status = BID_NOBID; - auctions[auctionId].addBidObj(bidObj); +function auctionEndHandler(eventType, auctionEndData) { + const auctionObj = mnetGlobals.auctions[auctionEndData.auctionId]; + if (!auctionObj) return; + + auctionObj.hasEnded = true; + auctionObj.auctionEndTime = auctionEndData.auctionEnd; + markWinningBidsAndImpressionStatus(auctionObj); + const execute = () => { + addS2sInfo(auctionObj, auctionEndData.bidderRequests); + Object.keys(auctionObj.adSlots).forEach((adUnitCode) => { + shouldLogAPPR(auctionObj, adUnitCode) && doLogging(auctionObj, adUnitCode, LOG_APPR); + }); + }; + const timeout = auctionObj.pendingRequests === 0 ? 0 : mnetGlobals.configuration.loggingDelay; + auctionObj.loggerTimeout = timeout; + Promise.race([ + new Promise((resolve) => setTimeout(resolve, timeout)), + new Promise((resolve) => onHidden(resolve)), + ]).finally(execute); } -function bidTimeoutHandler(timedOutBids) { - timedOutBids.map(({bidId, auctionId}) => { - if (!(auctions[auctionId] instanceof Auction)) { - return; - } - const bidReq = auctions[auctionId].findReqBid('bidId', bidId); - if (!(bidReq instanceof Bid) || bidReq.used) { - return; - } - const bidObj = {...bidReq}; - bidObj.status = BID_TIMEOUT; - auctions[auctionId].addBidObj(bidObj); - }) +function bidRequestedHandler(eventType, bidRequestedData) { + const auctionObj = mnetGlobals.auctions[bidRequestedData.auctionId]; + if (!auctionObj) return; + auctionObj.auctionStartTime = bidRequestedData.auctionStart; + bidRequestedData.bids + // In the case of video (instream), the `bidRequested` object might already exist. + .filter(({ bidId }) => !findBidObj(auctionObj.bidsRequested, 'bidId', bidId)) + .forEach((bidRequested) => { + const bidRequestObj = Object.assign({}, + pick(bidRequested, KeysMap.Pick.BidRequest), + ); + auctionObj.bidsRequested.push(bidRequestObj); + }); } -function auctionEndHandler({auctionId, auctionEnd}) { - if (!(auctions[auctionId] instanceof Auction)) { - return; - } - auctions[auctionId].status = AUCTION_COMPLETED; - auctions[auctionId].auctionEndTime = auctionEnd; -} +function bidResponseHandler(eventType, validBidResponse) { + const auctionObj = mnetGlobals.auctions[validBidResponse.auctionId]; + if (!auctionObj) return; -function setTargetingHandler(params) { - for (const adunit of Object.keys(params)) { - for (const auctionId of Object.keys(auctions)) { - let auctionObj = auctions[auctionId]; - let adunitObj = auctionObj.adSlots[adunit]; - if (!(adunitObj instanceof AdSlot)) { - continue; - } - adunitObj.targeting = params[adunit]; - auctionObj.setTargetingTime = Date.now(); - let targetingObj = Object.keys(params[adunit]).reduce((result, key) => { - if (key.indexOf(TARGETING_KEYS.AD_ID) !== -1) { - result[key] = params[adunit][key] - } - return result; - }, {}); - const winnerAdId = params[adunit][TARGETING_KEYS.AD_ID]; - let winningBid; - let bidAdIds = Object.keys(targetingObj).map(k => targetingObj[k]); - auctionObj.bidWrapper.bidObjs.filter((bid) => bidAdIds.indexOf(bid.adId) !== -1).map(function(bid) { - bid.iwb = 1; - if (bid.adId === winnerAdId) { - winningBid = bid; - } - }); - auctionObj.bidWrapper.bidObjs.forEach(bid => { - if (bid.bidder === DUMMY_BIDDER && bid.adUnitCode === adunit) { - bid.iwb = bidAdIds.length === 0 ? 0 : 1; - bid.width = deepAccess(winningBid, 'width'); - bid.height = deepAccess(winningBid, 'height'); - } - }); - sendEvent(auctionId, adunit, LOG_TYPE.APPR); - } + const requestId = validBidResponse.originalRequestId || validBidResponse.requestId; + const bidRequest = findBidObj(auctionObj.bidsRequested, 'bidId', requestId); + if (!bidRequest) return; + + const { bidIdAlreadyPresent = true, validBidResponseObj } = processBidResponse( + auctionObj, + bidRequest, + validBidResponse + ); + auctionObj.responseBids.push(validBidResponseObj); + if (!bidIdAlreadyPresent && validBidResponseObj) { + auctionObj.bidsReceived.push(validBidResponseObj); } } -function bidWonHandler(bid) { - const { auctionId, adUnitCode, adId, bidder, requestId, originalRequestId } = bid; - if (!(auctions[auctionId] instanceof Auction)) { - new ErrorLogger(ERROR_WINNING_AUCTION_MISSING, { +function bidWonHandler(eventType, winner) { + const { auctionId, adUnitCode, adId, bidder, requestId, originalRequestId } = winner; + const auctionObj = mnetGlobals.auctions[auctionId]; + if (!auctionObj) { + errorLogger(WINNING_AUCTION_MISSING_ERROR, { adId, auctionId, adUnitCode, bidder, requestId, - originalRequestId + originalRequestId, }).send(); return; } - let bidObj = auctions[auctionId].findBidObj('adId', adId); - if (!(bidObj instanceof Bid)) { - new ErrorLogger(ERROR_WINNING_BID_ABSENT, { + const bidObj = findBidObj(auctionObj.bidsReceived, 'adId', adId); + if (!bidObj) { + errorLogger(WINNING_BID_ABSENT_ERROR, { adId, auctionId, adUnitCode, bidder, requestId, - originalRequestId + originalRequestId, }).send(); return; } - auctions[auctionId].bidWonTime = Date.now(); - bidObj.winner = 1; - sendEvent(auctionId, adUnitCode, LOG_TYPE.RA, bidObj.adId); + auctionObj.bidWonTime = Date.now(); + // Update the bidObj with the winner details + Object.assign( + bidObj, + pick(winner, [ + 'latestTargetedAuctionId', + 'winner', () => 1, + 'utime', () => (bidObj.bidTs ? Date.now() - bidObj.bidTs : undefined) + ]) + ); + doLogging(auctionObj, adUnitCode, LOG_RA, bidObj); } -function isSampled() { - return Math.random() * 100 < parseFloat(config.loggingPercent); +function bidRejectedHandler(eventType, bidRejected) { + const auctionObj = mnetGlobals.auctions[bidRejected.auctionId]; + if (!auctionObj) return; + if (bidRejected.rejectionReason === REJECTION_REASON.FLOOR_NOT_MET) { + bidRejected.snm = BID_STATUS.BID_REJECTED; + bidResponseHandler(eventType, bidRejected); + } } -function isValidAuctionAdSlot(acid, adtag) { - return (auctions[acid] instanceof Auction) && (auctions[acid].adSlots[adtag] instanceof AdSlot); -} +function noBidResponseHandler(eventType, noBid) { + const auctionObj = mnetGlobals.auctions[noBid.auctionId]; + if (!auctionObj) return; -function sendEvent(id, adunit, logType, adId) { - if (!isValidAuctionAdSlot(id, adunit)) { - return; - } - if (logType === LOG_TYPE.RA) { - fireAuctionLog(id, adunit, logType, adId); - } else { - fireApPrLog(id, adunit, logType) - } -} + const bidRequest = findBidObj(auctionObj.bidsRequested, 'bidId', noBid.bidId); + if (!bidRequest || bidRequest.responseReceived) return; -function fireApPrLog(auctionId, adUnitName, logType) { - const adSlot = auctions[auctionId].adSlots[adUnitName]; - if (adSlot.getShouldBeLogged(logType) && !adSlot.logged[logType]) { - fireAuctionLog(auctionId, adUnitName, logType); - adSlot.logged[logType] = true; - } + const status = auctionObj.hasEnded ? NOBID_AFTER_AUCTION : BID_NOBID; + const noBidObj = Object.assign({}, bidRequest, { + status, + metrics: noBid.metrics, + }); + bidRequest.status = status; + bidRequest.responseReceived = true; + auctionObj.noBids.push(noBidObj); + auctionObj.bidsReceived.push(noBidObj); } -function getCommonLoggingData(acid, adtag) { - let commonParams = Object.assign(pageDetails.getLoggingData(), config.getLoggingData()); - let adunitParams = auctions[acid].adSlots[adtag].getLoggingData(); - let auctionParams = auctions[acid].getLoggingData(); - return Object.assign(commonParams, adunitParams, auctionParams); +function bidTimeoutHandler(eventType, timedOutBids) { + const auctionId = deepAccess(timedOutBids, '0.auctionId'); + const auctionObj = mnetGlobals.auctions[auctionId]; + if (!auctionObj) return; + + const status = auctionObj.hasEnded ? TIMEOUT_AFTER_AUCTION : BID_TIMEOUT; + timedOutBids.forEach((timedOutBid) => { + const bidRequest = findBidObj(auctionObj.bidsRequested, 'bidId', timedOutBid.bidId); + if (!bidRequest) return; + const timedOutObj = Object.assign({}, bidRequest, { + status, + metrics: timedOutBid.metrics, + }); + bidRequest.status = status; + bidRequest.responseReceived = true; + auctionObj.bidsTimeout.push(timedOutObj); + auctionObj.bidsReceived.push(timedOutObj); + }); } -function fireAuctionLog(acid, adtag, logType, adId) { - let commonParams = getCommonLoggingData(acid, adtag); - commonParams.lgtp = logType; - let targeting = deepAccess(commonParams, 'targ'); - - Object.keys(commonParams).forEach((key) => (commonParams[key] == null) && delete commonParams[key]); - delete commonParams.targ; - - let bidParams; - - if (logType === LOG_TYPE.RA) { - const winningBidObj = auctions[acid].findBidObj('adId', adId); - if (!winningBidObj) return; - const winLogData = winningBidObj.getLoggingData(); - bidParams = [winLogData]; - commonParams.lper = 1; - } else { - bidParams = auctions[acid].getAdSlotBids(adtag).map(({winner, ...restParams}) => restParams); - delete commonParams.wts; - } - let mnetPresent = bidParams.filter(b => b.pvnm === MEDIANET_BIDDER_CODE).length > 0; - if (!mnetPresent) { - bidParams = bidParams.map(({mpvid, crid, ext, pubcrid, ...restParams}) => restParams); - } - - let url = formatQS(commonParams) + '&'; - bidParams.forEach(function(bidParams) { - url = url + formatQS(bidParams) + '&'; - }); - url = url + formatQS({targ: targeting}); - firePixel(url); +function bidderDoneHandler(eventType, args) { + const auctionObj = mnetGlobals.auctions[args.auctionId]; + if (!auctionObj) return; + auctionObj.pendingRequests--; } -function formatQS(data) { - return _map(data, (value, key) => { - if (value === undefined) { - return key + '='; - } - if (isPlainObject(value)) { - value = JSON.stringify(value); - } - return key + '=' + encodeURIComponent(value); - }).join('&'); +function adRenderFailedHandler(eventType, args) { + const {reason, message, bid: { + auctionId, + adUnitCode, + bidder, + creativeId}} = args; + errorLogger(eventType, { + reason, + message, + auctionId, + adUnitCode, + bidder, + creativeId, + }).send(); } -function firePixel(qs) { - logsQueue.push(ENDPOINT + '&' + qs); - triggerPixel(ENDPOINT + '&' + qs); +function adRenderSucceededHandler(eventType, args) { + const {bid: {auctionId, adUnitCode, bidder, creativeId}} = args; + errorLogger(eventType, { + auctionId, + adUnitCode, + bidder, + creativeId, + }).send(); } -class URL { - static parseUrl(url) { - let parsed = document.createElement('a'); - parsed.href = decodeURIComponent(url); - return { - hostname: parsed.hostname, - search: URL.parseQS(parsed.search || ''), - host: parsed.host || window.location.host - }; - } - static parseQS(query) { - return !query ? {} : query - .replace(/^\?/, '') - .split('&') - .reduce((acc, criteria) => { - let [k, v] = criteria.split('='); - if (/\[\]$/.test(k)) { - k = k.replace('[]', ''); - acc[k] = acc[k] || []; - acc[k].push(v); - } else { - acc[k] = v || ''; - } - return acc; - }, {}); - } +function staleRenderHandler(eventType, args) { + const { auctionId, adUnitCode, bidder, creativeId, adId, cpm } = args; + errorLogger(eventType, { + adId, + auctionId, + adUnitCode, + bidder, + creativeId, + cpm, + }).send(); } -let medianetAnalytics = Object.assign(adapter({URL, analyticsType}), { +// ======================[ EVENT LISTENER MAP ]===================== +// Define listener functions for each event +const eventListeners = { + [LoggingEvents.SETUP_LISTENERS]: setupListeners, + [LoggingEvents.CONFIG_INIT]: initConfiguration, + [LoggingEvents.FETCH_CONFIG]: fetchAnalyticsConfig, + [LoggingEvents.AUCTION_INIT]: auctionInitHandler, + [LoggingEvents.AUCTION_END]: auctionEndHandler, + [LoggingEvents.BID_REQUESTED]: bidRequestedHandler, + [LoggingEvents.BID_RESPONSE]: bidResponseHandler, + [LoggingEvents.BID_REJECTED]: bidRejectedHandler, + [LoggingEvents.NO_BID]: noBidResponseHandler, + [LoggingEvents.BIDDER_DONE]: bidderDoneHandler, + [LoggingEvents.BID_TIMEOUT]: bidTimeoutHandler, + [LoggingEvents.BID_WON]: bidWonHandler, + [LoggingEvents.AD_RENDER_FAILED]: adRenderFailedHandler, + [LoggingEvents.AD_RENDER_SUCCEEDED]: adRenderSucceededHandler, + [LoggingEvents.STALE_RENDER]: staleRenderHandler, +}; + +let medianetAnalytics = Object.assign(adapter({ analyticsType: 'endpoint' }), { getlogsQueue() { - return logsQueue; + return mnetGlobals.logsQueue; }, + getErrorQueue() { - return errorQueue; + return mnetGlobals.errorQueue; }, + + getVastTrackerHandler() { + return vastTrackerHandler; + }, + clearlogsQueue() { - logsQueue = []; - errorQueue = []; - auctions = {}; + mnetGlobals.logsQueue = []; + mnetGlobals.errorQueue = []; + mnetGlobals.auctions = {}; }, + track({ eventType, args }) { - if (config.debug) { - logInfo(eventType, args); - } - switch (eventType) { - case EVENTS.AUCTION_INIT: { - auctionInitHandler(args); - break; - } - case EVENTS.BID_REQUESTED: { - bidRequestedHandler(args); - break; - } - case EVENTS.BID_RESPONSE: { - bidResponseHandler(args); - break; - } - case EVENTS.BID_TIMEOUT: { - bidTimeoutHandler(args); - break; - } - case EVENTS.NO_BID: { - noBidResponseHandler(args); - break; - } - case EVENTS.AUCTION_END: { - auctionEndHandler(args); - break; - } - case EVENTS.SET_TARGETING: { - setTargetingHandler(args); - break; - } - case EVENTS.BID_WON: { - bidWonHandler(args); - break; - } - } - }}); + mnetGlobals.eventQueue.enqueueEvent(eventType, args); + }, +}); -medianetAnalytics.originEnableAnalytics = medianetAnalytics.enableAnalytics; +function setupListeners() { + setupSlotResponseReceivedListener(); + registerVastTrackers(MODULE_TYPE_ANALYTICS, ADAPTER_CODE, vastTrackerHandler); +} +medianetAnalytics.originEnableAnalytics = medianetAnalytics.enableAnalytics; medianetAnalytics.enableAnalytics = function (configuration) { if (!configuration || !configuration.options || !configuration.options.cid) { logError('Media.net Analytics adapter: cid is required.'); @@ -889,20 +879,17 @@ medianetAnalytics.enableAnalytics = function (configuration) { } getGlobal().medianetGlobals = getGlobal().medianetGlobals || {}; getGlobal().medianetGlobals.analyticsEnabled = true; - - pageDetails = new PageDetail(); - - config = new Configure(configuration.options.cid); - config.pubLper = configuration.options.sampling || ''; - config.init(); + getGlobal().medianetGlobals.analytics = mnetGlobals; + mnetGlobals.eventQueue = eventQueue(); + mnetGlobals.eventQueue.enqueueEvent(LoggingEvents.CONFIG_INIT, configuration); configuration.options.sampling = 1; medianetAnalytics.originEnableAnalytics(configuration); }; adapterManager.registerAnalyticsAdapter({ adapter: medianetAnalytics, - code: 'medianetAnalytics', - gvlid: 142, + code: ADAPTER_CODE, + gvlid: GLOBAL_VENDOR_ID, }); export default medianetAnalytics; diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index c2c792ec866..38e8a17f442 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -1,23 +1,27 @@ import { - buildUrl, deepAccess, - getWindowTop, isArray, isEmpty, isEmptyStr, isStr, logError, logInfo, - triggerPixel + safeJSONEncode, + deepClone, + deepSetValue, getWindowTop } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getGlobal} from '../src/prebidGlobal.js'; import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {getViewportCoordinates} from '../libraries/viewport/viewport.js'; +import {filterBidsListByFilters, getTopWindowReferrer} from '../libraries/medianetUtils/utils.js'; +import {errorLogger} from '../libraries/medianetUtils/logger.js'; +import {GLOBAL_VENDOR_ID, MEDIANET} from '../libraries/medianetUtils/constants.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getBoundingClientRect} from '../libraries/boundingClientRect/boundingClientRect.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -25,7 +29,7 @@ import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid */ -const BIDDER_CODE = 'medianet'; +const BIDDER_CODE = MEDIANET; const TRUSTEDSTACK_CODE = 'trustedstack'; const BID_URL = 'https://prebid.media.net/rtb/prebid'; const TRUSTEDSTACK_URL = 'https://prebid.trustedstack.com/rtb/trustedstack'; @@ -35,70 +39,53 @@ const SLOT_VISIBILITY = { ABOVE_THE_FOLD: 1, BELOW_THE_FOLD: 2 }; -const EVENTS = { +export const EVENTS = { TIMEOUT_EVENT_NAME: 'client_timeout', - BID_WON_EVENT_NAME: 'client_bid_won' + BID_WON_EVENT_NAME: 'client_bid_won', + SET_TARGETING: 'client_set_targeting', + BIDDER_ERROR: 'client_bidder_error' }; -const EVENT_PIXEL_URL = 'qsearch-a.akamaihd.net/log'; const OUTSTREAM = 'outstream'; -// TODO: this should be picked from bidderRequest -let refererInfo = getRefererInfo(); - -let mnData = {}; +let pageMeta; +let customerId; window.mnet = window.mnet || {}; window.mnet.queue = window.mnet.queue || []; -mnData.urlData = { - domain: refererInfo.domain, - page: refererInfo.page, - isTop: refererInfo.reachedTop -}; - const aliases = [ { code: TRUSTEDSTACK_CODE, gvlid: 1288 }, ]; getGlobal().medianetGlobals = getGlobal().medianetGlobals || {}; -function getTopWindowReferrer() { - try { - return window.top.document.referrer; - } catch (e) { - return document.referrer; - } -} - function siteDetails(site, bidderRequest) { const urlData = bidderRequest.refererInfo; site = site || {}; let siteData = { domain: site.domain || urlData.domain, page: site.page || urlData.page, - ref: site.ref || getTopWindowReferrer(), + ref: getTopWindowReferrer(site.ref), topMostLocation: urlData.topmostLocation, isTop: site.isTop || urlData.reachedTop }; - - return Object.assign(siteData, getPageMeta()); + if (!pageMeta) { + pageMeta = getPageMeta(); + } + return Object.assign(siteData, pageMeta); } function getPageMeta() { - if (mnData.pageMeta) { - return mnData.pageMeta; + if (pageMeta) { + return pageMeta; } let canonicalUrl = getUrlFromSelector('link[rel="canonical"]', 'href'); - let ogUrl = getUrlFromSelector('meta[property="og:url"]', 'content'); - let twitterUrl = getUrlFromSelector('meta[name="twitter:url"]', 'content'); - mnData.pageMeta = Object.assign({}, + pageMeta = Object.assign({}, canonicalUrl && { 'canonical_url': canonicalUrl }, - ogUrl && { 'og_url': ogUrl }, - twitterUrl && { 'twitter_url': twitterUrl } ); - return mnData.pageMeta; + return pageMeta; } function getUrlFromSelector(selector, attribute) { @@ -123,10 +110,6 @@ function getAbsoluteUrl(url) { return aTag.href; } -function filterUrlsByType(urls, type) { - return urls.filter(url => url.type === type); -} - function transformSizes(sizes) { if (isArray(sizes) && sizes.length === 2 && !isArray(sizes[0])) { return [getSize(sizes)]; @@ -158,8 +141,8 @@ function getCoordinates(adUnitCode) { element = document.getElementById(divId); } } - if (element && element.getBoundingClientRect) { - const rect = element.getBoundingClientRect(); + if (element) { + const rect = getBoundingClientRect(element); let coordinates = {}; coordinates.top_left = { y: rect.top, @@ -184,9 +167,10 @@ function extParams(bidRequest, bidderRequests) { const gdprApplies = !!(gdpr && gdpr.gdprApplies); const uspApplies = !!(uspConsent); const coppaApplies = !!(config.getConfig('coppa')); + const {top = -1, right = -1, bottom = -1, left = -1} = getViewportCoordinates(); return Object.assign({}, { customer_id: params.cid }, - { prebid_version: getGlobal().version }, + { prebid_version: 'v' + '$prebid.version$' }, { gdpr_applies: gdprApplies }, (gdprApplies) && { gdpr_consent_string: gdpr.consentString || '' }, { usp_applies: uspApplies }, @@ -195,18 +179,24 @@ function extParams(bidRequest, bidderRequests) { windowSize.w !== -1 && windowSize.h !== -1 && { screen: windowSize }, userId && { user_id: userId }, getGlobal().medianetGlobals.analyticsEnabled && { analytics: true }, - !isEmpty(sChain) && {schain: sChain} + !isEmpty(sChain) && {schain: sChain}, + { + vcoords: { + top_left: { x: left, y: top }, + bottom_right: { x: right, y: bottom } + } + } ); } -function slotParams(bidRequest) { +function slotParams(bidRequest, bidderRequests) { // check with Media.net Account manager for bid floor and crid parameters let params = { id: bidRequest.bidId, transactionId: bidRequest.ortb2Imp?.ext?.tid, ext: { dfp_id: bidRequest.adUnitCode, - display_count: bidRequest.bidRequestsCount + display_count: bidRequest.auctionsCount }, all: bidRequest.params }; @@ -261,7 +251,9 @@ function slotParams(bidRequest) { if (floorInfo && floorInfo.length > 0) { params.bidfloors = floorInfo; } - + if (bidderRequests.paapi?.enabled) { + params.ext.ae = bidRequest?.ortb2Imp?.ext?.ae; + } return params; } @@ -270,7 +262,7 @@ function getBidFloorByType(bidRequest) { if (typeof bidRequest.getFloor === 'function') { [BANNER, VIDEO, NATIVE].forEach(mediaType => { if (bidRequest.mediaTypes.hasOwnProperty(mediaType)) { - if (mediaType == BANNER) { + if (mediaType === BANNER) { bidRequest.mediaTypes.banner.sizes.forEach( size => { setFloorInfo(bidRequest, mediaType, size, floorInfo) @@ -285,7 +277,7 @@ function getBidFloorByType(bidRequest) { return floorInfo; } function setFloorInfo(bidRequest, mediaType, size, floorInfo) { - let floor = bidRequest.getFloor({currency: 'USD', mediaType: mediaType, size: size}); + let floor = bidRequest.getFloor({currency: 'USD', mediaType: mediaType, size: size}) || {}; if (size.length > 1) floor.size = size; floor.mediaType = mediaType; floorInfo.push(floor); @@ -319,14 +311,15 @@ function getOverlapArea(topLeft1, bottomRight1, topLeft2, bottomRight2) { } function normalizeCoordinates(coordinates) { + const {scrollX, scrollY} = window; return { top_left: { - x: coordinates.top_left.x + window.pageXOffset, - y: coordinates.top_left.y + window.pageYOffset, + x: coordinates.top_left.x + scrollX, + y: coordinates.top_left.y + scrollY, }, bottom_right: { - x: coordinates.bottom_right.x + window.pageXOffset, - y: coordinates.bottom_right.y + window.pageYOffset, + x: coordinates.bottom_right.x + scrollX, + y: coordinates.bottom_right.y + scrollY, } } } @@ -336,14 +329,23 @@ function getBidderURL(bidderCode, cid) { return url + '?cid=' + encodeURIComponent(cid); } +function ortb2Data(ortb2, bidRequests) { + const ortb2Object = deepClone(ortb2); + const eids = deepAccess(bidRequests, '0.userIdAsEids'); + if (eids) { + deepSetValue(ortb2Object, 'user.ext.eids', eids) + } + return ortb2Object; +} + function generatePayload(bidRequests, bidderRequests) { return { site: siteDetails(bidRequests[0].params.site, bidderRequests), ext: extParams(bidRequests[0], bidderRequests), // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 id: bidRequests[0].auctionId, - imp: bidRequests.map(request => slotParams(request)), - ortb2: bidderRequests.ortb2, + imp: bidRequests.map(request => slotParams(request, bidderRequests)), + ortb2: ortb2Data(bidderRequests.ortb2, bidRequests), tmax: bidderRequests.timeout } } @@ -361,38 +363,42 @@ function fetchCookieSyncUrls(response) { return []; } -function getLoggingData(event, data) { - data = (isArray(data) && data) || []; - - let params = {}; - params.logid = 'kfk'; - params.evtid = 'projectevents'; - params.project = 'prebid'; - params.acid = deepAccess(data, '0.auctionId') || ''; - params.cid = getGlobal().medianetGlobals.cid || ''; - params.crid = data.map((adunit) => deepAccess(adunit, 'params.0.crid') || adunit.adUnitCode).join('|'); - params.adunit_count = data.length || 0; - params.dn = mnData.urlData.domain || ''; - params.requrl = mnData.urlData.page || ''; - params.istop = mnData.urlData.isTop || ''; - params.event = event.name || ''; - params.value = event.value || ''; - params.rd = event.related_data || ''; +function getBidData(bid) { + const params = {}; + params.acid = bid.auctionId || ''; + params.crid = deepAccess(bid, 'params.crid') || deepAccess(bid, 'params.0.crid') || bid.adUnitCode || ''; + params.ext = safeJSONEncode(bid.ext) || ''; + const rawobj = deepClone(bid); + delete rawobj.ad; + delete rawobj.vastXml; + params.rawobj = safeJSONEncode(rawobj); return params; } -function logEvent (event, data) { - let getParams = { - protocol: 'https', - hostname: EVENT_PIXEL_URL, - search: getLoggingData(event, data) - }; - triggerPixel(buildUrl(getParams)); +function getLoggingData(bids) { + const logData = {}; + if (!isArray(bids)) { + bids = []; + } + bids.forEach((bid) => { + let bidData = getBidData(bid); + Object.keys(bidData).forEach((key) => { + logData[key] = logData[key] || []; + logData[key].push(encodeURIComponent(bidData[key])); + }); + }); + return logData; +} + +function logEvent(event, data) { + const logData = getLoggingData(data); + event.cid = customerId; + errorLogger(event, logData, false).send(); } -function clearMnData() { - mnData = {}; +function clearPageMeta() { + pageMeta = undefined; } function addRenderer(bid) { @@ -401,7 +407,7 @@ function addRenderer(bid) { /* Adding renderer only when the context is Outstream and the provider has responded with a renderer. */ - if (videoContext == OUTSTREAM && vastTimeout) { + if (videoContext === OUTSTREAM && vastTimeout) { bid.renderer = newVideoRenderer(bid); } } @@ -432,7 +438,7 @@ function newVideoRenderer(bid) { export const spec = { code: BIDDER_CODE, - gvlid: 142, + gvlid: GLOBAL_VENDOR_ID, aliases, supportedMediaTypes: [BANNER, NATIVE, VIDEO], @@ -452,9 +458,7 @@ export const spec = { logError(`${BIDDER_CODE} : cid should be a string`); return false; } - - Object.assign(getGlobal().medianetGlobals, !getGlobal().medianetGlobals.cid && {cid: bid.params.cid}); - + customerId = bid.params.cid; return true; }, @@ -481,36 +485,43 @@ export const spec = { * Unpack the response from the server into a list of bids. * * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. + * @returns {{bids: *[], fledgeAuctionConfigs: *[]} | *[]} An object containing bids and fledgeAuctionConfigs if present, otherwise an array of bids. */ interpretResponse: function(serverResponse, request) { let validBids = []; - if (!serverResponse || !serverResponse.body) { logInfo(`${BIDDER_CODE} : response is empty`); return validBids; } - let bids = serverResponse.body.bidList; if (!isArray(bids) || bids.length === 0) { logInfo(`${BIDDER_CODE} : no bids`); + } else { + validBids = bids.filter(bid => isValidBid(bid)); + validBids.forEach(addRenderer); + } + const fledgeAuctionConfigs = deepAccess(serverResponse, 'body.ext.paApiAuctionConfigs') || []; + const ortbAuctionConfigs = deepAccess(serverResponse, 'body.ext.igi') || []; + if (fledgeAuctionConfigs.length === 0 && ortbAuctionConfigs.length === 0) { return validBids; } - validBids = bids.filter(bid => isValidBid(bid)); - - validBids.forEach(addRenderer); - - return validBids; + if (ortbAuctionConfigs.length > 0) { + fledgeAuctionConfigs.push(...ortbAuctionConfigs.map(({igs}) => igs || []).flat()); + } + return { + bids: validBids, + paapi: fledgeAuctionConfigs, + } }, getUserSyncs: function(syncOptions, serverResponses) { let cookieSyncUrls = fetchCookieSyncUrls(serverResponses); if (syncOptions.iframeEnabled) { - return filterUrlsByType(cookieSyncUrls, 'iframe'); + return filterBidsListByFilters(cookieSyncUrls, {type: 'iframe'}); } if (syncOptions.pixelEnabled) { - return filterUrlsByType(cookieSyncUrls, 'image'); + return filterBidsListByFilters(cookieSyncUrls, {type: 'image'}); } }, @@ -522,7 +533,7 @@ export const spec = { let eventData = { name: EVENTS.TIMEOUT_EVENT_NAME, value: timeoutData.length, - related_data: timeoutData[0].timeout || config.getConfig('bidderTimeout') + relatedData: timeoutData[0].timeout || config.getConfig('bidderTimeout') }; logEvent(eventData, timeoutData); } catch (e) {} @@ -541,7 +552,30 @@ export const spec = { } catch (e) {} }, - clearMnData, + onSetTargeting: (bid) => { + try { + let eventData = { + name: EVENTS.SET_TARGETING, + value: bid.cpm + }; + const enableSendAllBids = config.getConfig('enableSendAllBids'); + if (!enableSendAllBids) { + logEvent(eventData, [bid]); + } + } catch (e) {} + }, + + onBidderError: ({error, bidderRequest}) => { + try { + let eventData = { + name: EVENTS.BIDDER_ERROR, + relatedData: `timedOut:${error.timedOut}|status:${error.status}|message:${error.reason.message}` + }; + logEvent(eventData, bidderRequest.bids); + } catch (e) {} + }, + + clearPageMeta, getWindowSize, }; diff --git a/modules/medianetBidAdapter.md b/modules/medianetBidAdapter.md index fbe967122e9..500c9f3f12b 100644 --- a/modules/medianetBidAdapter.md +++ b/modules/medianetBidAdapter.md @@ -180,4 +180,24 @@ var adUnits = [{ }]; +``` + +# Protected Audience API (FLEDGE) + +In order to enable PAAPI auctions follow the instructions below: + +1. Add the paapiForGpt and paapi modules to your prebid bundle. +2. Add the following configuration for the module +``` +pbjs.que.push(function() { + pbjs.setConfig({ + paapi: { + enabled: true, + bidders: ['medianet'], + defaultForSlots: 1 + } + }); +}); +``` +For a detailed guide to enabling PAAPI auctions follow Prebid's documentation on [paapiForGpt](https://docs.prebid.org/dev-docs/modules/paapiForGpt.html) diff --git a/modules/medianetRtdProvider.js b/modules/medianetRtdProvider.js index 5a159b39081..42fcffbf576 100644 --- a/modules/medianetRtdProvider.js +++ b/modules/medianetRtdProvider.js @@ -3,6 +3,7 @@ import {loadExternalScript} from '../src/adloader.js'; import {submodule} from '../src/hook.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {includes} from '../src/polyfill.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; const MODULE_NAME = 'medianet'; const SOURCE = MODULE_NAME + 'rtd'; @@ -84,7 +85,7 @@ function executeCommand(command) { function loadRtdScript(customerId) { const url = getClientUrl(customerId, window.location.hostname); - loadExternalScript(url, MODULE_NAME) + loadExternalScript(url, MODULE_TYPE_RTD, MODULE_NAME) } function getAdUnits(adUnits, adUnitCodes) { diff --git a/modules/mediasniperBidAdapter.js b/modules/mediasniperBidAdapter.js index 5cf0ceaba18..796a15e1778 100644 --- a/modules/mediasniperBidAdapter.js +++ b/modules/mediasniperBidAdapter.js @@ -7,6 +7,7 @@ import { isEmpty, isFn, isNumber, + isPlainObject, isStr, logError, logMessage, @@ -262,7 +263,7 @@ function getFloor(bid, mediaType, size = '*') { size, }); - return !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY + return isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY ? floor.floor : false; } diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index 3f3a90c3c49..a64fb8a7e20 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -143,7 +143,7 @@ export const merkleIdSubmodule = { return; } - if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) { + if (consentData?.gdpr?.gdprApplies === true) { logError('User ID - merkleId submodule does not currently handle consent strings'); return; } diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index fb3990e97f1..86cd3fb8250 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -8,18 +8,22 @@ import { parseUrl, isEmpty, triggerPixel, + triggerNurlWithCpm, logWarn, isFn, isNumber, isBoolean, - isInteger, deepSetValue, getBidIdParameter, + extractDomainFromHost, + isInteger, deepSetValue, getBidIdParameter, setOnAny, + getWinDimensions } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {USERSYNC_DEFAULT_CONFIG} from '../src/userSync.js'; +import { getUserSyncs } from '../libraries/mgidUtils/mgidUtils.js' +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -86,7 +90,7 @@ _each(NATIVE_ASSETS, anAsset => { _NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAs _each(NATIVE_ASSETS, anAsset => { _NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset }); export const spec = { - VERSION: '1.6', + VERSION: '1.8', code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER, NATIVE], @@ -156,7 +160,7 @@ export const spec = { if (isStr(muid) && muid.length > 0) { url += '?muid=' + muid; } - const cur = setOnAny(validBidRequests, 'params.currency') || setOnAny(validBidRequests, 'params.cur') || config.getConfig('currency.adServerCurrency') || DEFAULT_CUR; + const cur = setOnAny(validBidRequests, 'params.currency') || setOnAny(validBidRequests, 'params.cur') || getCurrencyFromBidderRequest(bidderRequest) || DEFAULT_CUR; const secure = window.location.protocol === 'https:' ? 1 : 0; let imp = []; validBidRequests.forEach(bid => { @@ -167,6 +171,8 @@ export const spec = { tagid, secure, }; + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); + gpid && isStr(gpid) && deepSetValue(impObj, `ext.gpid`, gpid); const floorData = getBidFloor(bid, cur); if (floorData.floor) { impObj.bidfloor = floorData.floor; @@ -229,7 +235,7 @@ export const spec = { const page = deepAccess(bidderRequest, 'refererInfo.page') || info.location if (!isStr(deepAccess(request.site, 'domain'))) { const hostname = parseUrl(page).hostname; - request.site.domain = extractDomainFromHost(hostname) || hostname + request.site.domain = extractDomainFromHostExceptLocalhost(hostname) || hostname } if (!isStr(deepAccess(request.site, 'page'))) { request.site.page = page @@ -342,13 +348,7 @@ export const spec = { }, onBidWon: (bid) => { const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; - if (isStr(bid.nurl) && bid.nurl !== '') { - bid.nurl = bid.nurl.replace( - /\${AUCTION_PRICE}/, - cpm - ); - triggerPixel(bid.nurl); - } + triggerNurlWithCpm(bid, cpm) if (bid.isBurl) { if (bid.mediaType === BANNER) { bid.ad = bid.ad.replace( @@ -365,81 +365,11 @@ export const spec = { } logInfo(LOG_INFO_PREFIX + `onBidWon`); }, - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { - logInfo(LOG_INFO_PREFIX + `getUserSyncs`); - const spb = isPlainObject(config.getConfig('userSync')) && - isNumber(config.getConfig('userSync').syncsPerBidder) - ? config.getConfig('userSync').syncsPerBidder : USERSYNC_DEFAULT_CONFIG.syncsPerBidder; - - if (spb > 0 && isPlainObject(syncOptions) && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { - let pixels = []; - if (serverResponses && - isArray(serverResponses) && - serverResponses.length > 0 && - isPlainObject(serverResponses[0].body) && - isPlainObject(serverResponses[0].body.ext) && - isArray(serverResponses[0].body.ext.cm) && - serverResponses[0].body.ext.cm.length > 0) { - pixels = serverResponses[0].body.ext.cm; - } - - const syncs = []; - const query = []; - query.push('cbuster={cbuster}'); - query.push('gdpr_consent=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); - if (isPlainObject(gdprConsent) && typeof gdprConsent?.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { - query.push('gdpr=1'); - } else { - query.push('gdpr=0'); - } - if (isPlainObject(uspConsent) && uspConsent?.consentString) { - query.push(`us_privacy=${encodeURIComponent(uspConsent?.consentString)}`); - } - if (isPlainObject(gppConsent) && gppConsent?.gppString) { - query.push(`gppString=${encodeURIComponent(gppConsent?.gppString)}`); - } - if (config.getConfig('coppa')) { - query.push('coppa=1') - } - const q = query.join('&') - if (syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: 'https://cm.mgid.com/i.html?' + q.replace('{cbuster}', Math.round(new Date().getTime())) - }); - } else if (syncOptions.pixelEnabled) { - if (pixels.length === 0) { - for (let i = 0; i < spb; i++) { - syncs.push({ - type: 'image', - url: 'https://cm.mgid.com/i.gif?' + q.replace('{cbuster}', Math.round(new Date().getTime())) // randomly selects partner if sync required - }); - } - } else { - for (let i = 0; i < spb && i < pixels.length; i++) { - syncs.push({ - type: 'image', - url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + q.replace('{cbuster}', Math.round(new Date().getTime())) - }); - } - } - } - return syncs; - } - } + getUserSyncs: getUserSyncs, }; registerBidder(spec); -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; - } - } -} - /** * Unpack the Server's Bid into a Prebid-compatible one. * @param serverBid @@ -486,25 +416,11 @@ function setMediaType(bid, newBid) { } } -function extractDomainFromHost(pageHost) { +function extractDomainFromHostExceptLocalhost(pageHost) { if (pageHost === 'localhost') { return 'localhost' } - let domain = null; - try { - let domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); - if (domains != null && domains.length > 0) { - domain = domains[0]; - for (let i = 1; i < domains.length; i++) { - if (domains[i].length > domain.length) { - domain = domains[i]; - } - } - } - } catch (e) { - domain = null; - } - return domain; + return extractDomainFromHost(pageHost) } function getLanguage() { @@ -682,33 +598,25 @@ function commonNativeRequestObject(nativeAsset, params) { function parseNativeResponse(bid, newBid) { newBid.native = {}; if (bid.hasOwnProperty('adm')) { - let adm = ''; + let nativeAdm = ''; try { - adm = JSON.parse(bid.adm); + nativeAdm = JSON.parse(bid.adm); } catch (ex) { logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native response for ad response: ' + newBid.adm); return; } - if (adm && adm.native && adm.native.assets && adm.native.assets.length > 0) { + if (nativeAdm && nativeAdm.native && nativeAdm.native.assets && nativeAdm.native.assets.length > 0) { newBid.mediaType = NATIVE; - for (let i = 0, len = adm.native.assets.length; i < len; i++) { - switch (adm.native.assets[i].id) { + for (let i = 0, len = nativeAdm.native.assets.length; i < len; i++) { + switch (nativeAdm.native.assets[i].id) { case NATIVE_ASSETS.TITLE.ID: - newBid.native.title = adm.native.assets[i].title && adm.native.assets[i].title.text; + newBid.native.title = nativeAdm.native.assets[i].title && nativeAdm.native.assets[i].title.text; break; case NATIVE_ASSETS.IMAGE.ID: - newBid.native.image = { - url: adm.native.assets[i].img && adm.native.assets[i].img.url, - height: adm.native.assets[i].img && adm.native.assets[i].img.h, - width: adm.native.assets[i].img && adm.native.assets[i].img.w, - }; + newBid.native.image = copyFromAdmAsset(nativeAdm.native.assets[i]); break; case NATIVE_ASSETS.ICON.ID: - newBid.native.icon = { - url: adm.native.assets[i].img && adm.native.assets[i].img.url, - height: adm.native.assets[i].img && adm.native.assets[i].img.h, - width: adm.native.assets[i].img && adm.native.assets[i].img.w, - }; + newBid.native.icon = copyFromAdmAsset(nativeAdm.native.assets[i]); break; case NATIVE_ASSETS.SPONSOREDBY.ID: case NATIVE_ASSETS.SPONSORED.ID: @@ -718,14 +626,14 @@ function parseNativeResponse(bid, newBid) { case NATIVE_ASSETS.BODY.ID: case NATIVE_ASSETS.DISPLAYURL.ID: case NATIVE_ASSETS.CTA.ID: - newBid.native[spec.NATIVE_ASSET_ID_TO_KEY_MAP[adm.native.assets[i].id]] = adm.native.assets[i].data && adm.native.assets[i].data.value; + newBid.native[spec.NATIVE_ASSET_ID_TO_KEY_MAP[nativeAdm.native.assets[i].id]] = nativeAdm.native.assets[i].data && nativeAdm.native.assets[i].data.value; break; } } - newBid.native.clickUrl = adm.native.link && adm.native.link.url; - newBid.native.clickTrackers = (adm.native.link && adm.native.link.clicktrackers) || []; - newBid.native.impressionTrackers = adm.native.imptrackers || []; - newBid.native.jstracker = adm.native.jstracker || []; + newBid.native.clickUrl = nativeAdm.native.link && nativeAdm.native.link.url; + newBid.native.clickTrackers = (nativeAdm.native.link && nativeAdm.native.link.clicktrackers) || []; + newBid.native.impressionTrackers = nativeAdm.native.imptrackers || []; + newBid.native.jstracker = nativeAdm.native.jstracker || []; newBid.width = 0; newBid.height = 0; } @@ -746,8 +654,8 @@ function pageInfo() { location: l, referrer: r || '', masked: m, - wWidth: w.innerWidth, - wHeight: w.innerHeight, + wWidth: getWinDimensions().innerWidth, + wHeight: getWinDimensions().innerHeight, date: t.toUTCString(), timeOffset: t.getTimezoneOffset() }; @@ -785,3 +693,11 @@ function getBidFloor(bid, cur) { } return {floor: bidFloor, cur: cur} } + +function copyFromAdmAsset(asset) { + return { + url: asset.img && asset.img.url, + height: asset.img && asset.img.h, + width: asset.img && asset.img.w, + } +} diff --git a/modules/mgidXBidAdapter.js b/modules/mgidXBidAdapter.js index ac25a419de1..f15b76818b5 100644 --- a/modules/mgidXBidAdapter.js +++ b/modules/mgidXBidAdapter.js @@ -1,277 +1,35 @@ -import { - isFn, - deepAccess, - logMessage, - logError, - isPlainObject, - isNumber, - isArray, - isStr -} from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { USERSYNC_DEFAULT_CONFIG } from '../src/userSync.js'; +import { isBidRequestValid, buildRequestsBase, interpretResponse } from '../libraries/teqblazeUtils/bidderUtils.js'; +import { getUserSyncs } from '../libraries/mgidUtils/mgidUtils.js' const BIDDER_CODE = 'mgidX'; const GVLID = 358; const AD_URL = 'https://#{REGION}#.mgid.com/pbjs'; -const PIXEL_SYNC_URL = 'https://cm.mgid.com/i.gif'; -const IFRAME_SYNC_URL = 'https://cm.mgid.com/i.html'; - -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + const request = buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest }); + const region = validBidRequests[0].params?.region; -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); + if (region === 'eu') { + request.url = AD_URL.replace('#{REGION}#', 'eu-x'); + } else { + request.url = AD_URL.replace('#{REGION}#', 'us-east-x'); } - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} + return request; +}; export const spec = { code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, + isBidRequestValid: isBidRequestValid(), + buildRequests, + interpretResponse, - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - tmax: config.getConfig('bidderTimeout') - }; - - if (bidderRequest.gdprConsent) { - request.gdpr = { - consentString: bidderRequest.gdprConsent.consentString - }; - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - const region = validBidRequests[0].params?.region; - - let url; - if (region === 'eu') { - url = AD_URL.replace('#{REGION}#', 'eu'); - } else { - url = AD_URL.replace('#{REGION}#', 'us-east-x'); - } - - return { - method: 'POST', - url: url, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { - const spb = isPlainObject(config.getConfig('userSync')) && - isNumber(config.getConfig('userSync').syncsPerBidder) - ? config.getConfig('userSync').syncsPerBidder : USERSYNC_DEFAULT_CONFIG.syncsPerBidder; - - if (spb > 0 && isPlainObject(syncOptions) && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { - let pixels = []; - if (serverResponses && - isArray(serverResponses) && - serverResponses.length > 0 && - isPlainObject(serverResponses[0].body) && - isPlainObject(serverResponses[0].body.ext) && - isArray(serverResponses[0].body.ext.cm) && - serverResponses[0].body.ext.cm.length > 0) { - pixels = serverResponses[0].body.ext.cm; - } - - const syncs = []; - const query = []; - query.push('cbuster={cbuster}'); - query.push('gdpr_consent=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); - if (isPlainObject(gdprConsent) && typeof gdprConsent?.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { - query.push('gdpr=1'); - } else { - query.push('gdpr=0'); - } - if (isPlainObject(uspConsent) && uspConsent?.consentString) { - query.push(`us_privacy=${encodeURIComponent(uspConsent?.consentString)}`); - } - if (isPlainObject(gppConsent) && gppConsent?.gppString) { - query.push(`gppString=${encodeURIComponent(gppConsent?.gppString)}`); - } - if (config.getConfig('coppa')) { - query.push('coppa=1') - } - const q = query.join('&') - if (syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: IFRAME_SYNC_URL + '?' + q.replace('{cbuster}', Math.round(new Date().getTime())) - }); - } else if (syncOptions.pixelEnabled) { - if (pixels.length === 0) { - for (let i = 0; i < spb; i++) { - syncs.push({ - type: 'image', - url: PIXEL_SYNC_URL + '?' + q.replace('{cbuster}', Math.round(new Date().getTime())) // randomly selects partner if sync required - }); - } - } else { - for (let i = 0; i < spb && i < pixels.length; i++) { - syncs.push({ - type: 'image', - url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + q.replace('{cbuster}', Math.round(new Date().getTime())) - }); - } - } - } - return syncs; - } - } + getUserSyncs: getUserSyncs, }; registerBidder(spec); diff --git a/modules/michaoBidAdapter.js b/modules/michaoBidAdapter.js new file mode 100644 index 00000000000..56c073cddde --- /dev/null +++ b/modules/michaoBidAdapter.js @@ -0,0 +1,282 @@ +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 { + deepSetValue, + isBoolean, + isNumber, + isStr, + logError, + replaceAuctionPrice, + triggerPixel, +} from '../src/utils.js'; + +const ENV = { + BIDDER_CODE: 'michao', + SUPPORTED_MEDIA_TYPES: [BANNER, VIDEO, NATIVE], + ENDPOINT: 'https://rtb.michao-ssp.com/openrtb/prebid', + NET_REVENUE: true, + DEFAULT_CURRENCY: 'USD', + OUTSTREAM_RENDERER_URL: + 'https://cdn.jsdelivr.net/npm/in-renderer-js@1/dist/in-video-renderer.umd.min.js', +}; + +export const spec = { + code: ENV.BIDDER_CODE, + supportedMediaTypes: ENV.SUPPORTED_MEDIA_TYPES, + + isBidRequestValid: function (bid) { + const params = bid.params; + + if (!isNumber(params?.site)) { + domainLogger.invalidSiteError(params?.site); + return false; + } + + if (!isStr(params?.placement)) { + domainLogger.invalidPlacementError(params?.placement); + return false; + } + + if (params?.partner) { + if (!isNumber(params?.partner)) { + domainLogger.invalidPartnerError(params?.partner); + return false; + } + } + + if (params?.test) { + if (!isBoolean(params?.test)) { + domainLogger.invalidTestParamError(params?.test); + return false; + } + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const bidRequests = []; + + validBidRequests.forEach((validBidRequest) => { + let bidRequestEachFormat = []; + + if (validBidRequest.mediaTypes?.banner) { + bidRequestEachFormat.push({ + ...validBidRequest, + mediaTypes: { + banner: validBidRequest.mediaTypes.banner, + }, + }); + } + + if (validBidRequest.mediaTypes?.native) { + bidRequestEachFormat.push({ + ...validBidRequest, + mediaTypes: { + native: validBidRequest.mediaTypes.native, + }, + }); + } + + if (validBidRequest.mediaTypes?.video) { + bidRequestEachFormat.push({ + ...validBidRequest, + mediaTypes: { + video: validBidRequest.mediaTypes.video, + }, + }); + } + + bidRequests.push(buildRequest(bidRequestEachFormat, bidderRequest)); + }); + + return bidRequests; + }, + + interpretResponse: function (serverResponse, request) { + return converter.fromORTB({ + response: serverResponse.body, + request: request.data, + }).bids; + }, + + getUserSyncs: function ( + syncOptions, + serverResponses, + gdprConsent, + uspConsent + ) { + if (syncOptions.iframeEnabled) { + return [ + { + type: 'iframe', + url: + 'https://sync.michao-ssp.com/cookie-syncs?' + + generateGdprParams(gdprConsent), + }, + ]; + } + + return []; + }, + + onBidBillable: function (bid) { + if (bid.burl && isStr(bid.burl)) { + const billingUrls = generateBillableUrls(bid); + + billingUrls.forEach((billingUrl) => { + triggerPixel(billingUrl); + }); + } + }, +}; + +export const domainLogger = { + invalidSiteError(value) { + logError( + `Michao Bid Adapter: Invalid site ID. Expected number, got ${typeof value}. Value: ${value}` + ); + }, + + invalidPlacementError(value) { + logError( + `Michao Bid Adapter: Invalid placement. Expected string, got ${typeof value}. Value: ${value}` + ); + }, + + invalidPartnerError(value) { + logError( + `Michao Bid Adapter: Invalid partner ID. Expected number, got ${typeof value}. Value: ${value}` + ); + }, + + invalidTestParamError(value) { + logError( + `Michao Bid Adapter: Invalid test parameter. Expected boolean, got ${typeof value}. Value: ${value}` + ); + }, +}; + +function buildRequest(bidRequests, bidderRequest) { + const openRTBBidRequest = converter.toORTB({ + bidRequests: bidRequests, + bidderRequest, + }); + + return { + method: 'POST', + url: ENV.ENDPOINT, + data: openRTBBidRequest, + options: { contentType: 'application/json', withCredentials: true }, + }; +} + +function generateGdprParams(gdprConsent) { + let gdprParams = ''; + + if (typeof gdprConsent === 'object') { + if (gdprConsent?.gdprApplies) { + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${ + gdprConsent.consentString || '' + }`; + } + } + + return gdprParams; +} + +function generateBillableUrls(bid) { + const billingUrls = []; + const cpm = bid.originalCpm || bid.cpm; + + const billingUrl = new URL(bid.burl); + + const burlParam = billingUrl.searchParams.get('burl'); + + if (burlParam) { + billingUrl.searchParams.delete('burl'); + billingUrls.push(replaceAuctionPrice(burlParam, cpm)); + } + + billingUrls.push(replaceAuctionPrice(billingUrl.toString(), cpm)); + + return billingUrls; +} + +const converter = ortbConverter({ + request(buildRequest, imps, bidderRequest, context) { + const bidRequest = context.bidRequests[0]; + const openRTBBidRequest = buildRequest(imps, bidderRequest, context); + openRTBBidRequest.cur = [ENV.DEFAULT_CURRENCY]; + openRTBBidRequest.test = bidRequest.params?.test ? 1 : 0; + + deepSetValue( + openRTBBidRequest, + 'site.ext.michao.site', + bidRequest.params.site.toString() + ); + if (bidRequest?.schain) { + deepSetValue(openRTBBidRequest, 'source.schain', bidRequest.schain); + } + + if (bidRequest.params?.partner) { + deepSetValue( + openRTBBidRequest, + 'site.publisher.ext.michao.partner', + bidRequest.params.partner.toString() + ); + } + + return openRTBBidRequest; + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue( + imp, + 'ext.michao.placement', + bidRequest.params.placement.toString() + ); + + if (!bidRequest.mediaTypes?.native) { + delete imp.native; + } + + return imp; + }, + + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + const { bidRequest } = context; + if ( + bidResponse.mediaType === VIDEO && + bidRequest.mediaTypes.video.context === 'outstream' + ) { + bidResponse.vastXml = bid.adm; + const renderer = Renderer.install({ + url: ENV.OUTSTREAM_RENDERER_URL, + id: bidRequest.bidId, + adUnitCode: bidRequest.adUnitCode, + }); + renderer.setRender((bid) => { + bid.renderer.push(() => { + const inRenderer = new window.InVideoRenderer(); + inRenderer.render(bid.adUnitCode, bid); + }); + }); + bidResponse.renderer = renderer; + } + + return bidResponse; + }, + + context: { + netRevenue: ENV.NET_REVENUE, + currency: ENV.DEFAULT_CURRENCY, + ttl: 360, + }, +}); + +registerBidder(spec); diff --git a/modules/michaoBidAdapter.md b/modules/michaoBidAdapter.md new file mode 100644 index 00000000000..b45e8e2b5bd --- /dev/null +++ b/modules/michaoBidAdapter.md @@ -0,0 +1,87 @@ +# Overview + +```markdown +Module Name: Michao Bidder Adapter +Module Type: Bidder Adapter +Maintainer: miyamoto.kai@lookverin.com +``` + +# Description + +Module that connects to Michao’s demand sources + +Supported Ad format: +* Banner +* Video (instream and outstream) +* Native + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + bidder: 'michao', + params: { + site: 1, + placement: '1', + } + }] + }, + // Video adUnit + { + code: 'video-div', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + minduration: 0, + maxduration: 120, + mimes: ['video/mp4'], + protocols: [7] + } + }, + bids: [{ + bidder: 'michao', + params: { + site: 1, + placement: '1', + } + }] + }, + // Native AdUnit + { + code: 'native-div', + mediaTypes: { + native: { + ortb: { + assets: [ + { + id: 1, + required: 1, + img: { + type: 3, + w: 989, + h: 742, + }, + }, + ] + } + } + }, + bids: [{ + bidder: 'michao', + params: { + site: 1, + placement: '1', + } + }] + } +]; +``` diff --git a/modules/microadBidAdapter.js b/modules/microadBidAdapter.js index 61aa9b795de..82b9025766b 100644 --- a/modules/microadBidAdapter.js +++ b/modules/microadBidAdapter.js @@ -28,7 +28,6 @@ const AUDIENCE_IDS = [ {type: 8, bidKey: 'userId.id5id.uid', source: 'id5-sync.com'}, {type: 9, bidKey: 'userId.tdid', source: 'adserver.org'}, {type: 10, bidKey: 'userId.novatiq.snowflake', source: 'novatiq.com'}, - {type: 11, bidKey: 'userId.parrableId.eid', source: 'parrable.com'}, {type: 12, bidKey: 'userId.dacId.id', source: 'dac.co.jp'}, {type: 13, bidKey: 'userId.idl_env', source: 'liveramp.com'}, {type: 14, bidKey: 'userId.criteoId', source: 'criteo.com'}, diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js index d14af07210e..d5aae8a84d0 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -1,40 +1,19 @@ -import { - logWarn, - logInfo, - isArray, - isFn, - deepAccess, - isEmpty, - contains, - timestamp, - triggerPixel, - isInteger, - getBidIdParameter -} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; +import {makeBaseSpec} from '../libraries/riseUtils/index.js'; -const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'minutemedia'; -const ADAPTER_VERSION = '6.0.0'; -const TTL = 360; -const DEFAULT_CURRENCY = 'USD'; -const SELLER_ENDPOINT = 'https://hb.minutemedia-prebid.com/'; +const BASE_URL = 'https://hb.minutemedia-prebid.com/'; +const GVLID = 918; const MODES = { PRODUCTION: 'hb-mm-multi', TEST: 'hb-multi-mm-test' -} -const SUPPORTED_SYNC_METHODS = { - IFRAME: 'iframe', - PIXEL: 'pixel' -} +}; export const spec = { + ...makeBaseSpec(BASE_URL, MODES), code: BIDDER_CODE, - gvlid: 918, - version: ADAPTER_VERSION, - supportedMediaTypes: SUPPORTED_AD_TYPES, + gvlid: GVLID, isBidRequestValid: function (bidRequest) { if (!bidRequest.params) { logWarn('no params have been set to MinuteMedia adapter'); @@ -47,437 +26,7 @@ export const spec = { } return true; - }, - buildRequests: function (validBidRequests, bidderRequest) { - const combinedRequestsObject = {}; - - // use data from the first bid, to create the general params for all bids - const generalObject = validBidRequests[0]; - const testMode = generalObject.params.testMode; - - combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); - combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); - - return { - method: 'POST', - url: getEndpoint(testMode), - data: combinedRequestsObject - } - }, - interpretResponse: function ({body}) { - const bidResponses = []; - - if (body.bids) { - body.bids.forEach(adUnit => { - const bidResponse = { - requestId: adUnit.requestId, - cpm: adUnit.cpm, - currency: adUnit.currency || DEFAULT_CURRENCY, - width: adUnit.width, - height: adUnit.height, - ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, - netRevenue: adUnit.netRevenue || true, - nurl: adUnit.nurl, - mediaType: adUnit.mediaType, - meta: { - mediaType: adUnit.mediaType - } - }; - - if (adUnit.mediaType === VIDEO) { - bidResponse.vastXml = adUnit.vastXml; - } else if (adUnit.mediaType === BANNER) { - bidResponse.ad = adUnit.ad; - } - - if (adUnit.adomain && adUnit.adomain.length) { - bidResponse.meta.advertiserDomains = adUnit.adomain; - } - - bidResponses.push(bidResponse); - }); - } - - return bidResponses; - }, - getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; - for (const response of serverResponses) { - if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { - syncs.push({ - type: 'iframe', - url: response.body.params.userSyncURL - }); - } - if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { - const pixels = response.body.params.userSyncPixels.map(pixel => { - return { - type: 'image', - url: pixel - } - }) - syncs.push(...pixels) - } - } - return syncs; - }, - onBidWon: function (bid) { - if (bid == null) { - return; - } - - logInfo('onBidWon:', bid); - if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { - triggerPixel(bid.nurl); - } } }; registerBidder(spec); - -/** - * Get floor price - * @param bid {bid} - * @returns {Number} - */ -function getFloor(bid, mediaType) { - if (!isFn(bid.getFloor)) { - return 0; - } - let floorResult = bid.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: mediaType, - size: '*' - }); - return floorResult.currency === DEFAULT_CURRENCY && floorResult.floor ? floorResult.floor : 0; -} - -/** - * Get the the ad sizes array from the bid - * @param bid {bid} - * @returns {Array} - */ -function getSizesArray(bid, mediaType) { - let sizesArray = [] - - if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { - sizesArray = bid.mediaTypes[mediaType].sizes; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - sizesArray = bid.sizes; - } - - return sizesArray; -} - -/** - * Get schain string value - * @param schainObject {Object} - * @returns {string} - */ -function getSupplyChain(schainObject) { - if (isEmpty(schainObject)) { - return ''; - } - let scStr = `${schainObject.ver},${schainObject.complete}`; - schainObject.nodes.forEach((node) => { - scStr += '!'; - scStr += `${getEncodedValIfNotEmpty(node.asi)},`; - scStr += `${getEncodedValIfNotEmpty(node.sid)},`; - scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; - scStr += `${getEncodedValIfNotEmpty(node.rid)},`; - scStr += `${getEncodedValIfNotEmpty(node.name)},`; - scStr += `${getEncodedValIfNotEmpty(node.domain)}`; - }); - return scStr; -} - -/** - * Get encoded node value - * @param val {string} - * @returns {string} - */ -function getEncodedValIfNotEmpty(val) { - return !isEmpty(val) ? encodeURIComponent(val) : ''; -} - -/** - * Get preferred user-sync method based on publisher configuration - * @param bidderCode {string} - * @returns {string} - */ -function getAllowedSyncMethod(filterSettings, bidderCode) { - const iframeConfigsToCheck = ['all', 'iframe']; - const pixelConfigToCheck = 'image'; - if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { - return SUPPORTED_SYNC_METHODS.IFRAME; - } - if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { - return SUPPORTED_SYNC_METHODS.PIXEL; - } -} - -/** - * Check if sync rule is supported - * @param syncRule {Object} - * @param bidderCode {string} - * @returns {boolean} - */ -function isSyncMethodAllowed(syncRule, bidderCode) { - if (!syncRule) { - return false; - } - const isInclude = syncRule.filter === 'include'; - const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; - return isInclude && contains(bidders, bidderCode); -} - -/** - * Get the seller endpoint - * @param testMode {boolean} - * @returns {string} - */ -function getEndpoint(testMode) { - return testMode - ? SELLER_ENDPOINT + MODES.TEST - : SELLER_ENDPOINT + MODES.PRODUCTION; -} - -/** - * get device type - * @param uad {ua} - * @returns {string} - */ -function getDeviceType(ua) { - if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i - .test(ua.toLowerCase())) { - return '5'; - } - if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i - .test(ua.toLowerCase())) { - return '4'; - } - if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i - .test(ua.toLowerCase())) { - return '3'; - } - return '1'; -} - -function generateBidsParams(validBidRequests, bidderRequest) { - const bidsArray = []; - - if (validBidRequests.length) { - validBidRequests.forEach(bid => { - bidsArray.push(generateBidParameters(bid, bidderRequest)); - }); - } - - return bidsArray; -} - -/** - * Generate bid specific parameters - * @param {bid} bid - * @param {bidderRequest} bidderRequest - * @returns {Object} bid specific params object - */ -function generateBidParameters(bid, bidderRequest) { - const {params} = bid; - const mediaType = isBanner(bid) ? BANNER : VIDEO; - const sizesArray = getSizesArray(bid, mediaType); - - // fix floor price in case of NAN - if (isNaN(params.floorPrice)) { - params.floorPrice = 0; - } - - const bidObject = { - mediaType, - adUnitCode: getBidIdParameter('adUnitCode', bid), - sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), - bidId: getBidIdParameter('bidId', bid), - loop: getBidIdParameter('bidderRequestsCount', bid), - bidderRequestId: getBidIdParameter('bidderRequestId', bid), - transactionId: bid.ortb2Imp?.ext?.tid || '', - coppa: 0, - }; - - const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); - if (pos) { - bidObject.pos = pos; - } - - const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); - if (gpid) { - bidObject.gpid = gpid; - } - - const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); - if (placementId) { - bidObject.placementId = placementId; - } - - const mimes = deepAccess(bid, `mediaTypes.${mediaType}.mimes`); - if (mimes) { - bidObject.mimes = mimes; - } - const api = deepAccess(bid, `mediaTypes.${mediaType}.api`); - if (api) { - bidObject.api = api; - } - - const sua = deepAccess(bid, `ortb2.device.sua`); - if (sua) { - bidObject.sua = sua; - } - - const coppa = deepAccess(bid, `ortb2.regs.coppa`) - if (coppa) { - bidObject.coppa = 1; - } - - if (mediaType === VIDEO) { - const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); - let playbackMethodValue; - - // verify playbackMethod is of type integer array, or integer only. - if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { - // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation - playbackMethodValue = playbackMethod[0]; - } else if (isInteger(playbackMethod)) { - playbackMethodValue = playbackMethod; - } - - if (playbackMethodValue) { - bidObject.playbackMethod = playbackMethodValue; - } - - const placement = deepAccess(bid, `mediaTypes.video.placement`); - if (placement) { - bidObject.placement = placement; - } - - const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); - if (minDuration) { - bidObject.minDuration = minDuration; - } - - const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); - if (maxDuration) { - bidObject.maxDuration = maxDuration; - } - - const skip = deepAccess(bid, `mediaTypes.video.skip`); - if (skip) { - bidObject.skip = skip; - } - - const linearity = deepAccess(bid, `mediaTypes.video.linearity`); - if (linearity) { - bidObject.linearity = linearity; - } - - const protocols = deepAccess(bid, `mediaTypes.video.protocols`); - if (protocols) { - bidObject.protocols = protocols; - } - - const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); - if (plcmt) { - bidObject.plcmt = plcmt; - } - } - - return bidObject; -} - -function isBanner(bid) { - return bid.mediaTypes && bid.mediaTypes.banner; -} - -/** - * Generate params that are common between all bids - * @param {single bid object} generalObject - * @param {bidderRequest} bidderRequest - * @returns {object} the common params object - */ -function generateGeneralParams(generalObject, bidderRequest) { - const domain = window.location.hostname; - const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; - const {bidderCode} = bidderRequest; - const generalBidParams = generalObject.params; - const timeout = bidderRequest.timeout; - - // these params are snake_case instead of camelCase to allow backwards compatability on the server. - // in the future, these will be converted to camelCase to match our convention. - const generalParams = { - wrapper_type: 'prebidjs', - wrapper_vendor: '$$PREBID_GLOBAL$$', - wrapper_version: '$prebid.version$', - adapter_version: ADAPTER_VERSION, - auction_start: timestamp(), - publisher_id: generalBidParams.org, - publisher_name: domain, - site_domain: domain, - dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, - device_type: getDeviceType(navigator.userAgent), - ua: navigator.userAgent, - is_wrapper: !!generalBidParams.isWrapper, - session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), - tmax: timeout - } - - const userIdsParam = getBidIdParameter('userId', generalObject); - if (userIdsParam) { - generalParams.userIds = JSON.stringify(userIdsParam); - } - - const ortb2Metadata = bidderRequest.ortb2 || {}; - if (ortb2Metadata.site) { - generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); - } - if (ortb2Metadata.user) { - generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); - } - - if (syncEnabled) { - const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); - if (allowedSyncMethod) { - generalParams.cs_method = allowedSyncMethod; - } - } - - if (bidderRequest.uspConsent) { - generalParams.us_privacy = bidderRequest.uspConsent; - } - - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; - generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - - if (bidderRequest.gppConsent) { - generalParams.gpp = bidderRequest.gppConsent.gppString; - generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - generalParams.gpp = bidderRequest.ortb2.regs.gpp; - generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - - if (generalBidParams.ifa) { - generalParams.ifa = generalBidParams.ifa; - } - - if (generalObject.schain) { - generalParams.schain = getSupplyChain(generalObject.schain); - } - - if (bidderRequest && bidderRequest.refererInfo) { - generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); - generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); - } - - return generalParams -} diff --git a/modules/minutemediaBidAdapter.md b/modules/minutemediaBidAdapter.md index 66b54adaf0e..b22cf856364 100644 --- a/modules/minutemediaBidAdapter.md +++ b/modules/minutemediaBidAdapter.md @@ -13,7 +13,7 @@ Module that connects to MinuteMedia's demand sources. The MinuteMedia adapter requires setup and approval from the MinuteMedia. Please reach out to hb@minutemedia.com to create an MinuteMedia account. -The adapter supports Video(instream) & Banner. +The adapter supports Video(instream), Banner, Native and multi-format bid requests. # Bid Parameters ## Video diff --git a/modules/minutemediaplusBidAdapter.js b/modules/minutemediaplusBidAdapter.js deleted file mode 100644 index 146d437b1fa..00000000000 --- a/modules/minutemediaplusBidAdapter.js +++ /dev/null @@ -1,349 +0,0 @@ -import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {config} from '../src/config.js'; - -const GVLID = 918; -const DEFAULT_SUB_DOMAIN = 'exchange'; -const BIDDER_CODE = 'mmplus'; -const BIDDER_VERSION = '1.0.0'; -const CURRENCY = 'USD'; -const TTL_SECONDS = 60 * 5; -const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; - -const storage = getStorageManager({bidderCode: BIDDER_CODE}); - -function getTopWindowQueryParams() { - try { - const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); - return parsedUrl.search; - } catch (e) { - return ''; - } -} - -export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { - return `https://${subDomain}.minutemedia-prebid.com`; -} - -export function extractCID(params) { - return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; -} - -export function extractPID(params) { - return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; -} - -export function extractSubDomain(params) { - return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; -} - -function isBidRequestValid(bid) { - const params = bid.params || {}; - return !!(extractCID(params) && extractPID(params)); -} - -function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { - const { - params, - bidId, - userId, - adUnitCode, - schain, - mediaTypes, - auctionId, - ortb2Imp, - bidderRequestId, - bidRequestsCount, - bidderRequestsCount, - bidderWinsCount - } = bid; - let {bidFloor, ext} = params; - const hashUrl = hashCode(topWindowUrl); - const uniqueDealId = getUniqueDealId(hashUrl); - const cId = extractCID(params); - const pId = extractPID(params); - const subDomain = extractSubDomain(params); - - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); - - if (isFn(bid.getFloor)) { - const floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - - if (floorInfo.currency === 'USD') { - bidFloor = floorInfo.floor; - } - } - - let data = { - url: encodeURIComponent(topWindowUrl), - uqs: getTopWindowQueryParams(), - cb: Date.now(), - bidFloor: bidFloor, - bidId: bidId, - referrer: bidderRequest.refererInfo.ref, - adUnitCode: adUnitCode, - publisherId: pId, - sizes: sizes, - uniqueDealId: uniqueDealId, - bidderVersion: BIDDER_VERSION, - prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}`, - schain: schain, - mediaTypes: mediaTypes, - gpid: gpid, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: auctionId, - transactionId: ortb2Imp?.ext?.tid, - bidderRequestId: bidderRequestId, - bidRequestsCount: bidRequestsCount, - bidderRequestsCount: bidderRequestsCount, - bidderWinsCount: bidderWinsCount, - bidderTimeout: bidderTimeout - }; - - appendUserIdsToRequestPayload(data, userId); - - const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); - - if (sua) { - data.sua = sua; - } - - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.consentString) { - data.gdprConsent = bidderRequest.gdprConsent.consentString; - } - if (bidderRequest.gdprConsent.gdprApplies !== undefined) { - data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - } - if (bidderRequest.uspConsent) { - data.usPrivacy = bidderRequest.uspConsent; - } - - if (bidderRequest.gppConsent) { - data.gppString = bidderRequest.gppConsent.gppString; - data.gppSid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - data.gppString = bidderRequest.ortb2.regs.gpp; - data.gppSid = bidderRequest.ortb2.regs.gpp_sid; - } - - const dto = { - method: 'POST', - url: `${createDomain(subDomain)}/prebid/multi/${cId}`, - data: data - }; - - _each(ext, (value, key) => { - dto.data['ext.' + key] = value; - }); - - return dto; -} - -function appendUserIdsToRequestPayload(payloadRef, userIds) { - let key; - _each(userIds, (userId, idSystemProviderName) => { - key = `uid.${idSystemProviderName}`; - - switch (idSystemProviderName) { - case 'digitrustid': - payloadRef[key] = deepAccess(userId, 'data.id'); - break; - case 'lipb': - payloadRef[key] = userId.lipbid; - break; - case 'parrableId': - payloadRef[key] = userId.eid; - break; - case 'id5id': - payloadRef[key] = userId.uid; - break; - default: - payloadRef[key] = userId; - } - }); -} - -function buildRequests(validBidRequests, bidderRequest) { - const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - const bidderTimeout = config.getConfig('bidderTimeout'); - const requests = []; - validBidRequests.forEach(validBidRequest => { - const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); - requests.push(request); - }); - return requests; -} - -function interpretResponse(serverResponse, request) { - if (!serverResponse || !serverResponse.body) { - return []; - } - const {bidId} = request.data; - const {results} = serverResponse.body; - - let output = []; - - try { - results.forEach(result => { - const { - creativeId, - ad, - price, - exp, - width, - height, - currency, - metaData, - advertiserDomains, - mediaType = BANNER - } = result; - if (!ad || !price) { - return; - } - - const response = { - requestId: bidId, - cpm: price, - width: width, - height: height, - creativeId: creativeId, - currency: currency || CURRENCY, - netRevenue: true, - ttl: exp || TTL_SECONDS, - }; - - if (metaData) { - Object.assign(response, { - meta: metaData - }) - } else { - Object.assign(response, { - meta: { - advertiserDomains: advertiserDomains || [] - } - }) - } - - if (mediaType === BANNER) { - Object.assign(response, { - ad: ad, - }); - } else { - Object.assign(response, { - vastXml: ad, - mediaType: VIDEO - }); - } - output.push(response); - }); - return output; - } catch (e) { - return []; - } -} - -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { - let syncs = []; - const {iframeEnabled, pixelEnabled} = syncOptions; - const {gdprApplies, consentString = ''} = gdprConsent; - const {gppString, applicableSections} = gppConsent; - - const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` - - if (gppString && applicableSections?.length) { - params += '&gpp=' + encodeURIComponent(gppString); - params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); - } - - if (iframeEnabled) { - syncs.push({ - type: 'iframe', - url: `https://sync.minutemedia-prebid.com/api/sync/iframe/${params}` - }); - } - if (pixelEnabled) { - syncs.push({ - type: 'image', - url: `https://sync.minutemedia-prebid.com/api/sync/image/${params}` - }); - } - return syncs; -} - -export function hashCode(s, prefix = '_') { - const l = s.length; - let h = 0 - let i = 0; - if (l > 0) { - while (i < l) { - h = (h << 5) - h + s.charCodeAt(i++) | 0; - } - } - return prefix + h; -} - -export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { - const storageKey = `u_${key}`; - const now = Date.now(); - const data = getStorageItem(storageKey); - let uniqueId; - - if (!data || !data.value || now - data.created > expiry) { - uniqueId = `${key}_${now.toString()}`; - setStorageItem(storageKey, uniqueId); - } else { - uniqueId = data.value; - } - - return uniqueId; -} - -export function getStorageItem(key) { - try { - return tryParseJSON(storage.getDataFromLocalStorage(key)); - } catch (e) { - } - - return null; -} - -export function setStorageItem(key, value, timestamp) { - try { - const created = timestamp || Date.now(); - const data = JSON.stringify({value, created}); - storage.setDataInLocalStorage(key, data); - } catch (e) { - } -} - -export function tryParseJSON(value) { - try { - return JSON.parse(value); - } catch (e) { - return value; - } -} - -export const spec = { - code: BIDDER_CODE, - version: BIDDER_VERSION, - gvlid: GVLID, - supportedMediaTypes: [BANNER, VIDEO], - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs -}; - -registerBidder(spec); diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 99cad1c7bc6..0d2054ca09f 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -2,21 +2,27 @@ import { buildUrl, formatQS, generateUUID, + getWinDimensions, isFn, logInfo, safeJSONParse, triggerPixel, } from '../src/utils.js'; -import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { isAutoplayEnabled } from '../libraries/autoplayDetection/autoplay.js'; +import { normalizeBannerSizes } from '../libraries/sizeUtils/sizeUtils.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid */ const BIDDER_CODE = 'missena'; @@ -38,11 +44,44 @@ function getFloor(bidRequest) { mediaType: BANNER, }); - if (!isNaN(bidFloors.floor)) { + if (!isNaN(bidFloors?.floor)) { return bidFloors; } } +/* Helper function that converts the prebid data to the payload expected by our servers */ +function toPayload(bidRequest, bidderRequest) { + const payload = { + adunit: bidRequest.adUnitCode, + ik: window.msna_ik, + request_id: bidRequest.bidId, + timeout: bidderRequest.timeout, + }; + + const baseUrl = bidRequest.params.baseUrl || ENDPOINT_URL; + payload.params = bidRequest.params; + + payload.userEids = bidRequest.userIdAsEids || []; + payload.version = '$prebid.version$'; + + const bidFloor = getFloor(bidRequest); + payload.floor = bidFloor?.floor; + payload.floor_currency = bidFloor?.currency; + payload.currency = getCurrencyFromBidderRequest(bidderRequest); + payload.schain = bidRequest.schain; + payload.autoplay = isAutoplayEnabled() === true ? 1 : 0; + payload.screen = { height: getWinDimensions().screen.height, width: getWinDimensions().screen.width }; + payload.viewport = getViewportSize(); + payload.sizes = normalizeBannerSizes(bidRequest.mediaTypes.banner.sizes); + payload.ortb2 = bidderRequest.ortb2; + + return { + method: 'POST', + url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), + data: JSON.stringify(payload), + }; +} + export const spec = { aliases: ['msna'], code: BIDDER_CODE, @@ -62,7 +101,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {Array} validBidRequests + * @param {BidderRequest} bidderRequest * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { @@ -78,54 +118,11 @@ export const spec = { return []; } - return validBidRequests.map((bidRequest) => { - const payload = { - adunit: bidRequest.adUnitCode, - ik: window.msna_ik, - request_id: bidRequest.bidId, - timeout: bidderRequest.timeout, - }; - - if (bidderRequest && bidderRequest.refererInfo) { - // TODO: is 'topmostLocation' the right value here? - payload.referer = bidderRequest.refererInfo.topmostLocation; - payload.referer_canonical = bidderRequest.refererInfo.canonicalUrl; - } - - if (bidderRequest && bidderRequest.gdprConsent) { - payload.consent_string = bidderRequest.gdprConsent.consentString; - payload.consent_required = bidderRequest.gdprConsent.gdprApplies; - } - const baseUrl = bidRequest.params.baseUrl || ENDPOINT_URL; - if (bidRequest.params.test) { - payload.test = bidRequest.params.test; - } - if (bidRequest.params.placement) { - payload.placement = bidRequest.params.placement; - } - if (bidRequest.params.formats) { - payload.formats = bidRequest.params.formats; - } - if (bidRequest.params.isInternal) { - payload.is_internal = bidRequest.params.isInternal; - } - if (bidRequest.ortb2?.device?.ext?.cdep) { - payload.cdep = bidRequest.ortb2?.device?.ext?.cdep; - } - payload.userEids = bidRequest.userIdAsEids || []; - payload.version = '$prebid.version$'; - - const bidFloor = getFloor(bidRequest); - payload.floor = bidFloor?.floor; - payload.floor_currency = bidFloor?.currency; - payload.currency = config.getConfig('currency.adServerCurrency') || 'EUR'; - - return { - method: 'POST', - url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), - data: JSON.stringify(payload), - }; - }); + this.msnaApiKey = validBidRequests[0]?.params.apiKey; + + return validBidRequests.map((bidRequest) => + toPayload(bidRequest, bidderRequest), + ); }, /** @@ -147,30 +144,29 @@ export const spec = { getUserSyncs: function ( syncOptions, serverResponses, - gdprConsent, + gdprConsent = {}, uspConsent, ) { - if (!syncOptions.iframeEnabled) { + if (!syncOptions.iframeEnabled || !this.msnaApiKey) { return []; } - let gdprParams = ''; - if ( - gdprConsent && - 'gdprApplies' in gdprConsent && - typeof gdprConsent.gdprApplies === 'boolean' - ) { - gdprParams = `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${ - gdprConsent.consentString - }`; + const url = new URL('https://sync.missena.io/iframe'); + url.searchParams.append('t', this.msnaApiKey); + + if (typeof gdprConsent.gdprApplies === 'boolean') { + url.searchParams.append('gdpr', Number(gdprConsent.gdprApplies)); + url.searchParams.append('gdpr_consent', gdprConsent.consentString); } - return [ - { type: 'iframe', url: 'https://sync.missena.io/iframe' + gdprParams }, - ]; + if (uspConsent) { + url.searchParams.append('us_privacy', uspConsent); + } + + return [{ type: 'iframe', url: url.href }]; }, /** * Register bidder specific code, which will execute if bidder timed out after an auction - * @param {data} Containing timeout specific data + * @param {TimedOutBid} timeoutData - Containing timeout specific data */ onTimeout: function onTimeout(timeoutData) { logInfo('Missena - Timeout from adapter', timeoutData); @@ -178,7 +174,7 @@ export const spec = { /** * Register bidder specific code, which@ will execute if a bid from this bidder won the auction - * @param {Bid} The bid that won the auction + * @param {Bid} bid - The bid that won the auction */ onBidWon: function (bid) { const hostname = bid.params[0].baseUrl ? EVENTS_DOMAIN_DEV : EVENTS_DOMAIN; diff --git a/modules/mobfoxpbBidAdapter.js b/modules/mobfoxpbBidAdapter.js index 35e9b03c031..dcc6e9594c4 100644 --- a/modules/mobfoxpbBidAdapter.js +++ b/modules/mobfoxpbBidAdapter.js @@ -1,147 +1,17 @@ -import { isFn, deepAccess, getWindowTop } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { isBidRequestValid, buildRequests, interpretResponse } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'mobfoxpb'; const AD_URL = 'https://bes.mobfox.com/pbjs'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; - } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers); - default: - return false; - } -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && bid.params.placementId); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const winTop = getWindowTop(); - const location = winTop.location; - const placements = []; - const request = { - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - 'secure': 1, - 'host': location.host, - 'page': location.pathname, - 'placements': placements - }; - - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent; - } - - // Add GPP consent - if (bidderRequest.gppConsent) { - request.gpp = bidderRequest.gppConsent.gppString; - request.gpp_sid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - request.gpp = bidderRequest.ortb2.regs.gpp; - request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - const placement = { - placementId: bid.params.placementId, - bidId: bid.bidId, - schain: bid.schain || {}, - bidfloor: getBidFloor(bid) - }; - const mediaType = bid.mediaTypes; - - if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { - placement.traffic = BANNER; - placement.sizes = mediaType[BANNER].sizes; - } else if (mediaType && mediaType[VIDEO] && mediaType[VIDEO].playerSize) { - placement.traffic = VIDEO; - placement.wPlayer = mediaType[VIDEO].playerSize[0]; - placement.hPlayer = mediaType[VIDEO].playerSize[1]; - placement.playerSize = mediaType[VIDEO].playerSize; - placement.minduration = mediaType[VIDEO].minduration; - placement.maxduration = mediaType[VIDEO].maxduration; - placement.mimes = mediaType[VIDEO].mimes; - placement.protocols = mediaType[VIDEO].protocols; - placement.startdelay = mediaType[VIDEO].startdelay; - placement.placement = mediaType[VIDEO].placement; - placement.skip = mediaType[VIDEO].skip; - placement.skipafter = mediaType[VIDEO].skipafter; - placement.minbitrate = mediaType[VIDEO].minbitrate; - placement.maxbitrate = mediaType[VIDEO].maxbitrate; - placement.delivery = mediaType[VIDEO].delivery; - placement.playbackmethod = mediaType[VIDEO].playbackmethod; - placement.api = mediaType[VIDEO].api; - placement.linearity = mediaType[VIDEO].linearity; - } else if (mediaType && mediaType[NATIVE]) { - placement.traffic = NATIVE; - placement.native = mediaType[NATIVE]; - } - placements.push(placement); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - resItem.meta = resItem.meta || {}; - resItem.meta.advertiserDomains = resItem.adomain || []; - - response.push(resItem); - } - } - return response; - }, + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse }; registerBidder(spec); diff --git a/modules/mobianRtdProvider.js b/modules/mobianRtdProvider.js new file mode 100644 index 00000000000..01a0a5d93d1 --- /dev/null +++ b/modules/mobianRtdProvider.js @@ -0,0 +1,226 @@ +/** + * This module adds the Mobian RTD provider to the real time data module + * The {@link module:modules/realTimeData} module is required + */ +import { submodule } from '../src/hook.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { safeJSONParse, logMessage as _logMessage } from '../src/utils.js'; +import { setKeyValue } from '../libraries/gptUtils/gptUtils.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +/** + * @typedef {Object} MobianConfig + * @property {MobianConfigParams} params + */ + +/** + * @typedef {Object} MobianConfigParams + * @property {string} [prefix] - Optional prefix for targeting keys (default: 'mobian') + * @property {boolean} [publisherTargeting] - Optional boolean to enable targeting for publishers (default: false) + * @property {boolean} [advertiserTargeting] - Optional boolean to enable targeting for advertisers (default: false) + */ + +/** + * @typedef {Object} MobianContextData + * @property {Object} apValues + * @property {string[]} categories + * @property {string[]} emotions + * @property {string[]} genres + * @property {string} risk + * @property {string} sentiment + * @property {string[]} themes + * @property {string[]} tones + */ + +export const MOBIAN_URL = 'https://prebid.outcomes.net/api/prebid/v1/assessment/async'; + +export const AP_VALUES = 'apValues'; +export const CATEGORIES = 'categories'; +export const EMOTIONS = 'emotions'; +export const GENRES = 'genres'; +export const RISK = 'risk'; +export const SENTIMENT = 'sentiment'; +export const THEMES = 'themes'; +export const TONES = 'tones'; + +export const CONTEXT_KEYS = [ + AP_VALUES, + CATEGORIES, + EMOTIONS, + GENRES, + RISK, + SENTIMENT, + THEMES, + TONES +]; + +const AP_KEYS = ['a0', 'a1', 'p0', 'p1']; + +const logMessage = (...args) => { + _logMessage('Mobian', ...args); +}; + +function makeMemoizedFetch() { + let cachedResponse = null; + return async function () { + if (cachedResponse) { + return Promise.resolve(cachedResponse); + } + try { + const response = await fetchContextData(); + cachedResponse = makeDataFromResponse(response); + return cachedResponse; + } catch (error) { + logMessage('error', error); + return Promise.resolve({}); + } + } +} + +export const getContextData = makeMemoizedFetch(); + +const entriesToObjectReducer = (acc, [key, value]) => ({ ...acc, [key]: value }); + +export function makeContextDataToKeyValuesReducer(config) { + const { prefix } = config; + return function contextDataToKeyValuesReducer(keyValues, [key, value]) { + if (key === AP_VALUES) { + AP_KEYS.forEach((apKey) => { + if (!value?.[apKey]?.length) return; + keyValues.push([`${prefix}_ap_${apKey}`, value[apKey].map((v) => String(v))]); + }); + } + if (value?.length) { + keyValues.push([`${prefix}_${key}`, value]); + } + return keyValues; + } +} + +export async function fetchContextData() { + const pageUrl = encodeURIComponent(window.location.href); + const requestUrl = `${MOBIAN_URL}?url=${pageUrl}`; + const request = ajaxBuilder(); + + return new Promise((resolve, reject) => { + request(requestUrl, { success: resolve, error: reject }); + }); +} + +export function getConfig(config) { + const [advertiserTargeting, publisherTargeting] = ['advertiserTargeting', 'publisherTargeting'].map((key) => { + const value = config?.params?.[key]; + if (!value) { + return []; + } else if (value === true) { + return CONTEXT_KEYS; + } else if (Array.isArray(value) && value.length) { + return value.filter((key) => CONTEXT_KEYS.includes(key)); + } + return []; + }); + + const prefix = config?.params?.prefix || 'mobian'; + return { advertiserTargeting, prefix, publisherTargeting }; +} + +/** + * @param {MobianConfig} config + * @param {MobianContextData} contextData + */ +export function setTargeting(config, contextData) { + logMessage('context', contextData); + const keyValues = Object.entries(contextData) + .filter(([key]) => config.publisherTargeting.includes(key)) + .reduce(makeContextDataToKeyValuesReducer(config), []) + + keyValues.forEach(([key, value]) => setKeyValue(key, value)); +} + +/** + * @param {Object|string} contextData + * @returns {MobianContextData} + */ +export function makeDataFromResponse(contextData) { + const data = typeof contextData === 'string' ? safeJSONParse(contextData) : contextData; + const results = data.results; + if (!results) { + return {}; + } + return { + [AP_VALUES]: results.ap || {}, + [CATEGORIES]: results.mobianContentCategories, + [EMOTIONS]: results.mobianEmotions, + [GENRES]: results.mobianGenres, + [RISK]: results.mobianRisk || 'unknown', + [SENTIMENT]: results.mobianSentiment || 'unknown', + [THEMES]: results.mobianThemes, + [TONES]: results.mobianTones, + }; +} + +/** + * @param {Object} bidReqConfig + * @param {MobianContextData} contextData + * @param {MobianConfig} config + */ +export function extendBidRequestConfig(bidReqConfig, contextData, config) { + logMessage('extendBidRequestConfig', bidReqConfig, contextData); + const { site: ortb2Site } = bidReqConfig.ortb2Fragments.global; + const keyValues = Object.entries(contextData) + .filter(([key]) => config.advertiserTargeting.includes(key)) + .reduce(makeContextDataToKeyValuesReducer(config), []) + .reduce(entriesToObjectReducer, {}); + + ortb2Site.ext = ortb2Site.ext || {}; + ortb2Site.ext.data = { + ...(ortb2Site.ext.data || {}), + ...keyValues + }; + + return bidReqConfig; +} + +/** + * @param {MobianConfig} rawConfig + * @returns {boolean} + */ +function init(rawConfig) { + logMessage('init', rawConfig); + const config = getConfig(rawConfig); + if (config.publisherTargeting.length) { + getContextData().then((contextData) => setTargeting(config, contextData)); + } + return true; +} + +function getBidRequestData(bidReqConfig, callback, rawConfig) { + logMessage('getBidRequestData', bidReqConfig); + + const config = getConfig(rawConfig); + const { advertiserTargeting } = config; + + if (!advertiserTargeting.length) { + callback(); + return; + } + + getContextData() + .then((contextData) => { + extendBidRequestConfig(bidReqConfig, contextData, config); + }) + .catch(() => {}) + .finally(() => callback()); +} + +/** @type {RtdSubmodule} */ +export const mobianBrandSafetySubmodule = { + name: 'mobianBrandSafety', + init: init, + getBidRequestData: getBidRequestData +}; + +submodule('realTimeData', mobianBrandSafetySubmodule); diff --git a/modules/mobianRtdProvider.md b/modules/mobianRtdProvider.md new file mode 100644 index 00000000000..48e7a1c6cec --- /dev/null +++ b/modules/mobianRtdProvider.md @@ -0,0 +1,176 @@ +# Mobian Rtd Provider + +## Overview + +Module Name: Mobian Rtd Provider + +Module Type: Rtd Provider + +Maintainer: rich.rodriguez@themobian.com + +The Mobian Real-Time Data (RTD) Module is a plug-and-play Prebid.js adapter that is designed to provide Mobian Contextual results on the publisher’s page. + +## Downloading and Configuring the Mobian RTD module + +Navigate to https://docs.prebid.org/download.html and check the box labeled Mobian Prebid Contextual Evaluation. If you have installed Prebid.js on your site previously, please be sure to select any other modules and adaptors to suit your needs. When clicking the "Get Prebid.js" button at the bottom of the page, the site will build a version of Prebid.js with all of your selections. + +Direct link to the Mobian module in the Prebid.js repository: https://github.com/prebid/Prebid.js/blob/master/modules/mobianRtdProvider.js + +The client will need to provide Mobian with all the domains that would be using the prebid module so that Mobian can whitelist those domains. Failure to whitelist the domains will yield a 404 when making a request to the Mobian Contextual API at https://prebid.outcomes.net/. + +## Configuration Highlight + +Below is Mobian's suggested default for configuration: + +```js +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'mobianBrandSafety', + params: { + // Prefix for the targeting keys (default: 'mobian') + prefix: 'mobian', + + // Enable targeting keys for advertiser data + advertiserTargeting: true, + // Or set it as an array to pick specific targeting keys: + // advertiserTargeting: ['genres', 'emotions', 'themes'], + // Available values: 'apValues', 'categories', 'emotions', 'genres', 'risk', 'sentiment', 'themes', 'tones' + + // Enable targeting keys for publisher data + publisherTargeting: true, + // Or set it as an array to pick specific targeting keys: + // publisherTargeting: ['tones', 'risk'], + // Available values: 'apValues', 'categories', 'emotions', 'genres', 'risk', 'sentiment', 'themes', 'tones' + } + }] + } +}); +``` + +## Functionality + +At a high level, the Mobian RTD Module is designed to call the Mobian Contextal API on page load, requesting the Mobian classifications and results for the URL. The classifications and results are designed to be picked up by any SSP or DSP in the Prebid.js ecosystem. The module also supports placing the Mobian classifications on each ad slot on the page, thus allowing for targeting within GAM. + +## Available Classifications + +NOTE: The examples below for targetable keys for GAM or otherwise in the ortb2 object assume that your prefix is the default of "mobian". The prefix in the targetable key will change based on your settings. + +Risk: + +Prebid.outcomes.net endpoint key: mobianRisk + +Targetable Key: mobian_risk + +Possible values: "none", "low", "medium" or "high" + +Description: This category assesses whether content contains any potential risks or concerns to advertisers and returns a determination of Low Risk, Medium Risk, or High Risk based on the inclusion of sensitive or high-risk topics. Content that might be categorized as unsafe may include violence, hate speech, misinformation, or sensitive topics that most advertisers would like to avoid. Content that is explicit or overly graphic in nature will be more likely to fall into the High Risk tier compared to content that describes similar subjects in a more informative or educational manner. + +------------------ + +Content Categories: + +Prebid.outcomes.net endpoint key: mobianContentCategories + +Targetable Key: mobian_categories + +Possible values: "adult_content", "arms", "crime", "death_injury", "debated_issue", "hate_speech", "drugs_alcohol", "obscenity", "piracy", "spam", "terrorism" + +Description: Brand Safety Categories contain categorical results for brand safety when relevant (e.g. Low Risk Adult Content). Note there can be Medium and High Risk content that is not associated to a specific brand safety category. + +------------------ + +Sentiment: + +Prebid.outcomes.net endpoint key: mobianSentiment + +Targetable Key: mobian_sentiment + +Possible values: "negative", "neutral" or "positive" + +Description: This category analyzes the overall positivity, negativity, or neutrality of a piece of content. This is a broad categorization of the content’s tone; every piece of content receives one of three possible sentiment ratings: Positive, Negative, or Neutral. + +------------------ + +Emotion: + +Prebid.outcomes.net endpoint key: mobianEmotions + +Targetable Key: mobian_emotions + +Possible values: Various but some examples include "love", "joy", "surprise", "anger", "sadness", "fear" + +Description: This category represents the specific feelings expressed or evoked through the content. Emotions are the reactions tied to the content’s presentation. Multiple emotions may be evoked by a single piece of content as this category reflects the way humans engage with the content. + +------------------ + +Tone: + +Prebid.outcomes.net endpoint key: mobianTones + +Targetable Key: mobian_tones + +Possible values: Various, but some examples include "comedic", "serious" or "emotional" + +Description: This category represents the content’s stylistic attitude or perspective that is being conveyed. If the Genre classification above represents the more objective structure, the Tone classification represents the subjective form. This categorization influences the way audiences may receive the piece of content and how they could be impacted by it. + +------------------ + +Theme: + +Prebid.outcomes.net endpoint key: mobianThemes + +Targetable Key: mobian_themes + +Possible values: Various, but some examples include "skincare", "food" and "nightlife" + +Description: This category includes broad conceptual ideas or underlying topics that form the foundation of the content. Themes represent the central message or idea conveyed throughout, rather than the specific details of the subject. Themes are intended to be broad and high-level, describing the overall purpose and intent of the content, and can connect multiple pieces of content, even if they are not from the same property. + +------------------ + +Genre: + +Prebid.outcomes.net endpoint key: mobianGenres + +Targetable Key: mobian_genres + +Possible values: Various, but some examples include "journalism", "gaming" or "how-to" + +Description: This category represents the type or style of the content, focusing on the purpose, format, or presentation of the content. Genres group pieces of content into recognizable categories based on style and provide a framework for understanding the structure of the content. + +------------------ + +AP Values + +Prebid.outcomes.net endpoint key: ap (an array, containing values of a0, a1, p0, p1) + +Targetable Keys: mobian_ap_a0, mobian_ap_a1, mobian_ap_p0, mobian_ap_p1 + +Possible values: Various, numerically id-based and customizable based on Mobian Context Settings. + +Description: Mobian AI Personas are custom created based on prompts to find a specific audience. Please contact your Mobian contact directly for more information on this tool. The difference between the keys is below: + +a0 = Advertisers (via Campaign IDs) in this list should NOT want to advertise on this page + +a1 = Advertisers (via Campaign IDs) should want to advertise on this page + +p0 = Advertisers (via Campaign IDs) should AVOID targeting these personas + +p1 = Advertisers (via Campaign IDs) should target these personas + +*AP Values is in the early stages of testing and is subject to change. + +## GAM Targeting: + +On each page load, the Mobian RTD module finds each ad slot on the page and performs the following function: + +```js +window.googletag.cmd.push(() => { + window.googletag.pubads().setTargeting(key, value); +``` + +"key" and "value" will be replaced with the various classifications as described in the previous section. Notably, this function runs before ad calls are made to GAM, which enables the keys and value to be used for targeting or blocking in GAM. + +For more details on how to set up key-value pairs in GAM, please see this documentation from Google: https://support.google.com/admanager/answer/9796369 + +For example, if you wanted to target articles where mobianRisk is "low", the key to set in GAM would be "mobian_risk" and the value would be "low". Once these keys and values are set within the Inventory section in GAM as listed by their documentation, you can then reference the key value pair in Custom Targeting for any line item you create. diff --git a/modules/mobkoiAnalyticsAdapter.js b/modules/mobkoiAnalyticsAdapter.js new file mode 100644 index 00000000000..93913c19d64 --- /dev/null +++ b/modules/mobkoiAnalyticsAdapter.js @@ -0,0 +1,1338 @@ +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import { EVENTS } from '../src/constants.js'; +import { ajax } from '../src/ajax.js'; +import { + logInfo, + logWarn, + logError, + _each, + pick, + triggerPixel, + debugTurnedOn, + mergeDeep, + isEmpty, + deepClone, + deepAccess, +} from '../src/utils.js'; + +const BIDDER_CODE = 'mobkoi'; +const analyticsType = 'endpoint'; +const GVL_ID = 898; +/** + * !IMPORTANT: Must match the value in the mobkoiBidAdapter.js + * The name of the parameter that the publisher can use to specify the ad server endpoint. + */ +const PARAM_NAME_AD_SERVER_BASE_URL = 'adServerBaseUrl'; + +/** + * Order by events lifecycle + */ +const { + // Order events + AUCTION_INIT, + BID_RESPONSE, + AUCTION_END, + AD_RENDER_SUCCEEDED, + BID_WON, + BIDDER_DONE, + + // Error events (Not in order) + AUCTION_TIMEOUT, + NO_BID, + BID_REJECTED, + BIDDER_ERROR, + AD_RENDER_FAILED, +} = EVENTS; + +const CUSTOM_EVENTS = { + BID_LOSS: 'bidLoss', +}; + +export const DEBUG_EVENT_LEVELS = { + info: 'info', + warn: 'warn', + error: 'error', +}; + +/** + * Some fields contain large data that are not useful for debugging. This + * constant contains the fields that should be omitted from the payload and in + * error messages. + */ +const COMMON_FIELDS_TO_OMIT = ['ad', 'adm']; + +export class LocalContext { + /** + * A map of impression ID (ORTB terms) to BidContext object + */ + bidContexts = {}; + + /** + * Shouldn't be accessed directly. Use getPayloadByImpId method instead. + * Payload are indexed by impression ID. + */ + _impressionPayloadCache = { + // [impid]: { ... } + }; + /** + * The payload that is common to all bid contexts. The payload will be + * submitted to the server along with the debug events. + */ + getImpressionPayload(impid) { + if (!impid) { + throw new Error(`Impression ID is required. Given: "${impid}".`); + } + + return this._impressionPayloadCache[impid] || {}; + } + /** + * Update the payload for all impressions. The new values will be merged to + * the existing payload. + * @param {*} subPayloads Object containing new values to be merged indexed by SUB_PAYLOAD_TYPES + */ + mergeToAllImpressionsPayload(subPayloads) { + // Create clone for each impression ID and update the payload cache + _each(this.getAllBidderRequestImpIds(), currentImpid => { + // Avoid modifying the original object + const cloneSubPayloads = deepClone(subPayloads); + + // Initialise the payload cache if it doesn't exist + if (!this._impressionPayloadCache[currentImpid]) { + this._impressionPayloadCache[currentImpid] = {}; + } + + // Merge the new values to the existing payload + utils.mergePayloadAndAddCustomFields( + this._impressionPayloadCache[currentImpid], + cloneSubPayloads, + // Add the identity fields to all sub payloads + { + impid: currentImpid, + publisherId: this.publisherId, + } + ); + }); + } + + /** + * The Prebid auction object but only contains the key fields that we + * interested in. + */ + auction = null; + + /** + * Auction.bidderRequests object + */ + bidderRequests = null; + + get publisherId() { + if (!this.bidderRequests) { + throw new Error('Bidder requests are not available. Accessing before assigning.'); + } + return utils.getPublisherId(this.bidderRequests[0]); + } + + get adServerBaseUrl() { + if ( + !Array.isArray(this.bidderRequests) && + this.bidderRequests.length > 0 + ) { + throw new Error('Bidder requests are not available. Accessing before assigning.' + + JSON.stringify(this.bidderRequests, null, 2) + ); + } + + return utils.getAdServerEndpointBaseUrl(this.bidderRequests[0]); + } + + /** + * Extract all impression IDs from all bid requests. + */ + getAllBidderRequestImpIds() { + if (!Array.isArray(this.bidderRequests)) { + return []; + } + return this.bidderRequests.flatMap(br => br.bids.map(bid => utils.getImpId(bid))); + } + + /** + * Cache the debug events that are common to all bid contexts. + * When a new bid context is created, the events will be pushed to the new + * context. + */ + commonBidContextEvents = []; + + initialise(auction) { + this.auction = pick(auction, ['auctionId', 'auctionEnd']); + this.bidderRequests = auction.bidderRequests; + } + + /** + * Retrieve the BidContext object by the bid object. If the bid context is not + * available, it will create a new one. The new bid context will returned. + * @param {*} bid can be a prebid bid response or ortb bid response + * @returns BidContext object + */ + retrieveBidContext(bid) { + const ortbId = (() => { + try { + const id = utils.getOrtbId(bid); + if (!id) { + throw new Error( + 'ORTB ID is not available in the given bid object:' + + JSON.stringify(utils.omitRecursive(bid, COMMON_FIELDS_TO_OMIT), null, 2)); + } + return id; + } catch (error) { + throw new Error( + 'Failed to retrieve ORTB ID from bid object. Please ensure the given object contains an ORTB ID field.\n' + + `Sub Error: ${error.message}` + ); + } + })(); + const bidContext = this.bidContexts[ortbId]; + + if (bidContext) { + return bidContext; + } + + /** + * Create a new context object and return it. + */ + let newBidContext = new BidContext({ + localContext: this, + prebidOrOrtbBidResponse: bid, + }); + + /** + * Add the data that store in local context to the new bid context. + */ + _each( + this.commonBidContextEvents, + event => newBidContext.pushEvent( + { + eventInstance: event, + subPayloads: null, // Merge the payload later + }) + ); + // Merge cached payloads to the new bid context + newBidContext.mergePayload(this.getImpressionPayload(newBidContext.impid)); + + this.bidContexts[ortbId] = newBidContext; + return newBidContext; + } + + /** + * Immediately trigger the loss beacon for all bids (bid contexts) that haven't won the auction. + */ + triggerAllLossBidLossBeacon() { + _each(this.bidContexts, (bidContext) => { + const { ortbBidResponse, bidWin, lurlTriggered } = bidContext; + if (ortbBidResponse.lurl && !bidWin && !lurlTriggered) { + logInfo('TriggerLossBeacon. impid:', ortbBidResponse.impid); + utils.sendGetRequest(ortbBidResponse.lurl); + // Update the flog. Don't wait for the response to continue to avoid race conditions + bidContext.lurlTriggered = true; + } + }); + } + + /** + * Push an debug event to all bid contexts. This is useful for events that are + * related to all bids in the auction. + * @param {Object} params Object containing the event details + * @param {*} params.eventType Prebid event type or custom event type + * @param {*} params.level Debug level of the event. It can be one of the following: + * - info + * - warn + * - error + * @param {*} params.timestamp Default to current timestamp if not provided. + * @param {*} params.note Optional field. Additional information about the event. + * @param {*} params.subPayloads Objects containing additional data that are + * obtain from to the Prebid events indexed by SUB_PAYLOAD_TYPES. + */ + pushEventToAllBidContexts({eventType, level, timestamp, note, subPayloads}) { + // Create one event for each impression ID + _each(this.getAllBidderRequestImpIds(), impid => { + const eventClone = new Event({ + eventType, + impid, + publisherId: this.publisherId, + level, + timestamp, + note, + }); + // Save to the LocalContext + this.commonBidContextEvents.push(eventClone); + this.mergeToAllImpressionsPayload(subPayloads); + }); + + // If there are no bid contexts, push the event to the common events list + if (isEmpty(this.bidContexts)) { + this._commonBidContextEventsFlushed = false; + return; + } + + // Once the bid contexts are available, push the event to all bid contexts + _each(this.bidContexts, (bidContext) => { + bidContext.pushEvent({ + eventInstance: new Event({ + eventType, + impid: bidContext.impid, + publisherId: this.publisherId, + level, + timestamp, + note, + }), + subPayloads: this.getImpressionPayload(bidContext.impid), + }); + }); + } + + /** + * A flag to indicate if the common events have been flushed to the server. + * This is useful to avoid submitting the same events multiple times. + */ + _commonBidContextEventsFlushed = false; + + /** + * Flush all debug events in all bid contexts as well as the common events (in + * Local Context) to the server. + */ + async flushAllDebugEvents() { + if (this.commonBidContextEvents.length < 0 && isEmpty(this.bidContexts)) { + logInfo('No debug events to flush'); + return; + } + + const flushPromises = []; + const debugEndpoint = `${this.adServerBaseUrl}/debug`; + + // If there are no bid contexts, and there are error events, submit the + // common events to the server + if ( + isEmpty(this.bidContexts) && + !this._commonBidContextEventsFlushed && + this.commonBidContextEvents.some( + event => event.level === DEBUG_EVENT_LEVELS.error || + event.level === DEBUG_EVENT_LEVELS.warn + ) + ) { + logInfo('Flush common events to the server'); + const debugReports = this.bidderRequests.flatMap(currentBidderRequest => { + return currentBidderRequest.bids.map(bid => { + const impid = utils.getImpId(bid); + return { + impid: impid, + events: this.commonBidContextEvents, + bidWin: null, + // Unroll the payload object to the top level to make it easier for + // Grafana to process the data. + ...this.getImpressionPayload(impid), + }; + }); + }); + + _each(debugReports, debugReport => { + flushPromises.push(utils.postAjax( + debugEndpoint, + debugReport + )); + }); + + this._commonBidContextEventsFlushed = true; + } + + flushPromises.push( + ...Object.values(this.bidContexts) + .map(async (currentBidContext) => { + logInfo('Flush bid context events to the server', currentBidContext); + return utils.postAjax( + debugEndpoint, + { + impid: currentBidContext.impid, + bidWin: currentBidContext.bidWin, + events: currentBidContext.events, + // Unroll the payload object to the top level to make it easier for + // Grafana to process the data. + ...currentBidContext.subPayloads, + } + ); + })); + + await Promise.all(flushPromises); + } +} + +/** + * Select key fields from the given object based on the object type. This is + * useful for debugging to reduce the size of the API call payload. + * @param {*} objType The custom type of the object. Return by determineObjType function. + * @param {*} eventArgs The args object that is passed in to the event handler + * or any supported object. + * @returns the clone of the given object but only contains the key fields + */ +function pickKeyFields(objType, eventArgs) { + switch (objType) { + case SUB_PAYLOAD_TYPES.AUCTION: { + return pick(eventArgs, [ + 'auctionId', + 'adUnitCodes', + 'auctionStart', + 'auctionEnd', + 'auctionStatus', + 'bidderRequestId', + 'timeout', + 'timestamp', + ]); + } + case SUB_PAYLOAD_TYPES.BIDDER_REQUEST: { + return pick(eventArgs, [ + 'auctionId', + 'bidId', + 'bidderCode', + 'bidderRequestId', + 'timeout' + ]); + } + case SUB_PAYLOAD_TYPES.ORTB_BID: { + return pick(eventArgs, [ + 'impid', 'id', 'price', 'cur', 'crid', 'cid', 'lurl', 'cpm' + ]); + } + case SUB_PAYLOAD_TYPES.PREBID_RESPONSE_INTERPRETED: { + return { + ...pick(eventArgs, [ + 'requestId', + 'creativeId', + 'cpm', + 'currency', + 'bidderCode', + 'adUnitCode', + 'ttl', + 'adId', + 'width', + 'height', + 'requestTimestamp', + 'responseTimestamp', + 'seatBidId', + 'statusMessage', + 'timeToRespond', + 'rejectionReason', + 'ortbId', + 'auctionId', + 'mediaType', + 'bidderRequestId', + ]), + }; + } + case SUB_PAYLOAD_TYPES.PREBID_BID_REQUEST: { + return { + ...pick(eventArgs, [ + 'bidderRequestId' + ]), + bids: eventArgs.bids.map( + bid => pickKeyFields(SUB_PAYLOAD_TYPES.PREBID_RESPONSE_NOT_INTERPRETED, bid) + ), + }; + } + case SUB_PAYLOAD_TYPES.AD_DOC_AND_PREBID_BID: { + return { + // bid: 'Not included to reduce payload size', + doc: pick(eventArgs.doc, ['visibilityState', 'readyState', 'hidden']), + }; + } + case SUB_PAYLOAD_TYPES.AD_DOC_AND_PREBID_BID_WITH_ERROR: { + return { + // bid: 'Not included to reduce payload size', + reason: eventArgs.reason, + message: eventArgs.message, + doc: pick(eventArgs.doc, ['visibilityState', 'readyState', 'hidden']), + } + } + case SUB_PAYLOAD_TYPES.BIDDER_ERROR_ARGS: { + return { + bidderRequest: pickKeyFields(SUB_PAYLOAD_TYPES.BIDDER_REQUEST, eventArgs.bidderRequest), + error: eventArgs.error?.toJSON ? eventArgs.error?.toJSON() + : (eventArgs.error || 'Failed to convert error object to JSON'), + }; + } + default: { + // Include the entire object for debugging + return { eventArgs }; + } + } +} + +let mobkoiAnalytics = Object.assign(adapter({analyticsType}), { + localContext: new LocalContext(), + async track({ + eventType, + args: prebidEventArgs + }) { + try { + switch (eventType) { + case AUCTION_INIT: { + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + const auction = prebidEventArgs; + this.localContext.initialise(auction); + this.localContext.pushEventToAllBidContexts({ + eventType, + level: DEBUG_EVENT_LEVELS.info, + timestamp: auction.timestamp, + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + break; + } + case BID_RESPONSE: { + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + const prebidBid = prebidEventArgs; + const bidContext = this.localContext.retrieveBidContext(prebidBid); + bidContext.pushEvent({ + eventInstance: new Event({ + eventType, + impid: bidContext.impid, + publisherId: this.localContext.publisherId, + level: DEBUG_EVENT_LEVELS.info, + timestamp: prebidEventArgs.timestamp || Date.now(), + }), + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs), + [SUB_PAYLOAD_TYPES.ORTB_BID]: pickKeyFields(SUB_PAYLOAD_TYPES.ORTB_BID, prebidEventArgs.ortbBidResponse), + } + }); + break; + } + case BID_WON: { + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + const prebidBid = prebidEventArgs; + if (utils.isMobkoiBid(prebidBid)) { + this.localContext.retrieveBidContext(prebidBid).bidWin = true; + } + // Notify the server that the bidding results. + this.localContext.triggerAllLossBidLossBeacon(); + // Append the bid win/loss event to all bid contexts + _each(this.localContext.bidContexts, (currentBidContext) => { + currentBidContext.pushEvent({ + eventInstance: new Event({ + eventType: currentBidContext.bidWin ? eventType : CUSTOM_EVENTS.BID_LOSS, + impid: currentBidContext.impid, + publisherId: this.localContext.publisherId, + level: DEBUG_EVENT_LEVELS.info, + timestamp: prebidEventArgs.timestamp || Date.now(), + }), + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs), + } + }); + }); + break; + } + case AUCTION_TIMEOUT: + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + const auction = prebidEventArgs; + this.localContext.pushEventToAllBidContexts({ + eventType, + level: DEBUG_EVENT_LEVELS.error, + timestamp: auction.timestamp, + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + break; + case NO_BID: { + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + this.localContext.pushEventToAllBidContexts({ + eventType, + level: DEBUG_EVENT_LEVELS.warn, + timestamp: prebidEventArgs.timestamp || Date.now(), + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + break; + } + case BID_REJECTED: { + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + const prebidBid = prebidEventArgs; + const bidContext = this.localContext.retrieveBidContext(prebidBid); + bidContext.pushEvent({ + eventInstance: new Event({ + eventType, + impid: bidContext.impid, + publisherId: this.localContext.publisherId, + level: DEBUG_EVENT_LEVELS.error, + timestamp: prebidEventArgs.timestamp || Date.now(), + note: prebidEventArgs.rejectionReason, + }), + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + break; + }; + case BIDDER_ERROR: { + utils.logTrackEvent(eventType, prebidEventArgs) + const argsType = utils.determineObjType(prebidEventArgs); + this.localContext.pushEventToAllBidContexts({ + eventType, + level: DEBUG_EVENT_LEVELS.warn, + timestamp: prebidEventArgs.timestamp || Date.now(), + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + break; + } + case AD_RENDER_FAILED: { + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + const {bid: prebidBid} = prebidEventArgs; + const bidContext = this.localContext.retrieveBidContext(prebidBid); + bidContext.pushEvent({ + eventInstance: new Event({ + eventType, + impid: bidContext.impid, + publisherId: this.localContext.publisherId, + level: DEBUG_EVENT_LEVELS.error, + timestamp: prebidEventArgs.timestamp || Date.now(), + }), + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + break; + } + case AD_RENDER_SUCCEEDED: { + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + const prebidBid = prebidEventArgs.bid; + const bidContext = this.localContext.retrieveBidContext(prebidBid); + bidContext.pushEvent({ + eventInstance: new Event({ + eventType, + impid: bidContext.impid, + publisherId: this.localContext.publisherId, + level: DEBUG_EVENT_LEVELS.info, + timestamp: prebidEventArgs.timestamp || Date.now(), + }), + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + break; + } + case AUCTION_END: { + utils.logTrackEvent(eventType, prebidEventArgs); + const argsType = utils.determineObjType(prebidEventArgs); + const auction = prebidEventArgs; + this.localContext.pushEventToAllBidContexts({ + eventType, + level: DEBUG_EVENT_LEVELS.info, + timestamp: auction.timestamp, + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + break; + } + case BIDDER_DONE: { + utils.logTrackEvent(eventType, prebidEventArgs) + const argsType = utils.determineObjType(prebidEventArgs); + this.localContext.pushEventToAllBidContexts({ + eventType, + level: DEBUG_EVENT_LEVELS.info, + timestamp: prebidEventArgs.timestamp || Date.now(), + subPayloads: { + [argsType]: pickKeyFields(argsType, prebidEventArgs) + } + }); + this.localContext.triggerAllLossBidLossBeacon(); + await this.localContext.flushAllDebugEvents(); + break; + } + default: + // Do nothing in other events + break; + } + } catch (error) { + // If there is an unexpected error, such as a syntax error, we log + // log the error and submit the error to the server for debugging. + this.localContext.pushEventToAllBidContexts({ + eventType, + level: DEBUG_EVENT_LEVELS.error, + timestamp: prebidEventArgs.timestamp || Date.now(), + note: 'Error occurred when processing this event.', + subPayloads: { + // Include the entire object for debugging + [`errorInEvent_${eventType}`]: { + // Some fields contain large data. Omits them to reduce API call payload size + eventArgs: utils.omitRecursive(prebidEventArgs, COMMON_FIELDS_TO_OMIT), + error: error.message, + } + } + }); + // Throw the error to skip the current Prebid event + throw error; + } + } +}); + +// save the base class function +mobkoiAnalytics.originEnableAnalytics = mobkoiAnalytics.enableAnalytics; + +// override enableAnalytics so we can get access to the config passed in from the page +mobkoiAnalytics.enableAnalytics = function (config) { + mobkoiAnalytics.originEnableAnalytics(config); // call the base class function +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: mobkoiAnalytics, + code: BIDDER_CODE, + gvlid: GVL_ID +}); + +export default mobkoiAnalytics; + +class BidContext { + /** + * The impression ID (ORTB term) of the bid. This ID is initialised in Prebid + * bid requests. The ID is reserved in requests and responses but have + * different names from object to object. + */ + get impid() { + if (this.ortbBidResponse) { + return this.ortbBidResponse.impid; + } else if (this.prebidBidResponse) { + return this.prebidBidResponse.requestId; + } else if (this.prebidBidRequest) { + return this.prebidBidRequest.bidId; + } else if ( + this.subPayloads && + utils.getImpId(this.subPayloads) + ) { + return utils.getImpId(this.subPayloads); + } else { + throw new Error('ORTB bid response and Prebid bid response are not available for extracting Impression ID'); + } + } + + /** + * ORTB ID generated by Ad Server + */ + get ortbId() { + if (this.ortbBidResponse) { + return utils.getOrtbId(this.ortbBidResponse); + } else if (this.prebidBidResponse) { + return utils.getOrtbId(this.prebidBidResponse); + } else if (this.subPayloads) { + return utils.getOrtbId(this.subPayloads); + } else { + throw new Error('ORTB bid response and Prebid bid response are not available for extracting ORTB ID'); + } + }; + + get publisherId() { + if (this.prebidBidRequest) { + return utils.getPublisherId(this.prebidBidRequest); + } else { + throw new Error('ORTB bid response and Prebid bid response are not available for extracting Publisher ID'); + } + } + + /** + * The prebid bid request object before converted to ORTB request in our + * custom adapter. + */ + get prebidBidRequest() { + if (!this.prebidBidResponse) { + throw new Error('Prebid bid response is not available. Accessing before assigning.'); + } + + return this.localContext.bidderRequests.flatMap(br => br.bids) + .find(bidRequest => bidRequest.bidId === this.prebidBidResponse.requestId); + } + + /** + * To avoid overriding the subPayloads object, we merge the new values to the + * existing subPayloads object. + */ + _subPayloads = null; + /** + * A group of payloads that are useful for debugging. The payloads are indexed + * by SUB_PAYLOAD_TYPES. + */ + get subPayloads() { + return this._subPayloads; + } + /** + * To avoid overriding the subPayloads object, we merge the new values to the + * existing subPayloads object. Identity fields will automatically added to the + * new values. + * @param {*} newSubPayloads Object containing new values to be merged + */ + mergePayload(newSubPayloads) { + utils.mergePayloadAndAddCustomFields( + this._subPayloads, + newSubPayloads, + // Add the identity fields to all sub payloads + { + impid: this.impid, + publisherId: this.publisherId, + } + ); + } + + /** + * The prebid bid response object after converted from ORTB response in our + * custom adapter. + */ + prebidBidResponse = null; + + /** + * The raw ORTB bid response object from the server. + */ + ortbBidResponse = null; + + /** + * A flag to indicate if the bid has won the auction. It only updated to true + * if the winning bid is from Mobkoi in the BID_WON event. + */ + bidWin = false; + + /** + * A flag to indicate if the loss beacon has been triggered. + */ + lurlTriggered = false; + + /** + * A list of DebugEvent objects + */ + events = []; + + /** + * Keep the reference of LocalContext object for easy accessing data. + */ + localContext = null; + + /** + * A object to store related data of a bid for easy access. + * i.e. bid request and bid response. + * @param {*} param0 + */ + constructor({ + localContext, + prebidOrOrtbBidResponse: bidResponse, + }) { + this.localContext = localContext; + this._subPayloads = {}; + + if (!bidResponse) { + throw new Error('prebidOrOrtbBidResponse field is required'); + } + + const objType = utils.determineObjType(bidResponse); + if (![SUB_PAYLOAD_TYPES.ORTB_BID, SUB_PAYLOAD_TYPES.PREBID_RESPONSE_INTERPRETED].includes(objType)) { + throw new Error( + 'Unable to create a new Bid Context as the given object is not a bid response object. ' + + 'Expect a Prebid Bid Object or ORTB Bid Object. Given object:\n' + + JSON.stringify(utils.omitRecursive(bidResponse, COMMON_FIELDS_TO_OMIT), null, 2) + ); + } + + if (objType === SUB_PAYLOAD_TYPES.ORTB_BID) { + this.ortbBidResponse = bidResponse; + this.prebidBidResponse = null; + } else if (objType === SUB_PAYLOAD_TYPES.PREBID_RESPONSE_INTERPRETED) { + this.ortbBidResponse = bidResponse.ortbBidResponse; + this.prebidBidResponse = bidResponse; + } else { + throw new Error('Expect a Prebid Bid Object or ORTB Bid Object. Given object:\n' + + JSON.stringify(utils.omitRecursive(bidResponse, COMMON_FIELDS_TO_OMIT), null, 2)); + } + } + + /** + * Push a debug event to the context which will be submitted to the server for debugging. + * @param {Object} params Object containing the following properties: + * @param {Event} params.eventInstance - DebugEvent object. If it does not contain the same impid as the BidContext, the event will be ignored. + * @param {Object|null} params.subPayloads - Object containing various payloads obtained from the Prebid Event args. The payloads will be merged into the existing subPayloads. + */ + pushEvent({eventInstance, subPayloads}) { + if (!(eventInstance instanceof Event)) { + throw new Error('bugEvent must be an instance of DebugEvent'); + } + if (eventInstance.impid != this.impid) { + // Ignore the event if the impression ID is not matched. + return; + } + // Accept only object or null + if (subPayloads !== null && typeof subPayloads !== 'object') { + throw new Error('subPayloads must be an object or null'); + } + + this.events.push(eventInstance); + + if (subPayloads !== null) { + this.mergePayload(subPayloads); + } + } +} + +/** + * A class to represent an event happened in the bid processing lifecycle. + */ +class Event { + /** + * Impression ID must set before appending to event lists. + */ + impid = null; + + /** + * Publisher ID. It is a unique identifier for the publisher. + */ + publisherId = null; + + /** + * Prebid Event Type or Custom Event Type + */ + eventType = null; + /** + * Debug level of the event. It can be one of the following: + * - info + * - warn + * - error + */ + level = null; + /** + * Timestamp of the event. It represents the time when the event occurred. + */ + timestamp = null; + + constructor({eventType, impid, publisherId, level, timestamp, note = undefined}) { + if (!eventType) { + throw new Error('eventType is required'); + } + if (!impid) { + throw new Error(`Impression ID is required. Given: "${impid}"`); + } + + if (typeof publisherId !== 'string') { + throw new Error(`Publisher ID must be a string. Given: "${publisherId}"`); + } + + if (!DEBUG_EVENT_LEVELS[level]) { + throw new Error(`Event level must be one of ${Object.keys(DEBUG_EVENT_LEVELS).join(', ')}. Given: "${level}"`); + } + if (typeof timestamp !== 'number') { + throw new Error('Timestamp must be a number'); + } + this.eventType = eventType; + this.impid = impid; + this.publisherId = publisherId; + this.level = level; + this.timestamp = timestamp; + if (note) { + this.note = note; + } + + if ( + debugTurnedOn() && + ( + level === DEBUG_EVENT_LEVELS.error || + level === DEBUG_EVENT_LEVELS.warn + )) { + logWarn(`New Debug Event - Type: ${eventType} Level: ${level}.`); + } + } +} + +/** + * Various types of payloads that are submitted to the server for debugging. + * Mostly they are obtain from the Prebid event args. + */ +export const SUB_PAYLOAD_TYPES = { + AUCTION: 'prebid_auction', + BIDDER_REQUEST: 'bidder_request', + ORTB_BID: 'ortb_bid', + PREBID_RESPONSE_INTERPRETED: 'prebid_bid_interpreted', + PREBID_RESPONSE_NOT_INTERPRETED: 'prebid_bid_not_interpreted', + PREBID_BID_REQUEST: 'prebid_bid_request', + AD_DOC_AND_PREBID_BID: 'ad_doc_and_prebid_bid', + AD_DOC_AND_PREBID_BID_WITH_ERROR: 'ad_doc_and_prebid_bid_with_error', + BIDDER_ERROR_ARGS: 'bidder_error_args', +}; + +/** + * Fields that are unique to objects used to identify the sub-payload type. + */ +export const SUB_PAYLOAD_UNIQUE_FIELDS_LOOKUP = { + [SUB_PAYLOAD_TYPES.AUCTION]: ['auctionStatus'], + [SUB_PAYLOAD_TYPES.BIDDER_REQUEST]: ['bidderRequestId'], + [SUB_PAYLOAD_TYPES.ORTB_BID]: ['adm', 'impid'], + [SUB_PAYLOAD_TYPES.PREBID_RESPONSE_INTERPRETED]: ['requestId', 'ortbBidResponse'], + [SUB_PAYLOAD_TYPES.PREBID_RESPONSE_NOT_INTERPRETED]: ['requestId'], // This must be paste under PREBID_RESPONSE_INTERPRETED + [SUB_PAYLOAD_TYPES.PREBID_BID_REQUEST]: ['bidId'], + [SUB_PAYLOAD_TYPES.AD_DOC_AND_PREBID_BID]: ['doc', 'bid'], + [SUB_PAYLOAD_TYPES.AD_DOC_AND_PREBID_BID_WITH_ERROR]: ['bid', 'reason', 'message'], + [SUB_PAYLOAD_TYPES.BIDDER_ERROR_ARGS]: ['error', 'bidderRequest'], +}; + +/** + * Required fields for the sub payloads. The property value defines the type of the required field. + */ +const PAYLOAD_REQUIRED_FIELDS = { + impid: 'string', + publisherId: 'string', +} + +export const utils = { + /** + * Make a POST request to the given URL with the given data. + * @param {*} url + * @param {*} data JSON data + * @returns + */ + postAjax: async function (url, data) { + return new Promise((resolve, reject) => { + try { + logInfo('postAjax:', url, data); + ajax(url, resolve, JSON.stringify(data), { + contentType: 'application/json', + method: 'POST', + withCredentials: false, // No user-specific data is tied to the request + referrerPolicy: 'unsafe-url', + crossOrigin: true + }); + } catch (error) { + reject(new Error( + `Failed to make post request to endpoint "${url}". With data: ` + + JSON.stringify(utils.omitRecursive(data, COMMON_FIELDS_TO_OMIT), null, 2), + { error: error.message } + )); + } + }); + }, + + /** + * Make a GET request to the given URL. If the request fails, it will fall back + * to AJAX request. + * @param {*} url URL with the query string + * @returns + */ + sendGetRequest: async function(url) { + return new Promise((resolve, reject) => { + try { + logInfo('triggerPixel', url); + triggerPixel(url, resolve); + } catch (error) { + try { + logWarn(`triggerPixel failed. URL: (${url}) Falling back to ajax. Error: `, error); + ajax(url, resolve, null, { + contentType: 'application/json', + method: 'GET', + withCredentials: false, // No user-specific data is tied to the request + referrerPolicy: 'unsafe-url', + crossOrigin: true + }); + } catch (error) { + // If failed with both methods, reject the promise + reject(error); + } + } + }); + }, + + /** + * Check if the given Prebid bid is from Mobkoi. + * @param {*} prebidBid + * @returns + */ + isMobkoiBid: function (prebidBid) { + return prebidBid && prebidBid.bidderCode === BIDDER_CODE; + }, + + /** + * !IMPORTANT: Make sure the implementation of this function matches utils.getOrtbId in + * mobkoiAnalyticsAdapter.js. + * We use the bidderRequestId as the ortbId. We could do so because we only + * make one ORTB request per Prebid Bidder Request. + * The ID field named differently when the value passed on to different contexts. + * @param {*} bid Prebid Bidder Request Object or Prebid Bid Response/Request + * or ORTB Request/Response Object + * @returns {string} The ORTB ID + * @throws {Error} If the ORTB ID cannot be found in the given object. + */ + getOrtbId(bid) { + const ortbId = + // called bidderRequestId in Prebid Request + bid.bidderRequestId || + // called seatBidId in Prebid Bid Response Object + bid.seatBidId || + // called ortbId in Interpreted Prebid Response Object + bid.ortbId || + // called id in ORTB object + (Object.hasOwn(bid, 'imp') && bid.id); + + if (!ortbId) { + throw new Error( + 'Failed to obtain ORTB ID from the given object. Given object:\n' + + JSON.stringify(utils.omitRecursive(bid, COMMON_FIELDS_TO_OMIT), null, 2) + ); + } + + return ortbId; + }, + + /** + * Impression ID is named differently in different objects. This function will + * return the impression ID from the given bid object. + * @param {*} bid ORTB bid response or Prebid bid response or Prebid bid request + * @returns string | null + */ + getImpId: function (bid) { + return (bid && (bid.impid || bid.requestId || bid.bidId)) || null; + }, + + /** + * !IMPORTANT: Make sure the implementation of this function matches utils.getPublisherId in + * both adapters. + * Extract the publisher ID from the given object. + * @param {*} bid Prebid Bidder Request Object or Prebid Bid Response/Request + * or ORTB Request/Response Object + * @returns string + * @throws {Error} If the publisher ID is not found in the given object. + */ + getPublisherId: function (bid) { + const ortbPath = 'site.publisher.id'; + const prebidPath = `ortb2.${ortbPath}`; + + const publisherId = + deepAccess(bid, prebidPath) || + deepAccess(bid, ortbPath); + + if (!publisherId) { + throw new Error( + 'Failed to obtain publisher ID from the given object. ' + + `Please set it via the "${prebidPath}" field with pbjs.setBidderConfig.\n` + + 'Given object:\n' + + JSON.stringify(bid, null, 2) + ); + } + + return publisherId; + }, + + /** + * !IMPORTANT: Make sure the implementation of this function matches getAdServerEndpointBaseUrl + * in both adapters. + * Obtain the Ad Server Base URL from the given Prebid object. + * @param {*} bid Prebid Bidder Request Object or Prebid Bid Response/Request + * or ORTB Request/Response Object + * @returns {string} The Ad Server Base URL + * @throws {Error} If the ORTB ID cannot be found in the given + */ + getAdServerEndpointBaseUrl (bid) { + const path = `site.publisher.ext.${PARAM_NAME_AD_SERVER_BASE_URL}`; + const preBidPath = `ortb2.${path}`; + + const adServerBaseUrl = + // For Prebid Bid objects + deepAccess(bid, preBidPath) || + // For ORTB objects + deepAccess(bid, path); + + if (!adServerBaseUrl) { + throw new Error('Failed to find the Ad Server Base URL in the given object. ' + + `Please set it via the "${preBidPath}" field with pbjs.setBidderConfig.\n` + + 'Given Object:\n' + + JSON.stringify(bid, null, 2) + ); + } + + return adServerBaseUrl; + }, + + logTrackEvent: function (eventType, eventArgs) { + if (!debugTurnedOn()) { + return; + } + const argsType = (() => { + try { + return utils.determineObjType(eventArgs); + } catch (error) { + logError(`Error when logging track event: [${eventType}]\n`, error); + return 'Unknown'; + } + })(); + logInfo(`Track event: [${eventType}]. Args Type Determination: ${argsType}`, eventArgs); + }, + + /** + * Determine the type of the given object based on the object's fields. + * This is useful for identifying the type of object that is passed in to the + * handler functions. + * @param {*} eventArgs + * @returns string + */ + determineObjType: function (eventArgs) { + if (typeof eventArgs !== 'object' || eventArgs === null) { + throw new Error( + 'determineObjType: Expect an object. Given object is not an object or null. Given object:' + + JSON.stringify(utils.omitRecursive(eventArgs, COMMON_FIELDS_TO_OMIT), null, 2) + ); + } + + let objType = null; + for (const type of Object.values(SUB_PAYLOAD_TYPES)) { + const identifyFields = SUB_PAYLOAD_UNIQUE_FIELDS_LOOKUP[type]; + if (!identifyFields) { + throw new Error( + `Identify fields for type "${type}" is not defined in COMMON_OBJECT_UNIT_FIELDS.` + ); + } + + // If all fields are available in the object, then it's the type we are looking for + if (identifyFields.every(field => eventArgs.hasOwnProperty(field))) { + objType = type; + break; + } + } + + if (!objType) { + throw new Error( + 'Unable to determine track args type. Please update COMMON_OBJECT_UNIT_FIELDS for the new object type.\n' + + 'Given object:\n' + + JSON.stringify(utils.omitRecursive(eventArgs, COMMON_FIELDS_TO_OMIT), null, 2) + ); + } + + return objType; + }, + + /** + * Merge a Payload object with new values. The payload object must be in + * specific format where root level keys are SUB_PAYLOAD_TYPES values and the + * property values must be an object of the given type. + * @param {*} targetPayload + * @param {*} newSubPayloads + * @param {*} customFields Custom fields that are required for the sub payloads. + */ + mergePayloadAndAddCustomFields: function (targetPayload, newSubPayloads, customFields = undefined) { + if (typeof targetPayload !== 'object') { + throw new Error('Target must be an object'); + } + + if (typeof newSubPayloads !== 'object') { + throw new Error('New values must be an object'); + } + + // Ensure all the required custom fields are available + if (customFields) { + _each(customFields, (fieldType, fieldName) => { + if (fieldType === 'string' && typeof newSubPayloads[fieldName] !== 'string') { + throw new Error( + `Field "${fieldName}" must be a string. Given: ${newSubPayloads[fieldName]}` + ); + } + }); + } + + mergeDeep(targetPayload, newSubPayloads); + + // Add the custom fields to the sub-payloads just added to the target payload + if (customFields) { + utils.addCustomFieldsToSubPayloads(targetPayload, customFields); + } + }, + + /** + * Should not use this function directly. Use mergePayloadAndCustomFields + * instead. This function add custom fields to the sub-payloads. The provided + * custom fields will be validated. + * @param {*} subPayloads A group of payloads that are useful for debugging. Indexed by SUB_PAYLOAD_TYPES. + * @param {*} customFields Custom fields that are required for the sub + * payloads. Fields are defined in PAYLOAD_REQUIRED_FIELDS. + */ + addCustomFieldsToSubPayloads: function (subPayloads, customFields) { + _each(subPayloads, (currentSubPayload, subPayloadType) => { + if (!Object.values(SUB_PAYLOAD_TYPES).includes(subPayloadType)) { + return; + } + + // Add the custom fields to the sub-payloads + mergeDeep(currentSubPayload, customFields); + }); + + // Before leaving the function, validate the payload to ensure all + // required fields are available. + utils.validateSubPayloads(subPayloads); + }, + + /** + * Recursively omit the given keys from the object. + * @param {*} subPayloads - The payload objects index by their payload types. + * @throws {Error} - If the given object is not valid. + */ + validateSubPayloads: function (subPayloads) { + _each(subPayloads, (currentSubPayload, subPayloadType) => { + if (!Object.values(SUB_PAYLOAD_TYPES).includes(subPayloadType)) { + return; + } + + const validationErrors = []; + // Validate the required fields + _each(PAYLOAD_REQUIRED_FIELDS, (requiredFieldType, requiredFieldName) => { + // eslint-disable-next-line valid-typeof + if (typeof currentSubPayload[requiredFieldName] !== requiredFieldType) { + validationErrors.push(new Error( + `Field "${requiredFieldName}" in "${subPayloadType}" must be a ${requiredFieldType}. Given: ${currentSubPayload[requiredFieldName]}` + )); + } + }); + + if (validationErrors.length > 0) { + throw new Error( + `Validation failed for "${subPayloadType}".\n` + + `Object: ${JSON.stringify(utils.omitRecursive(currentSubPayload, COMMON_FIELDS_TO_OMIT), null, 2)}\n` + + validationErrors.map(error => `Error: ${error.message}`).join('\n') + ); + } + }); + }, + + /** + * Recursively omit the given keys from the object. + * @param {*} obj - The object to process. + * @param {Array} keysToOmit - The keys to omit from the object. + * @param {*} [placeholder='OMITTED'] - The placeholder value to use for omitted keys. + * @returns {Object} - A clone of the given object with the specified keys omitted. + */ + omitRecursive: function (obj, keysToOmit, placeholder = 'OMITTED') { + return Object.keys(obj).reduce((acc, currentKey) => { + // If the current key is in the keys to omit, replace the value with the placeholder + if (keysToOmit.includes(currentKey)) { + acc[currentKey] = placeholder; + return acc; + } + + // If the current value is an object and not null, recursively omit keys + if (typeof obj[currentKey] === 'object' && obj[currentKey] !== null) { + acc[currentKey] = utils.omitRecursive(obj[currentKey], keysToOmit, placeholder); + } else { + // Otherwise, directly assign the value to the accumulator object + acc[currentKey] = obj[currentKey]; + } + return acc; + }, {}); + } +}; diff --git a/modules/mobkoiAnalyticsAdapter.md b/modules/mobkoiAnalyticsAdapter.md new file mode 100644 index 00000000000..07e10be184b --- /dev/null +++ b/modules/mobkoiAnalyticsAdapter.md @@ -0,0 +1,9 @@ +# Overview + +Module Name: Mobkoi Analytics Adapter +Module Type: Analytics Adapter +Maintainer: platformteam@mobkoi.com + +# Description + +Analytics adapter for Mobkoi. Contact platformteam@mobkoi.com for information. \ No newline at end of file diff --git a/modules/mobkoiBidAdapter.js b/modules/mobkoiBidAdapter.js new file mode 100644 index 00000000000..ec5337447ed --- /dev/null +++ b/modules/mobkoiBidAdapter.js @@ -0,0 +1,198 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { deepAccess, deepSetValue, logError } from '../src/utils.js'; + +const BIDDER_CODE = 'mobkoi'; +const GVL_ID = 898; +export const DEFAULT_AD_SERVER_BASE_URL = 'https://adserver.maximus.mobkoi.com'; + +const PUBLISHER_PARAMS = { + /** + * !IMPORTANT: This value must match the value in mobkoiAnalyticsAdapter.js + * The name of the parameter that the publisher can use to specify the ad server endpoint. + */ + PARAM_NAME_AD_SERVER_BASE_URL: 'adServerBaseUrl', + PARAM_NAME_PLACEMENT_ID: 'placementId', +} + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30, + }, + request(buildRequest, imps, bidderRequest, context) { + const ortbRequest = buildRequest(imps, bidderRequest, context); + const prebidBidRequest = context.bidRequests[0]; + + ortbRequest.id = utils.getOrtbId(prebidBidRequest); + deepSetValue(ortbRequest, 'site.publisher.ext.adServerBaseUrl', utils.getAdServerEndpointBaseUrl(prebidBidRequest)); + // We only support one impression per request. + deepSetValue(ortbRequest, 'imp.0.tagid', utils.getPlacementId(prebidBidRequest)); + deepSetValue(ortbRequest, 'user.id', context.bidRequests[0].userId?.mobkoiId || null); + + return ortbRequest; + }, + bidResponse(buildPrebidBidResponse, ortbBidResponse, context) { + const prebidBid = buildPrebidBidResponse(ortbBidResponse, context); + utils.addCustomFieldsToPrebidBidResponse(prebidBid, ortbBidResponse); + return prebidBid; + }, +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + gvlid: GVL_ID, + + /** + * Determines whether or not the given bid request is valid. + */ + isBidRequestValid(bid) { + if ( + !deepAccess(bid, `params.${PUBLISHER_PARAMS.PARAM_NAME_PLACEMENT_ID}`) + ) { + logError(`The ${PUBLISHER_PARAMS.PARAM_NAME_PLACEMENT_ID} field is required in the bid request. ` + + 'Please follow the setup guideline to set the placement ID field.') + return false; + } + + return true; + }, + /** + * Make a server request from the list of BidRequests. + */ + buildRequests(prebidBidRequests, prebidBidderRequest) { + const adServerEndpoint = utils.getAdServerEndpointBaseUrl(prebidBidderRequest) + '/bid'; + + return { + method: 'POST', + url: adServerEndpoint, + options: { + contentType: 'application/json', + }, + data: converter.toORTB({ + bidRequests: prebidBidRequests, + bidderRequest: prebidBidderRequest + }), + }; + }, + /** + * Unpack the response from the server into a list of bids. + */ + interpretResponse(serverResponse, customBidRequest) { + if (!serverResponse.body) return []; + + const responseBody = {...serverResponse.body, seatbid: serverResponse.body.seatbid}; + const prebidBidResponse = converter.fromORTB({ + request: customBidRequest.data, + response: responseBody, + }); + return prebidBidResponse.bids; + }, +}; + +registerBidder(spec); + +export const utils = { + /** + * !IMPORTANT: Make sure the implementation of this function matches getAdServerEndpointBaseUrl + * in both adapters. + * Obtain the Ad Server Base URL from the given Prebid object. + * @param {*} bid Prebid Bidder Request Object or Prebid Bid Response/Request + * or ORTB Request/Response Object + * @returns {string} The Ad Server Base URL + */ + getAdServerEndpointBaseUrl (bid) { + // Fields that would be automatically set if the publisher set it via pbjs.setBidderConfig. + const ortbPath = `site.publisher.ext.${PUBLISHER_PARAMS.PARAM_NAME_AD_SERVER_BASE_URL}`; + const prebidPath = `ortb2.${ortbPath}`; + + // Fields that would be set by the publisher in the bid + // configuration object in ad unit. + const paramPath = `params.${PUBLISHER_PARAMS.PARAM_NAME_AD_SERVER_BASE_URL}`; + const bidRequestFirstBidParam = `bids.0.${paramPath}`; + + const adServerBaseUrl = + deepAccess(bid, paramPath) || + deepAccess(bid, bidRequestFirstBidParam) || + deepAccess(bid, prebidPath) || + deepAccess(bid, ortbPath) || + DEFAULT_AD_SERVER_BASE_URL; + + return adServerBaseUrl; + }, + + /** + * Extract the placement ID from the given object. + * @param {*} prebidBidRequestOrOrtbBidRequest + * @returns string + * @throws {Error} If the placement ID is not found in the given object. + */ + getPlacementId: function (prebidBidRequestOrOrtbBidRequest) { + // Fields that would be set by the publisher in the bid configuration object in ad unit. + const paramPath = 'params.placementId'; + const bidRequestFirstBidParam = `bids.0.${paramPath}`; + + // ORTB path for placement ID + const ortbPath = 'imp.0.tagid'; + + const placementId = + deepAccess(prebidBidRequestOrOrtbBidRequest, paramPath) || + deepAccess(prebidBidRequestOrOrtbBidRequest, bidRequestFirstBidParam) || + deepAccess(prebidBidRequestOrOrtbBidRequest, ortbPath); + + if (!placementId) { + throw new Error( + 'Failed to obtain placement ID from the given object. ' + + `Please set it via the "${paramPath}" field in the bid configuration.\n` + + 'Given object:\n' + + JSON.stringify({functionParam: prebidBidRequestOrOrtbBidRequest}, null, 3) + ); + } + + return placementId; + }, + + /** + * !IMPORTANT: Make sure the implementation of this function matches utils.getOrtbId in + * mobkoiAnalyticsAdapter.js. + * We use the bidderRequestId as the ortbId. We could do so because we only + * make one ORTB request per Prebid Bidder Request. + * The ID field named differently when the value passed on to different contexts. + * @param {*} bid Prebid Bidder Request Object or Prebid Bid Response/Request + * or ORTB Request/Response Object + * @returns {string} The ORTB ID + * @throws {Error} If the ORTB ID cannot be found in the given object. + */ + getOrtbId(bid) { + const ortbId = + // called bidderRequestId in Prebid Request + bid.bidderRequestId || + // called seatBidId in Prebid Bid Response Object + bid.seatBidId || + // called ortbId in Interpreted Prebid Response Object + bid.ortbId || + // called id in ORTB object + (Object.hasOwn(bid, 'imp') && bid.id); + + if (!ortbId) { + throw new Error('Unable to find the ORTB ID in the bid object. Given Object:\n' + + JSON.stringify(bid, null, 2) + ); + } + + return ortbId; + }, + + /** + * Append custom fields to the prebid bid response. so that they can be accessed + * in various event handlers. + * @param {*} prebidBidResponse + * @param {*} ortbBidResponse + */ + addCustomFieldsToPrebidBidResponse(prebidBidResponse, ortbBidResponse) { + prebidBidResponse.ortbBidResponse = ortbBidResponse; + prebidBidResponse.ortbId = ortbBidResponse.id; + }, +} diff --git a/modules/mobkoiBidAdapter.md b/modules/mobkoiBidAdapter.md new file mode 100644 index 00000000000..e5c6c3734ab --- /dev/null +++ b/modules/mobkoiBidAdapter.md @@ -0,0 +1,53 @@ +# Overview + +Module Name: Mobkoi Bidder Adapter +Module Type: Bidder Adapter +Maintainer: platformteam@mobkoi.com + +# Description + +Module that connects to Mobkoi Ad Server + +### Supported formats: +- Banner + +# Test Parameters +```js +const adUnits = [ + { + code: 'banner-ad', + mediaTypes: { + banner: { sizes: [300, 200] }, + }, + bids: [ + { + bidder: 'mobkoi', + params: { + publisherId: 'module-test-publisher-id', + placementId: 'moudle-test-placement-id', + adServerBaseUrl: 'https://not.an.adserver.endpoint.com', + } + }, + ], + }, +]; + +pbjs.que.push(function () { + pbjs.addAdUnits(adUnits); +}); +``` + + +# Serve Prebid.js Locally + +To serve Prebid.js locally with specific modules, you can use the following command: + +```sh +gulp serve-fast --modules=consentManagementTcf,tcfControl,mobkoiBidAdapter +``` + +# Run bid adapter test locally + +```sh +gulp test --file=test/spec/modules/mobkoiBidAdapter_spec.js +``` diff --git a/modules/mobkoiIdSystem.js b/modules/mobkoiIdSystem.js new file mode 100644 index 00000000000..d95b5effa77 --- /dev/null +++ b/modules/mobkoiIdSystem.js @@ -0,0 +1,144 @@ +/** + * This module adds mobkoiId support to the User ID module + * The {@link module:modules/userId} module is required. + * @module modules/mobkoiIdSystem + * @requires module:modules/userId + */ + +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { logError, logInfo, deepAccess, insertUserSyncIframe } from '../src/utils.js'; + +const GVL_ID = 898; +const MODULE_NAME = 'mobkoiId'; +export const PROD_AD_SERVER_BASE_URL = 'https://adserver.maximus.mobkoi.com'; +export const EQUATIV_BASE_URL = 'https://sync.smartadserver.com'; +export const EQUATIV_NETWORK_ID = '5290'; +/** + * !IMPORTANT: This value must match the value in mobkoiAnalyticsAdapter.js + * The name of the parameter that the publisher can use to specify the ad server endpoint. + */ +const PARAM_NAME_AD_SERVER_BASE_URL = 'adServerBaseUrl'; + +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); + +export const mobkoiIdSubmodule = { + name: MODULE_NAME, + gvlid: GVL_ID, + + decode(value) { + return value ? { [MODULE_NAME]: value } : undefined; + }, + + getId(userSyncOptions, gdprConsent) { + logInfo('Getting Equativ SAS ID.'); + + if (!storage.cookiesAreEnabled()) { + logError('Cookies are not enabled. Module will not work.'); + return { + id: null + }; + } + + const storageName = userSyncOptions && userSyncOptions.storage && userSyncOptions.storage.name; + if (!storageName) { + logError('Storage name is not defined. Module will not work.'); + return { + id: null + }; + } + + const existingId = storage.getCookie(storageName); + + if (existingId) { + logInfo(`Found "${storageName}" from local cookie: "${existingId}"`); + return { id: existingId }; + } + + logInfo(`Cannot found "${storageName}" in local cookie with name.`); + return { + callback: () => { + return new Promise((resolve, _reject) => { + utils.requestEquativSasId( + userSyncOptions, + gdprConsent, + (sasId) => { + if (!sasId) { + logError('Equativ SAS ID is empty'); + resolve({ id: null }); + return; + } + + logInfo(`Fetched Equativ SAS ID: "${sasId}"`); + storage.setCookie(storageName, sasId, userSyncOptions.storage.expires); + logInfo(`Stored Equativ SAS ID in local cookie with name: "${storageName}"`); + resolve({ id: sasId }); + } + ); + }); + } + }; + }, +}; + +submodule('userId', mobkoiIdSubmodule); + +export const utils = { + requestEquativSasId(syncUserOptions, gdprConsent, onCompleteCallback) { + logInfo('Start requesting Equativ SAS ID'); + const adServerBaseUrl = deepAccess( + syncUserOptions, + `params.${PARAM_NAME_AD_SERVER_BASE_URL}`) || PROD_AD_SERVER_BASE_URL; + + const equativPixelUrl = utils.buildEquativPixelUrl(syncUserOptions, gdprConsent); + logInfo('Equativ SAS ID request URL:', equativPixelUrl); + + const url = adServerBaseUrl + '/pixeliframe?' + + 'pixelUrl=' + encodeURIComponent(equativPixelUrl) + + '&cookieName=sas_uid'; + + /** + * Listen for messages from the iframe + */ + window.addEventListener('message', function(event) { + switch (event.data.type) { + case 'MOBKOI_PIXEL_SYNC_COMPLETE': + const sasUid = event.data.syncData; + logInfo('Parent window Sync completed. SAS ID:', sasUid); + onCompleteCallback(sasUid); + break; + case 'MOBKOI_PIXEL_SYNC_ERROR': + logError('Parent window Sync failed:', event.data.error); + onCompleteCallback(null); + break; + } + }); + + insertUserSyncIframe(url, () => { + logInfo('insertUserSyncIframe loaded'); + }); + + // Return the URL for testing purposes + return url; + }, + + /** + * Build a pixel URL that will be placed in an iframe to fetch the Equativ SAS ID + */ + buildEquativPixelUrl(syncUserOptions, gdprConsent) { + logInfo('Generating Equativ SAS ID request URL'); + const adServerBaseUrl = + deepAccess( + syncUserOptions, + `params.${PARAM_NAME_AD_SERVER_BASE_URL}`) || PROD_AD_SERVER_BASE_URL; + + const gdprConsentString = gdprConsent && gdprConsent.gdprApplies ? gdprConsent.consentString : ''; + const smartServerUrl = EQUATIV_BASE_URL + '/getuid?' + + `url=` + encodeURIComponent(`${adServerBaseUrl}/getPixel?value=`) + '[sas_uid]' + + `&gdpr_consent=${gdprConsentString}` + + `&nwid=${EQUATIV_NETWORK_ID}`; + + return smartServerUrl; + } +}; diff --git a/modules/mobkoiIdSystem.md b/modules/mobkoiIdSystem.md new file mode 100644 index 00000000000..b122cad213e --- /dev/null +++ b/modules/mobkoiIdSystem.md @@ -0,0 +1,38 @@ +## Mobkoi User ID Submodule + +For assistance setting up your module please contact us at platformteam@mobkoi.com. + +### Prebid Params + +Individual params may be set for the IDx Submodule. +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'mobkoiId', + storage: { + name : 'mobkoi_uid', + type : 'cookie', + expires : 30 + } + }] + } +}); +``` +## Parameter Descriptions for the `userSync` Configuration Section +The below parameters apply only to the Mobkoi integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID of the module - `"mobkoiId"` | `"mobkoiId"` | +| storage.name | Required | String | The name of the cookie local storage where the user ID will be stored. | `"mobkoi_uid"` | +| storage.type | Required | String | Must be "`cookie`". This is where the results of the user ID will be stored. | `"cookie"` | +| storage.expires | Required | Integer | How long (in days) the user ID information will be stored. | `30` | + +## Serving the Custom Build Locally + +To serve the custom build locally, use the following command: + +```sh +gulp serve-fast --modules=consentManagementTcf,tcfControl,mobkoiBidAdapter,mobkoiIdSystem,userId +``` diff --git a/modules/mygaruIdSystem.js b/modules/mygaruIdSystem.js index 9133480477b..e05bcba1ecb 100644 --- a/modules/mygaruIdSystem.js +++ b/modules/mygaruIdSystem.js @@ -77,8 +77,8 @@ export const mygaruIdSubmodule = { * @returns {{id: string | undefined}} */ getId(config, consentData) { - const gdprApplies = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies ? 1 : 0; - const gdprConsentString = gdprApplies ? consentData.consentString : undefined; + const gdprApplies = consentData?.gdpr?.gdprApplies === true ? 1 : 0; + const gdprConsentString = gdprApplies ? consentData.gdpr.consentString : undefined; const url = buildUrl({ gdprApplies, gdprConsentString diff --git a/modules/mytargetBidAdapter.md b/modules/mytargetBidAdapter.md deleted file mode 100644 index 3292ff561fa..00000000000 --- a/modules/mytargetBidAdapter.md +++ /dev/null @@ -1,40 +0,0 @@ -# Overview - -``` -Module Name: myTarget Bidder Adapter -Module Type: Bidder Adapter -Maintainer: support_target@corp.my.com -``` - -# Description - -Module that connects to myTarget demand sources. - -# Test Parameters - -``` - var adUnits = [{ - code: 'placementCode', - mediaTypes: { - banner: { - sizes: [[240, 400]], - } - }, - bids: [{ - bidder: 'mytarget', - params: { - placementId: '379783', - - // OPTIONAL: custom bid floor - bidfloor: 10000, - - // OPTIONAL: if you know the ad position on the page, specify it here - // (this corresponds to "Ad Position" in OpenRTB 2.3, section 5.4) - position: 0, - - // OPTIONAL: bid response type: 0 - ad url (default), 1 - ad markup - response: 0 - } - }] - }]; -``` diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index 69a270247cd..bdd0fa7e054 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -1,6 +1,6 @@ import { deepAccess, isEmpty } from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' -import { BANNER } from '../src/mediaTypes.js' +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js' import { getGlobal } from '../src/prebidGlobal.js' import { ortbConverter } from '../libraries/ortbConverter/converter.js' @@ -8,14 +8,16 @@ const converter = ortbConverter({ context: { // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false - ttl: 30 // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + ttl: 30, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) }, imp(buildImp, bidRequest, context) { - const imp = buildImp(bidRequest, context); + const imp = buildImp(bidRequest, context) imp.tagid = bidRequest.adUnitCode - return imp; - } -}); + if (imp.ext) imp.ext.placementId = bidRequest.params.placementId + + return imp + }, +}) const BIDDER_CODE = 'nativo' const BIDDER_ENDPOINT = 'https://exchange.postrelease.com/prebid' @@ -24,17 +26,27 @@ const GVLID = 263 const TIME_TO_LIVE = 360 -const SUPPORTED_AD_TYPES = [BANNER] +const SUPPORTED_AD_TYPES = [BANNER, VIDEO, NATIVE] const FLOOR_PRICE_CURRENCY = 'USD' const PRICE_FLOOR_WILDCARD = '*' const localPbjsRef = getGlobal() +function getMediaType(accessObj) { + if (deepAccess(accessObj, 'mediaTypes.video')) { + return VIDEO + } else if (deepAccess(accessObj, 'mediaTypes.native')) { + return NATIVE + } else { + return BANNER + } +} + /** * Keep track of bid data by keys * @returns {Object} - Map of bid data that can be referenced by multiple keys */ -export const BidDataMap = () => { +export function BidDataMap() { const referenceMap = {} const bids = [] @@ -122,8 +134,7 @@ export const spec = { */ isBidRequestValid: function (bid) { // We don't need any specific parameters to make a bid request - // If not parameters are supplied just verify it's the correct bidder code - if (!bid.params) return bid.bidder === BIDDER_CODE + if (!bid.params) return true // Check if any supplied parameters are invalid const hasInvalidParameters = Object.keys(bid.params).some((key) => { @@ -150,7 +161,10 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { // Get OpenRTB Data - const openRTBData = converter.toORTB({bidRequests: validBidRequests, bidderRequest}) + const openRTBData = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest, + }) const openRTBDataString = JSON.stringify(openRTBData) const requestData = new RequestData() @@ -201,7 +215,8 @@ export const spec = { let params = [ // Prebid version { - key: 'ntv_pbv', value: localPbjsRef.version + key: 'ntv_pbv', + value: localPbjsRef.version, }, // Prebid request id { key: 'ntv_pb_rid', value: bidderRequest.bidderRequestId }, @@ -278,19 +293,31 @@ export const spec = { }) } + // Add GPP params + if (bidderRequest.gppConsent) { + params.unshift({ + key: 'ntv_gpp_consent', + value: bidderRequest.gppConsent.gppString, + }) + } + // Add USP params if (bidderRequest.uspConsent) { // Put on the beginning of the qs param array params.unshift({ key: 'us_privacy', value: bidderRequest.uspConsent }) } - const qsParamStrings = [requestData.getRequestDataQueryString(), arrayToQS(params)] + const qsParamStrings = [ + requestData.getRequestDataQueryString(), + arrayToQS(params), + ] const requestUrl = buildRequestUrl(BIDDER_ENDPOINT, qsParamStrings) let serverRequest = { method: 'POST', url: requestUrl, data: openRTBDataString, + bidderRequest: bidderRequest, } return serverRequest @@ -320,9 +347,10 @@ export const spec = { // Step through and grab pertinent data let bidResponse, adUnit - seatbids.forEach((seatbid) => { + seatbids.forEach((seatbid, i) => { seatbid.bid.forEach((bid) => { adUnit = this.getAdUnitData(body.id, bid) + bidResponse = { requestId: adUnit.bidId, cpm: bid.price, @@ -337,10 +365,18 @@ export const spec = { meta: { advertiserDomains: bid.adomain, }, + mediaType: getMediaType(request.bidderRequest.bids[i]), } if (bid.ext) extData[bid.id] = bid.ext - + if (bidResponse.mediaType === VIDEO) { + bidResponse.vastUrl = bid.adm + } + if (bidResponse.mediaType === NATIVE) { + bidResponse.native = { + ortb: JSON.parse(bidResponse.ad), + } + } bidResponses.push(bidResponse) }) }) @@ -414,23 +450,27 @@ export const spec = { typeof response.body === 'string' ? JSON.parse(response.body) : response.body - } catch (err) { return } + } catch (err) { + return + } // Make sure we have valid content if (!body || !body.seatbid || body.seatbid.length === 0) return body.seatbid.forEach((seatbid) => { // Grab the syncs for each seatbid - seatbid.syncUrls.forEach((sync) => { - if (types[sync.type]) { - if (sync.url.trim() !== '') { - syncs.push({ - type: sync.type, - url: sync.url.replace('{GDPR_params}', params), - }) + if (seatbid.syncUrls) { + seatbid.syncUrls.forEach((sync) => { + if (types[sync.type]) { + if (sync.url.trim() !== '') { + syncs.push({ + type: sync.type, + url: sync.url.replace('{GDPR_params}', params), + }) + } } - } - }) + }) + } }) }) @@ -491,7 +531,9 @@ export class RequestData { getRequestDataQueryString() { if (this.bidRequestDataSources.length == 0) return - const queryParams = this.bidRequestDataSources.map(dataSource => dataSource.getRequestQueryString()).filter(queryString => queryString !== '') + const queryParams = this.bidRequestDataSources + .map((dataSource) => dataSource.getRequestQueryString()) + .filter((queryString) => queryString !== '') return queryParams.join('&') } } @@ -500,8 +542,10 @@ export class BidRequestDataSource { constructor() { this.type = 'BidRequestDataSource' } - processBidRequestData(bidRequest, bidderRequest) { } - getRequestQueryString() { return '' } + processBidRequestData(bidRequest, bidderRequest) {} + getRequestQueryString() { + return '' + } } export class UserEIDs extends BidRequestDataSource { @@ -540,7 +584,7 @@ QueryStringParam.prototype.toString = function () { export function encodeToBase64(value) { try { return btoa(JSON.stringify(value)) - } catch (err) { } + } catch (err) {} } export function parseFloorPriceData(bidRequest) { @@ -565,7 +609,7 @@ export function parseFloorPriceData(bidRequest) { size, }) // Save the data and track the sizes - mediaTypeFloorPriceData[sizeToString(size)] = priceFloorData.floor + mediaTypeFloorPriceData[sizeToString(size)] = priceFloorData?.floor sizeOptions.add(size) }) bidRequestFloorPriceData[mediaType] = mediaTypeFloorPriceData @@ -573,7 +617,7 @@ export function parseFloorPriceData(bidRequest) { // Get floor price of current media type with a wildcard size const sizeWildcardFloor = getSizeWildcardPrice(bidRequest, mediaType) // Save the wildcard floor price if it was retrieved successfully - if (sizeWildcardFloor.floor > 0) { + if (sizeWildcardFloor?.floor > 0) { mediaTypeFloorPriceData['*'] = sizeWildcardFloor.floor } }) @@ -708,9 +752,13 @@ function getLargestSize(sizes, method = area) { * Build the final request url */ export function buildRequestUrl(baseUrl, qsParamStringArray = []) { - if (qsParamStringArray.length === 0 || !Array.isArray(qsParamStringArray)) return baseUrl + if (qsParamStringArray.length === 0 || !Array.isArray(qsParamStringArray)) { + return baseUrl + } - const nonEmptyQSParamStrings = qsParamStringArray.filter(qsParamString => qsParamString.trim() !== '') + const nonEmptyQSParamStrings = qsParamStringArray.filter( + (qsParamString) => qsParamString.trim() !== '' + ) if (nonEmptyQSParamStrings.length === 0) return baseUrl @@ -752,7 +800,7 @@ export function getPageUrlFromBidRequest(bidRequest) { try { const url = new URL(paramPageUrl) return url.href - } catch (err) { } + } catch (err) {} } export function hasProtocol(url) { diff --git a/modules/nativoBidAdapter.md b/modules/nativoBidAdapter.md index f83fb45b52e..515d87af28e 100644 --- a/modules/nativoBidAdapter.md +++ b/modules/nativoBidAdapter.md @@ -16,24 +16,91 @@ gulp serve --modules=nativoBidAdapter # Test Parameters +## Banner + +```js +var adUnits = [ + { + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + // Replace this object to test a new Adapter! + bids: [ + { + bidder: 'nativo', + params: { + url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html', + }, + }, + ], + }, +] ``` + +## Video + +```js var adUnits = [ - { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]], - } - }, - // Replace this object to test a new Adapter! - bids: [{ - bidder: 'nativo', - params: { - url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html' - } - }] - - } - ]; + { + code: 'ntvPlaceholder-1', + mediaTypes: { + video: { + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + playbackmethod: [1, 2], + skip: 1, + skipafter: 5, + }, + }, + video: { + divId: 'player', + }, + bids: [ + { + bidder: 'nativo', + params: { + url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html', + }, + }, + ], + }, +] +``` + +## Native +```js +var adUnits = [ + { + code: '/416881364/prebid-native-test-unit', + sizes: [[300, 250]], + mediaTypes: { + native: { + title: { + required: true, + }, + image: { + required: true, + }, + sponsoredBy: { + required: true, + }, + }, + }, + bids: [ + { + bidder: 'nativo', + params: { + url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html', + }, + }, + ], + }, +] ``` diff --git a/modules/naveggIdSystem.js b/modules/naveggIdSystem.js index 42c6b113566..6f1964b11df 100644 --- a/modules/naveggIdSystem.js +++ b/modules/naveggIdSystem.js @@ -6,9 +6,9 @@ */ import { isStr, isPlainObject, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import { ajax } from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -19,61 +19,72 @@ const MODULE_NAME = 'naveggId'; const OLD_NAVEGG_ID = 'nid'; const NAVEGG_ID = 'nvggid'; const BASE_URL = 'https://id.navegg.com/uid/'; -const DEFAULT_EXPIRE = 8 * 24 * 3600 * 1000; -const INVALID_EXPIRE = 3600 * 1000; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); -function getNaveggIdFromApi() { - const callbacks = { - success: response => { - if (response) { - try { - const responseObj = JSON.parse(response); - writeCookie(NAVEGG_ID, responseObj[NAVEGG_ID]); - } catch (error) { - logError(error); +function getIdFromAPI() { + const resp = function (callback) { + ajaxBuilder()( + BASE_URL, + response => { + if (response) { + let responseObj; + try { + responseObj = JSON.parse(response); + } catch (error) { + logError(error); + const fallbackValue = getNaveggIdFromLocalStorage() || getOldCookie(); + callback(fallbackValue); + } + + if (responseObj && responseObj[NAVEGG_ID]) { + callback(responseObj[NAVEGG_ID]); + } else { + const fallbackValue = getNaveggIdFromLocalStorage() || getOldCookie(); + callback(fallbackValue); + } } - } - }, - error: error => { - logError('Navegg ID fetch encountered an error', error); - } + }, + error => { + logError('Navegg ID fetch encountered an error', error); + const fallbackValue = getNaveggIdFromLocalStorage() || getOldCookie(); + callback(fallbackValue); + }, + {method: 'GET', withCredentials: false}); }; - ajax(BASE_URL, callbacks, undefined, { method: 'GET', withCredentials: false }); -} - -function writeCookie(key, value) { - try { - if (storage.cookiesAreEnabled) { - let expTime = new Date(); - const expires = value ? DEFAULT_EXPIRE : INVALID_EXPIRE; - expTime.setTime(expTime.getTime() + expires); - storage.setCookie(key, value, expTime.toUTCString(), 'none'); - } - } catch (e) { - logError(e); - } + return resp; } -function readnaveggIdFromLocalStorage() { - return storage.localStorageIsEnabled ? storage.getDataFromLocalStorage(NAVEGG_ID) : null; +/** + * @returns {string | null} + */ +function readNvgIdFromCookie() { + return storage.cookiesAreEnabled ? (storage.findSimilarCookies('nvg') ? storage.findSimilarCookies('nvg')[0] : null) : null; } - -function readnaveggIDFromCookie() { - return storage.cookiesAreEnabled ? storage.getCookie(NAVEGG_ID) : null; +/** + * @returns {string | null} + */ +function readNavIdFromCookie() { + return storage.cookiesAreEnabled() ? (storage.findSimilarCookies('nav') ? storage.findSimilarCookies('nav')[0] : null) : null; } - -function readoldnaveggIDFromCookie() { - return storage.cookiesAreEnabled ? storage.getCookie(OLD_NAVEGG_ID) : null; +/** + * @returns {string | null} + */ +function readOldNaveggIdFromCookie() { + return storage.cookiesAreEnabled() ? storage.getCookie(OLD_NAVEGG_ID) : null; } - -function readnvgIDFromCookie() { - return storage.cookiesAreEnabled ? (storage.findSimilarCookies('nvg') ? storage.findSimilarCookies('nvg')[0] : null) : null; +/** + * @returns {string | null} + */ +function getOldCookie() { + const oldCookie = readOldNaveggIdFromCookie() || readNvgIdFromCookie() || readNavIdFromCookie(); + return oldCookie; } - -function readnavIDFromCookie() { - return storage.cookiesAreEnabled ? (storage.findSimilarCookies('nav') ? storage.findSimilarCookies('nav')[0] : null) : null; +/** + * @returns {string | null} + */ +function getNaveggIdFromLocalStorage() { + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(NAVEGG_ID) : null; } /** @type {Submodule} */ @@ -95,23 +106,16 @@ export const naveggIdSubmodule = { 'naveggId': naveggIdVal.split('|')[0] } : undefined; }, + /** * performs action to obtain id and return a value in the callback's response argument * @function * @param {SubmoduleConfig} config * @return {{id: string | undefined } | undefined} */ - getId() { - const naveggIdString = readnaveggIdFromLocalStorage() || readnaveggIDFromCookie() || getNaveggIdFromApi() || readoldnaveggIDFromCookie() || readnvgIDFromCookie() || readnavIDFromCookie(); - - if (typeof naveggIdString == 'string' && naveggIdString) { - try { - return { id: naveggIdString }; - } catch (error) { - logError(error); - } - } - return undefined; + getId(config, consentData) { + const resp = getIdFromAPI() + return {callback: resp} }, eids: { 'naveggId': { diff --git a/modules/naveggIdSystem.md b/modules/naveggIdSystem.md index f758fbc9d5d..81d9841c78f 100644 --- a/modules/naveggIdSystem.md +++ b/modules/naveggIdSystem.md @@ -10,6 +10,11 @@ pbjs.setConfig({ userSync: { userIds: [{ name: 'naveggId', + storage: { + name : 'nvggid', + type : 'cookie&html5', + expires: 8 + } }] } }); @@ -20,3 +25,6 @@ The below parameters apply only to the naveggID integration. | Param under usersync.userIds[] | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | name | Required | String | ID of the module - `"naveggId"` | `"naveggId"` | +| storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. | `"nvggid"` | +| storage.type | Required | String | Must be "`cookie`", "`html5`" or "`cookie&html5`". This is where the results of the user ID will be stored. | `"cookie&html5"` | +| storage.expires | Required | Integer | How long (in days) the user ID information will be stored. | `8` | diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js index a2c2249285b..1e2a90987f7 100644 --- a/modules/neuwoRtdProvider.js +++ b/modules/neuwoRtdProvider.js @@ -111,7 +111,7 @@ export function injectTopics(topics, bidsConfig) { logInfo('NeuwoRTDModule', 'injectTopics: post-injection bidsConfig', bidsConfig) } -/* eslint-disable object-property-newline */ + 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', diff --git a/modules/newspassid.md b/modules/newspassid.md deleted file mode 100644 index 6fa709e5ba6..00000000000 --- a/modules/newspassid.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -Module Name: NewspassId Bidder Adapter -Module Type: Bidder Adapter -Maintainer: techsupport@newspassid.com -layout: bidder -title: Newspass ID -description: LMC Newspass ID Prebid JS Bidder Adapter -biddercode: newspassid -gdpr_supported: false -gvl_id: none -usp_supported: true -coppa_supported: false -schain_supported: true -dchain_supported: false -userIds: criteo, id5Id, tdid, identityLink, liveIntentId, parrableId, pubCommonId, lotamePanoramaId, sharedId, fabrickId -media_types: banner -safeframes_ok: true -deals_supported: true -floors_supported: false -fpd_supported: false -pbjs: true -pbs: false -prebid_member: false -multiformat_supported: will-bid-on-any ---- - -### Description - -LMC Newspass ID Prebid JS Bidder Adapter that connects to the NewspassId demand source(s). - -The Newspass bid adapter supports Banner mediaTypes ONLY. -This is intended for USA audiences only, and does not support GDPR - - -### Bid Params - -{: .table .table-bordered .table-striped } - -| Name | Scope | Description | Example | Type | -|-----------|----------|---------------------------|------------|----------| -| `siteId` | required | The site ID. | `"NPID0000001"` | `string` | -| `publisherId` | required | The publisher ID. | `"4204204201"` | `string` | -| `placementId` | required | The placement ID. | `"0420420421"` | `string` | -| `customData` | optional | publisher key-values used for targeting | `[{"settings":{},"targeting":{"key1": "value1", "key2": "value2"}}], ` | `array` | - -### Test Parameters - - -A test ad unit that will consistently return test creatives: - -``` - -//Banner adUnit - -adUnits = [{ - code: 'id-of-your-banner-div', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]] - } - }, - bids: [{ - bidder: 'newspassid', - params: { - publisherId: 'NEWSPASS0001', /* an ID to identify the publisher account - required */ - siteId: '4204204201', /* An ID used to identify a site within a publisher account - required */ - placementId: '8000000015', /* an ID used to identify the piece of inventory - required - for appnexus test use 13144370. */ - customData: [{"settings": {}, "targeting": {"key": "value", "key2": ["value1", "value2"]}}],/* optional array with 'targeting' placeholder for passing publisher specific key-values for targeting. */ - } - }] - }]; -``` - -### Note: - -Please contact us at techsupport@newspassid.com for any assistance testing your implementation before going live into production. diff --git a/modules/newspassidBidAdapter.js b/modules/newspassidBidAdapter.js index 2a4b2da186b..fac9841318d 100644 --- a/modules/newspassidBidAdapter.js +++ b/modules/newspassidBidAdapter.js @@ -1,677 +1,165 @@ -import { - logInfo, - logError, - deepAccess, - logWarn, - deepSetValue, - isArray, - contains, - parseUrl, - generateUUID -} from '../src/utils.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { deepSetValue } from '../src/utils.js'; +import { hasPurpose1Consent } from '../src/utils/gdpr.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE } from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {getPriceBucketString} from '../src/cpmBucketManager.js'; -import {getRefererInfo} from '../src/refererDetection.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + */ + const BIDDER_CODE = 'newspassid'; -const ORIGIN = 'https://bidder.newspassid.com' // applies only to auction & cookie -const AUCTIONURI = '/openrtb2/auction'; -const NEWSPASSCOOKIESYNC = '/static/load-cookie.html'; -const NEWSPASSVERSION = '1.1.4'; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_TTL = 300; +const ENDPOINT_URL = 'https://npid.amspbs.com/v0/bid/request'; +const GVL_ID = 1317; +const SYNC_URL = 'https://npid.amspbs.com/v0/user/sync'; + +const converter = ortbConverter({ + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext.newspassid', { + publisher: resolveNewpassidPublisherId(bidRequest), + placementId: bidRequest.params.placementId, + }) + return imp; + }, + context: { + ttl: DEFAULT_TTL, + netRevenue: DEFAULT_NET_REVENUE + } +}); + +/** + * Helper function to add params to url + * @param {string} url + * @param {object} params + * @returns {string} + */ +const addParamsToUrl = (url, params) => { + const urlObj = new URL(url); + Object.entries(params).forEach(([key, value]) => { + urlObj.searchParams.set(key, value); + }); + return urlObj.toString(); +}; + +/** + * Get the global publisherId for the newspassid bidder + * @returns {string|null} + */ +const getGlobalPublisherIdOrNull = () => { + const globalPublisherId = config.getConfig('newspassid.publisherId'); + if (globalPublisherId) return globalPublisherId; + return null; +}; + +/** + * Resolve the publisherId for the newspassid bidder + * @param {BidRequest|undefined} bidRequest + * @returns {string|null} + */ +export const resolveNewpassidPublisherId = (bidRequest) => { + if (typeof bidRequest !== 'object') return getGlobalPublisherIdOrNull(); + + // get publisherId from bidRequest params + const { params } = bidRequest; + if (params?.publisherId) return params?.publisherId; + + return getGlobalPublisherIdOrNull(); +}; + +/** + * @type {BidderSpec} + */ export const spec = { - version: NEWSPASSVERSION, code: BIDDER_CODE, - supportedMediaTypes: [BANNER], - cookieSyncBag: {publisherId: null, siteId: null, userIdObject: {}}, // variables we want to make available to cookie sync - propertyBag: {config: null, pageId: null, buildRequestsStart: 0, buildRequestsEnd: 0, endpointOverride: null}, /* allow us to store vars in instance scope - needs to be an object to be mutable */ - config_defaults: { - 'logId': 'NEWSPASSID', - 'bidder': 'newspassid', - 'auctionUrl': ORIGIN + AUCTIONURI, - 'cookieSyncUrl': ORIGIN + NEWSPASSCOOKIESYNC - }, - loadConfiguredData(bid) { - if (this.propertyBag.config) { return; } - this.propertyBag.config = JSON.parse(JSON.stringify(this.config_defaults)); - let bidder = bid.bidder || 'newspassid'; - this.propertyBag.config.logId = bidder.toUpperCase(); - this.propertyBag.config.bidder = bidder; - let bidderConfig = config.getConfig(bidder) || {}; - logInfo('got bidderConfig: ', JSON.parse(JSON.stringify(bidderConfig))); - let arrGetParams = this.getGetParametersAsObject(); - if (bidderConfig.endpointOverride) { - if (bidderConfig.endpointOverride.origin) { - this.propertyBag.endpointOverride = bidderConfig.endpointOverride.origin; - this.propertyBag.config.auctionUrl = bidderConfig.endpointOverride.origin + AUCTIONURI; - this.propertyBag.config.cookieSyncUrl = bidderConfig.endpointOverride.origin + NEWSPASSCOOKIESYNC; - } - if (bidderConfig.endpointOverride.cookieSyncUrl) { - this.propertyBag.config.cookieSyncUrl = bidderConfig.endpointOverride.cookieSyncUrl; - } - if (bidderConfig.endpointOverride.auctionUrl) { - this.propertyBag.endpointOverride = bidderConfig.endpointOverride.auctionUrl; - this.propertyBag.config.auctionUrl = bidderConfig.endpointOverride.auctionUrl; - } - } - try { - if (arrGetParams.hasOwnProperty('auction')) { - logInfo('GET: setting auction endpoint to: ' + arrGetParams.auction); - this.propertyBag.config.auctionUrl = arrGetParams.auction; - } - if (arrGetParams.hasOwnProperty('cookiesync')) { - logInfo('GET: setting cookiesync to: ' + arrGetParams.cookiesync); - this.propertyBag.config.cookieSyncUrl = arrGetParams.cookiesync; - } - } catch (e) {} - logInfo('set propertyBag.config to', this.propertyBag.config); - }, - getAuctionUrl() { - return this.propertyBag.config.auctionUrl; - }, - getCookieSyncUrl() { - return this.propertyBag.config.cookieSyncUrl; - }, - isBidRequestValid(bid) { - this.loadConfiguredData(bid); - logInfo('isBidRequestValid : ', config.getConfig(), bid); - let adUnitCode = bid.adUnitCode; // adunit[n].code - let err1 = 'VALIDATION FAILED : missing {param} : siteId, placementId and publisherId are REQUIRED'; - if (!(bid.params.hasOwnProperty('placementId'))) { - logError(err1.replace('{param}', 'placementId'), adUnitCode); - return false; - } - if (!this.isValidPlacementId(bid.params.placementId)) { - logError('VALIDATION FAILED : placementId must be exactly 10 numeric characters', adUnitCode); - return false; - } - if (!(bid.params.hasOwnProperty('publisherId'))) { - logError(err1.replace('{param}', 'publisherId'), adUnitCode); - return false; - } - if (!(bid.params.publisherId).toString().match(/^[a-zA-Z0-9\-]{12}$/)) { - logError('VALIDATION FAILED : publisherId must be exactly 12 alphanumeric characters including hyphens', adUnitCode); - return false; - } - if (!(bid.params.hasOwnProperty('siteId'))) { - logError(err1.replace('{param}', 'siteId'), adUnitCode); - return false; - } - if (!(bid.params.siteId).toString().match(/^[0-9]{10}$/)) { - logError('VALIDATION FAILED : siteId must be exactly 10 numeric characters', adUnitCode); - return false; - } - if (bid.params.hasOwnProperty('customParams')) { - logError('VALIDATION FAILED : customParams should be renamed to customData', adUnitCode); - return false; - } - if (bid.params.hasOwnProperty('customData')) { - if (!Array.isArray(bid.params.customData)) { - logError('VALIDATION FAILED : customData is not an Array', adUnitCode); - return false; - } - if (bid.params.customData.length < 1) { - logError('VALIDATION FAILED : customData is an array but does not contain any elements', adUnitCode); - return false; - } - if (!(bid.params.customData[0]).hasOwnProperty('targeting')) { - logError('VALIDATION FAILED : customData[0] does not contain "targeting"', adUnitCode); - return false; - } - if (typeof bid.params.customData[0]['targeting'] != 'object') { - logError('VALIDATION FAILED : customData[0] targeting is not an object', adUnitCode); - return false; - } - } - return true; - }, - isValidPlacementId(placementId) { - return placementId.toString().match(/^[0-9]{10}$/); - }, - buildRequests(validBidRequests, bidderRequest) { - this.loadConfiguredData(validBidRequests[0]); - this.propertyBag.buildRequestsStart = new Date().getTime(); - logInfo(`buildRequests time: ${this.propertyBag.buildRequestsStart} v ${NEWSPASSVERSION} validBidRequests`, JSON.parse(JSON.stringify(validBidRequests)), 'bidderRequest', JSON.parse(JSON.stringify(bidderRequest))); - if (this.blockTheRequest()) { - return []; - } - let htmlParams = {'publisherId': '', 'siteId': ''}; - if (validBidRequests.length > 0) { - this.cookieSyncBag.userIdObject = Object.assign(this.cookieSyncBag.userIdObject, this.findAllUserIds(validBidRequests[0])); - this.cookieSyncBag.siteId = deepAccess(validBidRequests[0], 'params.siteId'); - this.cookieSyncBag.publisherId = deepAccess(validBidRequests[0], 'params.publisherId'); - htmlParams = validBidRequests[0].params; - } - logInfo('cookie sync bag', this.cookieSyncBag); - let singleRequest = config.getConfig('newspassid.singleRequest'); - singleRequest = singleRequest !== false; // undefined & true will be true - logInfo(`config newspassid.singleRequest : `, singleRequest); - let npRequest = {}; // we only want to set specific properties on this, not validBidRequests[0].params - logInfo('going to get ortb2 from bidder request...'); - let fpd = deepAccess(bidderRequest, 'ortb2', null); - logInfo('got fpd: ', fpd); - if (fpd && deepAccess(fpd, 'user')) { - logInfo('added FPD user object'); - npRequest.user = fpd.user; - } - const getParams = this.getGetParametersAsObject(); - const isTestMode = getParams['nptestmode'] || null; // this can be any string, it's used for testing ads - npRequest.device = {'w': window.innerWidth, 'h': window.innerHeight}; - let placementIdOverrideFromGetParam = this.getPlacementIdOverrideFromGetParam(); // null or string - let schain = null; - let tosendtags = validBidRequests.map(npBidRequest => { - var obj = {}; - let placementId = placementIdOverrideFromGetParam || this.getPlacementId(npBidRequest); // prefer to use a valid override param, else the bidRequest placement Id - obj.id = npBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder newspass made bid for unknown request ID: mb7953.859498327448. Ignoring." - obj.tagid = placementId; - let parsed = parseUrl(this.getRefererInfo().page); - obj.secure = parsed.protocol === 'https' ? 1 : 0; - let arrBannerSizes = []; - if (!npBidRequest.hasOwnProperty('mediaTypes')) { - if (npBidRequest.hasOwnProperty('sizes')) { - logInfo('no mediaTypes detected - will use the sizes array in the config root'); - arrBannerSizes = npBidRequest.sizes; - } else { - logInfo('Cannot set sizes for banner type'); - } - } else { - if (npBidRequest.mediaTypes.hasOwnProperty(BANNER)) { - arrBannerSizes = npBidRequest.mediaTypes[BANNER].sizes; /* Note - if there is a sizes element in the config root it will be pushed into here */ - logInfo('setting banner size from the mediaTypes.banner element for bidId ' + obj.id + ': ', arrBannerSizes); - } - if (npBidRequest.mediaTypes.hasOwnProperty(NATIVE)) { - obj.native = npBidRequest.mediaTypes[NATIVE]; - logInfo('setting native object from the mediaTypes.native element: ' + obj.id + ':', obj.native); - } - } - if (arrBannerSizes.length > 0) { - obj.banner = { - topframe: 1, - w: arrBannerSizes[0][0] || 0, - h: arrBannerSizes[0][1] || 0, - format: arrBannerSizes.map(s => { - return {w: s[0], h: s[1]}; - }) - }; - } - obj.placementId = placementId; - deepSetValue(obj, 'ext.prebid', {'storedrequest': {'id': placementId}}); - obj.ext['newspassid'] = {}; - obj.ext['newspassid'].adUnitCode = npBidRequest.adUnitCode; // eg. 'mpu' - if (npBidRequest.params.hasOwnProperty('customData')) { - obj.ext['newspassid'].customData = npBidRequest.params.customData; - } - logInfo(`obj.ext.newspassid is `, obj.ext['newspassid']); - if (isTestMode != null) { - logInfo('setting isTestMode to ', isTestMode); - if (obj.ext['newspassid'].hasOwnProperty('customData')) { - for (let i = 0; i < obj.ext['newspassid'].customData.length; i++) { - obj.ext['newspassid'].customData[i]['targeting']['nptestmode'] = isTestMode; - } - } else { - obj.ext['newspassid'].customData = [{'settings': {}, 'targeting': {}}]; - obj.ext['newspassid'].customData[0].targeting['nptestmode'] = isTestMode; + gvlid: GVL_ID, + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + + isBidRequestValid: function(bidRequest) { + const publisherId = resolveNewpassidPublisherId(bidRequest); + return !!(bidRequest.params && publisherId && bidRequest.params.placementId); + }, + + buildRequests: function(bidRequests, bidderRequest) { + // convert to ortb using the converter utility + const data = converter.toORTB({ bidRequests, bidderRequest }); + + return [ + { + method: 'POST', + url: ENDPOINT_URL, + data: data, + options: { + withCredentials: true } } - if (fpd && deepAccess(fpd, 'site')) { - logInfo('adding fpd.site'); - if (deepAccess(obj, 'ext.newspassid.customData.0.targeting', false)) { - obj.ext.newspassid.customData[0].targeting = Object.assign(obj.ext.newspassid.customData[0].targeting, fpd.site); - } else { - deepSetValue(obj, 'ext.newspassid.customData.0.targeting', fpd.site); + ]; + }, + + interpretResponse: function(serverResponse, bidRequest) { + const response = serverResponse.body; + const bidResponses = []; + + if (!response || !response.seatbid || !response.seatbid[0].bid) { + return bidResponses; + } + + response.seatbid[0].bid.forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + currency: response.cur || DEFAULT_CURRENCY, + netRevenue: true, + ttl: DEFAULT_TTL, + ad: bid.adm, + meta: { + advertiserDomains: bid.adomain || [], } - } - if (!schain && deepAccess(npBidRequest, 'schain')) { - schain = npBidRequest.schain; - } - let gpid = deepAccess(npBidRequest, 'ortb2Imp.ext.gpid'); - if (gpid) { - deepSetValue(obj, 'ext.gpid', gpid); - } - return obj; + }); }); - let extObj = {}; - extObj['newspassid'] = {}; - extObj['newspassid']['np_pb_v'] = NEWSPASSVERSION; - extObj['newspassid']['np_rw'] = placementIdOverrideFromGetParam ? 1 : 0; - if (validBidRequests.length > 0) { - let userIds = this.cookieSyncBag.userIdObject; // 2021-01-06 - slight optimisation - we've already found this info - if (userIds.hasOwnProperty('pubcid')) { - extObj['newspassid'].pubcid = userIds.pubcid; - } - } - extObj['newspassid'].pv = this.getPageId(); // attach the page ID that will be common to all auction calls for this page if refresh() is called - let whitelistAdserverKeys = config.getConfig('newspassid.np_whitelist_adserver_keys'); - let useWhitelistAdserverKeys = isArray(whitelistAdserverKeys) && whitelistAdserverKeys.length > 0; - extObj['newspassid']['np_kvp_rw'] = useWhitelistAdserverKeys ? 1 : 0; - if (getParams.hasOwnProperty('npf')) { extObj['newspassid']['npf'] = getParams.npf === 'true' || getParams.npf === '1' ? 1 : 0; } - if (getParams.hasOwnProperty('nppf')) { extObj['newspassid']['nppf'] = getParams.nppf === 'true' || getParams.nppf === '1' ? 1 : 0; } - if (getParams.hasOwnProperty('nprp') && getParams.nprp.match(/^[0-3]$/)) { extObj['newspassid']['nprp'] = parseInt(getParams.nprp); } - if (getParams.hasOwnProperty('npip') && getParams.npip.match(/^\d+$/)) { extObj['newspassid']['npip'] = parseInt(getParams.npip); } - if (this.propertyBag.endpointOverride != null) { extObj['newspassid']['origin'] = this.propertyBag.endpointOverride; } - let userExtEids = deepAccess(validBidRequests, '0.userIdAsEids', []); // generate the UserIDs in the correct format for UserId module - npRequest.site = { - 'publisher': {'id': htmlParams.publisherId}, - 'page': this.getRefererInfo().page, - 'id': htmlParams.siteId + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + if (!syncOptions.iframeEnabled) return []; // disable if iframe sync is disabled + if (!hasPurpose1Consent(gdprConsent)) return []; // disable if no purpose1 consent + if (config.getConfig('coppa') === true) return []; // disable syncs for coppa + + const params = { + gdpr: gdprConsent?.gdprApplies ? 1 : 0, + gdpr_consent: gdprConsent?.gdprApplies + ? encodeURIComponent(gdprConsent?.consentString || '') + : '', + gpp: encodeURIComponent(gppConsent?.gppString || ''), + gpp_sid: encodeURIComponent(gppConsent?.applicableSections || ''), + us_privacy: encodeURIComponent(uspConsent || ''), }; - npRequest.test = config.getConfig('debug') ? 1 : 0; - if (bidderRequest && bidderRequest.uspConsent) { - logInfo('ADDING USP consent info'); - deepSetValue(npRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } else { - logInfo('WILL NOT ADD USP consent info; no bidderRequest.uspConsent.'); - } - if (schain) { // we set this while iterating over the bids - logInfo('schain found'); - deepSetValue(npRequest, 'source.ext.schain', schain); - } - if (config.getConfig('coppa') === true) { - deepSetValue(npRequest, 'regs.coppa', 1); - } - if (singleRequest) { - logInfo('buildRequests starting to generate response for a single request'); - npRequest.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) - npRequest.imp = tosendtags; - npRequest.ext = extObj; - deepSetValue(npRequest, 'user.ext.eids', userExtEids); - var ret = { - method: 'POST', - url: this.getAuctionUrl(), - data: JSON.stringify(npRequest), - bidderRequest: bidderRequest - }; - logInfo('buildRequests request data for single = ', JSON.parse(JSON.stringify(npRequest))); - this.propertyBag.buildRequestsEnd = new Date().getTime(); - logInfo(`buildRequests going to return for single at time ${this.propertyBag.buildRequestsEnd} (took ${this.propertyBag.buildRequestsEnd - this.propertyBag.buildRequestsStart}ms): `, ret); - return ret; - } - let arrRet = tosendtags.map(imp => { - logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); - let npRequestSingle = Object.assign({}, npRequest); - npRequestSingle.id = generateUUID(); - npRequestSingle.imp = [imp]; - npRequestSingle.ext = extObj; - deepSetValue(npRequestSingle, 'user.ext.eids', userExtEids); - logInfo('buildRequests RequestSingle (for non-single) = ', npRequestSingle); - return { - method: 'POST', - url: this.getAuctionUrl(), - data: JSON.stringify(npRequestSingle), - bidderRequest: bidderRequest - }; + + const globalPublisherId = resolveNewpassidPublisherId({}); + if (globalPublisherId) { + // "publisher" is a convention on the server side + params.publisher = globalPublisherId; + } + + let syncs = []; + + // iframe sync + syncs.push({ + type: 'iframe', + url: addParamsToUrl(SYNC_URL, params), }); - this.propertyBag.buildRequestsEnd = new Date().getTime(); - logInfo(`buildRequests going to return for non-single at time ${this.propertyBag.buildRequestsEnd} (took ${this.propertyBag.buildRequestsEnd - this.propertyBag.buildRequestsStart}ms): `, arrRet); - return arrRet; - }, - interpretResponse(serverResponse, request) { - if (request && request.bidderRequest && request.bidderRequest.bids) { this.loadConfiguredData(request.bidderRequest.bids[0]); } - let startTime = new Date().getTime(); - logInfo(`interpretResponse time: ${startTime}. buildRequests done -> interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); - logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); - serverResponse = serverResponse.body || {}; - let aucId = serverResponse.id; // this will be correct for single requests and non-single - if (!serverResponse.hasOwnProperty('seatbid')) { - return []; - } - if (typeof serverResponse.seatbid !== 'object') { - return []; - } - let arrAllBids = []; - let enhancedAdserverTargeting = config.getConfig('newspassid.enhancedAdserverTargeting'); - logInfo('enhancedAdserverTargeting', enhancedAdserverTargeting); - if (typeof enhancedAdserverTargeting == 'undefined') { - enhancedAdserverTargeting = true; - } - logInfo('enhancedAdserverTargeting', enhancedAdserverTargeting); - serverResponse.seatbid = injectAdIdsIntoAllBidResponses(serverResponse.seatbid); // we now make sure that each bid in the bidresponse has a unique (within page) adId attribute. - serverResponse.seatbid = this.removeSingleBidderMultipleBids(serverResponse.seatbid); - let whitelistAdserverKeys = config.getConfig('newspassid.np_whitelist_adserver_keys'); - let useWhitelistAdserverKeys = isArray(whitelistAdserverKeys) && whitelistAdserverKeys.length > 0; - for (let i = 0; i < serverResponse.seatbid.length; i++) { - let sb = serverResponse.seatbid[i]; - for (let j = 0; j < sb.bid.length; j++) { - let thisRequestBid = this.getBidRequestForBidId(sb.bid[j].impid, request.bidderRequest.bids); - logInfo(`seatbid:${i}, bid:${j} Going to set default w h for seatbid/bidRequest`, sb.bid[j], thisRequestBid); - const {defaultWidth, defaultHeight} = defaultSize(thisRequestBid); - let thisBid = this.addStandardProperties(sb.bid[j], defaultWidth, defaultHeight); - thisBid.meta = {advertiserDomains: thisBid.adomain || []}; - let bidType = deepAccess(thisBid, 'ext.prebid.type'); - logInfo(`this bid type is : ${bidType}`, j); - let adserverTargeting = {}; - if (enhancedAdserverTargeting) { - let allBidsForThisBidid = this.getAllBidsForBidId(thisBid.bidId, serverResponse.seatbid); - logInfo('Going to iterate allBidsForThisBidId', allBidsForThisBidid); - Object.keys(allBidsForThisBidid).forEach((bidderName, index, ar2) => { - logInfo(`adding adserverTargeting for ${bidderName} for bidId ${thisBid.bidId}`); - adserverTargeting['np_' + bidderName] = bidderName; - adserverTargeting['np_' + bidderName + '_crid'] = String(allBidsForThisBidid[bidderName].crid); - adserverTargeting['np_' + bidderName + '_adv'] = String(allBidsForThisBidid[bidderName].adomain); - adserverTargeting['np_' + bidderName + '_adId'] = String(allBidsForThisBidid[bidderName].adId); - adserverTargeting['np_' + bidderName + '_pb_r'] = getRoundedBid(allBidsForThisBidid[bidderName].price, allBidsForThisBidid[bidderName].ext.prebid.type); - if (allBidsForThisBidid[bidderName].hasOwnProperty('dealid')) { - adserverTargeting['np_' + bidderName + '_dealid'] = String(allBidsForThisBidid[bidderName].dealid); - } - }); - } else { - logInfo(`newspassid.enhancedAdserverTargeting is set to false, no per-bid keys will be sent to adserver.`); - } - let {seat: winningSeat, bid: winningBid} = this.getWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); - adserverTargeting['np_auc_id'] = String(aucId); - adserverTargeting['np_winner'] = String(winningSeat); - adserverTargeting['np_bid'] = 'true'; - if (enhancedAdserverTargeting) { - adserverTargeting['np_imp_id'] = String(winningBid.impid); - adserverTargeting['np_pb_r'] = getRoundedBid(winningBid.price, bidType); - adserverTargeting['np_adId'] = String(winningBid.adId); - adserverTargeting['np_size'] = `${winningBid.width}x${winningBid.height}`; - } - if (useWhitelistAdserverKeys) { // delete any un-whitelisted keys - logInfo('Going to filter out adserver targeting keys not in the whitelist: ', whitelistAdserverKeys); - Object.keys(adserverTargeting).forEach(function(key) { if (whitelistAdserverKeys.indexOf(key) === -1) { delete adserverTargeting[key]; } }); - } - thisBid.adserverTargeting = adserverTargeting; - arrAllBids.push(thisBid); - } - } - let endTime = new Date().getTime(); - logInfo(`interpretResponse going to return at time ${endTime} (took ${endTime - startTime}ms) Time from buildRequests Start -> interpretRequests End = ${endTime - this.propertyBag.buildRequestsStart}ms`, arrAllBids); - return arrAllBids; - }, - removeSingleBidderMultipleBids(seatbid) { - var ret = []; - for (let i = 0; i < seatbid.length; i++) { - let sb = seatbid[i]; - var retSeatbid = {'seat': sb.seat, 'bid': []}; - var bidIds = []; - for (let j = 0; j < sb.bid.length; j++) { - var candidate = sb.bid[j]; - if (contains(bidIds, candidate.impid)) { - continue; // we've already fully assessed this impid, found the highest bid from this seat for it - } - bidIds.push(candidate.impid); - for (let k = j + 1; k < sb.bid.length; k++) { - if (sb.bid[k].impid === candidate.impid && sb.bid[k].price > candidate.price) { - candidate = sb.bid[k]; - } - } - retSeatbid.bid.push(candidate); - } - ret.push(retSeatbid); - } - return ret; - }, - getUserSyncs(optionsType, serverResponse, gdprConsent, usPrivacy) { - logInfo('getUserSyncs optionsType', optionsType, 'serverResponse', serverResponse, 'usPrivacy', usPrivacy, 'cookieSyncBag', this.cookieSyncBag); - if (!serverResponse || serverResponse.length === 0) { - return []; - } - if (optionsType.iframeEnabled) { - var arrQueryString = []; - if (config.getConfig('debug')) { - arrQueryString.push('pbjs_debug=true'); - } - arrQueryString.push('usp_consent=' + (usPrivacy || '')); - for (let keyname in this.cookieSyncBag.userIdObject) { - arrQueryString.push(keyname + '=' + this.cookieSyncBag.userIdObject[keyname]); - } - arrQueryString.push('publisherId=' + this.cookieSyncBag.publisherId); - arrQueryString.push('siteId=' + this.cookieSyncBag.siteId); - arrQueryString.push('cb=' + Date.now()); - arrQueryString.push('bidder=' + this.propertyBag.config.bidder); - var strQueryString = arrQueryString.join('&'); - if (strQueryString.length > 0) { - strQueryString = '?' + strQueryString; - } - logInfo('getUserSyncs going to return cookie sync url : ' + this.getCookieSyncUrl() + strQueryString); - return [{ - type: 'iframe', - url: this.getCookieSyncUrl() + strQueryString - }]; - } - }, - getBidRequestForBidId(bidId, arrBids) { - for (let i = 0; i < arrBids.length; i++) { - if (arrBids[i].bidId === bidId) { // bidId in the request comes back as impid in the seatbid bids - return arrBids[i]; - } - } - return null; - }, - findAllUserIds(bidRequest) { - var ret = {}; - let searchKeysSingle = ['pubcid', 'tdid', 'idl_env', 'criteoId', 'lotamePanoramaId', 'fabrickId']; - if (bidRequest.hasOwnProperty('userId')) { - for (let arrayId in searchKeysSingle) { - let key = searchKeysSingle[arrayId]; - if (bidRequest.userId.hasOwnProperty(key)) { - if (typeof (bidRequest.userId[key]) == 'string') { - ret[key] = bidRequest.userId[key]; - } else if (typeof (bidRequest.userId[key]) == 'object') { - logError(`WARNING: findAllUserIds had to use first key in user object to get value for bid.userId key: ${key}. Prebid adapter should be updated.`); - ret[key] = bidRequest.userId[key][Object.keys(bidRequest.userId[key])[0]]; // cannot use Object.values - } else { - logError(`failed to get string key value for userId : ${key}`); - } - } - } - let lipbid = deepAccess(bidRequest.userId, 'lipb.lipbid'); - if (lipbid) { - ret['lipb'] = {'lipbid': lipbid}; - } - let id5id = deepAccess(bidRequest.userId, 'id5id.uid'); - if (id5id) { - ret['id5id'] = id5id; - } - let parrableId = deepAccess(bidRequest.userId, 'parrableId.eid'); - if (parrableId) { - ret['parrableId'] = parrableId; - } - let sharedid = deepAccess(bidRequest.userId, 'sharedid.id'); - if (sharedid) { - ret['sharedid'] = sharedid; - } - } - if (!ret.hasOwnProperty('pubcid')) { - let pubcid = deepAccess(bidRequest, 'crumbs.pubcid'); - if (pubcid) { - ret['pubcid'] = pubcid; // if built with old pubCommonId module - } - } - return ret; - }, - getPlacementId(bidRequest) { - return (bidRequest.params.placementId).toString(); - }, - getPlacementIdOverrideFromGetParam() { - let arr = this.getGetParametersAsObject(); - if (arr.hasOwnProperty('npstoredrequest')) { - if (this.isValidPlacementId(arr['npstoredrequest'])) { - logInfo(`using GET npstoredrequest ` + arr['npstoredrequest'] + ' to replace placementId'); - return arr['npstoredrequest']; - } else { - logError(`GET npstoredrequest FAILED VALIDATION - will not use it`); - } - } - return null; - }, - getGetParametersAsObject() { - let parsed = parseUrl(this.getRefererInfo().location); // was getRefererInfo().page but this is not backwards compatible - logInfo('getGetParametersAsObject found:', parsed.search); - return parsed.search; - }, - getRefererInfo() { - if (getRefererInfo().hasOwnProperty('location')) { - logInfo('FOUND location on getRefererInfo OK (prebid >= 7); will use getRefererInfo for location & page'); - return getRefererInfo(); - } else { - logInfo('DID NOT FIND location on getRefererInfo (prebid < 7); will use legacy code that ALWAYS worked reliably to get location & page ;-)'); - try { - return { - page: top.location.href, - location: top.location.href - }; - } catch (e) { - return { - page: window.location.href, - location: window.location.href - }; - } - } - }, - blockTheRequest() { - let npRequest = config.getConfig('newspassid.np_request'); - if (typeof npRequest == 'boolean' && !npRequest) { - logWarn(`Will not allow auction : np_request is set to false`); - return true; - } - return false; - }, - getPageId: function() { - if (this.propertyBag.pageId == null) { - let randPart = ''; - let allowable = '0123456789abcdefghijklmnopqrstuvwxyz'; - for (let i = 20; i > 0; i--) { - randPart += allowable[Math.floor(Math.random() * 36)]; - } - this.propertyBag.pageId = new Date().getTime() + '_' + randPart; - } - return this.propertyBag.pageId; - }, - addStandardProperties(seatBid, defaultWidth, defaultHeight) { - seatBid.cpm = seatBid.price; - seatBid.bidId = seatBid.impid; - seatBid.requestId = seatBid.impid; - seatBid.width = seatBid.w || defaultWidth; - seatBid.height = seatBid.h || defaultHeight; - seatBid.ad = seatBid.adm; - seatBid.netRevenue = true; - seatBid.creativeId = seatBid.crid; - seatBid.currency = 'USD'; - seatBid.ttl = 300; - return seatBid; - }, - getWinnerForRequestBid(requestBidId, serverResponseSeatBid) { - let thisBidWinner = null; - let winningSeat = null; - for (let j = 0; j < serverResponseSeatBid.length; j++) { - let theseBids = serverResponseSeatBid[j].bid; - let thisSeat = serverResponseSeatBid[j].seat; - for (let k = 0; k < theseBids.length; k++) { - if (theseBids[k].impid === requestBidId) { - if ((thisBidWinner == null) || (thisBidWinner.price < theseBids[k].price)) { - thisBidWinner = theseBids[k]; - winningSeat = thisSeat; - break; - } - } - } - } - return {'seat': winningSeat, 'bid': thisBidWinner}; - }, - getAllBidsForBidId(matchBidId, serverResponseSeatBid) { - let objBids = {}; - for (let j = 0; j < serverResponseSeatBid.length; j++) { - let theseBids = serverResponseSeatBid[j].bid; - let thisSeat = serverResponseSeatBid[j].seat; - for (let k = 0; k < theseBids.length; k++) { - if (theseBids[k].impid === matchBidId) { - if (objBids.hasOwnProperty(thisSeat)) { // > 1 bid for an adunit from a bidder - only use the one with the highest bid - if (objBids[thisSeat]['price'] < theseBids[k].price) { - objBids[thisSeat] = theseBids[k]; - } - } else { - objBids[thisSeat] = theseBids[k]; - } - } - } - } - return objBids; + + return syncs; } }; -export function injectAdIdsIntoAllBidResponses(seatbid) { - logInfo('injectAdIdsIntoAllBidResponses', seatbid); - for (let i = 0; i < seatbid.length; i++) { - let sb = seatbid[i]; - for (let j = 0; j < sb.bid.length; j++) { - sb.bid[j]['adId'] = `${sb.bid[j]['impid']}-${i}-np-${j}`; - } - } - return seatbid; -} -export function checkDeepArray(Arr) { - if (Array.isArray(Arr)) { - if (Array.isArray(Arr[0])) { - return Arr[0]; - } else { - return Arr; - } - } else { - return Arr; - } -} -export function defaultSize(thebidObj) { - if (!thebidObj) { - logInfo('defaultSize received empty bid obj! going to return fixed default size'); - return { - 'defaultHeight': 250, - 'defaultWidth': 300 - }; - } - const {sizes} = thebidObj; - const returnObject = {}; - returnObject.defaultWidth = checkDeepArray(sizes)[0]; - returnObject.defaultHeight = checkDeepArray(sizes)[1]; - return returnObject; -} -export function getRoundedBid(price, mediaType) { - const mediaTypeGranularity = config.getConfig(`mediaTypePriceGranularity.${mediaType}`); // might be string or object or nothing; if set then this takes precedence over 'priceGranularity' - let objBuckets = config.getConfig('customPriceBucket'); // this is always an object - {} if strBuckets is not 'custom' - let strBuckets = config.getConfig('priceGranularity'); // priceGranularity value, always a string ** if priceGranularity is set to an object then it's always 'custom' ** - let theConfigObject = getGranularityObject(mediaType, mediaTypeGranularity, strBuckets, objBuckets); - let theConfigKey = getGranularityKeyName(mediaType, mediaTypeGranularity, strBuckets); - logInfo('getRoundedBid. price:', price, 'mediaType:', mediaType, 'configkey:', theConfigKey, 'configObject:', theConfigObject, 'mediaTypeGranularity:', mediaTypeGranularity, 'strBuckets:', strBuckets); - let priceStringsObj = getPriceBucketString( - price, - theConfigObject, - config.getConfig('currency.granularityMultiplier') - ); - logInfo('priceStringsObj', priceStringsObj); - let granularityNamePriceStringsKeyMapping = { - 'medium': 'med', - 'custom': 'custom', - 'high': 'high', - 'low': 'low', - 'dense': 'dense' - }; - if (granularityNamePriceStringsKeyMapping.hasOwnProperty(theConfigKey)) { - let priceStringsKey = granularityNamePriceStringsKeyMapping[theConfigKey]; - logInfo('getRoundedBid: looking for priceStringsKey:', priceStringsKey); - return priceStringsObj[priceStringsKey]; - } - return priceStringsObj['auto']; -} -export function getGranularityKeyName(mediaType, mediaTypeGranularity, strBuckets) { - if (typeof mediaTypeGranularity === 'string') { - return mediaTypeGranularity; - } - if (typeof mediaTypeGranularity === 'object') { - return 'custom'; - } - if (typeof strBuckets === 'string') { - return strBuckets; - } - return 'auto'; // fall back to a default key - should literally never be needed. -} -export function getGranularityObject(mediaType, mediaTypeGranularity, strBuckets, objBuckets) { - if (typeof mediaTypeGranularity === 'object') { - return mediaTypeGranularity; - } - if (strBuckets === 'custom') { - return objBuckets; - } - return ''; -} + registerBidder(spec); -logInfo(`*BidAdapter ${NEWSPASSVERSION} was loaded`); diff --git a/modules/newspassidBidAdapter.md b/modules/newspassidBidAdapter.md new file mode 100644 index 00000000000..aff1d902c3e --- /dev/null +++ b/modules/newspassidBidAdapter.md @@ -0,0 +1,47 @@ +Overview +======== + +``` +Module Name: NewsPassID Bid Adapter +Module Type: Bidder Adapter +Maintainer: techsupport@newspassid.com +``` + +Description +=========== + +Bid adapter to connect to Local Media Consortium's NewsPassID (NPID) demand source(s). This adapter runs bid requests through ad server technology built and maintained by Aditude. + +# Bid Parameters + +| Key | Required | Example | Description | +| --- | -------- | ------- | ----------- | +| `publisherId` | yes | `"123456"` | For associating the publisher account for the NewsPassID initiative | +| `placementId` | yes | `"leftrail-mobile-1"` | For associating the ad placement inventory with demand. This ID must be predefined by NewsPassID provider | + + +# Test Parameters + +```javascript +pbjs.setConfig({ + newspassid: { + publisherId: '123456', + } +}); + +var adUnits = [ + { + code: 'newspass-test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: 'newspassid', + params: { + publisherId: '123456', // optional if you set in bidder config + placementId: 'test-group1' + } + } + ] + } +] +``` diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index 65f530d9e58..bbc51e0914b 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -1,10 +1,10 @@ import { _each, - createTrackPixelHtml, deepAccess, deepSetValue, getBidIdParameter, getDefinedParams, + getWinDimensions, getWindowTop, isArray, isStr, @@ -13,15 +13,18 @@ import { triggerPixel, } from '../src/utils.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import {getAd} from '../libraries/targetVideoUtils/bidderUtils.js'; + import { EVENTS } from '../src/constants.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; -const NM_VERSION = '3.1.0'; +const NM_VERSION = '4.3.0'; +const PBJS_VERSION = 'v$prebid.version$'; const GVLID = 1060; const BIDDER_CODE = 'nextMillennium'; const ENDPOINT = 'https://pbs.nextmillmedia.com/openrtb2/auction'; @@ -30,6 +33,7 @@ const SYNC_ENDPOINT = 'https://cookies.nextmillmedia.com/sync?gdpr={{.GDPR}}&gdp const REPORT_ENDPOINT = 'https://report2.hb.brainlyads.com/statistics/metric'; const TIME_TO_LIVE = 360; const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_TMAX = 1500; const VIDEO_PARAMS_DEFAULT = { api: undefined, @@ -65,6 +69,10 @@ const ALLOWED_ORTB2_PARAMETERS = [ 'site.keywords', 'site.content.keywords', 'user.keywords', + 'bcat', + 'badv', + 'wlang', + 'wlangb', ]; export const spec = { @@ -74,91 +82,76 @@ export const spec = { isBidRequestValid: function(bid) { return !!( - (bid.params.placement_id && isStr(bid.params.placement_id)) || (bid.params.group_id && isStr(bid.params.group_id)) + (bid.params.placement_id && isStr(bid.params.placement_id)) || + (bid.params.group_id && isStr(bid.params.group_id)) ); }, buildRequests: function(validBidRequests, bidderRequest) { const requests = []; window.nmmRefreshCounts = window.nmmRefreshCounts || {}; + const site = getSiteObj(); + const device = getDeviceObj(); + const source = getSourceObj(validBidRequests, bidderRequest); + const tmax = deepAccess(bidderRequest, 'timeout') || DEFAULT_TMAX; + + const postBody = { + id: bidderRequest?.bidderRequestId, + tmax, + ext: { + next_mil_imps: [], + }, - _each(validBidRequests, (bid) => { - window.nmmRefreshCounts[bid.adUnitCode] = window.nmmRefreshCounts[bid.adUnitCode] || 0; - const id = getPlacementId(bid); - const auctionId = bid.auctionId; - const bidId = bid.bidId; - - const site = getSiteObj(); - const device = getDeviceObj(); - const {cur, mediaTypes} = getCurrency(bid); - - const postBody = { - id: bidderRequest?.bidderRequestId, - cur, - ext: { - prebid: { - storedrequest: { - id, - }, - }, + device, + site, + source, + imp: [], + }; - nextMillennium: { - nm_version: NM_VERSION, - pbjs_version: getGlobal()?.version || undefined, - refresh_count: window.nmmRefreshCounts[bid.adUnitCode]++, - elOffsets: getBoundingClient(bid), - scrollTop: window.pageYOffset || document.documentElement.scrollTop, - }, - }, + setConsentStrings(postBody, bidderRequest); + setOrtb2Parameters(postBody, bidderRequest?.ortb2); - device, - site, - imp: [], - }; + const urlParameters = parseUrl(getWindowTop().location.href).search; + const isTest = urlParameters['pbs'] && urlParameters['pbs'] === 'test'; + setEids(postBody, validBidRequests); + _each(validBidRequests, (bid, i) => { + window.nmmRefreshCounts[bid.adUnitCode] = window.nmmRefreshCounts[bid.adUnitCode] || 0; + const id = getPlacementId(bid); + const {cur, mediaTypes} = getCurrency(bid); + if (i === 0) postBody.cur = cur; postBody.imp.push(getImp(bid, id, mediaTypes)); - setConsentStrings(postBody, bidderRequest); - setOrtb2Parameters(postBody, bidderRequest?.ortb2); - setEids(postBody, bid); - - const urlParameters = parseUrl(getWindowTop().location.href).search; - const isTest = urlParameters['pbs'] && urlParameters['pbs'] === 'test'; - const params = bid.params; - - requests.push({ - method: 'POST', - url: isTest ? TEST_ENDPOINT : ENDPOINT, - data: JSON.stringify(postBody), - options: { - contentType: 'text/plain', - withCredentials: true, - }, + postBody.ext.next_mil_imps.push(getExtNextMilImp(bid)); + }); - bidId, - params, - auctionId, - }); + this.getUrlPixelMetric(EVENTS.BID_REQUESTED, validBidRequests); - this.getUrlPixelMetric(EVENTS.BID_REQUESTED, bid); + requests.push({ + method: 'POST', + url: isTest ? TEST_ENDPOINT : ENDPOINT, + data: JSON.stringify(postBody), + options: { + contentType: 'text/plain', + withCredentials: true, + }, }); return requests; }, - interpretResponse: function(serverResponse, bidRequest) { + interpretResponse: function(serverResponse) { const response = serverResponse.body; const bidResponses = []; + const bids = []; _each(response.seatbid, (resp) => { _each(resp.bid, (bid) => { - const requestId = bidRequest.bidId; - const params = bidRequest.params; + const requestId = bid.impid; const {ad, adUrl, vastUrl, vastXml} = getAd(bid); const bidResponse = { requestId, - params, cpm: bid.price, width: bid.w, height: bid.h, @@ -182,11 +175,13 @@ export const spec = { }; bidResponses.push(bidResponse); - - this.getUrlPixelMetric(EVENTS.BID_RESPONSE, bid); }); + + bids.push(resp.bid); }); + this.getUrlPixelMetric(EVENTS.BID_RESPONSE, bids.flat()); + return bidResponses; }, @@ -226,22 +221,26 @@ export const spec = { triggerPixel(url); }, - _getUrlPixelMetric(eventName, bid) { - const bidder = bid.bidder || bid.bidderCode; + _getUrlPixelMetric(eventName, bids) { + if (!Array.isArray(bids)) bids = [bids]; + + const bidder = bids[0]?.bidder || bids[0]?.bidderCode; if (bidder != BIDDER_CODE) return; - let params; - if (bid.params) { - params = Array.isArray(bid.params) ? bid.params : [bid.params]; - } else { - if (Array.isArray(bid.bids)) params = bid.bids.map(bidI => bidI.params); - }; + let params = []; + _each(bids, bid => { + if (bid.params) { + params.push(bid.params); + } else { + if (Array.isArray(bid.bids)) params.push(bid.bids.map(bidI => bidI.params)); + }; + }); if (!params.length) return; const placementIdsArray = []; const groupIdsArray = []; - params.forEach(paramsI => { + params.flat().forEach(paramsI => { if (paramsI.group_id) { groupIdsArray.push(paramsI.group_id); } else { @@ -252,9 +251,7 @@ export const spec = { const placementIds = (placementIdsArray.length && `&placements=${placementIdsArray.join(';')}`) || ''; const groupIds = (groupIdsArray.length && `&groups=${groupIdsArray.join(';')}`) || ''; - if (!(groupIds || placementIds)) { - return; - }; + if (!(groupIds || placementIds)) return; const url = `${REPORT_ENDPOINT}?event=${eventName}&bidder=${bidder}&source=pbjs${groupIds}${placementIds}`; @@ -268,10 +265,25 @@ export const spec = { }, }; +function getExtNextMilImp(bid) { + if (typeof window?.nmmRefreshCounts[bid.adUnitCode] === 'number') ++window.nmmRefreshCounts[bid.adUnitCode]; + const nextMilImp = { + impId: bid.bidId, + nextMillennium: { + nm_version: NM_VERSION, + pbjs_version: PBJS_VERSION, + refresh_count: window?.nmmRefreshCounts[bid.adUnitCode] || 0, + scrollTop: window.pageYOffset || getWinDimensions().document.documentElement.scrollTop, + }, + }; + + return nextMilImp; +} + export function getImp(bid, id, mediaTypes) { const {banner, video} = mediaTypes; const imp = { - id: bid.adUnitCode, + id: bid.bidId, ext: { prebid: { storedrequest: { @@ -281,6 +293,11 @@ export function getImp(bid, id, mediaTypes) { }, }; + const gpid = bid?.ortb2Imp?.ext?.gpid; + const pbadslot = bid?.ortb2Imp?.ext?.data?.pbadslot; + if (gpid) imp.ext.gpid = gpid; + if (pbadslot) imp.ext.data = { pbadslot }; + getImpBanner(imp, banner); getImpVideo(imp, video); @@ -293,13 +310,15 @@ export function getImpBanner(imp, banner) { if (banner.bidfloorcur) imp.bidfloorcur = banner.bidfloorcur; if (banner.bidfloor) imp.bidfloor = banner.bidfloor; - const format = (banner.data?.sizes || []).map(s => { return {w: s[0], h: s[1]} }) + const format = (banner.data?.sizes || []).map(s => { return {w: s[0], h: s[1]} }); const {w, h} = (format[0] || {}) imp.banner = { w, h, format, }; + + setImpPos(imp.banner, banner?.pos); }; export function getImpVideo(imp, video) { @@ -321,6 +340,12 @@ export function getImpVideo(imp, video) { imp.video.w = video.data.w; imp.video.h = video.data.h; }; + + setImpPos(imp.video, video?.pos); +}; + +export function setImpPos(obj, pos) { + if (typeof pos === 'number' && pos >= 0 && pos <= 7) obj.pos = pos; }; export function setConsentStrings(postBody = {}, bidderRequest) { @@ -330,10 +355,10 @@ export function setConsentStrings(postBody = {}, bidderRequest) { if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) gppConsent = bidderRequest?.ortb2?.regs; if (gdprConsent || uspConsent || gppConsent) { - postBody.regs = { ext: {} }; + postBody.regs = {}; if (uspConsent) { - postBody.regs.ext.us_privacy = uspConsent; + postBody.regs.us_privacy = uspConsent; }; if (gppConsent) { @@ -343,15 +368,19 @@ export function setConsentStrings(postBody = {}, bidderRequest) { if (gdprConsent) { if (typeof gdprConsent.gdprApplies !== 'undefined') { - postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; + postBody.regs.gdpr = gdprConsent.gdprApplies ? 1 : 0; }; if (typeof gdprConsent.consentString !== 'undefined') { postBody.user = { - ext: { consent: gdprConsent.consentString }, + consent: gdprConsent.consentString, }; }; }; + + if (typeof bidderRequest?.ortb2?.regs?.coppa === 'number') { + postBody.regs.coppa = bidderRequest?.ortb2?.regs?.coppa; + }; }; }; @@ -360,12 +389,20 @@ export function setOrtb2Parameters(postBody, ortb2 = {}) { const value = deepAccess(ortb2, parameter); if (value) deepSetValue(postBody, parameter, value); } + + if (postBody.wlang) delete postBody.wlangb } -export function setEids(postBody, bid) { - if (!isArray(bid.userIdAsEids) || !bid.userIdAsEids.length) return; +export function setEids(postBody = {}, bids = []) { + let isFind = false; + _each(bids, bid => { + if (isFind || !isArray(bid.userIdAsEids) || !bid.userIdAsEids.length) return; - deepSetValue(postBody, 'user.eids', bid.userIdAsEids); + if (bid.userIdAsEids.length) { + deepSetValue(postBody, 'user.eids', bid.userIdAsEids); + isFind = true; + }; + }); } export function replaceUsersyncMacros(url, gdprConsent = {}, uspConsent = '', gppConsent = {}, type = '') { @@ -397,8 +434,8 @@ function getCurrency(bid = {}) { if (typeof bid.getFloor === 'function') { let floorInfo = bid.getFloor({currency, mediaType, size: '*'}); - mediaTypes[mediaType].bidfloorcur = floorInfo.currency; - mediaTypes[mediaType].bidfloor = floorInfo.floor; + mediaTypes[mediaType].bidfloorcur = floorInfo?.currency; + mediaTypes[mediaType].bidfloor = floorInfo?.floor; } else { mediaTypes[mediaType].bidfloorcur = currency; }; @@ -411,21 +448,7 @@ function getCurrency(bid = {}) { return {cur, mediaTypes}; } -function getAdEl(bid) { - // best way I could think of to get El, is by matching adUnitCode to google slots... - const slot = window.googletag && window.googletag.pubads && window.googletag.pubads().getSlots().find(slot => slot.getAdUnitPath() === bid.adUnitCode); - const slotElementId = slot && slot.getSlotElementId(); - if (!slotElementId) return null; - return document.querySelector('#' + slotElementId); -} - -function getBoundingClient(bid) { - const el = getAdEl(bid); - if (!el) return {}; - return el.getBoundingClientRect(); -} - -function getPlacementId(bid) { +export function getPlacementId(bid) { const groupId = getBidIdParameter('group_id', bid.params); const placementId = getBidIdParameter('placement_id', bid.params); if (!groupId) return placementId; @@ -433,8 +456,8 @@ function getPlacementId(bid) { let windowTop = getTopWindow(window); let sizes = []; if (bid.mediaTypes) { - if (bid.mediaTypes.banner) sizes = bid.mediaTypes.banner.sizes; - if (bid.mediaTypes.video) sizes = [bid.mediaTypes.video.playerSize]; + if (bid.mediaTypes.banner) sizes = [...bid.mediaTypes.banner.sizes]; + if (bid.mediaTypes.video) sizes.push(bid.mediaTypes.video.playerSize); }; const host = (windowTop && windowTop.location && windowTop.location.host) || ''; @@ -455,32 +478,6 @@ function getTopWindow(curWindow, nesting = 0) { }; } -function getAd(bid) { - let ad, adUrl, vastXml, vastUrl; - - switch (deepAccess(bid, 'ext.prebid.type')) { - case VIDEO: - if (bid.adm.substr(0, 4) === 'http') { - vastUrl = bid.adm; - } else { - vastXml = bid.adm; - }; - - break; - default: - if (bid.adm && bid.nurl) { - ad = bid.adm; - ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); - } else if (bid.adm) { - ad = bid.adm; - } else if (bid.nurl) { - adUrl = bid.nurl; - }; - }; - - return {ad, adUrl, vastXml, vastUrl}; -} - function getSiteObj() { const refInfo = (getRefererInfo && getRefererInfo()) || {}; @@ -503,14 +500,28 @@ function getSiteObj() { } function getDeviceObj() { + const { width, height } = getViewportSize(); return { - w: window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth || 0, - h: window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight || 0, + w: width, + h: height, ua: window.navigator.userAgent || undefined, sua: getSua(), }; } +export function getSourceObj(validBidRequests, bidderRequest) { + const schain = validBidRequests?.[0]?.schain || + (bidderRequest?.ortb2?.source && (bidderRequest?.ortb2?.source?.schain || bidderRequest?.ortb2?.source?.ext?.schain)); + + if (!schain) return; + + const source = { + schain, + }; + + return source; +} + function getSua() { let {brands, mobile, platform} = (window?.navigator?.userAgentData || {}); if (!(brands && platform)) return undefined; diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index 8a41efe4dcc..689f2120de2 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -11,6 +11,7 @@ import { import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { getOsVersion } from '../libraries/advangUtils/index.js'; import {find} from '../src/polyfill.js'; @@ -312,7 +313,7 @@ function _getDevice(_bidRequest) { ua: navigator.userAgent, language: navigator['language'], os: _getOs(navigator.userAgent.toLowerCase()), - osv: _getOsVersion(navigator.userAgent) + osv: getOsVersion() }; } @@ -343,25 +344,4 @@ function _getOs(userAgent) { }) || 'etc'; } -function _getOsVersion(userAgent) { - const clientStrings = [ - { s: 'Android', r: /Android/ }, - { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, - { s: 'Mac OS X', r: /Mac OS X/ }, - { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, - { s: 'Linux', r: /(Linux|X11)/ }, - { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, - { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, - { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, - { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, - { s: 'Windows Vista', r: /Windows NT 6.0/ }, - { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, - { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, - { s: 'UNIX', r: /UNIX/ }, - { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } - ]; - let cs = find(clientStrings, cs => cs.r.test(userAgent)); - return cs ? cs.s : 'unknown'; -} - registerBidder(spec); diff --git a/modules/nexverseBidAdapter.js b/modules/nexverseBidAdapter.js new file mode 100644 index 00000000000..4c342032b7d --- /dev/null +++ b/modules/nexverseBidAdapter.js @@ -0,0 +1,247 @@ + + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { isArray } from '../src/utils.js'; +import {getConnectionType} from '../libraries/connectionInfo/connectionUtils.js' +import { getDeviceType, getOS } from '../libraries/userAgentUtils/index.js'; +import { getDeviceModel, buildEndpointUrl, isBidRequestValid, parseNativeResponse, printLog, getUid } from '../libraries/nexverseUtils/index.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; +import { getOsVersion } from '../libraries/advangUtils/index.js'; + +const BIDDER_CODE = 'nexverse'; +const BIDDER_ENDPOINT = 'https://rtb.nexverse.ai'; +const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO, NATIVE]; +const DEFAULT_CURRENCY = 'USD'; +const BID_TTL = 300; +const DEFAULT_LANG = 'en'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: BIDDER_CODE}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + isBidRequestValid, + /** + * Builds the OpenRTB server request from the list of valid bid requests. + * + * @param {Array} validBidRequests - Array of valid bid requests. + * @param {Object} bidderRequest - The bidder request object containing additional data. + * @returns {Array} Array of server requests to be sent to the endpoint. + */ + buildRequests(validBidRequests, bidderRequest) { + const requests = validBidRequests.map((bid) => { + // Build the endpoint URL with query parameters + const endpointUrl = buildEndpointUrl(BIDDER_ENDPOINT, bid); + + // Build the OpenRTB payload + const payload = buildOpenRtbRequest(bid, bidderRequest); + + if (!payload) { + printLog('error', 'Payload could not be built.'); + return null; // Skip this bid + } + + // Return the server request + return { + method: 'POST', + url: endpointUrl, + data: JSON.stringify(payload), + bidRequest: bid, + }; + }); + + return requests.filter((request) => request !== null); // Remove null entries + }, + + /** + * Interprets the server's response and extracts bid information. + * + * @param {Object} serverResponse - The response from the server. + * @param {Object} request - The original server request. + * @returns {Array} Array of bids to be passed to the auction. + */ + interpretResponse(serverResponse, request) { + if (serverResponse && serverResponse.status === 204) { + printLog('info', 'No ad available (204 response).'); + return []; + } + + const bidResponses = []; + const response = serverResponse.body; + + if (!response || !response.seatbid || !isArray(response.seatbid)) { + printLog('warning', 'No valid bids in the response.'); + return bidResponses; + } + + response.seatbid.forEach((seatbid) => { + seatbid.bid.forEach((bid) => { + const bidResponse = { + requestId: bid.impid, + cpm: bid.price, + currency: response.cur || DEFAULT_CURRENCY, + width: bid.width || 0, + height: bid.height || 0, + creativeId: bid.crid || bid.id, + ttl: BID_TTL, + netRevenue: true, + meta: {}, + }; + // Determine media type and assign the ad content + if (bid.ext && bid.ext.mediaType) { + bidResponse.mediaType = bid.ext.mediaType; + } else if (bid.adm && bid.adm.indexOf(' ({ w: size[0], h: size[1] })), // List of size objects + w: bid.sizes[0][0], + h: bid.sizes[0][1], + }, + secure: window.location.protocol === 'https:' ? 1 : 0, // Indicates whether the request is secure (HTTPS) + }); + } + if (bid.mediaTypes.video) { + imp.push({ + id: bid.bidId, + video: { + w: bid.sizes[0][0], + h: bid.sizes[0][1], + mimes: bid.mediaTypes.video.mimes || ['video/mp4'], // Default to video/mp4 if not specified + protocols: bid.mediaTypes.video.protocols || [2, 3, 5, 6], // RTB video ad serving protocols + maxduration: bid.mediaTypes.video.maxduration || 30, + linearity: bid.mediaTypes.video.linearity || 1, + playbackmethod: bid.mediaTypes.video.playbackmethod || [2], + }, + secure: window.location.protocol === 'https:' ? 1 : 0, // Indicates whether the request is secure (HTTPS) + }); + } + if (bid.mediaTypes.native) { + imp.push({ + id: bid.bidId, + native: { + request: JSON.stringify(bid.mediaTypes.native), // Convert native request to JSON string + }, + secure: window.location.protocol === 'https:' ? 1 : 0, // Indicates whether the request is secure (HTTPS) + }); + } + + // Construct the OpenRTB request object + const openRtbRequest = { + id: bidderRequest.auctionId, + imp: imp, + site: { + page: bidderRequest.refererInfo.page, + domain: bidderRequest.refererInfo.domain, + ref: bidderRequest.refererInfo.ref || '', // Referrer URL + }, + device: { + ua: navigator.userAgent, + devicetype: getDeviceType(), // 1 = Mobile/Tablet, 2 = Desktop + os: getOS(), + osv: getOsVersion(), + make: navigator.vendor || '', + model: getDeviceModel(), + connectiontype: getConnectionType(), // Include connection type + geo: { + lat: bid.params.geoLat || 0, + lon: bid.params.geoLon || 0, + }, + language: navigator.language || DEFAULT_LANG, + dnt: navigator.doNotTrack === '1' ? 1 : 0, // Do Not Track flag + }, + user: { + id: getUid(storage), + buyeruid: bidderRequest.userId || '', // User ID or Buyer ID + ext: { + consent: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.consentString : null, // GDPR consent string + }, + }, + regs: { + ext: { + gdpr: bidderRequest.gdprConsent ? (bidderRequest.gdprConsent.gdprApplies ? 1 : 0) : 0, + }, + }, + ext: { + prebid: { + auctiontimestamp: bidderRequest.auctionStart, + }, + }, + }; + + // Add app object if the request comes from a mobile app + if (bidderRequest.app) { + openRtbRequest.app = { + id: bidderRequest.app.id, + name: bidderRequest.app.name, + bundle: bidderRequest.app.bundle, + domain: bidderRequest.app.domain, + storeurl: bidderRequest.app.storeUrl, + cat: bidderRequest.app.cat || [], + }; + } + // Add additional fields related to GDPR, US Privacy, CCPA + if (bidderRequest.uspConsent) { + openRtbRequest.regs.ext.us_privacy = bidderRequest.uspConsent; + } + return openRtbRequest; +} + +registerBidder(spec); diff --git a/modules/nexverseBidAdapter.md b/modules/nexverseBidAdapter.md new file mode 100644 index 00000000000..1de5dda01e9 --- /dev/null +++ b/modules/nexverseBidAdapter.md @@ -0,0 +1,41 @@ +# Nexverse Bid Adapter + +## Overview +The Nexverse Bid Adapter enables publishers to connect with the Nexverse Real-Time Bidding (RTB) platform. This adapter supports multiple ad formats, including Banner, Video, and Native ads. By integrating this adapter, publishers can send bid requests to Nexverse’s marketplace and receive high-quality ads in response. + +- **Module name**: Nexverse +- **Module type**: Bidder Adapter +- **Supported Media Types**: Banner, Video, Native +- **Maintainer**: anand.kumar@nexverse.ai + +## Bidder Parameters +To correctly configure the Nexverse Bid Adapter, the following parameters are required: + +| Param Name | Scope | Type | Description | +|--------------|----------|--------|-----------------------------------------------------| +| `uid` | required | string | Unique User ID assigned by Nexverse for the publisher | +| `pubId` | required | string | The unique ID for the publisher | +| `pubEpid` | required | string | The unique endpoint ID for the publisher | + +### Example Configuration +The following is an example configuration for a Nexverse bid request using Prebid.js: + +```javascript +var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'nexverse', + params: { + uid: '12345', + pubId: '54321', + pubEpid: 'abcde' + }, + isDebug: false // Optional, i.e True for debug mode + }] +}]; +``` diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index c31c3d81aeb..a2c1d57ffae 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -1,4 +1,3 @@ -import {config} from '../src/config.js'; import { deepAccess, deepSetValue, generateUUID, logError, logInfo } from '../src/utils.js'; import {Renderer} from '../src/Renderer.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -7,6 +6,7 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js' import { INSTREAM, OUTSTREAM } from '../src/video.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -22,7 +22,7 @@ const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstre const BIDDER_CODE = 'nexx360'; const REQUEST_URL = 'https://fast.nexx360.io/booster'; const PAGE_VIEW_ID = generateUUID(); -const BIDDER_VERSION = '4.0'; +const BIDDER_VERSION = '5.0'; const GVLID = 965; const NEXXID_KEY = 'nexx360_storage'; @@ -33,6 +33,9 @@ const ALIASES = [ { code: 'league-m', gvlid: 965 }, { code: 'prjads' }, { code: 'pubtech' }, + { code: '1accord', gvlid: 965 }, + { code: 'easybid', gvlid: 1068 }, + { code: 'prismassp', gvlid: 965 } ]; export const storage = getStorageManager({ @@ -63,10 +66,17 @@ export function getNexx360LocalStorage() { } } -function getAdContainer(container) { - if (document.getElementById(container)) { - return document.getElementById(container); +/** + * Get the AMX ID + * @return { string | false } false if localstorageNotEnabled + */ +export function getAmxId() { + if (!storage.localStorageIsEnabled()) { + logInfo(`localstorage not enabled for Nexx360`); + return false; } + const amxId = storage.getDataFromLocalStorage('__amuidpb'); + return amxId || false; } const converter = ortbConverter({ @@ -75,45 +85,58 @@ const converter = ortbConverter({ ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) }, imp(buildImp, bidRequest, context) { - // console.log(bidRequest, context); const imp = buildImp(bidRequest, context); - const tagid = bidRequest.params.adUnitName ? bidRequest.params.adUnitName : bidRequest.adUnitCode; - deepSetValue(imp, 'tagid', tagid); + deepSetValue(imp, 'tagid', bidRequest.adUnitCode); deepSetValue(imp, 'ext.adUnitCode', bidRequest.adUnitCode); - const divId = bidRequest.params.divId ? bidRequest.params.divId : bidRequest.adUnitCode; + if (bidRequest.params.adUnitPath) deepSetValue(imp, 'ext.adUnitPath', bidRequest.params.adUnitPath); + if (bidRequest.params.adUnitName) deepSetValue(imp, 'ext.adUnitName', bidRequest.params.adUnitName); + const divId = bidRequest.params.divId || bidRequest.adUnitCode; deepSetValue(imp, 'ext.divId', divId); - const slotEl = getAdContainer(divId); + const slotEl = document.getElementById(divId); if (slotEl) { deepSetValue(imp, 'ext.dimensions.slotW', slotEl.offsetWidth); deepSetValue(imp, 'ext.dimensions.slotH', slotEl.offsetHeight); deepSetValue(imp, 'ext.dimensions.cssMaxW', slotEl.style?.maxWidth); deepSetValue(imp, 'ext.dimensions.cssMaxH', slotEl.style?.maxHeight); } - deepSetValue(imp, 'ext.nexx360', bidRequest.params.tagId); - deepSetValue(imp, 'ext.nexx360.tagId', bidRequest.params.tagId); - deepSetValue(imp, 'ext.nexx360.videoTagId', bidRequest.params.videoTagId); - deepSetValue(imp, 'ext.nexx360.allBids', bidRequest.params.allBids); + 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.allBids) deepSetValue(imp, 'ext.nexx360.allBids', bidRequest.params.allBids); 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); } - - if (bidRequest.params.adUnitName) deepSetValue(imp, 'ext.adUnitName', bidRequest.params.adUnitName); - if (bidRequest.params.adUnitPath) deepSetValue(imp, 'ext.adUnitPath', bidRequest.params.adUnitPath); return imp; }, request(buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context); const nexx360LocalStorage = getNexx360LocalStorage(); - if (nexx360LocalStorage) deepSetValue(request, 'ext.nexx360Id', nexx360LocalStorage.nexx360Id); + if (nexx360LocalStorage) { + deepSetValue(request, 'ext.localStorage.nexx360Id', nexx360LocalStorage.nexx360Id); + } + const amxId = getAmxId(); + if (amxId) deepSetValue(request, 'ext.localStorage.amxId', amxId); deepSetValue(request, 'ext.version', '$prebid.version$'); deepSetValue(request, 'ext.source', 'prebid.js'); deepSetValue(request, 'ext.pageViewId', PAGE_VIEW_ID); deepSetValue(request, 'ext.bidderVersion', BIDDER_VERSION); - deepSetValue(request, 'cur', [config.getConfig('currency.adServerCurrency') || 'USD']); - if (!request.user) deepSetValue(request, 'user', {}); + deepSetValue(request, 'cur', [getCurrencyFromBidderRequest(bidderRequest) || 'USD']); + if (!request.user) request.user = {}; + if (getAmxId()) { + 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: `${getAmxId()}`, + atype: 1 + }] + }); + } + return request; }, }); @@ -141,8 +164,8 @@ function isBidRequestValid(bid) { logError('bid.params.allBids needs to be a boolean'); return false; } - if (!bid.params.tagId && !bid.params.videoTagId && !bid.params.nativeTagId) { - logError('bid.params.tagId or bid.params.videoTagId or bid.params.nativeTagId must be defined'); + 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; @@ -179,16 +202,17 @@ function interpretResponse(serverResponse) { const { bidderSettings } = getGlobal(); const allowAlternateBidderCodes = bidderSettings && bidderSettings.standard ? bidderSettings.standard.allowAlternateBidderCodes : false; - let bids = []; - respBody.seatbid.forEach(seatbid => { - bids = [...bids, ...seatbid.bid.map(bid => { + let 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 = { requestId: bid.impid, cpm: bid.price, width: bid.w, height: bid.h, creativeId: bid.crid, - dealId: bid.dealid, currency: respBody.cur, netRevenue: true, ttl: 120, @@ -198,6 +222,7 @@ function interpretResponse(serverResponse) { demandSource: bid.ext.ssp, }, }; + if (bid.dealid) response.dealid = bid.dealid; if (allowAlternateBidderCodes) response.bidderCode = `n360_${bid.ext.ssp}`; if (bid.ext.mediaType === BANNER) { @@ -211,7 +236,7 @@ function interpretResponse(serverResponse) { if (bid.ext.mediaType === OUTSTREAM) { response.renderer = createRenderer(bid, OUTSTREAM_RENDERER_URL); - response.divId = bid.ext.divId + if (bid.ext.divId) response.divId = bid.ext.divId }; if (bid.ext.mediaType === NATIVE) { try { @@ -220,10 +245,10 @@ function interpretResponse(serverResponse) { } } catch (e) {} } - return response; - })]; - }); - return bids; + responses.push(response); + } + } + return responses; } /** diff --git a/modules/nexx360BidAdapter.md b/modules/nexx360BidAdapter.md index 532d48418b6..d9038bfc0b1 100644 --- a/modules/nexx360BidAdapter.md +++ b/modules/nexx360BidAdapter.md @@ -10,7 +10,7 @@ Maintainer: gabriel@nexx360.io Connects to Nexx360 network for bids. -To use us as a bidder you must have an account and an active "tagId" on our Nexx360 platform. +To use us as a bidder you must have an account and an active "tagId" or "placement" on our Nexx360 platform. # Test Parameters @@ -30,8 +30,7 @@ var adUnits = [ bids: [{ bidder: 'nexx360', params: { - account: '1067', - tagId: 'luvxjvgn' + tagId: 'testnexx' } }] }, @@ -51,8 +50,7 @@ var adUnits = [ bids: [{ bidder: 'nexx360', params: { - account: '1067', - tagId: 'luvxjvgn' + tagId: 'testnexx' } }] }; diff --git a/modules/nobidAnalyticsAdapter.js b/modules/nobidAnalyticsAdapter.js index a0aa5ee4989..afa980b05c9 100644 --- a/modules/nobidAnalyticsAdapter.js +++ b/modules/nobidAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import {deepClone, logError, getParameterByName} from '../src/utils.js'; +import {deepClone, logError, getParameterByName, logMessage} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; @@ -26,8 +26,7 @@ const { AD_RENDER_SUCCEEDED } = EVENTS; function log (msg) { - // eslint-disable-next-line no-console - console.log(`%cNoBid Analytics ${VERSION}`, 'padding: 2px 8px 2px 8px; background-color:#f50057; color: white', msg); + logMessage(`%cNoBid Analytics ${VERSION}: ${msg}`); } function isJson (str) { return str && str.startsWith('{') && str.endsWith('}'); @@ -241,7 +240,7 @@ window.nobidCarbonizer = { adunit.bids = allowedBidders; } for (const adunit of adunits) { - if (!nobidAnalytics.originalAdUnits[adunit.code]) nobidAnalytics.originalAdUnits[adunit.code] = JSON.parse(JSON.stringify(adunit)); + if (!nobidAnalytics.originalAdUnits[adunit.code]) nobidAnalytics.originalAdUnits[adunit.code] = deepClone(adunit); }; if (this.isActive()) { // 5% of the time do not block; diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 77e5a1c6fed..f9d4058b646 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -1,9 +1,9 @@ -import { logInfo, deepAccess, logWarn, isArray, getParameterByName } from '../src/utils.js'; +import { logInfo, deepAccess, logWarn, isArray, getParameterByName, getWinDimensions } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; -import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { hasPurpose1Consent } from '../src/utils/gdpr.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -123,8 +123,9 @@ function nobidBuildRequests(bids, bidderRequest) { }; var clientDim = function() { try { - var width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); - var height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); + const winDimensions = getWinDimensions(); + var width = Math.max(winDimensions.document.documentElement.clientWidth, winDimensions.innerWidth || 0); + var height = Math.max(winDimensions.document.documentElement.clientHeight, winDimensions.innerHeight || 0); return `${width}x${height}`; } catch (e) { logWarn('Could not parse screen dimensions, error details:', e); @@ -231,7 +232,7 @@ function nobidBuildRequests(bids, bidderRequest) { return adunits; } function getFloor (bid) { - if (bid && typeof bid.getFloor === 'function' && bid.getFloor().floor) { + if (bid && typeof bid.getFloor === 'function' && bid.getFloor()?.floor) { return bid.getFloor().floor; } return null; diff --git a/modules/nodalsAiRtdProvider.js b/modules/nodalsAiRtdProvider.js new file mode 100644 index 00000000000..9359d5187b9 --- /dev/null +++ b/modules/nodalsAiRtdProvider.js @@ -0,0 +1,424 @@ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { loadExternalScript } from '../src/adloader.js'; +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 { mergeDeep, prefixLog } from '../src/utils.js'; + +const MODULE_NAME = 'nodalsAi'; +const GVLID = 1360; +const ENGINE_VESION = '1.x.x'; +const PUB_ENDPOINT_ORIGIN = 'https://nodals.io'; +const LOCAL_STORAGE_KEY = 'signals.nodals.ai'; +const STORAGE_TTL = 3600; // 1 hour in seconds + + +const fillTemplate = (strings, ...keys) => { + return function (values) { + return strings.reduce((result, str, i) => { + const key = keys[i - 1]; + return result + (key ? values[key] || '' : '') + str; + }); + }; +}; + +const PUB_ENDPOINT_PATH = fillTemplate`/p/v1/${'propertyId'}/config?${'consentParams'}`; +const { logInfo, logWarn, logError } = prefixLog('[NodalsAiRTDProvider]'); + +class NodalsAiRtdProvider { + // Public properties + name = MODULE_NAME; + gvlid = GVLID; + + // Exposed for testing + storage = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: MODULE_NAME, + }); + + STORAGE_KEY = LOCAL_STORAGE_KEY; + + // Private properties + #propertyId = null; + #overrides = {}; + #dataFetchInProgress = false; + #userConsent = null; + + // Public methods + + /** + * Initialises the class with the provided config and user consent. + * @param {Object} config - Configuration object for the module. + * @param {Object} userConsent - User consent object for GDPR or other purposes. + */ + init(config, userConsent) { + const params = config?.params || {}; + if ( + this.#isValidConfig(params) && + this.#hasRequiredUserConsent(userConsent) + ) { + this.#propertyId = params.propertyId; + this.#userConsent = userConsent; + this.#setOverrides(params); + const storedData = this.#readFromStorage(); + if (storedData === null) { + this.#fetchData(); + } else { + this.#loadAdLibraries(storedData.deps || []); + } + return true; + } else { + logWarn('Invalid configuration or missing user consent.'); + return false; + } + } + + /** + * Retrieves targeting data by fetching and processing signals. + * @param {Array} adUnitArray - Array of ad units. + * @param {Object} config - Configuration object. + * @param {Object} userConsent - User consent object. + * @returns {Object} - Targeting data. + */ + getTargetingData(adUnitArray, config, userConsent) { + let targetingData = {}; + if (!this.#hasRequiredUserConsent(userConsent)) { + return targetingData; + } + this.#userConsent = userConsent; + const storedData = this.#getData(); + const engine = this.#initialiseEngine(config); + if (!storedData || !engine) { + return targetingData; + } + try { + targetingData = engine.getTargetingData( + adUnitArray, + userConsent, + storedData + ); + } catch (error) { + logError(`Error determining targeting keys: ${error}`); + } + return targetingData; + } + + getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + if (!this.#hasRequiredUserConsent(userConsent)) { + callback(); + return; + } + this.#userConsent = userConsent; + const storedData = this.#getData(); + if (!storedData) { + callback(); + return; + } + const engine = this.#initialiseEngine(config); + if (!engine) { + this.#addToCommandQueue('getBidRequestData', {config, reqBidsConfigObj, callback, userConsent, storedData }); + } else { + try { + engine.getBidRequestData( + reqBidsConfigObj, + callback, + userConsent, + storedData + ); + } catch (error) { + logError(`Error getting bid request data: ${error}`); + callback(); + } + } + } + + onBidResponseEvent(bidResponse, config, userConsent) { + if (!this.#hasRequiredUserConsent(userConsent)) { + return; + } + this.#userConsent = userConsent; + const storedData = this.#getData(); + if (!storedData) { + return; + } + const engine = this.#initialiseEngine(config); + if (!engine) { + this.#addToCommandQueue('onBidResponseEvent', {config, bidResponse, userConsent, storedData }) + return; + } + try { + engine.onBidResponseEvent(bidResponse, userConsent, storedData); + } catch (error) { + logError(`Error processing bid response event: ${error}`); + } + } + + onAuctionEndEvent(auctionDetails, config, userConsent) { + if (!this.#hasRequiredUserConsent(userConsent)) { + return; + } + this.#userConsent = userConsent; + const storedData = this.#getData(); + if (!storedData) { + return; + } + const engine = this.#initialiseEngine(config); + if (!engine) { + this.#addToCommandQueue('onAuctionEndEvent', {config, auctionDetails, userConsent, storedData }); + return; + } + try { + engine.onAuctionEndEvent(auctionDetails, userConsent, storedData); + } catch (error) { + logError(`Error processing auction end event: ${error}`); + } + } + + + // Private methods + #getData() { + const storedData = this.#readFromStorage(); + if (storedData === null) { + this.#fetchData(); + return null; + } + if (storedData.facts === undefined) { + storedData.facts = {}; + } + storedData.facts = mergeDeep(storedData.facts, this.#getRuntimeFacts()); + return storedData; + } + + #initialiseEngine(config) { + const engine = this.#getEngine(); + if (!engine) { + logInfo(`Engine v${ENGINE_VESION} not found`); + return null; + } + try { + engine.init(config); + return engine + } catch (error) { + logError(`Error initialising engine: ${error}`); + return null; + } + } + + #getEngine() { + return window?.$nodals?.adTargetingEngine[ENGINE_VESION]; + } + + #setOverrides(params) { + if (params?.storage?.ttl && typeof params.storage.ttl === 'number') { + this.#overrides.storageTTL = params.storage.ttl; + } + this.#overrides.storageKey = params?.storage?.key; + this.#overrides.endpointOrigin = params?.endpoint?.origin; + } + + #getRuntimeFacts() { + return { + 'page.url': getRefererInfo().page, + 'prebid.version': '$prebid.version$', + }; + } + + /** + * Validates if the provided module input parameters are valid. + * @param {Object} params - Parameters object from the module configuration. + * @returns {boolean} - True if parameters are valid, false otherwise. + */ + + #isValidConfig(params) { + // Basic validation logic + if (typeof params === 'object' && params?.propertyId) { + return true; + } + logWarn('Invalid configuration'); + return false; + } + + /** + * Checks if the user has provided the required consent. + * @param {Object} userConsent - User consent object. + * @returns {boolean} - True if the user consent is valid, false otherwise. + */ + + #hasRequiredUserConsent(userConsent) { + if (userConsent.gdpr === undefined || userConsent.gdpr?.gdprApplies === false) { + return true; + } + if ( + [false, undefined].includes(userConsent.gdpr.vendorData?.vendor?.consents?.[this.gvlid]) + ) { + return false; + } else if (userConsent.gdpr.vendorData?.purpose?.consents[1] === false || + userConsent.gdpr.vendorData?.purpose?.consents[7] === false + ) { + return false; + } + return true; + } + + #readFromStorage() { + const key = this.#overrides?.storageKey || this.STORAGE_KEY; + if ( + this.storage.hasLocalStorage() && + this.storage.localStorageIsEnabled() + ) { + try { + const entry = this.storage.getDataFromLocalStorage(key); + if (!entry) { + return null; + } + const dataEnvelope = JSON.parse(entry); + if (this.#dataIsStale(dataEnvelope)) { + logInfo('Stale data found in storage. Refreshing data.'); + this.#fetchData(); + } + if (!dataEnvelope.data) { + throw new Error('Data envelope is missing \'data\' property.'); + } + return dataEnvelope.data; + } catch (error) { + logError(`Corrupted data in local storage: ${error}`); + return null; + } + } else { + logError('Local storage is not available or not enabled.'); + return null; + } + } + + /** + * Writes data to localStorage. + * @param {string} key - The key under which to store the data. + * @param {Object} data - The data to store. + */ + + #writeToStorage(key, data) { + if ( + this.storage.hasLocalStorage() && + this.storage.localStorageIsEnabled() + ) { + const storageObject = { + createdAt: Date.now(), + data, + }; + this.storage.setDataInLocalStorage(key, JSON.stringify(storageObject)); + } else { + logError('Local storage is not available or not enabled.'); + } + } + + /** + * Checks if the provided data is stale. + * @param {Object} dataEnvelope - The data envelope object. + * @returns {boolean} - True if the data is stale, false otherwise. + */ + + #dataIsStale(dataEnvelope) { + const currentTime = Date.now(); + const dataTime = dataEnvelope.createdAt || 0; + const staleThreshold = this.#overrides?.storageTTL ?? dataEnvelope?.data?.meta?.ttl ?? STORAGE_TTL; + return currentTime - dataTime >= (staleThreshold * 1000); + } + + + #getEndpointUrl(userConsent) { + const endpointOrigin = + this.#overrides.endpointOrigin || PUB_ENDPOINT_ORIGIN; + const parameterMap = { + gdpr_consent: userConsent?.gdpr?.consentString ?? '', + gdpr: userConsent?.gdpr?.gdprApplies ? '1' : '0', + us_privacy: userConsent?.uspConsent ?? '', + gpp: userConsent?.gpp?.gppString ?? '', + gpp_sid: + userConsent.gpp && Array.isArray(userConsent.gpp.applicableSections) + ? userConsent.gpp.applicableSections.join(',') + : '', + }; + const querystring = new URLSearchParams(parameterMap).toString(); + const values = { + propertyId: this.#propertyId, + consentParams: querystring, + }; + const path = PUB_ENDPOINT_PATH(values); + return `${endpointOrigin}${path}`; + } + + /** + * Initiates the request to fetch rule data from the publisher endpoint. + */ + + #fetchData() { + if (this.#dataFetchInProgress) { + return; + } + this.#dataFetchInProgress = true; + const endpointUrl = this.#getEndpointUrl(this.#userConsent); + const callback = { + success: (response, req) => { + this.#dataFetchInProgress = false; + this.#handleServerResponse(response, req); + }, + error: (error, req) => { + this.#dataFetchInProgress = false; + this.#handleServerError(error, req); + }, + }; + + const options = { + method: 'GET', + withCredentials: false, + }; + + logInfo(`Fetching ad rules from: ${endpointUrl}`); + ajax(endpointUrl, callback, null, options); + } + + #addToCommandQueue(cmd, payload) { + window.$nodals = window.$nodals || {}; + window.$nodals.cmdQueue = window.$nodals.cmdQueue || []; + window.$nodals.cmdQueue.push({ cmd, runtimeFacts: this.#getRuntimeFacts(), data: payload }); + } + + /** + * Handles the server response, processes it and extracts relevant data. + * @param {Object} response - The server response object. + * @returns {Object} - Processed data from the response. + */ + + #handleServerResponse(response, req) { + let data; + try { + data = JSON.parse(response); + } catch (error) { + throw `Error parsing response: ${error}`; + } + this.#writeToStorage(this.#overrides?.storageKey || this.STORAGE_KEY, data); + this.#loadAdLibraries(data.deps || []); + } + + + #handleServerError(error, req) { + logError(`Publisher endpoint response error: ${error}`); + } + + + #loadAdLibraries(deps) { + // eslint-disable-next-line no-unused-vars + for (const [key, value] of Object.entries(deps)) { + if (typeof value === 'string') { + loadExternalScript(value, MODULE_TYPE_RTD, MODULE_NAME, () => { + // noop + }); + } + } + } +} + +export const nodalsAiRtdSubmodule = new NodalsAiRtdProvider(); + +submodule('realTimeData', nodalsAiRtdSubmodule); diff --git a/modules/nodalsAiRtdProvider.md b/modules/nodalsAiRtdProvider.md new file mode 100644 index 00000000000..78cfe534cef --- /dev/null +++ b/modules/nodalsAiRtdProvider.md @@ -0,0 +1,58 @@ +# Nodals AI Real-Time Data Module + +## Overview + +Module Name: Nodals AI Rtd Provider +Module Type: Rtd Provider +Maintainer: prebid-integrations@nodals.ai + +Nodals AI provides a real-time data prebid module that will analyse first-party signals present on page load, determine the value of them to Nodals’ advertisers and add a key-value to the ad server call to indicate that value. The Nodals AI RTD module loads external code as part of this process. + +In order to be able to utilise this module, please contact [info@nodals.ai](mailto:info@nodals.ai) for account setup and detailed GAM setup instructions. + +## Build + +First, ensure that you include the generic Prebid RTD Module _and_ the Nodals AI RTD module into your Prebid build: + +```bash +gulp build --modules=rtdModule,nodalsAiRtdProvider +``` + +## Configuration + +Update your Prebid configuration to enable the Nodals AI RTD module, as illustrated in the example below: + +```javascript +pbjs.setConfig({ + ..., + realTimeData: { + auctionDelay: 100, // optional auction delay + dataProviders: [{ + name: 'nodalsAi', + waitForIt: true, // should be true only if there's an `auctionDelay` + params: { + propertyId: 'c10516af' // obtain your property id from Nodals AI support + } + }] + }, + ... +}) +``` + +Configuration parameters: + +{: .table .table-bordered .table-striped } + +| Name | Scope | Description | Example | Type | +| --------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------- | +| `name` | required | Real time data module name: Always `'nodalsAi'` | `'nodalsAi'` | `String` | +| `waitForIt` | optional | Set to `true` if there's an `auctionDelay` defined (defaults to `false`) | `false` | `Boolean` | +| `params` | required | Submodule configuration parameters | `{}` | `Object` | +| `params.propertyId` | required | Publisher specific identifier, provided by Nodals AI | `'76346cf3'` | `String` | +| `params.storage` | optional | Optional storage configiration | `{}` | `Object` | +| `params.storage.key` | optional | Storage key used to store Nodals AI data in local storage | `'yourKey'` | `String` | +| `params.storage.ttl` | optional | Time in seconds to retain Nodals AI data in storage until a refresh is required | `900` | `Integer` | +| `params.ptr` | optional | Optional partner configiration | `{}` | `Object` | +| `params.ptr.permutive` | optional | Optional configiration for Permutive Audience Platform | `{}` | `Object` | +| `params.ptr.permutive.cohorts` | optional | A method for the publisher to explicitly supply Permutive Cohort IDs, disabling automatic fetching by this RTD module | `['66711', '39032', '311']` | `Array` | +| `params.ptr.permutive.storageKey` | optional | Publisher specific Permutive storage key where cohort data is held. | `'_psegs'` | `String` | diff --git a/modules/novatiqIdSystem.md b/modules/novatiqIdSystem.md index f33fc700311..a78363e8fe3 100644 --- a/modules/novatiqIdSystem.md +++ b/modules/novatiqIdSystem.md @@ -19,12 +19,11 @@ pbjs.setConfig({ name: 'novatiq', params: { // change to the Partner Number you received from Novatiq - sourceid '1a3' - } + sourceid: '1a3' } }], // 50ms maximum auction delay, applies to all userId modules - auctionDelay: 50 + auctionDelay: 50 } }); ``` diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index 9937391f6e7..d4e5fea1cf0 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -1,10 +1,11 @@ 'use strict'; -import {BANNER} from '../src/mediaTypes.js'; -import {getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {ajax} from '../src/ajax.js'; -import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getWindowSelf, getWindowTop, isFn, deepAccess, isPlainObject, deepSetValue, mergeDeep } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { ajax } from '../src/ajax.js'; +import { getAdUnitSizes } from '../libraries/sizeUtils/sizeUtils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'ogury'; const GVLID = 31; @@ -12,48 +13,80 @@ const DEFAULT_TIMEOUT = 1000; const BID_HOST = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_MONITORING_HOST = 'https://ms-ads-monitoring-events.presage.io'; const MS_COOKIE_SYNC_DOMAIN = 'https://ms-cookie-sync.presage.io'; -const ADAPTER_VERSION = '1.6.0'; +const ADAPTER_VERSION = '2.0.0'; + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 60, + mediaType: 'banner' + }, + + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + req.tmax = DEFAULT_TIMEOUT; + deepSetValue(req, 'device.pxratio', window.devicePixelRatio); + deepSetValue(req, 'site.page', getWindowContext().location.href); + + req.ext = mergeDeep({}, req.ext, { + adapterversion: ADAPTER_VERSION, + prebidversion: '$prebid.version$' + }); -function getClientWidth() { - const documentElementClientWidth = window.top.document.documentElement.clientWidth - ? window.top.document.documentElement.clientWidth - : 0 - const innerWidth = window.top.innerWidth ? window.top.innerWidth : 0 - const outerWidth = window.top.outerWidth ? window.top.outerWidth : 0 - const screenWidth = window.top.screen.width ? window.top.screen.width : 0 + const bidWithAssetKey = bidderRequest.bids.find(bid => Boolean(deepAccess(bid, 'params.assetKey', false))); + if (bidWithAssetKey) deepSetValue(req, 'site.id', bidWithAssetKey.params.assetKey); - return documentElementClientWidth || innerWidth || outerWidth || screenWidth -} + const bidWithUserIds = bidderRequest.bids.find(bid => Boolean(bid.userId)); + if (bidWithUserIds) deepSetValue(req, 'user.ext.uids', bidWithUserIds.userId); -function getClientHeight() { - const documentElementClientHeight = window.top.document.documentElement.clientHeight - ? window.top.document.documentElement.clientHeight - : 0 - const innerHeight = window.top.innerHeight ? window.top.innerHeight : 0 - const outerHeight = window.top.outerHeight ? window.top.outerHeight : 0 - const screenHeight = window.top.screen.height ? window.top.screen.height : 0 + return req; + }, - return documentElementClientHeight || innerHeight || outerHeight || screenHeight -} + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const timeSpentOnPage = document.timeline && document.timeline.currentTime ? document.timeline.currentTime : 0 + const gpid = bidRequest.adUnitCode; + imp.tagid = bidRequest.adUnitCode; + imp.ext = mergeDeep({}, bidRequest.params, { timeSpentOnPage, gpid }, imp.ext); + + const bidfloor = getFloor(bidRequest); + + if (!bidfloor) { + delete imp.bidfloor; + } else { + imp.bidfloor = bidfloor; + } + + return imp; + }, + + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.currency = 'USD'; + return bidResponse; + } +}); function isBidRequestValid(bid) { const adUnitSizes = getAdUnitSizes(bid); - const isValidSizes = Boolean(adUnitSizes) && adUnitSizes.length > 0; - const isValidAdUnitId = !!bid.params.adUnitId; - const isValidAssetKey = !!bid.params.assetKey; + const isValidSize = (Boolean(adUnitSizes) && adUnitSizes.length > 0); + const hasAssetKeyAndAdUnitId = !!deepAccess(bid, 'params.adUnitId') && !!deepAccess(bid, 'params.assetKey'); + const hasPublisherIdAndAdUnitCode = !!deepAccess(bid, 'ortb2.site.publisher.id') && !!bid.adUnitCode; - return (isValidSizes && isValidAdUnitId && isValidAssetKey); + return isValidSize && (hasAssetKeyAndAdUnitId || hasPublisherIdAndAdUnitCode); } -function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { const consent = (gdprConsent && gdprConsent.consentString) || ''; + const gpp = (gppConsent && gppConsent.gppString) || ''; + const gppSid = (gppConsent && gppConsent.applicableSections && gppConsent.applicableSections.toString()) || ''; if (syncOptions.iframeEnabled) { return [ { type: 'iframe', - url: `${MS_COOKIE_SYNC_DOMAIN}/user-sync.html?gdpr_consent=${consent}&source=prebid` + url: `${MS_COOKIE_SYNC_DOMAIN}/user-sync.html?gdpr_consent=${consent}&source=prebid&gpp=${gpp}&gpp_sid=${gppSid}` } ]; } @@ -62,15 +95,15 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { return [ { type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/v1/init-sync/bid-switch?iab_string=${consent}&source=prebid` + url: `${MS_COOKIE_SYNC_DOMAIN}/v1/init-sync/bid-switch?iab_string=${consent}&source=prebid&gpp=${gpp}&gpp_sid=${gppSid}` }, { type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/ttd/init-sync?iab_string=${consent}&source=prebid` + url: `${MS_COOKIE_SYNC_DOMAIN}/ttd/init-sync?iab_string=${consent}&source=prebid&gpp=${gpp}&gpp_sid=${gppSid}` }, { type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/xandr/init-sync?iab_string=${consent}&source=prebid` + url: `${MS_COOKIE_SYNC_DOMAIN}/xandr/init-sync?iab_string=${consent}&source=prebid&gpp=${gpp}&gpp_sid=${gppSid}` } ]; } @@ -78,117 +111,19 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { return []; } -function buildRequests(validBidRequests, bidderRequest) { - const openRtbBidRequestBanner = { - id: bidderRequest.bidderRequestId, - tmax: DEFAULT_TIMEOUT, - at: 1, - regs: { - ext: { - gdpr: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? 1 : 0 - }, - }, - site: { - domain: location.hostname, - page: location.href - }, - user: { - ext: { - consent: '' - } - }, - imp: [], - ext: { - adapterversion: ADAPTER_VERSION, - prebidversion: '$prebid.version$' - }, - device: { - w: getClientWidth(), - h: getClientHeight(), - pxratio: window.devicePixelRatio - } - }; - - if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) { - openRtbBidRequestBanner.user.ext.consent = bidderRequest.gdprConsent.consentString - } - - validBidRequests.forEach((bidRequest) => { - const sizes = getAdUnitSizes(bidRequest) - .map(size => ({ w: size[0], h: size[1] })); - - if (bidRequest.mediaTypes && - bidRequest.mediaTypes.hasOwnProperty('banner')) { - openRtbBidRequestBanner.site.id = bidRequest.params.assetKey; - const floor = getFloor(bidRequest); - - if (bidRequest.userId) { - openRtbBidRequestBanner.user.ext.uids = bidRequest.userId - } - if (bidRequest.userIdAsEids) { - openRtbBidRequestBanner.user.ext.eids = bidRequest.userIdAsEids - } - - openRtbBidRequestBanner.imp.push({ - id: bidRequest.bidId, - tagid: bidRequest.params.adUnitId, - ...(floor && {bidfloor: floor}), - banner: { - format: sizes - }, - ext: { - ...bidRequest.params, - timeSpentOnPage: document.timeline && document.timeline.currentTime ? document.timeline.currentTime : 0 - } - }); - } - }); +function buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({bidRequests, bidderRequest}); return { method: 'POST', url: BID_HOST, - data: openRtbBidRequestBanner, + data, options: {contentType: 'application/json'}, }; } -function interpretResponse(openRtbBidResponse) { - if (!openRtbBidResponse || - !openRtbBidResponse.body || - typeof openRtbBidResponse.body != 'object' || - Object.keys(openRtbBidResponse.body).length === 0) { - logWarn('no response or body is malformed'); - return []; - } - - const bidResponses = []; - - openRtbBidResponse.body.seatbid.forEach((seatbid) => { - seatbid.bid.forEach((bid) => { - let bidResponse = { - requestId: bid.impid, - cpm: bid.price, - currency: 'USD', - width: bid.w, - height: bid.h, - creativeId: bid.id, - netRevenue: true, - ttl: 60, - ext: bid.ext, - meta: { - advertiserDomains: bid.adomain - }, - nurl: bid.nurl, - adapterVersion: ADAPTER_VERSION, - prebidVersion: '$prebid.version$' - }; - - bidResponse.ad = bid.adm; - - bidResponses.push(bidResponse); - }); - }); - return bidResponses; +function interpretResponse(response, request) { + return converter.fromORTB({response: response.body, request: request.data}).bids; } function getFloor(bid) { @@ -200,7 +135,8 @@ function getFloor(bid) { mediaType: 'banner', size: '*' }); - return floorResult.currency === 'USD' ? floorResult.floor : 0; + + return (isPlainObject(floorResult) && floorResult.currency === 'USD') ? floorResult.floor : 0; } function getWindowContext() { diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js index e6c8f8b098e..f5b55a68715 100644 --- a/modules/omsBidAdapter.js +++ b/modules/omsBidAdapter.js @@ -10,21 +10,24 @@ import { isPlainObject, getBidIdParameter, getUniqueIdentifierStr, + formatQS, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; import {percentInView} from '../libraries/percentInView/percentInView.js'; +import {getUserSyncParams} from '../libraries/userSyncUtils/userSyncUtils.js'; const BIDDER_CODE = 'oms'; const URL = 'https://rt.marphezis.com/hb'; -const TRACK_EVENT_URL = 'https://rt.marphezis.com/prebid' +const TRACK_EVENT_URL = 'https://rt.marphezis.com/prebid'; +const USER_SYNC_URL_IFRAME = 'https://rt.marphezis.com/sync?dpid=0'; export const spec = { code: BIDDER_CODE, aliases: ['brightcom', 'bcmssp'], gvlid: 883, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, buildRequests, interpretResponse, @@ -36,7 +39,7 @@ export const spec = { function buildRequests(bidReqs, bidderRequest) { try { const impressions = bidReqs.map(bid => { - let bidSizes = bid?.mediaTypes?.banner?.sizes || bid.sizes; + let bidSizes = bid?.mediaTypes?.banner?.sizes || bid?.mediaTypes?.video?.playerSize || bid.sizes; bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]); bidSizes = bidSizes.filter(size => isArray(size)); const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})); @@ -49,18 +52,25 @@ function buildRequests(bidReqs, bidderRequest) { const imp = { id: bid.bidId, - banner: { - format: processedSizes, - ext: { - viewability: viewabilityAmountRounded, - } - }, ext: { ...gpidData }, tagid: String(bid.adUnitCode) }; + if (bid?.mediaTypes?.video) { + imp.video = { + ...bid.mediaTypes.video, + } + } else { + imp.banner = { + format: processedSizes, + ext: { + viewability: viewabilityAmountRounded, + } + } + } + const bidFloor = _getBidFloor(bid); if (bidFloor) { @@ -136,7 +146,7 @@ function buildRequests(bidReqs, bidderRequest) { } function isBidRequestValid(bid) { - if (bid.bidder !== BIDDER_CODE || !bid.params || !bid.params.publisherId) { + if (!bid.params || !bid.params.publisherId) { return false; } @@ -155,7 +165,7 @@ function interpretResponse(serverResponse) { try { if (id && seatbid && seatbid.length > 0 && seatbid[0].bid && seatbid[0].bid.length > 0) { response = seatbid[0].bid.map(bid => { - return { + const bidResponse = { requestId: bid.impid, cpm: parseFloat(bid.price), width: parseInt(bid.w), @@ -163,13 +173,20 @@ function interpretResponse(serverResponse) { creativeId: bid.crid || bid.id, currency: 'USD', netRevenue: true, - mediaType: BANNER, ad: _getAdMarkup(bid), ttl: 300, meta: { advertiserDomains: bid?.adomain || [] } }; + + if (bid.mtype === 2) { + bidResponse.mediaType = VIDEO; + } else { + bidResponse.mediaType = BANNER; + } + + return bidResponse; }); } } catch (e) { @@ -179,9 +196,20 @@ function interpretResponse(serverResponse) { return response; } -// Don't do user sync for now -function getUserSyncs(syncOptions, responses, gdprConsent) { - return []; +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + const syncs = []; + + if (syncOptions.iframeEnabled) { + let params = getUserSyncParams(gdprConsent, uspConsent, gppConsent); + params = Object.keys(params).length ? `&${formatQS(params)}` : ''; + + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + params, + }); + } + + return syncs; } function onBidderError(errorData) { @@ -242,7 +270,7 @@ function _isViewabilityMeasurable(element) { } function _getViewability(element, topWin, {w, h} = {}) { - return getWindowTop().document.visibilityState === 'visible' ? percentInView(element, topWin, {w, h}) : 0; + return getWindowTop().document.visibilityState === 'visible' ? percentInView(element, {w, h}) : 0; } function _extractGpidData(bid) { diff --git a/modules/omsBidAdapter.md b/modules/omsBidAdapter.md index f1e2d459eca..506ba5fdbd5 100644 --- a/modules/omsBidAdapter.md +++ b/modules/omsBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: OMS Bid Adapter Module Type: Bidder Adapter -Maintainer: alexandruc@onlinemediasolutions.com +Maintainer: devsupport@onlinemediasolutions.com ``` # Description @@ -41,6 +41,21 @@ var adUnits = [ publisherId: 2141020 } }] + }, + { + code: 'video-instream', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480] + } + }, + bids: [{ + bidder: 'oms', + params: { + publisherId: 2141020 + } + }] } ] ``` diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index d8423253aaf..eec7dca9c23 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -1,12 +1,13 @@ 'use strict'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; import { INSTREAM, OUTSTREAM } from '../src/video.js'; import { Renderer } from '../src/Renderer.js'; import { find } from '../src/polyfill.js'; import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { deepClone, logError, deepAccess } from '../src/utils.js'; +import { deepClone, logError, deepAccess, getWinDimensions } from '../src/utils.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -17,6 +18,7 @@ const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; const GVLID = 241; +const NATIVE_SUFFIX = 'Ad'; const storage = getStorageManager({ bidderCode: BIDDER_CODE }); @@ -30,7 +32,7 @@ function isBidRequestValid(bid) { if (typeof bid === 'undefined' || typeof bid.params === 'undefined' || typeof bid.params.pubId !== 'string') { return false; } - return isValid(BANNER, bid) || isValid(VIDEO, bid); + return isValid(BANNER, bid) || isValid(VIDEO, bid) || isValid(NATIVE, bid); } export function hasTypeVideo(bid) { @@ -45,14 +47,54 @@ export function isValid(type, bid) { if (context === 'outstream' || context === 'instream') { return parseVideoSize(bid).length > 0; } + } else if (type === NATIVE) { + if (typeof bid.mediaTypes.native !== 'object' || bid.mediaTypes.native === null) return false; + + const assets = bid.mediaTypes.native?.ortb?.assets; + const eventTrackers = bid.mediaTypes.native?.ortb?.eventtrackers; + + let isValidAssets = false; + let isValidEventTrackers = false; + + if (assets && Array.isArray(assets) && assets.length > 0 && assets.every(asset => isValidAsset(asset))) { + isValidAssets = true; + } + + if (eventTrackers && Array.isArray(eventTrackers) && eventTrackers.length > 0) { + if (eventTrackers.every(eventTracker => isValidEventTracker(eventTracker))) { + isValidEventTrackers = true; + } + } else if (!eventTrackers) { + isValidEventTrackers = true; + } + return isValidAssets && isValidEventTrackers; } return false; } +const isValidEventTracker = function(et) { + if (!et.event || !et.methods || !Number.isInteger(et.event) || !Array.isArray(et.methods) || !et.methods.length > 0) { + return false; + } + return true; +} + +const isValidAsset = function(asset) { + if (!asset.id || !Number.isInteger(asset.id)) return false; + const hasValidContent = asset.title || asset.img || asset.data || asset.video; + if (!hasValidContent) return false; + if (asset.title && (!asset.title.len || !Number.isInteger(asset.title.len))) return false; + if (asset.img && ((!asset.img.wmin || !Number.isInteger(asset.img.wmin)) || (!asset.img.hmin || !Number.isInteger(asset.img.hmin)))) return false; + if (asset.data && !asset.data.type) return false; + if (asset.video && (!asset.video.mimes || !asset.video.minduration || !asset.video.maxduration || !asset.video.protocols)) return false; + return true; +} + /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {Array} validBidRequests - an array of bids + * @param bidderRequest * @return ServerRequest Info describing the request to the server. */ function buildRequests(validBidRequests, bidderRequest) { @@ -93,7 +135,7 @@ function buildRequests(validBidRequests, bidderRequest) { const connection = navigator.connection || navigator.webkitConnection; payload.networkConnectionType = (connection && connection.type) ? connection.type : null; payload.networkEffectiveConnectionType = (connection && connection.effectiveType) ? connection.effectiveType : null; - payload.fledgeEnabled = Boolean(bidderRequest && bidderRequest.fledgeEnabled) + payload.fledgeEnabled = Boolean(bidderRequest?.paapi?.enabled) return { method: 'POST', url: ENDPOINT, @@ -121,7 +163,7 @@ function interpretResponse(serverResponse, bidderRequest) { dealId: bid.dealId == null ? bid.dealId : '', currency: bid.currency, netRevenue: bid.netRevenue || false, - mediaType: bid.mediaType, + mediaType: (bid.mediaType === NATIVE + NATIVE_SUFFIX) ? NATIVE : bid.mediaType, meta: { mediaType: bid.mediaType, advertiserDomains: bid.adomain @@ -148,6 +190,8 @@ function interpretResponse(serverResponse, bidderRequest) { responseBid.renderer = createRenderer({ ...bid, adUnitCode }); } } + } else if (bid.mediaType === NATIVE || bid.mediaType === NATIVE + NATIVE_SUFFIX) { + responseBid.native = bid.native; } bids.push(responseBid); }); @@ -156,7 +200,7 @@ function interpretResponse(serverResponse, bidderRequest) { const fledgeAuctionConfigs = body.fledgeAuctionConfigs return { bids, - fledgeAuctionConfigs, + paapi: fledgeAuctionConfigs, } } else { return bids; @@ -225,20 +269,21 @@ function getDocumentVisibility(window) { * @returns {{location: *, referrer: (*|string), stack: (*|Array.), numIframes: (*|Number), wWidth: (*|Number), wHeight: (*|Number), sWidth, sHeight, date: string, timeOffset: number}} */ function getPageInfo(bidderRequest) { + const winDimensions = getWinDimensions(); const topmostFrame = getFrameNesting(); return { location: deepAccess(bidderRequest, 'refererInfo.page', null), referrer: deepAccess(bidderRequest, 'refererInfo.ref', null), stack: deepAccess(bidderRequest, 'refererInfo.stack', []), numIframes: deepAccess(bidderRequest, 'refererInfo.numIframes', 0), - wWidth: topmostFrame.innerWidth, - wHeight: topmostFrame.innerHeight, - oWidth: topmostFrame.outerWidth, - oHeight: topmostFrame.outerHeight, - sWidth: topmostFrame.screen.width, - sHeight: topmostFrame.screen.height, - aWidth: topmostFrame.screen.availWidth, - aHeight: topmostFrame.screen.availHeight, + wWidth: getWinDimensions().innerWidth, + wHeight: getWinDimensions().innerHeight, + oWidth: winDimensions.outerWidth, + oHeight: winDimensions.outerHeight, + sWidth: winDimensions.screen.width, + sHeight: winDimensions.screen.height, + aWidth: winDimensions.screen.availWidth, + aHeight: winDimensions.screen.availHeight, sLeft: 'screenLeft' in topmostFrame ? topmostFrame.screenLeft : topmostFrame.screenX, sTop: 'screenTop' in topmostFrame ? topmostFrame.screenTop : topmostFrame.screenY, xOffset: topmostFrame.pageXOffset, @@ -249,7 +294,7 @@ function getPageInfo(bidderRequest) { timing: getTiming(), version: { prebid: '$prebid.version$', - adapter: '1.1.1' + adapter: '1.1.2' } }; } @@ -278,7 +323,16 @@ function requestsToBids(bidRequests) { bannerObj['priceFloors'] = getBidFloor(bidRequest, BANNER, bannerObj['sizes']); return bannerObj; }); - return videoBidRequests.concat(bannerBidRequests); + const nativeBidRequests = bidRequests.filter(bidRequest => isValid(NATIVE, bidRequest)).map(bidRequest => { + const bannerObj = {}; + setGeneralInfo.call(bannerObj, bidRequest); + bannerObj['sizes'] = parseSizes(bidRequest); + bannerObj['type'] = NATIVE + NATIVE_SUFFIX; + bannerObj['mediaTypeInfo'] = deepClone(bidRequest.mediaTypes.native); + bannerObj['priceFloors'] = getBidFloor(bidRequest, NATIVE, bannerObj['sizes']); + return bannerObj; + }); + return videoBidRequests.concat(bannerBidRequests).concat(nativeBidRequests); } function setGeneralInfo(bidRequest) { @@ -291,6 +345,7 @@ function setGeneralInfo(bidRequest) { this['gpid'] = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); this['pubId'] = params.pubId; this['ext'] = params.ext; + this['ortb2Imp'] = deepAccess(bidRequest, 'ortb2Imp'); if (params.pubClick) { this['click'] = params.pubClick; } @@ -306,12 +361,12 @@ function setGeneralInfo(bidRequest) { function getSpaceCoords(id) { const space = document.getElementById(id); try { - const { top, left, width, height } = space.getBoundingClientRect(); + const { top, left, width, height } = getBoundingClientRect(space); let window = space.ownerDocument.defaultView; const coords = { top: top + window.pageYOffset, left: left + window.pageXOffset, width, height }; let frame = window.frameElement; while (frame != null) { - const { top, left } = frame.getBoundingClientRect(); + const { top, left } = getBoundingClientRect(frame); coords.top += top + window.pageYOffset; coords.left += left + window.pageXOffset; window = window.parent; @@ -410,7 +465,7 @@ function getBidFloor(bidRequest, mediaType, sizes) { currency: 'EUR', mediaType: mediaType || '*', size: [size.width, size.height] - }); + }) || {}; floor.size = deepClone(size); if (!floor.floor) { floor.floor = null; } priceFloors.push(floor); @@ -436,7 +491,7 @@ export function isSchainValid(schain) { export const spec = { code: BIDDER_CODE, gvlid: GVLID, - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, interpretResponse: interpretResponse, diff --git a/modules/onomagicBidAdapter.js b/modules/onomagicBidAdapter.js index 78f00153a8b..642cae3996e 100644 --- a/modules/onomagicBidAdapter.js +++ b/modules/onomagicBidAdapter.js @@ -12,6 +12,7 @@ import { } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +import { percentInView } from '../libraries/percentInView/percentInView.js'; const BIDDER_CODE = 'onomagic'; const URL = 'https://bidder.onomagic.com/hb'; @@ -172,7 +173,7 @@ function _isViewabilityMeasurable(element) { function _getViewability(element, topWin, { w, h } = {}) { return getWindowTop().document.visibilityState === 'visible' - ? _getPercentInView(element, topWin, { w, h }) + ? percentInView(element, { w, h }) : 0; } @@ -188,75 +189,6 @@ function _getMinSize(sizes) { return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); } -function _getBoundingBox(element, { w, h } = {}) { - let { width, height, left, top, right, bottom } = element.getBoundingClientRect(); - - if ((width === 0 || height === 0) && w && h) { - width = w; - height = h; - right = left + w; - bottom = top + h; - } - - return { width, height, left, top, right, bottom }; -} - -function _getIntersectionOfRects(rects) { - const bbox = { - left: rects[0].left, - right: rects[0].right, - top: rects[0].top, - bottom: rects[0].bottom - }; - - for (let i = 1; i < rects.length; ++i) { - bbox.left = Math.max(bbox.left, rects[i].left); - bbox.right = Math.min(bbox.right, rects[i].right); - - if (bbox.left >= bbox.right) { - return null; - } - - bbox.top = Math.max(bbox.top, rects[i].top); - bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); - - if (bbox.top >= bbox.bottom) { - return null; - } - } - - bbox.width = bbox.right - bbox.left; - bbox.height = bbox.bottom - bbox.top; - - return bbox; -} - -function _getPercentInView(element, topWin, { w, h } = {}) { - const elementBoundingBox = _getBoundingBox(element, { w, h }); - - // Obtain the intersection of the element and the viewport - const elementInViewBoundingBox = _getIntersectionOfRects([ { - left: 0, - top: 0, - right: topWin.innerWidth, - bottom: topWin.innerHeight - }, elementBoundingBox ]); - - let elementInViewArea, elementTotalArea; - - if (elementInViewBoundingBox !== null) { - // Some or all of the element is in view - elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; - elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; - - return ((elementInViewArea / elementTotalArea) * 100); - } - - // No overlap between element and the viewport; therefore, the element - // lies completely out of view - return 0; -} - function _getBidFloor(bid) { if (!isFn(bid.getFloor)) { return bid.params.bidFloor ? bid.params.bidFloor : null; diff --git a/modules/ooloAnalyticsAdapter.js b/modules/ooloAnalyticsAdapter.js index 8a6ef88a7fb..573fee3b0b3 100644 --- a/modules/ooloAnalyticsAdapter.js +++ b/modules/ooloAnalyticsAdapter.js @@ -433,6 +433,11 @@ function sendPage() { function sendHbConfigData() { const conf = {} const pbjsConfig = config.getConfig() + // Check if pbjsConfig.userSync exists and has userIds property + if (pbjsConfig.userSync && pbjsConfig.userSync.userIds) { + // Delete the userIds property + delete pbjsConfig.userSync.userIds; + } Object.keys(pbjsConfig).forEach(key => { if (key[0] !== '_') { diff --git a/modules/openPairIdSystem.js b/modules/openPairIdSystem.js new file mode 100644 index 00000000000..6ce4f365848 --- /dev/null +++ b/modules/openPairIdSystem.js @@ -0,0 +1,146 @@ +/** + * This module adds Open PAIR Id to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/openPairIdSystem + * @requires module:modules/userId + */ + +import {submodule} from '../src/hook.js'; +import {getStorageManager} from '../src/storageManager.js' +import {logInfo} from '../src/utils.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {VENDORLESS_GVLID} from '../src/consentHandler.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + */ + +const MODULE_NAME = 'openPairId'; +const DEFAULT_PUBLISHER_ID_KEY = 'pairId'; + +const DEFAULT_STORAGE_PUBLISHER_ID_KEYS = { + liveramp: '_lr_pairId' +}; + +const DEFAULT_ATYPE = 3; +const DEFAULT_SOURCE = 'pair-protocol.com'; + +const MATCH_METHOD = 3; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); + +function publisherIdFromLocalStorage(key) { + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(key) : null; +} + +function publisherIdFromCookie(key) { + return storage.cookiesAreEnabled() ? storage.getCookie(key) : null; +} + +/** @type {Submodule} */ +export const openPairIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * used to specify vendor id + * @type {number} + */ + gvlid: VENDORLESS_GVLID, + /** + * decode the stored id value for passing to bid requests + * @function + * @param { string | undefined } value + * @returns {{pairId:string} | undefined } + */ + decode(value) { + return value && Array.isArray(value) ? {'openPairId': value} : undefined; + }, + /** + * Performs action to obtain ID and return a value in the callback's response argument. + * @function getId + * @param {Object} config - The configuration object. + * @param {Object} config.params - The parameters from the configuration. + * @returns {{id: string[] | undefined}} The obtained IDs or undefined if no IDs are found. + */ + getId(config) { + const publisherIdsString = publisherIdFromLocalStorage(DEFAULT_PUBLISHER_ID_KEY) || publisherIdFromCookie(DEFAULT_PUBLISHER_ID_KEY); + let ids = [] + + if (publisherIdsString && typeof publisherIdsString == 'string') { + try { + ids = ids.concat(JSON.parse(atob(publisherIdsString))); + } catch (error) { + logInfo(error) + } + } + + const configParams = (config && config.params) ? config.params : {}; + const cleanRooms = Object.keys(configParams); + + for (let i = 0; i < cleanRooms.length; i++) { + const cleanRoom = cleanRooms[i]; + const cleanRoomParams = configParams[cleanRoom]; + + const cleanRoomStorageLocation = cleanRoomParams.storageKey || DEFAULT_STORAGE_PUBLISHER_ID_KEYS[cleanRoom]; + const cleanRoomValue = publisherIdFromLocalStorage(cleanRoomStorageLocation) || publisherIdFromCookie(cleanRoomStorageLocation); + + if (cleanRoomValue) { + try { + const parsedValue = atob(cleanRoomValue); + + if (parsedValue) { + const obj = JSON.parse(parsedValue); + + if (obj && typeof obj === 'object' && obj.envelope) { + ids = ids.concat(obj.envelope); + } else { + logInfo('Open Pair ID: Parsed object is not valid or does not contain envelope'); + } + } else { + logInfo('Open Pair ID: Decoded value is empty'); + } + } catch (error) { + logInfo('Open Pair ID: Error parsing JSON: ', error); + } + } else { + logInfo('Open Pair ID: data clean room value for pairId from storage is empty or null'); + } + } + + if (ids.length == 0) { + logInfo('Open Pair ID: no ids found') + return undefined; + } + + return {'id': ids}; + }, + eids: { + openPairId: function(values, config = {}) { + const inserter = config.inserter; + const matcher = config.matcher; + + const source = DEFAULT_SOURCE; + const atype = DEFAULT_ATYPE; + + return [ + { + source: source, + mm: MATCH_METHOD, + inserter: inserter, + matcher: matcher, + uids: values.map(function(value) { + return { + id: value, + atype: atype + } + }) + } + ]; + } + }, +}; + +submodule('userId', openPairIdSubmodule); diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index cf0334b7f29..0dff314ce17 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -1,40 +1,19 @@ -import { - logWarn, - logInfo, - isArray, - isFn, - deepAccess, - isEmpty, - contains, - triggerPixel, - isInteger, - getBidIdParameter, - getDNT -} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; +import {makeBaseSpec} from '../libraries/riseUtils/index.js'; -const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'openweb'; -const ADAPTER_VERSION = '6.0.0'; -const TTL = 360; -const DEFAULT_CURRENCY = 'USD'; -const SELLER_ENDPOINT = 'https://hb.openwebmp.com/'; +const BASE_URL = 'https://hb.openwebmp.com/'; +const GVLID = 280; const MODES = { PRODUCTION: 'hb-multi', TEST: 'hb-multi-test' -} -const SUPPORTED_SYNC_METHODS = { - IFRAME: 'iframe', - PIXEL: 'pixel' -} +}; export const spec = { + ...makeBaseSpec(BASE_URL, MODES), code: BIDDER_CODE, - gvlid: 280, - version: ADAPTER_VERSION, - supportedMediaTypes: SUPPORTED_AD_TYPES, + gvlid: GVLID, isBidRequestValid: function (bidRequest) { if (!bidRequest.params) { logWarn('no params have been set to OpenWeb adapter'); @@ -46,440 +25,13 @@ export const spec = { return false; } - return true; - }, - buildRequests: function (validBidRequests, bidderRequest) { - const combinedRequestsObject = {}; - - // use data from the first bid, to create the general params for all bids - const generalObject = validBidRequests[0]; - const testMode = generalObject.params.testMode; - - combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); - combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); - - return { - method: 'POST', - url: getEndpoint(testMode), - data: combinedRequestsObject - } - }, - interpretResponse: function ({body}) { - const bidResponses = []; - - if (body && body.bids && body.bids.length) { - body.bids.forEach(adUnit => { - const bidResponse = { - requestId: adUnit.requestId, - cpm: adUnit.cpm, - currency: adUnit.currency || DEFAULT_CURRENCY, - width: adUnit.width, - height: adUnit.height, - ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, - netRevenue: adUnit.netRevenue || true, - nurl: adUnit.nurl, - mediaType: adUnit.mediaType, - meta: { - mediaType: adUnit.mediaType - } - }; - - if (adUnit.mediaType === VIDEO) { - bidResponse.vastXml = adUnit.vastXml; - } else if (adUnit.mediaType === BANNER) { - bidResponse.ad = adUnit.ad; - } - - if (adUnit.adomain && adUnit.adomain.length) { - bidResponse.meta.advertiserDomains = adUnit.adomain; - } - - bidResponses.push(bidResponse); - }); - } - - return bidResponses; - }, - getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; - for (const response of serverResponses) { - if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { - syncs.push({ - type: 'iframe', - url: response.body.params.userSyncURL - }); - } - if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { - const pixels = response.body.params.userSyncPixels.map(pixel => { - return { - type: 'image', - url: pixel - } - }) - syncs.push(...pixels) - } - } - return syncs; - }, - onBidWon: function (bid) { - if (bid == null) { - return; + if (!bidRequest.params.placementId) { + logWarn('placementId is a mandatory param for OpenWeb adapter'); + return false; } - logInfo('onBidWon:', bid); - if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { - triggerPixel(bid.nurl); - } + return true; } }; registerBidder(spec); - -/** - * Get floor price - * @param bid {bid} - * @returns {Number} - */ -function getFloor(bid, mediaType) { - if (!isFn(bid.getFloor)) { - return 0; - } - let floorResult = bid.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: mediaType, - size: '*' - }); - return floorResult.currency === DEFAULT_CURRENCY && floorResult.floor ? floorResult.floor : 0; -} - -/** - * Get the the ad sizes array from the bid - * @param bid {bid} - * @returns {Array} - */ -function getSizesArray(bid, mediaType) { - let sizesArray = [] - - if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { - sizesArray = bid.mediaTypes[mediaType].sizes; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - sizesArray = bid.sizes; - } - - return sizesArray; -} - -/** - * Get schain string value - * @param schainObject {Object} - * @returns {string} - */ -function getSupplyChain(schainObject) { - if (isEmpty(schainObject)) { - return ''; - } - let scStr = `${schainObject.ver},${schainObject.complete}`; - schainObject.nodes.forEach((node) => { - scStr += '!'; - scStr += `${getEncodedValIfNotEmpty(node.asi)},`; - scStr += `${getEncodedValIfNotEmpty(node.sid)},`; - scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; - scStr += `${getEncodedValIfNotEmpty(node.rid)},`; - scStr += `${getEncodedValIfNotEmpty(node.name)},`; - scStr += `${getEncodedValIfNotEmpty(node.domain)}`; - }); - return scStr; -} - -/** - * Get encoded node value - * @param val {string} - * @returns {string} - */ -function getEncodedValIfNotEmpty(val) { - return !isEmpty(val) ? encodeURIComponent(val) : ''; -} - -/** - * Get preferred user-sync method based on publisher configuration - * @param bidderCode {string} - * @returns {string} - */ -function getAllowedSyncMethod(filterSettings, bidderCode) { - const iframeConfigsToCheck = ['all', 'iframe']; - const pixelConfigToCheck = 'image'; - if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { - return SUPPORTED_SYNC_METHODS.IFRAME; - } - if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { - return SUPPORTED_SYNC_METHODS.PIXEL; - } -} - -/** - * Check if sync rule is supported - * @param syncRule {Object} - * @param bidderCode {string} - * @returns {boolean} - */ -function isSyncMethodAllowed(syncRule, bidderCode) { - if (!syncRule) { - return false; - } - const isInclude = syncRule.filter === 'include'; - const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; - return isInclude && contains(bidders, bidderCode); -} - -/** - * Get the seller endpoint - * @param testMode {boolean} - * @returns {string} - */ -function getEndpoint(testMode) { - return testMode - ? SELLER_ENDPOINT + MODES.TEST - : SELLER_ENDPOINT + MODES.PRODUCTION; -} - -/** - * get device type - * @param uad {ua} - * @returns {string} - */ -function getDeviceType(ua) { - if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i - .test(ua.toLowerCase())) { - return '5'; - } - if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i - .test(ua.toLowerCase())) { - return '4'; - } - if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i - .test(ua.toLowerCase())) { - return '3'; - } - return '1'; -} - -function generateBidsParams(validBidRequests, bidderRequest) { - const bidsArray = []; - - if (validBidRequests.length) { - validBidRequests.forEach(bid => { - bidsArray.push(generateBidParameters(bid, bidderRequest)); - }); - } - - return bidsArray; -} - -/** - * Generate bid specific parameters - * @param {bid} bid - * @param {bidderRequest} bidderRequest - * @returns {Object} bid specific params object - */ -function generateBidParameters(bid, bidderRequest) { - const {params} = bid; - const mediaType = isBanner(bid) ? BANNER : VIDEO; - const sizesArray = getSizesArray(bid, mediaType); - - // fix floor price in case of NAN - if (isNaN(params.floorPrice)) { - params.floorPrice = 0; - } - - const bidObject = { - mediaType, - adUnitCode: getBidIdParameter('adUnitCode', bid), - sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), - bidId: getBidIdParameter('bidId', bid), - loop: getBidIdParameter('bidderRequestsCount', bid), - bidderRequestId: getBidIdParameter('bidderRequestId', bid), - transactionId: bid.ortb2Imp?.ext?.tid || '', - coppa: 0, - }; - - const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); - if (pos) { - bidObject.pos = pos; - } - - const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); - if (gpid) { - bidObject.gpid = gpid; - } - - const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); - if (placementId) { - bidObject.placementId = placementId; - } - - const mimes = deepAccess(bid, `mediaTypes.${mediaType}.mimes`); - if (mimes) { - bidObject.mimes = mimes; - } - const api = deepAccess(bid, `mediaTypes.${mediaType}.api`); - if (api) { - bidObject.api = api; - } - - const sua = deepAccess(bid, `ortb2.device.sua`); - if (sua) { - bidObject.sua = sua; - } - - const coppa = deepAccess(bid, `ortb2.regs.coppa`); - if (coppa) { - bidObject.coppa = 1; - } - - if (mediaType === VIDEO) { - const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); - let playbackMethodValue; - - // verify playbackMethod is of type integer array, or integer only. - if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { - // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation - playbackMethodValue = playbackMethod[0]; - } else if (isInteger(playbackMethod)) { - playbackMethodValue = playbackMethod; - } - - if (playbackMethodValue) { - bidObject.playbackMethod = playbackMethodValue; - } - - const placement = deepAccess(bid, `mediaTypes.video.placement`); - if (placement) { - bidObject.placement = placement; - } - - const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); - if (minDuration) { - bidObject.minDuration = minDuration; - } - - const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); - if (maxDuration) { - bidObject.maxDuration = maxDuration; - } - - const skip = deepAccess(bid, `mediaTypes.video.skip`); - if (skip) { - bidObject.skip = skip; - } - - const linearity = deepAccess(bid, `mediaTypes.video.linearity`); - if (linearity) { - bidObject.linearity = linearity; - } - - const protocols = deepAccess(bid, `mediaTypes.video.protocols`); - if (protocols) { - bidObject.protocols = protocols; - } - - const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); - if (plcmt) { - bidObject.plcmt = plcmt; - } - } - - return bidObject; -} - -function isBanner(bid) { - return bid.mediaTypes && bid.mediaTypes.banner; -} - -/** - * Generate params that are common between all bids - * @param {single bid object} generalObject - * @param {bidderRequest} bidderRequest - * @returns {object} the common params object - */ -function generateGeneralParams(generalObject, bidderRequest) { - const domain = deepAccess(bidderRequest, 'refererInfo.domain') || window.location.hostname; - const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; - const {bidderCode, timeout} = bidderRequest; - const generalBidParams = generalObject.params; - - // these params are snake_case instead of camelCase to allow backwards compatability on the server. - // in the future, these will be converted to camelCase to match our convention. - const generalParams = { - wrapper_type: 'prebidjs', - wrapper_vendor: '$$PREBID_GLOBAL$$', - wrapper_version: '$prebid.version$', - adapter_version: ADAPTER_VERSION, - publisher_id: generalBidParams.org, - publisher_name: domain, - site_domain: domain, - dnt: getDNT() ? 1 : 0, - device_type: getDeviceType(navigator.userAgent), - ua: navigator.userAgent, - is_wrapper: !!generalBidParams.isWrapper, - session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), - tmax: timeout - } - - const userIdsParam = getBidIdParameter('userId', generalObject); - if (userIdsParam) { - generalParams.userIds = JSON.stringify(userIdsParam); - } - - const ortb2Metadata = bidderRequest.ortb2 || {}; - if (ortb2Metadata.site) { - generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); - } - if (ortb2Metadata.user) { - generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); - } - - if (syncEnabled) { - const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); - if (allowedSyncMethod) { - generalParams.cs_method = allowedSyncMethod; - } - } - - if (bidderRequest.auctionStart) { - generalParams.auction_start = bidderRequest.auctionStart; - } - - if (bidderRequest.uspConsent) { - generalParams.us_privacy = bidderRequest.uspConsent; - } - - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; - generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - - if (bidderRequest.gppConsent) { - generalParams.gpp = bidderRequest.gppConsent.gppString; - generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - generalParams.gpp = bidderRequest.ortb2.regs.gpp; - generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - - if (generalBidParams.ifa) { - generalParams.ifa = generalBidParams.ifa; - } - - if (generalObject.schain) { - generalParams.schain = getSupplyChain(generalObject.schain); - } - - if (bidderRequest && bidderRequest.refererInfo) { - generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); - generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); - } - - return generalParams; -} diff --git a/modules/openwebBidAdapter.md b/modules/openwebBidAdapter.md index 5450182265c..c5bc10c3c12 100644 --- a/modules/openwebBidAdapter.md +++ b/modules/openwebBidAdapter.md @@ -13,7 +13,7 @@ Module that connects to OpenWeb's demand sources. The OpenWeb adapter requires setup and approval from OpenWeb. Please reach out to monetization@openweb.com to create an OpenWeb account. -The adapter supports Video and Display demand. +The adapter supports Video(instream), Banner, Native and multi-format bid requests. # Bid Parameters ## Video diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index a99bd1c5325..56752d1302c 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -2,9 +2,8 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import {mergeDeep} from '../src/utils.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const bidderConfig = 'hb_pb_ortb'; const bidderVersion = '2.0'; @@ -14,12 +13,11 @@ export const DEFAULT_PH = '2d1251ae-7f3a-47cf-bd2a-2f288854a0ba'; export const spec = { code: 'openx', gvlid: 69, - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid, buildRequests, interpretResponse, - getUserSyncs, - transformBidParams + getUserSyncs }; registerBidder(spec); @@ -27,7 +25,12 @@ registerBidder(spec); const converter = ortbConverter({ context: { netRevenue: true, - ttl: 300 + ttl: 300, + nativeRequest: { + eventtrackers: [ + {event: 1, methods: [1, 2]}, + ] + } }, imp(buildImp, bidRequest, context) { const imp = buildImp(bidRequest, context); @@ -82,11 +85,6 @@ const converter = ortbConverter({ bidResponse.meta.advertiserId = bid.ext.buyer_id; bidResponse.meta.brandId = bid.ext.brand_id; } - const {ortbResponse} = context; - if (ortbResponse.ext && ortbResponse.ext.paf) { - bidResponse.meta.paf = Object.assign({}, ortbResponse.ext.paf); - bidResponse.meta.paf.content_id = utils.deepAccess(bid, 'ext.paf.content_id'); - } return bidResponse; }, response(buildResponse, bidResponses, ortbResponse, context) { @@ -116,10 +114,10 @@ const converter = ortbConverter({ }); return { bids: response.bids, - fledgeAuctionConfigs, + paapi: fledgeAuctionConfigs, } } else { - return response.bids + return response } }, overrides: { @@ -144,22 +142,12 @@ const converter = ortbConverter({ bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}} } orig(imp, bidRequest, context); - if (imp.video && videoParams?.context === 'outstream') { - imp.video.placement = imp.video.placement || 4; - } } } } } }); -function transformBidParams(params, isOpenRtb) { - return convertTypes({ - 'unit': 'string', - 'customFloor': 'number' - }, params); -} - function isBidRequestValid(bidRequest) { const hasDelDomainOrPlatform = bidRequest.params.delDomain || bidRequest.params.platform; @@ -173,11 +161,14 @@ function isBidRequestValid(bidRequest) { return !!(bidRequest.params.unit && hasDelDomainOrPlatform); } -function buildRequests(bids, bidderRequest) { - let videoBids = bids.filter(bid => isVideoBid(bid)); - let bannerBids = bids.filter(bid => isBannerBid(bid)); - let requests = bannerBids.length ? [createRequest(bannerBids, bidderRequest, BANNER)] : []; - videoBids.forEach(bid => { +function buildRequests(bidRequests, bidderRequest) { + let videoRequests = bidRequests.filter(bidRequest => isVideoBidRequest(bidRequest)); + let bannerAndNativeRequests = bidRequests.filter(bidRequest => isBannerBidRequest(bidRequest) || isNativeBidRequest(bidRequest)) + // In case of multi-format bids remove `video` from mediaTypes as for video a separate bid request is built + .map(bid => ({...bid, mediaTypes: {...bid.mediaTypes, video: undefined}})); + + let requests = bannerAndNativeRequests.length ? [createRequest(bannerAndNativeRequests, bidderRequest, null)] : []; + videoRequests.forEach(bid => { requests.push(createRequest([bid], bidderRequest, VIDEO)); }); return requests; @@ -191,12 +182,17 @@ function createRequest(bidRequests, bidderRequest, mediaType) { } } -function isVideoBid(bid) { - return utils.deepAccess(bid, 'mediaTypes.video'); +function isVideoBidRequest(bidRequest) { + return utils.deepAccess(bidRequest, 'mediaTypes.video'); +} + +function isNativeBidRequest(bidRequest) { + return utils.deepAccess(bidRequest, 'mediaTypes.native'); } -function isBannerBid(bid) { - return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +function isBannerBidRequest(bidRequest) { + const isNotVideoOrNativeBid = !isVideoBidRequest(bidRequest) && !isNativeBidRequest(bidRequest) + return utils.deepAccess(bidRequest, 'mediaTypes.banner') || isNotVideoOrNativeBid; } function interpretResponse(resp, req) { diff --git a/modules/openxBidAdapter.md b/modules/openxBidAdapter.md index a39aa1580cd..61b6426d113 100644 --- a/modules/openxBidAdapter.md +++ b/modules/openxBidAdapter.md @@ -34,6 +34,16 @@ Please note you should only include either openxBidAdapter or openxOrtbBidAdapte | `delDomain` | required | String | OpenX delivery domain provided by your OpenX representative. | "PUBLISHER-d.openx.net" | `video` | optional | OpenRTB video subtypes | Use of adUnit.mediaTypes.video is now preferred. | `{ video: {mimes: ['video/mp4']}` +## Native + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `delDomain` or `platform` | required | String | OpenX delivery domain or platform id provided by your OpenX representative. | "PUBLISHER-d.openx.net" or "555not5a-real-plat-form-id0123456789" +| `unit` | required | String | OpenX ad unit ID provided by your OpenX representative. | "1611023122" +| `customParams` | optional | Object | User-defined targeting key-value pairs. customParams applies to a specific unit. | `{key1: "v1", key2: ["v2","v3"]}` +| `customFloor` | optional | Number | Minimum price in USD. customFloor applies to a specific unit. For example, use the following value to set a $1.50 floor: 1.50

**WARNING:**
Misuse of this parameter can impact revenue | 1.50 +| `doNotTrack` | optional | Boolean | Prevents advertiser from using data for this user.

**WARNING:**
Request-level setting. May impact revenue. | true +| `coppa` | optional | Boolean | Enables Child's Online Privacy Protection Act (COPPA) regulations. Use of `pbjs.setConfig({coppa: true});` is now preferred. | true # Example ```javascript @@ -84,7 +94,42 @@ var adUnits = [ mimes: ['video/x-ms-wmv, video/mp4'] // mediaTypes.video preferred } } - }]p + }] + }, + { + code: 'native1', + mediaTypes: { + native: { + ortb: { + ver: '1.2', + assets: [ + { + required: 1, + img: { + type: 1, + hmin: 50 + }, + }, { + required: 1, + title: { + len: 80 + } + } + ] + } + } + }, + bids: [{ + bidder: 'openx', + params: { + unit: '1611023124', + delDomain: 'PUBLISHER-d.openx.net', + customParams: { + key1: 'v1', + key2: ['v2', 'v3'] + } + } + }] } ]; ``` diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index 957192d1bec..486d5ac726b 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -525,7 +525,6 @@ function createImp(bidRequest) { playbackmethod: videoReq.playbackmethod || VIDEO_DEFAULTS.PLAYBACK_METHODS, delivery: videoReq.delivery || VIDEO_DEFAULTS.DELIVERY, api: videoReq.api || VIDEO_DEFAULTS.API, - placement: videoReq.context === OUTSTREAM ? 3 : 1, }; mediaType = VIDEO; diff --git a/modules/opscoBidAdapter.js b/modules/opscoBidAdapter.js index 87d00f14de0..2ad14227804 100644 --- a/modules/opscoBidAdapter.js +++ b/modules/opscoBidAdapter.js @@ -19,7 +19,11 @@ export const spec = { Array.isArray(bid.mediaTypes?.banner?.sizes)), buildRequests: (validBidRequests, bidderRequest) => { - const {publisherId, placementId, siteId} = validBidRequests[0].params; + if (!validBidRequests || !bidderRequest) { + return; + } + + const {publisherId, siteId} = validBidRequests[0].params; const payload = { id: bidderRequest.bidderRequestId, @@ -28,7 +32,7 @@ export const spec = { banner: {format: extractSizes(bidRequest)}, ext: { opsco: { - placementId: placementId, + placementId: bidRequest.params.placementId, publisherId: publisherId, } } diff --git a/modules/optableBidAdapter.js b/modules/optableBidAdapter.js index f6c7cf00a35..d2dae252e6c 100644 --- a/modules/optableBidAdapter.js +++ b/modules/optableBidAdapter.js @@ -17,17 +17,42 @@ const BIDDER_CODE = 'optable'; const DEFAULT_REGION = 'ca' const DEFAULT_ORIGIN = 'https://ads.optable.co' +function getOrigin() { + return config.getConfig('optable.origin') ?? DEFAULT_ORIGIN; +} + +function getBaseUrl() { + const region = config.getConfig('optable.region') ?? DEFAULT_REGION; + return `${getOrigin()}/${region}` +} + export const spec = { code: BIDDER_CODE, isBidRequestValid: function(bid) { return !!bid.params?.site }, buildRequests: function(bidRequests, bidderRequest) { - const region = config.getConfig('optable.region') ?? DEFAULT_REGION - const origin = config.getConfig('optable.origin') ?? DEFAULT_ORIGIN - const requestURL = `${origin}/${region}/ortb2/v1/ssp/bid` + const requestURL = `${getBaseUrl()}/ortb2/v1/ssp/bid` const data = converter.toORTB({ bidRequests, bidderRequest, context: { mediaType: BANNER } }); - return { method: 'POST', url: requestURL, data } }, + buildPAAPIConfigs: function(bidRequests) { + const origin = getOrigin(); + return bidRequests + .filter(req => req.ortb2Imp?.ext?.ae) + .map(bid => ({ + bidId: bid.bidId, + config: { + seller: origin, + decisionLogicURL: `${getBaseUrl()}/paapi/v1/ssp/decision-logic.js?origin=${bid.params.site}`, + interestGroupBuyers: [origin], + perBuyerMultiBidLimits: { + [origin]: 100 + }, + perBuyerCurrencies: { + [origin]: 'USD' + } + } + })) + }, interpretResponse: function(response, request) { const bids = converter.fromORTB({ response: response.body, request: request.data }).bids const auctionConfigs = (response.body.ext?.optable?.fledge?.auctionconfigs ?? []).map((cfg) => { @@ -35,7 +60,7 @@ export const spec = { return { bidId: impid, config } }) - return { bids, fledgeAuctionConfigs: auctionConfigs } + return { bids, paapi: auctionConfigs } }, supportedMediaTypes: [BANNER] } diff --git a/modules/optableRtdProvider.js b/modules/optableRtdProvider.js new file mode 100644 index 00000000000..a8a69ce2345 --- /dev/null +++ b/modules/optableRtdProvider.js @@ -0,0 +1,201 @@ +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {config} from '../src/config.js'; +import {submodule} from '../src/hook.js'; +import {deepAccess, mergeDeep, prefixLog} from '../src/utils.js'; + +const MODULE_NAME = 'optable'; +export const LOG_PREFIX = `[${MODULE_NAME} RTD]:`; +const optableLog = prefixLog(LOG_PREFIX); +const {logMessage, logWarn, logError} = optableLog; + +/** + * Extracts the parameters for Optable RTD module from the config object passed at instantiation + * @param {Object} moduleConfig Configuration object for the module + */ +export const parseConfig = (moduleConfig) => { + let bundleUrl = deepAccess(moduleConfig, 'params.bundleUrl', null); + let adserverTargeting = deepAccess(moduleConfig, 'params.adserverTargeting', true); + let handleRtd = deepAccess(moduleConfig, 'params.handleRtd', null); + + // If present, trim the bundle URL + if (typeof bundleUrl === 'string') { + bundleUrl = bundleUrl.trim(); + } + + // Verify that bundleUrl is a valid URL: only secure (HTTPS) URLs are allowed + if (typeof bundleUrl === 'string' && bundleUrl.length && !bundleUrl.startsWith('https://')) { + throw new Error( + LOG_PREFIX + ' Invalid URL format for bundleUrl in moduleConfig. Only HTTPS URLs are allowed.' + ); + } + + if (handleRtd && typeof handleRtd !== 'function') { + throw new Error(LOG_PREFIX + ' handleRtd must be a function'); + } + + return {bundleUrl, adserverTargeting, handleRtd}; +} + +/** + * Default function to handle/enrich RTD data + * @param reqBidsConfigObj Bid request configuration object + * @param optableExtraData Additional data to be used by the Optable SDK + * @param mergeFn Function to merge data + * @returns {Promise} + */ +export const defaultHandleRtd = async (reqBidsConfigObj, optableExtraData, mergeFn) => { + const optableBundle = /** @type {Object} */ (window.optable); + // Get targeting data from cache, if available + let targetingData = optableBundle?.instance?.targetingFromCache(); + // If no targeting data is found in the cache, call the targeting function + if (!targetingData) { + // Call Optable DCN for targeting data and return the ORTB2 object + targetingData = await optableBundle?.instance?.targeting(); + } + logMessage('Original targeting data from targeting(): ', targetingData); + + if (!targetingData || !targetingData.ortb2) { + logWarn('No targeting data found'); + return; + } + + mergeFn( + reqBidsConfigObj.ortb2Fragments.global, + targetingData.ortb2, + ); + logMessage('Prebid\'s global ORTB2 object after merge: ', reqBidsConfigObj.ortb2Fragments.global); +}; + +/** + * Get data from Optable and merge it into the global ORTB2 object + * @param {Function} handleRtdFn Function to handle RTD data + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Object} optableExtraData Additional data to be used by the Optable SDK + * @param {Function} mergeFn Function to merge data + */ +export const mergeOptableData = async (handleRtdFn, reqBidsConfigObj, optableExtraData, mergeFn) => { + if (handleRtdFn.constructor.name === 'AsyncFunction') { + await handleRtdFn(reqBidsConfigObj, optableExtraData, mergeFn); + } else { + handleRtdFn(reqBidsConfigObj, optableExtraData, mergeFn); + } +}; + +/** + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} callback Called on completion + * @param {Object} moduleConfig Configuration for Optable RTD module + * @param {Object} userConsent + */ +export const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { + try { + // Extract the bundle URL from the module configuration + const {bundleUrl, handleRtd} = parseConfig(moduleConfig); + + const handleRtdFn = handleRtd || defaultHandleRtd; + const optableExtraData = config.getConfig('optableRtdConfig') || {}; + + if (bundleUrl) { + // If bundleUrl is present, load the Optable JS bundle + // by using the loadExternalScript function + logMessage('Custom bundle URL found in config: ', bundleUrl); + + // Load Optable JS bundle and merge the data + loadExternalScript(bundleUrl, MODULE_TYPE_RTD, MODULE_NAME, () => { + logMessage('Successfully loaded Optable JS bundle'); + mergeOptableData(handleRtdFn, reqBidsConfigObj, optableExtraData, mergeDeep).then(callback, callback); + }, document); + } else { + // At this point, we assume that the Optable JS bundle is already + // present on the page. If it is, we can directly merge the data + // by passing the callback to the optable.cmd.push function. + logMessage('Custom bundle URL not found in config. ' + + 'Assuming Optable JS bundle is already present on the page'); + window.optable = window.optable || { cmd: [] }; + window.optable.cmd.push(() => { + logMessage('Optable JS bundle found on the page'); + mergeOptableData(handleRtdFn, reqBidsConfigObj, optableExtraData, mergeDeep).then(callback, callback); + }); + } + } catch (error) { + // If an error occurs, log it and call the callback + // to continue with the auction + logError(error); + callback(); + } +} + +/** + * Get Optable targeting data and merge it into the ad units + * @param adUnits Array of ad units + * @param moduleConfig Module configuration + * @param userConsent User consent + * @param auction Auction object + * @returns {Object} Targeting data + */ +export const getTargetingData = (adUnits, moduleConfig, userConsent, auction) => { + // Extract `adserverTargeting` from the module configuration + const {adserverTargeting} = parseConfig(moduleConfig); + logMessage('Ad Server targeting: ', adserverTargeting); + + if (!adserverTargeting) { + logMessage('Ad server targeting is disabled'); + return {}; + } + + const targetingData = {}; + + // Get the Optable targeting data from the cache + const optableTargetingData = window?.optable?.instance?.targetingKeyValuesFromCache() || {}; + + // If no Optable targeting data is found, return an empty object + if (!Object.keys(optableTargetingData).length) { + logWarn('No Optable targeting data found'); + return targetingData; + } + + // Merge the Optable targeting data into the ad units + adUnits.forEach(adUnit => { + targetingData[adUnit] = targetingData[adUnit] || {}; + mergeDeep(targetingData[adUnit], optableTargetingData); + }); + + // If the key contains no data, remove it + Object.keys(targetingData).forEach((adUnit) => { + Object.keys(targetingData[adUnit]).forEach((key) => { + if (!targetingData[adUnit][key] || !targetingData[adUnit][key].length) { + delete targetingData[adUnit][key]; + } + }); + + // If the ad unit contains no data, remove it + if (!Object.keys(targetingData[adUnit]).length) { + delete targetingData[adUnit]; + } + }); + + logMessage('Optable targeting data: ', targetingData); + return targetingData; +}; + +/** + * Dummy init function + * @param {Object} config Module configuration + * @param {boolean} userConsent User consent + * @returns true + */ +const init = (config, userConsent) => { + return true; +} + +// Optable RTD submodule +export const optableSubmodule = { + name: MODULE_NAME, + init, + getBidRequestData, + getTargetingData, +} + +// Register the Optable RTD submodule +submodule('realTimeData', optableSubmodule); diff --git a/modules/optableRtdProvider.md b/modules/optableRtdProvider.md new file mode 100644 index 00000000000..1250437f6f0 --- /dev/null +++ b/modules/optableRtdProvider.md @@ -0,0 +1,141 @@ +# Optable RTD Submodule + +## Overview + + Module Name: Optable RTD Provider + Module Type: RTD Provider + Maintainer: prebid@optable.co + +## 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. + +## Usage + +### Integration + +Compile the Optable RTD Module with other modules and adapters into your Prebid.js build: + +```bash +gulp build --modules="rtdModule,optableRtdProvider,appnexusBidAdapter,..." +``` + +> Note that Optable RTD module is dependent on the global real-time data module, `rtdModule`. + +### Preloading Optable SDK bundle + +In order to use the module you first need to register with Optable and obtain a bundle URL. The bundle URL may be specified as a `bundleUrl` parameter to the script, or otherwise it can be added directly to the page source as: + +```html + +``` + +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. + +```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: '', + }, + }, + ], + }, +}); +``` + +### 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 | + +## Publisher Customized RTD Handler Function + +When there is more pre-processing or post-processing needed prior/post calling Optable bundle - a custom `handleRtd` +function can be supplied to do that. +This function will also be responsible for the `reqBidsConfigObj` enrichment. +It will also receive the `optableExtraData` object, which can contain the extra data required for the enrichment and +shouldn't be shared with other RTD providers/bidders. +`mergeFn` parameter taken by `handleRtd` is a standard Prebid.js utility function that take an object to be enriched and +an object to enrich with: the second object's fields will be merged into the first one (also see the code of an example +mentioned below): + +```javascript +mergeFn( + reqBidsConfigObj.ortb2Fragments.global, // or other nested object as needed + rtdData, +); +``` + +A `handleRtd` function implementation has access to its surrounding context including capturing a `pbjs` object, calling `pbjs.getConfig()` and f.e. reading off the `consentManagement` config to make the appropriate decision based on it. + +## Example + +If you want to see an example of how the optable RTD module works, run the following command: + +```bash +gulp serve --modules=optableRtdProvider,consentManagementGpp,consentManagementTcf,appnexusBidAdapter +``` + +and then open the following URL in your browser: + +[`http://localhost:9999/integrationExamples/gpt/optableRtdProvider_example.html`](http://localhost:9999/integrationExamples/gpt/optableRtdProvider_example.html) + +Open the browser console to see the logs. + +## Maintainer contacts + +Any suggestions or questions can be directed to [prebid@optable.co](mailto:prebid@optable.co). + +Alternatively please open a new [issue](https://github.com/prebid/prebid-server-java/issues/new) or [pull request](https://github.com/prebid/prebid-server-java/pulls) in this repository. diff --git a/modules/optidigitalBidAdapter.js b/modules/optidigitalBidAdapter.js index 396131fd8aa..f8dea0f5a92 100755 --- a/modules/optidigitalBidAdapter.js +++ b/modules/optidigitalBidAdapter.js @@ -1,6 +1,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import {deepAccess, parseSizesInput} from '../src/utils.js'; +import {deepAccess, isPlainObject, parseSizesInput} from '../src/utils.js'; import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; /** @@ -247,7 +247,7 @@ function _getFloor (bid, sizes, currency) { mediaType: 'banner', size: size }); - if (typeof floorInfo === 'object' && floorInfo.currency === CUR && !isNaN(parseFloat(floorInfo.floor))) { + if (isPlainObject(floorInfo) && floorInfo.currency === CUR && !isNaN(parseFloat(floorInfo.floor))) { floor = parseFloat(floorInfo.floor); } } catch (err) {} diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index bd564e3a260..71cd2a3b79b 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -187,7 +187,11 @@ export function setScoresURL() { if (apiVersion === 'v1') { newScoresURL = `${baseUrl}api/products/scores?c=${clientID}&h=${optimeraHost}&p=${optimeraPathName}&s=${device}`; } else { - newScoresURL = `${baseUrl}${clientID}/${optimeraHost}${optimeraPathName}.js`; + let encoded = encodeURIComponent(`${optimeraHost}${optimeraPathName}`) + .replaceAll('%2F', '/') + .replaceAll('%20', '+'); + + newScoresURL = `${baseUrl}${clientID}/${encoded}.js`; } if (scoresURL !== newScoresURL) { diff --git a/modules/optoutBidAdapter.js b/modules/optoutBidAdapter.js index f7b5934665c..f0010d54833 100644 --- a/modules/optoutBidAdapter.js +++ b/modules/optoutBidAdapter.js @@ -1,7 +1,7 @@ import { deepAccess } from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; const BIDDER_CODE = 'optout'; diff --git a/modules/orakiBidAdapter.js b/modules/orakiBidAdapter.js new file mode 100644 index 00000000000..d3ef143249b --- /dev/null +++ b/modules/orakiBidAdapter.js @@ -0,0 +1,19 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'oraki'; +const AD_URL = 'https://eu1.oraki.io/pbjs'; +const SYNC_URL = 'https://sync.oraki.io'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/orakiBidAdapter.md b/modules/orakiBidAdapter.md new file mode 100644 index 00000000000..ce35021a937 --- /dev/null +++ b/modules/orakiBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Oraki Bidder Adapter +Module Type: Oraki Bidder Adapter +Maintainer: ben@oraki.io +``` + +# Description + +Connects to Oraki exchange for bids. +Oraki 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: 'oraki', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'oraki', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'oraki', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index 0f912384db7..dc8293e74f2 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -3,7 +3,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { getGlobal } from '../src/prebidGlobal.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -93,6 +92,9 @@ export const spec = { if (bidderRequest && bidderRequest.refererInfo) { referer = bidderRequest.refererInfo.page || ''; } + if (bidRequest?.mediaTypes?.video) { + delete bidRequest.mediaTypes.video; + } bidRequest.params.bidfloor = getBidFloor(bidRequest); @@ -101,7 +103,7 @@ export const spec = { method: 'POST', options: { withCredentials: true }, data: { - v: getGlobal().version, + v: 'v' + '$prebid.version$', pageUrl: referer, ...bidRequest // get all data provided by bid request } diff --git a/modules/orbitsoftBidAdapter.js b/modules/orbitsoftBidAdapter.js index f55c7ff9917..a22afd16a49 100644 --- a/modules/orbitsoftBidAdapter.js +++ b/modules/orbitsoftBidAdapter.js @@ -25,7 +25,7 @@ let styleParamsMap = { }; export const spec = { code: BIDDER_CODE, - aliases: ['oas', '152media'], // short code and customer aliases + aliases: ['oas', '152media', 'paradocs'], // short code and customer aliases isBidRequestValid: function (bid) { switch (true) { case !('params' in bid): diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 6015ff37e08..c308cc2e067 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -5,7 +5,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import {OUTSTREAM} from '../src/video.js'; -import {_map, deepAccess, deepSetValue, isArray, logWarn, replaceAuctionPrice} from '../src/utils.js'; +import {_map, deepAccess, deepSetValue, logWarn, replaceAuctionPrice, setOnAny, parseGPTSingleSizeArrayToRtbSize, isPlainObject} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; @@ -14,7 +14,6 @@ import {Renderer} from '../src/Renderer.js'; const BIDDER_CODE = 'outbrain'; const GVLID = 164; const CURRENCY = 'USD'; -const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; const NATIVE_PARAMS = { title: { id: 0, name: 'title' }, icon: { id: 2, type: 1, name: 'img' }, @@ -23,6 +22,10 @@ const NATIVE_PARAMS = { body: { id: 4, name: 'data', type: 2 }, cta: { id: 1, type: 12, name: 'data' } }; +const NATIVE_ASSET_IDS = Object.entries(NATIVE_PARAMS).reduce((acc, [key, value]) => { + acc[value.id] = key; + return acc; +}, {}); const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const OB_USER_TOKEN_KEY = 'OB-USER-TOKEN'; @@ -75,7 +78,6 @@ export const spec = { const timeout = bidderRequest.timeout; const imps = validBidRequests.map((bid, id) => { - bid.netRevenue = 'net'; const imp = { id: id + 1 + '' } @@ -94,7 +96,7 @@ export const spec = { imp.video = getVideoAsset(bid); } else { imp.banner = { - format: transformSizes(bid.sizes) + format: bid.sizes?.map((size) => parseGPTSingleSizeArrayToRtbSize(size)) } } @@ -111,7 +113,7 @@ export const spec = { const request = { id: bidderRequest.bidderRequestId, site: { page, publisher }, - device: { ua }, + device: ortb2?.device || { ua }, source: { fd: 1 }, cur: [cur], tmax: timeout, @@ -174,7 +176,7 @@ export const spec = { } const { seatbid, cur } = serverResponse.body; - const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { + const bidResponses = seatbid.map(seat => seat.bid).flat().reduce((result, bid) => { result[bid.impid - 1] = bid; return result; }, []); @@ -193,7 +195,7 @@ export const spec = { cpm: bidResponse.price, creativeId: bidResponse.crid, ttl: 360, - netRevenue: bid.netRevenue === 'net', + netRevenue: true, currency: cur, mediaType: type, nurl: bidResponse.nurl, @@ -288,19 +290,6 @@ function parseNative(bid) { return result; } -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; - } - } -} - -function flatten(arr) { - return [].concat(...arr); -} - function getNativeAssets(bid) { return _map(bid.nativeParams, (bidParams, key) => { const props = NATIVE_PARAMS[key]; @@ -319,7 +308,7 @@ function getNativeAssets(bid) { } if (bidParams.sizes) { - const sizes = flatten(bidParams.sizes); + const sizes = bidParams.sizes.flat(); w = parseInt(sizes[0], 10); h = parseInt(sizes[1], 10); } @@ -339,7 +328,7 @@ function getNativeAssets(bid) { } function getVideoAsset(bid) { - const sizes = flatten(bid.mediaTypes.video.playerSize); + const sizes = bid.mediaTypes.video.playerSize.flat(); return { w: parseInt(sizes[0], 10), h: parseInt(sizes[1], 10), @@ -355,40 +344,18 @@ function getVideoAsset(bid) { maxduration: bid.mediaTypes.video.maxduration, startdelay: bid.mediaTypes.video.startdelay, placement: bid.mediaTypes.video.placement, + plcmt: bid.mediaTypes.video.plcmt, linearity: bid.mediaTypes.video.linearity }; } -/* Turn bid request sizes into ut-compatible format */ -function transformSizes(requestSizes) { - if (!isArray(requestSizes)) { - return []; - } - - if (requestSizes.length === 2 && !isArray(requestSizes[0])) { - return [{ - w: parseInt(requestSizes[0], 10), - h: parseInt(requestSizes[1], 10) - }]; - } else if (isArray(requestSizes[0])) { - return requestSizes.map(item => - ({ - w: parseInt(item[0], 10), - h: parseInt(item[1], 10) - }) - ); - } - - return []; -} - function _getFloor(bid, type) { const floorInfo = bid.getFloor({ currency: CURRENCY, mediaType: type, size: '*' }); - if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + if (isPlainObject(floorInfo) && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { return parseFloat(floorInfo.floor); } return null; diff --git a/modules/overtoneRtdProvider.js b/modules/overtoneRtdProvider.js new file mode 100644 index 00000000000..ab8b45f3544 --- /dev/null +++ b/modules/overtoneRtdProvider.js @@ -0,0 +1,79 @@ +import { submodule } from '../src/hook.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { safeJSONParse, logMessage as _logMessage } from '../src/utils.js'; + +export const OVERTONE_URL = 'https://prebid-1.overtone.ai/contextual'; + +const logMessage = (...args) => { + _logMessage('Overtone', ...args); +}; + +export async function fetchContextData(url = window.location.href) { + const pageUrl = encodeURIComponent(url); + const requestUrl = `${OVERTONE_URL}?URL=${pageUrl}&InApp=False`; + const request = window.ajaxBuilder || ajaxBuilder(); + + return new Promise((resolve, reject) => { + logMessage('Sending request to:', requestUrl); + request(requestUrl, { + success: (response) => { + const data = safeJSONParse(response); + logMessage('Fetched data:', data); + + if (!data || typeof data.status !== 'number') { + reject(new Error('Invalid response format')); + return; + } + + switch (data.status) { + case 1: // Success + resolve({ categories: data.categories || [] }); + break; + case 3: // Fail + case 4: // Ignore + resolve({ categories: [] }); + break; + default: + reject(new Error(`Unexpected response status: ${data.status}`)); + } + }, + error: (err) => { + logMessage('Error during request:', err); + reject(err); + }, + }); + }); +} + +function init(config) { + logMessage('init', config); + return true; +} + +export const overtoneRtdProvider = { + name: 'overtone', + init: init, + getBidRequestData: function (bidReqConfig, callback) { + fetchContextData() + .then((contextData) => { + if (contextData) { + if (!bidReqConfig.ortb2Fragments.global.site.ext) { + bidReqConfig.ortb2Fragments.global.site.ext = {}; + } + + bidReqConfig.ortb2Fragments.global.site.ext.data = contextData; + } + callback(); + }) + .catch((error) => { + logMessage('Error fetching context data', error); + callback(); + }); + }, +}; + +submodule('realTimeData', overtoneRtdProvider); + +export const overtoneModule = { + fetchContextData, +}; diff --git a/modules/overtoneRtdProvider.md b/modules/overtoneRtdProvider.md new file mode 100644 index 00000000000..aa2b3b0164a --- /dev/null +++ b/modules/overtoneRtdProvider.md @@ -0,0 +1,97 @@ +# Overtone Rtd Provider + +## Overview + +Module Name: Overtone Rtd Provider + +Module Type: Rtd Provider + +Maintainer: tech@overtone.ai + +The Overtone Real-Time Data (RTD) Module is a plug-and-play Prebid.js adapter designed to provide contextual classification results on the publisher’s page through Overtone’s contextual API. + + +## Downloading and Configuring the Overtone RTD Module + +Navigate to https://docs.prebid.org/download.html and select the box labeled Overtone Prebid Contextual Evaluation. If Prebid.js is already installed on your site, ensure other necessary modules and adapters are selected. Upon clicking the "Get Prebid.js" button, a customized Prebid.js version will be built with your selections. + +Direct link to the Overtone module in the Prebid.js repository: + +The client must provide Overtone with all the addresses using the Prebid module to whitelist those domains. Failure to whitelist addresses will result in an invalid request to the Overtone Contextual API. + + +## Functionality + +At a high level, the Overtone RTD Module makes requests to the Overtone Contextual API during page load. It fetches and categorizes content for each page, which is then available for targeting in Prebid.js. Contextual data includes content classifications, which help advertisers make informed decisions about ad placements. + + +## Available Classifications + +Content Categories: + +Key: categories + +Possible Values: Various identifiers such as ovtn_004, ovtn_104, etc. + +Description: Content Categories represent Overtone’s classification of page content based on its contextual analysis. + +Please contact tech@overtone.ai for more information about our exact categories in brand safety, type, and tone. + + +## Configuration Highlight + +The configuration for the Overtone RTD module in Prebid.js might resemble the following: + +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'overtone', + params: { + + } + }] + } +}); + + +## API Response Handling + +The Overtone RTD module processes responses from the Overtone Contextual API. A typical response might include the following: + +Status: Indicates the API request status (1 for success, 3 for fail, 4 for ignore). + +Categories: An array of classification identifiers. + +For example: + +{ + "categories": ["ovtn_004", "ovtn_104", "ovtn_309", "ovtn_202"], + "status": 1 +} + +The module ensures that these values are integrated into Prebid.js’s targeting configuration for the current page. + + +## Testing and Validation + +The functionality of the Overtone RTD module can be validated using the associated test suite provided in overtoneRtdProvider_spec.mjs. The test suite simulates different API response scenarios to verify module behavior under varied conditions. + +Example Test Cases: + +Successful Data Retrieval: + +Input: URL with valid classification data. + +Expected Output: Categories array populated with identifiers. + +Failed Request: + +Input: URL resulting in a failure. + +Expected Output: Empty categories array. + +Ignored URL: + +Input: URL to be ignored by the API. + +Expected Output: Empty categories array. diff --git a/modules/ownadxBidAdapter.js b/modules/ownadxBidAdapter.js new file mode 100644 index 00000000000..5843735f314 --- /dev/null +++ b/modules/ownadxBidAdapter.js @@ -0,0 +1,99 @@ +import { parseSizesInput, isEmpty } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.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 + */ + +const BIDDER_CODE = 'ownadx'; +const CUR = 'USD'; +const CREATIVE_TTL = 300; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * 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. + */ + isBidRequestValid: function (bid) { + return !!(bid.params.tokenId && bid.params.sspId && bid.params.seatId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param validBidRequests + * @param bidderRequest + * @return Array Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map(bidRequest => { + const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); + let mtype = 0; + if (bidRequest.mediaTypes[BANNER]) { + mtype = 1; + } else { + mtype = 2; + } + + let tkn = bidRequest.params.tokenId; + let seatid = bidRequest.params.seatId; + let sspid = bidRequest.params.sspId; + + const payload = { + sizes: sizes, + slotBidId: bidRequest.bidId, + PageUrl: bidderRequest.refererInfo.page, + mediaChannel: mtype + }; + return { + method: 'POST', + url: `https://pbs-js.prebid-ownadx.com/publisher/prebid/${seatid}/${sspid}?token=${tkn}`, + data: payload + }; + }); + }, + + /** + * 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. + */ + interpretResponse: function (serverResponse) { + const response = serverResponse.body; + const bids = []; + if (isEmpty(response)) { + return bids; + } + const responseBid = { + width: response.width, + height: response.height, + token: response.tokenId, + ttl: CREATIVE_TTL, + requestId: response.slotBidId, + aType: response.adType || '1', + cpm: response.cpm, + creativeId: response.creativeId || 0, + netRevenue: response.netRevenue || false, + currency: response.currency || CUR, + meta: { + mediaType: response.mediaType || BANNER, + advertiserDomains: response.advertiserDomains || [] + }, + ad: response.adm + }; + bids.push(responseBid); + return bids; + } + +}; + +registerBidder(spec); diff --git a/modules/oxxionAnalyticsAdapter.js b/modules/oxxionAnalyticsAdapter.js index b3bc2d3479f..9e18c92d25b 100644 --- a/modules/oxxionAnalyticsAdapter.js +++ b/modules/oxxionAnalyticsAdapter.js @@ -3,6 +3,7 @@ import adapterManager from '../src/adapterManager.js'; import { EVENTS } from '../src/constants.js'; import { ajax } from '../src/ajax.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import { deepClone } from '../src/utils.js'; const analyticsType = 'endpoint'; const url = 'URL_TO_SERVER_ENDPOINT'; @@ -125,7 +126,7 @@ function addTimeout(args) { let stringArgs = JSON.parse(dereferenceWithoutRenderer(args)); argsDereferenced = stringArgs; argsDereferenced.forEach((attr) => { - argsCleaned.push(filterAttributes(JSON.parse(JSON.stringify(attr)), false)); + argsCleaned.push(filterAttributes(deepClone(attr), false)); }); if (auctionEnd[eventType] == undefined) { auctionEnd[eventType] = [] } auctionEnd[eventType].push(argsCleaned); diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 0d921f57cda..ead659a7f3b 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -8,7 +8,7 @@ import { contains, mergeDeep, parseUrl, - generateUUID + generateUUID, isInteger, deepClone, getBidIdParameter } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; @@ -17,15 +17,15 @@ import {getPriceBucketString} from '../src/cpmBucketManager.js'; import { Renderer } from '../src/Renderer.js'; import {getRefererInfo} from '../src/refererDetection.js'; const BIDDER_CODE = 'ozone'; -const ORIGIN = 'https://elb.the-ozone-project.com' // applies only to auction & cookie +const ORIGIN = 'https://elb.the-ozone-project.com'; // applies only to auction & cookie const AUCTIONURI = '/openrtb2/auction'; const OZONECOOKIESYNC = '/static/load-cookie.html'; const OZONE_RENDERER_URL = 'https://prebid.the-ozone-project.com/ozone-renderer.js'; const ORIGIN_DEV = 'https://test.ozpr.net'; -const OZONEVERSION = '2.9.1'; +const OZONEVERSION = '3.0.0'; export const spec = { gvlid: 524, - aliases: [{code: 'lmc', gvlid: 524}, {code: 'venatus', gvlid: 524}], + aliases: [{code: 'venatus', gvlid: 524}], version: OZONEVERSION, code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], @@ -38,7 +38,7 @@ export const spec = { 'auctionUrl': ORIGIN + AUCTIONURI, 'cookieSyncUrl': ORIGIN + OZONECOOKIESYNC, 'rendererUrl': OZONE_RENDERER_URL, - 'batchRequests': false /* you can change this to true OR override it in the config: config.ozone.batchRequests */ + 'batchRequests': false /* you can change this to true OR numeric OR override it in the config: config.ozone.batchRequests = true/false/number */ }, loadWhitelabelData(bid) { if (this.propertyBag.whitelabel) { return; } @@ -47,7 +47,7 @@ export const spec = { this.propertyBag.whitelabel.logId = bidder.toUpperCase(); this.propertyBag.whitelabel.bidder = bidder; let bidderConfig = config.getConfig(bidder) || {}; - logInfo('got bidderConfig: ', JSON.parse(JSON.stringify(bidderConfig))); + logInfo('got bidderConfig: ', deepClone(bidderConfig)); if (bidderConfig.kvpPrefix) { this.propertyBag.whitelabel.keyPrefix = bidderConfig.kvpPrefix; } @@ -76,10 +76,22 @@ export const spec = { } } if (bidderConfig.hasOwnProperty('batchRequests')) { - this.propertyBag.whitelabel.batchRequests = bidderConfig.batchRequests; + if (this.batchValueIsValid(bidderConfig.batchRequests)) { + this.propertyBag.whitelabel.batchRequests = bidderConfig.batchRequests; + } else { + logError('invalid config: batchRequest'); + } + } + if (bidderConfig.hasOwnProperty('videoParams')) { + this.propertyBag.whitelabel.videoParams = bidderConfig.videoParams; } if (arr.hasOwnProperty('batchRequests')) { - this.propertyBag.whitelabel.batchRequests = true; + let getBatch = parseInt(arr.batchRequests); + if (this.batchValueIsValid(getBatch)) { + this.propertyBag.whitelabel.batchRequests = getBatch; + } else { + logError('invalid GET: batchRequests'); + } } try { if (arr.hasOwnProperty('auction') && arr.auction === 'dev') { @@ -91,7 +103,10 @@ export const spec = { this.propertyBag.whitelabel.cookieSyncUrl = ORIGIN_DEV + OZONECOOKIESYNC; } } catch (e) {} - logInfo('set propertyBag.whitelabel to', this.propertyBag.whitelabel); + logInfo('whitelabel: ', this.propertyBag.whitelabel); + }, + batchValueIsValid(batch) { + return typeof batch === 'boolean' || (typeof batch === 'number' && batch > 0); }, getAuctionUrl() { return this.propertyBag.whitelabel.auctionUrl; @@ -102,68 +117,76 @@ export const spec = { getRendererUrl() { return this.propertyBag.whitelabel.rendererUrl; }, - isBatchRequests() { - logInfo('isBatchRequests going to return ', this.propertyBag.whitelabel.batchRequests); - return this.propertyBag.whitelabel.batchRequests; + getVideoPlacementValue: function(context) { + if (['instream', 'outstream'].indexOf(context) < 0) return null; /* do not allow arbitrary strings */ + return deepAccess(this.propertyBag, `whitelabel.videoParams.${context}`, null); + }, + getBatchRequests() { + if (this.propertyBag.whitelabel.batchRequests === true) { return 10; } + if (typeof this.propertyBag.whitelabel.batchRequests === 'number' && this.propertyBag.whitelabel.batchRequests > 0) { + return this.propertyBag.whitelabel.batchRequests; + } + return false; }, isBidRequestValid(bid) { + let vf = 'VALIDATION FAILED'; this.loadWhitelabelData(bid); logInfo('isBidRequestValid : ', config.getConfig(), bid); let adUnitCode = bid.adUnitCode; // adunit[n].code - let err1 = 'VALIDATION FAILED : missing {param} : siteId, placementId and publisherId are REQUIRED' - if (!(bid.params.hasOwnProperty('placementId'))) { + let err1 = `${vf} : missing {param} : siteId, placementId and publisherId are REQUIRED`; + if (!(getBidIdParameter('placementId', bid.params))) { logError(err1.replace('{param}', 'placementId'), adUnitCode); return false; } if (!this.isValidPlacementId(bid.params.placementId)) { - logError('VALIDATION FAILED : placementId must be exactly 10 numeric characters', adUnitCode); + logError(`${vf} : placementId must be exactly 10 numbers`, adUnitCode); return false; } - if (!(bid.params.hasOwnProperty('publisherId'))) { + if (!(getBidIdParameter('publisherId', bid.params))) { logError(err1.replace('{param}', 'publisherId'), adUnitCode); return false; } if (!(bid.params.publisherId).toString().match(/^[a-zA-Z0-9\-]{12}$/)) { - logError('VALIDATION FAILED : publisherId must be exactly 12 alphanumeric characters including hyphens', adUnitCode); + logError(`${vf} : publisherId must be /^[a-zA-Z0-9\\-]{12}$/`, adUnitCode); return false; } - if (!(bid.params.hasOwnProperty('siteId'))) { + if (!(getBidIdParameter('siteId', bid.params))) { logError(err1.replace('{param}', 'siteId'), adUnitCode); return false; } if (!(bid.params.siteId).toString().match(/^[0-9]{10}$/)) { - logError('VALIDATION FAILED : siteId must be exactly 10 numeric characters', adUnitCode); + logError(`${vf} : siteId must be /^[0-9]{10}$/`, adUnitCode); return false; } if (bid.params.hasOwnProperty('customParams')) { - logError('VALIDATION FAILED : customParams should be renamed to customData', adUnitCode); + logError(`${vf} : customParams should be renamed: customData`, adUnitCode); return false; } if (bid.params.hasOwnProperty('customData')) { - if (!Array.isArray(bid.params.customData)) { - logError('VALIDATION FAILED : customData is not an Array', adUnitCode); + if (!isArray(bid.params.customData)) { + logError(`${vf} : customData is not an Array`, adUnitCode); return false; } if (bid.params.customData.length < 1) { - logError('VALIDATION FAILED : customData is an array but does not contain any elements', adUnitCode); + logError(`${vf} : empty customData`, adUnitCode); return false; } if (!(bid.params.customData[0]).hasOwnProperty('targeting')) { - logError('VALIDATION FAILED : customData[0] does not contain "targeting"', adUnitCode); + logError(`${vf} :no customData[0].targeting`, adUnitCode); return false; } if (typeof bid.params.customData[0]['targeting'] != 'object') { - logError('VALIDATION FAILED : customData[0] targeting is not an object', adUnitCode); + logError(`${vf} : customData[0].targeting is not an Object`, adUnitCode); return false; } } if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { - if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { - logError('No video context key/value in bid. Rejecting bid: ', bid); + if (!bid.mediaTypes?.[VIDEO]?.context) { + logError(`${vf} No video context key/value`); return false; } - if (bid.mediaTypes[VIDEO].context !== 'instream' && bid.mediaTypes[VIDEO].context !== 'outstream') { - logError('video.context is invalid. Only instream/outstream video is supported. Rejecting bid: ', bid); + if (['instream', 'outstream'].indexOf(bid.mediaTypes?.[VIDEO]?.context) < 0) { + logError(`${vf} video.context is invalid.`); return false; } } @@ -177,13 +200,14 @@ export const spec = { this.propertyBag.buildRequestsStart = new Date().getTime(); let whitelabelBidder = this.propertyBag.whitelabel.bidder; // by default = ozone let whitelabelPrefix = this.propertyBag.whitelabel.keyPrefix; - logInfo(`buildRequests time: ${this.propertyBag.buildRequestsStart} v ${OZONEVERSION} validBidRequests`, JSON.parse(JSON.stringify(validBidRequests)), 'bidderRequest', JSON.parse(JSON.stringify(bidderRequest))); + logInfo(`buildRequests time: ${this.propertyBag.buildRequestsStart} v ${OZONEVERSION} validBidRequests`, deepClone(validBidRequests), 'bidderRequest', deepClone(bidderRequest)); if (this.blockTheRequest()) { return []; } + let fledgeEnabled = !!bidderRequest.fledgeEnabled; // IF true then this is added as each bid[].ext.ae=1 let htmlParams = {'publisherId': '', 'siteId': ''}; if (validBidRequests.length > 0) { - this.cookieSyncBag.userIdObject = Object.assign(this.cookieSyncBag.userIdObject, this.findAllUserIds(validBidRequests[0])); + Object.assign(this.cookieSyncBag.userIdObject, this.findAllUserIdsFromEids(validBidRequests[0])); this.cookieSyncBag.siteId = deepAccess(validBidRequests[0], 'params.siteId'); this.cookieSyncBag.publisherId = deepAccess(validBidRequests[0], 'params.publisherId'); htmlParams = validBidRequests[0].params; @@ -191,11 +215,9 @@ export const spec = { logInfo('cookie sync bag', this.cookieSyncBag); let singleRequest = this.getWhitelabelConfigItem('ozone.singleRequest'); singleRequest = singleRequest !== false; // undefined & true will be true - logInfo(`config ${whitelabelBidder}.singleRequest : `, singleRequest); let ozoneRequest = {}; // we only want to set specific properties on this, not validBidRequests[0].params - logInfo('going to get ortb2 from bidder request...'); let fpd = deepAccess(bidderRequest, 'ortb2', null); - logInfo('got fpd: ', fpd); + logInfo('got ortb2 fpd: ', fpd); if (fpd && deepAccess(fpd, 'user')) { logInfo('added FPD user object'); ozoneRequest.user = fpd.user; @@ -203,28 +225,30 @@ export const spec = { const getParams = this.getGetParametersAsObject(); const wlOztestmodeKey = whitelabelPrefix + 'testmode'; const isTestMode = getParams[wlOztestmodeKey] || null; // this can be any string, it's used for testing ads - ozoneRequest.device = {'w': window.innerWidth, 'h': window.innerHeight}; + ozoneRequest.device = bidderRequest?.ortb2?.device || {}; // 20240925 rupesh changed this let placementIdOverrideFromGetParam = this.getPlacementIdOverrideFromGetParam(); // null or string let schain = null; + var auctionId = deepAccess(validBidRequests, '0.ortb2.source.tid'); + if (auctionId === '0') { + auctionId = null; + } let tosendtags = validBidRequests.map(ozoneBidRequest => { var obj = {}; let placementId = placementIdOverrideFromGetParam || this.getPlacementId(ozoneBidRequest); // prefer to use a valid override param, else the bidRequest placement Id obj.id = ozoneBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder ozone made bid for unknown request ID: mb7953.859498327448. Ignoring." obj.tagid = placementId; - let parsed = parseUrl(this.getRefererInfo().page); - obj.secure = parsed.protocol === 'https' ? 1 : 0; + obj.secure = parseUrl(getRefererInfo().page).protocol === 'https' ? 1 : 0; let arrBannerSizes = []; if (!ozoneBidRequest.hasOwnProperty('mediaTypes')) { if (ozoneBidRequest.hasOwnProperty('sizes')) { - logInfo('no mediaTypes detected - will use the sizes array in the config root'); arrBannerSizes = ozoneBidRequest.sizes; } else { - logInfo('no mediaTypes detected, no sizes array in the config root either. Cannot set sizes for banner type'); + logInfo('no mediaTypes or sizes array. Cannot set sizes for banner type'); } } else { if (ozoneBidRequest.mediaTypes.hasOwnProperty(BANNER)) { arrBannerSizes = ozoneBidRequest.mediaTypes[BANNER].sizes; /* Note - if there is a sizes element in the config root it will be pushed into here */ - logInfo('setting banner size from the mediaTypes.banner element for bidId ' + obj.id + ': ', arrBannerSizes); + logInfo('setting banner size from mediaTypes.banner for bidId ' + obj.id + ': ', arrBannerSizes); } if (ozoneBidRequest.mediaTypes.hasOwnProperty(VIDEO)) { logInfo('openrtb 2.5 compliant video'); @@ -234,31 +258,31 @@ export const spec = { obj.video = this.addVideoDefaults(obj.video, ozoneBidRequest.mediaTypes[VIDEO], childConfig); } let wh = getWidthAndHeightFromVideoObject(obj.video); - logInfo('setting video object from the mediaTypes.video element: ' + obj.id + ':', obj.video, 'wh=', wh); + logInfo(`setting video object ${obj.id} from mediaTypes.video: `, obj.video, 'wh=', wh); + let settingToBe = 'setting obj.video.format to be '; // partial, reusable phrase if (wh && typeof wh === 'object') { obj.video.w = wh['w']; obj.video.h = wh['h']; if (playerSizeIsNestedArray(obj.video)) { // this should never happen; it was in the original spec for this change though. - logInfo('setting obj.video.format to be an array of objects'); + logInfo(`${settingToBe} an array of objects`); obj.video.ext.format = [wh]; } else { - logInfo('setting obj.video.format to be an object'); + logInfo(`${settingToBe} an object`); obj.video.ext.format = wh; } } else { - logWarn('cannot set w, h & format values for video; the config is not right'); + logWarn(`Failed ${settingToBe} anything - bad config`); } } if (ozoneBidRequest.mediaTypes.hasOwnProperty(NATIVE)) { obj.native = ozoneBidRequest.mediaTypes[NATIVE]; - logInfo('setting native object from the mediaTypes.native element: ' + obj.id + ':', obj.native); + logInfo(`setting native object ${obj.id} from mediaTypes.native element:`, obj.native); } if (ozoneBidRequest.hasOwnProperty('getFloor')) { - logInfo('This bidRequest object has property: getFloor'); obj.floor = this.getFloorObjectForAuction(ozoneBidRequest); logInfo('obj.floor is : ', obj.floor); } else { - logInfo('This bidRequest object DOES NOT have property: getFloor'); + logInfo('no getFloor property'); } } if (arrBannerSizes.length > 0) { @@ -278,9 +302,17 @@ export const spec = { if (ozoneBidRequest.params.hasOwnProperty('customData')) { obj.ext[whitelabelBidder].customData = ozoneBidRequest.params.customData; } + if (ozoneBidRequest.params.hasOwnProperty('ozFloor')) { + let ozFloorParsed = parseFloat(ozoneBidRequest.params.ozFloor); + if (!isNaN(ozFloorParsed)) { + obj.ext[whitelabelBidder].ozFloor = ozFloorParsed; + } else { + logError(`Ignoring invalid ozFloor value for adunit code: ${ozoneBidRequest.adUnitCode}`); + } + } logInfo(`obj.ext.${whitelabelBidder} is `, obj.ext[whitelabelBidder]); if (isTestMode != null) { - logInfo('setting isTestMode to ', isTestMode); + logInfo(`setting isTestMode: ${isTestMode}`); if (obj.ext[whitelabelBidder].hasOwnProperty('customData')) { for (let i = 0; i < obj.ext[whitelabelBidder].customData.length; i++) { obj.ext[whitelabelBidder].customData[i]['targeting'][wlOztestmodeKey] = isTestMode; @@ -292,10 +324,10 @@ export const spec = { } if (fpd && deepAccess(fpd, 'site')) { logInfo('adding fpd.site'); - if (deepAccess(obj, 'ext.' + whitelabelBidder + '.customData.0.targeting', false)) { - obj.ext[whitelabelBidder].customData[0].targeting = Object.assign(obj.ext[whitelabelBidder].customData[0].targeting, fpd.site); + if (deepAccess(obj, `ext.${whitelabelBidder}.customData.0.targeting`, false)) { + Object.assign(obj.ext[whitelabelBidder].customData[0].targeting, fpd.site); } else { - deepSetValue(obj, 'ext.' + whitelabelBidder + '.customData.0.targeting', fpd.site); + deepSetValue(obj, `ext.${whitelabelBidder}.customData.0.targeting`, fpd.site); } } if (!schain && deepAccess(ozoneBidRequest, 'schain')) { @@ -305,25 +337,40 @@ export const spec = { if (gpid) { deepSetValue(obj, 'ext.gpid', gpid); } + let transactionId = deepAccess(ozoneBidRequest, 'ortb2Imp.ext.tid'); + if (transactionId) { + obj.ext[whitelabelBidder].transactionId = transactionId; // this is the transactionId PER adUnit, common across bidders for this unit + } + if (auctionId) { + obj.ext[whitelabelBidder].auctionId = auctionId; // we were sent a valid auctionId to use - this will also be used as the root id value for the request + } + if (fledgeEnabled) { // fledge is enabled at some config level - pbjs.setBidderConfig or pbjs.setConfig + const auctionEnvironment = deepAccess(ozoneBidRequest, 'ortb2Imp.ext.ae'); // this will be set for one of 3 reasons; adunit, setBidderConfig, setConfig + if (isInteger(auctionEnvironment)) { + deepSetValue(obj, 'ext.ae', auctionEnvironment); + } else { + logError(`ignoring ortb2Imp.ext.ae - not an integer for obj.id=${obj.id}`); + } + } return obj; }); let extObj = {}; extObj[whitelabelBidder] = {}; - extObj[whitelabelBidder][whitelabelPrefix + '_pb_v'] = OZONEVERSION; - extObj[whitelabelBidder][whitelabelPrefix + '_rw'] = placementIdOverrideFromGetParam ? 1 : 0; + extObj[whitelabelBidder][`${whitelabelPrefix}_pb_v`] = OZONEVERSION; + extObj[whitelabelBidder][`${whitelabelPrefix}_rw`] = placementIdOverrideFromGetParam ? 1 : 0; if (validBidRequests.length > 0) { let userIds = this.cookieSyncBag.userIdObject; // 2021-01-06 - slight optimisation - we've already found this info - if (userIds.hasOwnProperty('pubcid')) { - extObj[whitelabelBidder].pubcid = userIds.pubcid; + if (userIds.hasOwnProperty('pubcid.org')) { + extObj[whitelabelBidder].pubcid = userIds['pubcid.org']; } } extObj[whitelabelBidder].pv = this.getPageId(); // attach the page ID that will be common to all auction calls for this page if refresh() is called let ozOmpFloorDollars = this.getWhitelabelConfigItem('ozone.oz_omp_floor'); // valid only if a dollar value (typeof == 'number') logInfo(`${whitelabelPrefix}_omp_floor dollar value = `, ozOmpFloorDollars); if (typeof ozOmpFloorDollars === 'number') { - extObj[whitelabelBidder][whitelabelPrefix + '_omp_floor'] = ozOmpFloorDollars; + extObj[whitelabelBidder][`${whitelabelPrefix}_omp_floor`] = ozOmpFloorDollars; } else if (typeof ozOmpFloorDollars !== 'undefined') { - logError(`${whitelabelPrefix}_omp_floor is invalid - IF SET then this must be a number, representing dollar value eg. ${whitelabelPrefix}_omp_floor: 1.55. You have it set as a ` + (typeof ozOmpFloorDollars)); + logError(`IF set, ${whitelabelPrefix}_omp_floor must be a number eg. 1.55. Found:` + (typeof ozOmpFloorDollars)); } let ozWhitelistAdserverKeys = this.getWhitelabelConfigItem('ozone.oz_whitelist_adserver_keys'); let useOzWhitelistAdserverKeys = isArray(ozWhitelistAdserverKeys) && ozWhitelistAdserverKeys.length > 0; @@ -332,26 +379,22 @@ export const spec = { logInfo('setting aliases object'); extObj.prebid = {aliases: {'ozone': whitelabelBidder}}; } - if (getParams.hasOwnProperty('ozf')) { extObj[whitelabelBidder]['ozf'] = getParams.ozf === 'true' || getParams.ozf === '1' ? 1 : 0; } - if (getParams.hasOwnProperty('ozpf')) { extObj[whitelabelBidder]['ozpf'] = getParams.ozpf === 'true' || getParams.ozpf === '1' ? 1 : 0; } - if (getParams.hasOwnProperty('ozrp') && getParams.ozrp.match(/^[0-3]$/)) { extObj[whitelabelBidder]['ozrp'] = parseInt(getParams.ozrp); } - if (getParams.hasOwnProperty('ozip') && getParams.ozip.match(/^\d+$/)) { extObj[whitelabelBidder]['ozip'] = parseInt(getParams.ozip); } if (this.propertyBag.endpointOverride != null) { extObj[whitelabelBidder]['origin'] = this.propertyBag.endpointOverride; } let userExtEids = deepAccess(validBidRequests, '0.userIdAsEids', []); // generate the UserIDs in the correct format for UserId module ozoneRequest.site = { 'publisher': {'id': htmlParams.publisherId}, - 'page': this.getRefererInfo().page, + 'page': getRefererInfo().page, 'id': htmlParams.siteId }; ozoneRequest.test = config.getConfig('debug') ? 1 : 0; if (bidderRequest && bidderRequest.gdprConsent) { - logInfo('ADDING GDPR info'); + logInfo('ADDING GDPR'); let apiVersion = deepAccess(bidderRequest, 'gdprConsent.apiVersion', 1); ozoneRequest.regs = {ext: {gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, apiVersion: apiVersion}}; if (deepAccess(ozoneRequest, 'regs.ext.gdpr')) { deepSetValue(ozoneRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); } else { - logInfo('**** Strange CMP info: bidderRequest.gdprConsent exists BUT bidderRequest.gdprConsent.gdprApplies is false. See bidderRequest logged above. ****'); + logWarn('**** Strange CMP info: bidderRequest.gdprConsent exists BUT bidderRequest.gdprConsent.gdprApplies is false. See bidderRequest logged above. ****'); } } else { logInfo('WILL NOT ADD GDPR info; no bidderRequest.gdprConsent object'); @@ -362,6 +405,10 @@ export const spec = { } else { logInfo('WILL NOT ADD USP consent info; no bidderRequest.uspConsent.'); } + if (bidderRequest?.ortb2?.regs?.gpp) { + deepSetValue(ozoneRequest, 'regs.gpp', bidderRequest.ortb2.regs.gpp); + deepSetValue(ozoneRequest, 'regs.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); + } if (schain) { // we set this while iterating over the bids logInfo('schain found'); deepSetValue(ozoneRequest, 'source.ext.schain', schain); @@ -369,15 +416,20 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(ozoneRequest, 'regs.coppa', 1); } - let ozUuid = generateUUID(); - if (this.isBatchRequests()) { - logInfo('going to batch the requests'); + extObj[whitelabelBidder].cookieDeprecationLabel = deepAccess(bidderRequest, 'ortb2.device.ext.cdep', 'none'); + logInfo(`cookieDeprecationLabel ortb2.device.ext.cdep = ${extObj[whitelabelBidder].cookieDeprecationLabel}`); + let batchRequestsVal = this.getBatchRequests(); // false|numeric + if (typeof batchRequestsVal === 'number') { + logInfo(`Batching = ${batchRequestsVal}`); let arrRet = []; // return an array of objects containing data describing max 10 bids - for (let i = 0; i < tosendtags.length; i += 10) { - ozoneRequest.id = ozUuid; // Unique ID of the bid request, provided by the exchange. (REQUIRED) - ozoneRequest.imp = tosendtags.slice(i, i + 10); - ozoneRequest.ext = extObj; + for (let i = 0; i < tosendtags.length; i += batchRequestsVal) { + ozoneRequest.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); + if (auctionId) { + deepSetValue(ozoneRequest, 'source.tid', auctionId); + } + ozoneRequest.imp = tosendtags.slice(i, i + batchRequestsVal); + ozoneRequest.ext = extObj; if (ozoneRequest.imp.length > 0) { arrRet.push({ method: 'POST', @@ -390,32 +442,35 @@ export const spec = { logInfo('batch request going to return : ', arrRet); return arrRet; } - logInfo('requests will not be batched.'); if (singleRequest) { - logInfo('buildRequests starting to generate response for a single request'); - ozoneRequest.id = ozUuid; // Unique ID of the bid request, provided by the exchange. (REQUIRED) + logInfo('single request starting'); + ozoneRequest.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) ozoneRequest.imp = tosendtags; ozoneRequest.ext = extObj; deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); + if (auctionId) { + deepSetValue(ozoneRequest, 'source.tid', auctionId); + } var ret = { method: 'POST', url: this.getAuctionUrl(), data: JSON.stringify(ozoneRequest), bidderRequest: bidderRequest }; - logInfo('buildRequests request data for single = ', JSON.parse(JSON.stringify(ozoneRequest))); this.propertyBag.buildRequestsEnd = new Date().getTime(); - logInfo(`buildRequests going to return for single at time ${this.propertyBag.buildRequestsEnd} (took ${this.propertyBag.buildRequestsEnd - this.propertyBag.buildRequestsStart}ms): `, ret); + logInfo(`buildRequests going to return for single at time ${this.propertyBag.buildRequestsEnd} (took ${this.propertyBag.buildRequestsEnd - this.propertyBag.buildRequestsStart}ms): `, deepClone(ret)); return ret; } let arrRet = tosendtags.map(imp => { - logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); + logInfo('non-single response, working on imp : ', imp); let ozoneRequestSingle = Object.assign({}, ozoneRequest); ozoneRequestSingle.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) ozoneRequestSingle.imp = [imp]; ozoneRequestSingle.ext = extObj; deepSetValue(ozoneRequestSingle, 'user.ext.eids', userExtEids); - logInfo('buildRequests RequestSingle (for non-single) = ', ozoneRequestSingle); + if (auctionId) { + deepSetValue(ozoneRequestSingle, 'source.tid', auctionId); + } return { method: 'POST', url: this.getAuctionUrl(), @@ -436,15 +491,15 @@ export const spec = { logInfo('getFloorObjectForAuction mediaTypesSizes : ', mediaTypesSizes); let ret = {}; if (mediaTypesSizes.banner) { - ret.banner = bidRequestRef.getFloor({mediaType: 'banner', currency: 'USD', size: mediaTypesSizes.banner}); + ret.banner = bidRequestRef.getFloor({mediaType: 'banner', currency: 'USD', size: mediaTypesSizes.banner[0]}); } if (mediaTypesSizes.video) { - ret.video = bidRequestRef.getFloor({mediaType: 'video', currency: 'USD', size: mediaTypesSizes.video}); + ret.video = bidRequestRef.getFloor({mediaType: 'video', currency: 'USD', size: mediaTypesSizes.video[0]}); } if (mediaTypesSizes.native) { - ret.native = bidRequestRef.getFloor({mediaType: 'native', currency: 'USD', size: mediaTypesSizes.native}); + ret.native = bidRequestRef.getFloor({mediaType: 'native', currency: 'USD', size: mediaTypesSizes.native[0]}); } - logInfo('getFloorObjectForAuction returning : ', JSON.parse(JSON.stringify(ret))); + logInfo('getFloorObjectForAuction returning : ', deepClone(ret)); return ret; }, interpretResponse(serverResponse, request) { @@ -453,7 +508,7 @@ export const spec = { let whitelabelBidder = this.propertyBag.whitelabel.bidder; // by default = ozone let whitelabelPrefix = this.propertyBag.whitelabel.keyPrefix; logInfo(`interpretResponse time: ${startTime} . Time between buildRequests done and interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); - logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); + logInfo(`serverResponse, request`, deepClone(serverResponse), deepClone(request)); serverResponse = serverResponse.body || {}; let aucId = serverResponse.id; // this will be correct for single requests and non-single if (!serverResponse.hasOwnProperty('seatbid')) { @@ -463,6 +518,7 @@ export const spec = { return []; } let arrAllBids = []; + let labels; let enhancedAdserverTargeting = this.getWhitelabelConfigItem('ozone.enhancedAdserverTargeting'); logInfo('enhancedAdserverTargeting', enhancedAdserverTargeting); if (typeof enhancedAdserverTargeting == 'undefined') { @@ -480,26 +536,26 @@ export const spec = { for (let j = 0; j < sb.bid.length; j++) { let thisRequestBid = this.getBidRequestForBidId(sb.bid[j].impid, request.bidderRequest.bids); logInfo(`seatbid:${i}, bid:${j} Going to set default w h for seatbid/bidRequest`, sb.bid[j], thisRequestBid); - const {defaultWidth, defaultHeight} = defaultSize(thisRequestBid); + let {defaultWidth, defaultHeight} = defaultSize(thisRequestBid); let thisBid = ozoneAddStandardProperties(sb.bid[j], defaultWidth, defaultHeight); thisBid.meta = {advertiserDomains: thisBid.adomain || []}; let videoContext = null; let isVideo = false; let bidType = deepAccess(thisBid, 'ext.prebid.type'); - logInfo(`this bid type is : ${bidType}`, j); + logInfo(`this bid type is : ${bidType}`); let adserverTargeting = {}; if (bidType === VIDEO) { isVideo = true; this.setBidMediaTypeIfNotExist(thisBid, VIDEO); videoContext = this.getVideoContextForBidId(thisBid.bidId, request.bidderRequest.bids); // should be instream or outstream (or null if error) if (videoContext === 'outstream') { - logInfo('going to set thisBid.mediaType = VIDEO & attach a renderer to OUTSTREAM video : ', j); + logInfo('setting thisBid.mediaType = VIDEO & attach a renderer to OUTSTREAM video'); thisBid.renderer = newRenderer(thisBid.bidId); + thisBid.vastUrl = `https://${deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_host', 'missing_host')}${deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_path', 'missing_path')}?uuid=${deepAccess(thisBid, 'ext.prebid.targeting.hb_uuid', 'missing_uuid')}`; + thisBid.vastXml = thisBid.adm; } else { - logInfo('bid is not an outstream video, will set thisBid.mediaType = VIDEO and thisBid.vastUrl and not attach a renderer: ', j); - thisBid.vastUrl = `https://${deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_host', 'missing_host')}${deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_path', 'missing_path')}?id=${deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_id', 'missing_id')}`; // need to see if this works ok for ozone - adserverTargeting['hb_cache_host'] = deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_host', 'no-host'); - adserverTargeting['hb_cache_path'] = deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_path', 'no-path'); + logInfo('not an outstream video (presumably instream), will set thisBid.mediaType = VIDEO and thisBid.vastUrl and not attach a renderer'); + thisBid.vastUrl = `https://${deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_host', 'missing_host')}${deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_path', 'missing_path')}?uuid=${deepAccess(thisBid, 'ext.prebid.targeting.hb_uuid', 'missing_uuid')}`; // need to see if this works ok for ozone if (!thisBid.hasOwnProperty('videoCacheKey')) { let videoCacheUuid = deepAccess(thisBid, 'ext.prebid.targeting.hb_uuid', 'no_hb_uuid'); logInfo(`Adding videoCacheKey: ${videoCacheUuid}`); @@ -511,9 +567,10 @@ export const spec = { } else { this.setBidMediaTypeIfNotExist(thisBid, BANNER); } + adserverTargeting = Object.assign(adserverTargeting, deepAccess(thisBid, 'ext.prebid.targeting', {})); if (enhancedAdserverTargeting) { - let allBidsForThisBidid = ozoneGetAllBidsForBidId(thisBid.bidId, serverResponse.seatbid); - logInfo('Going to iterate allBidsForThisBidId', allBidsForThisBidid); + let allBidsForThisBidid = ozoneGetAllBidsForBidId(thisBid.bidId, serverResponse.seatbid, defaultWidth, defaultHeight); + logInfo('Going to iterate allBidsForThisBidId', deepClone(allBidsForThisBidid)); Object.keys(allBidsForThisBidid).forEach((bidderName, index, ar2) => { logInfo(`adding adserverTargeting for ${bidderName} for bidId ${thisBid.bidId}`); adserverTargeting[whitelabelPrefix + '_' + bidderName] = bidderName; @@ -521,6 +578,7 @@ export const spec = { adserverTargeting[whitelabelPrefix + '_' + bidderName + '_adv'] = String(allBidsForThisBidid[bidderName].adomain); adserverTargeting[whitelabelPrefix + '_' + bidderName + '_adId'] = String(allBidsForThisBidid[bidderName].adId); adserverTargeting[whitelabelPrefix + '_' + bidderName + '_pb_r'] = getRoundedBid(allBidsForThisBidid[bidderName].price, allBidsForThisBidid[bidderName].ext.prebid.type); + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_size'] = String(allBidsForThisBidid[bidderName].width) + 'x' + String(allBidsForThisBidid[bidderName].height); if (allBidsForThisBidid[bidderName].hasOwnProperty('dealid')) { adserverTargeting[whitelabelPrefix + '_' + bidderName + '_dealid'] = String(allBidsForThisBidid[bidderName].dealid); } @@ -541,21 +599,31 @@ export const spec = { if (bidderName.match(/^ozappnexus/)) { adserverTargeting[whitelabelPrefix + '_' + bidderName + '_sid'] = String(allBidsForThisBidid[bidderName].cid); } + labels = deepAccess(allBidsForThisBidid[bidderName], 'ext.prebid.labels', null); + if (labels) { + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_labels'] = labels.join(','); + } }); } else { + let perBidInfo = `${whitelabelBidder}.enhancedAdserverTargeting is set to false. No per-bid keys will be sent to adserver.`; if (useOzWhitelistAdserverKeys) { - logWarn(`You have set a whitelist of adserver keys but this will be ignored because ${whitelabelBidder}.enhancedAdserverTargeting is set to false. No per-bid keys will be sent to adserver.`); + logWarn(`Your adserver keys whitelist will be ignored - ${perBidInfo}`); } else { - logInfo(`${whitelabelBidder}.enhancedAdserverTargeting is set to false, so no per-bid keys will be sent to adserver.`); + logInfo(perBidInfo); } } let {seat: winningSeat, bid: winningBid} = ozoneGetWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); + winningBid = ozoneAddStandardProperties(winningBid, defaultWidth, defaultHeight); adserverTargeting[whitelabelPrefix + '_auc_id'] = String(aucId); // was request.bidderRequest.auctionId adserverTargeting[whitelabelPrefix + '_winner'] = String(winningSeat); adserverTargeting[whitelabelPrefix + '_bid'] = 'true'; adserverTargeting[whitelabelPrefix + '_cache_id'] = deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_id', 'no-id'); adserverTargeting[whitelabelPrefix + '_uuid'] = deepAccess(thisBid, 'ext.prebid.targeting.hb_uuid', 'no-id'); if (enhancedAdserverTargeting) { + labels = deepAccess(winningBid, 'ext.prebid.labels', null); + if (labels) { + adserverTargeting[whitelabelPrefix + '_labels'] = labels.join(','); + } adserverTargeting[whitelabelPrefix + '_imp_id'] = String(winningBid.impid); adserverTargeting[whitelabelPrefix + '_pb_v'] = OZONEVERSION; adserverTargeting[whitelabelPrefix + '_pb'] = winningBid.price; @@ -564,17 +632,35 @@ export const spec = { adserverTargeting[whitelabelPrefix + '_size'] = `${winningBid.width}x${winningBid.height}`; } if (useOzWhitelistAdserverKeys) { // delete any un-whitelisted keys - logInfo('Going to filter out adserver targeting keys not in the whitelist: ', ozWhitelistAdserverKeys); + logInfo('Filtering out adserver targeting keys not in the whitelist: ', ozWhitelistAdserverKeys); Object.keys(adserverTargeting).forEach(function(key) { if (ozWhitelistAdserverKeys.indexOf(key) === -1) { delete adserverTargeting[key]; } }); } thisBid.adserverTargeting = adserverTargeting; arrAllBids.push(thisBid); } } + let ret = arrAllBids; + let fledgeAuctionConfigs = deepAccess(serverResponse, 'ext.igi') || []; // 20240606 standardising + if (isArray(fledgeAuctionConfigs) && fledgeAuctionConfigs.length > 0) { + fledgeAuctionConfigs = fledgeAuctionConfigs.filter(config => { + if (!this.isValidAuctionConfig(config)) { + logWarn('Removing malformed fledge auction config:', config); + return false; + } + return true; + }); + ret = { + bids: arrAllBids, + fledgeAuctionConfigs, + }; + } let endTime = new Date().getTime(); logInfo(`interpretResponse going to return at time ${endTime} (took ${endTime - startTime}ms) Time from buildRequests Start -> interpretRequests End = ${endTime - this.propertyBag.buildRequestsStart}ms`); - logInfo('interpretResponse arrAllBids (serialised): ', JSON.parse(JSON.stringify(arrAllBids))); // this is ok to log because the renderer has not been attached yet - return arrAllBids; + logInfo('will return: ', deepClone(ret)); // this is ok to log because the renderer has not been attached yet + return ret; + }, + isValidAuctionConfig(config) { + return typeof config === 'object' && config !== null; }, setBidMediaTypeIfNotExist(thisBid, mediaType) { if (!thisBid.hasOwnProperty('mediaType')) { @@ -613,11 +699,12 @@ export const spec = { } return ret; }, - getUserSyncs(optionsType, serverResponse, gdprConsent, usPrivacy) { + getUserSyncs(optionsType, serverResponse, gdprConsent, usPrivacy, gppConsent = {}) { logInfo('getUserSyncs optionsType', optionsType, 'serverResponse', serverResponse, 'gdprConsent', gdprConsent, 'usPrivacy', usPrivacy, 'cookieSyncBag', this.cookieSyncBag); if (!serverResponse || serverResponse.length === 0) { return []; } + let { gppString = '', applicableSections = [] } = gppConsent; if (optionsType.iframeEnabled) { var arrQueryString = []; if (config.getConfig('debug')) { @@ -626,6 +713,10 @@ export const spec = { arrQueryString.push('gdpr=' + (deepAccess(gdprConsent, 'gdprApplies', false) ? '1' : '0')); arrQueryString.push('gdpr_consent=' + deepAccess(gdprConsent, 'consentString', '')); arrQueryString.push('usp_consent=' + (usPrivacy || '')); + arrQueryString.push('gpp=' + gppString); + if (isArray(applicableSections)) { + arrQueryString.push(`gpp_sid=${applicableSections.join()}`); + } for (let keyname in this.cookieSyncBag.userIdObject) { arrQueryString.push(keyname + '=' + this.cookieSyncBag.userIdObject[keyname]); } @@ -659,47 +750,26 @@ export const spec = { } return null; }, - findAllUserIds(bidRequest) { - var ret = {}; - let searchKeysSingle = ['pubcid', 'tdid', 'idl_env', 'criteoId', 'lotamePanoramaId', 'fabrickId']; - if (bidRequest.hasOwnProperty('userId')) { - for (let arrayId in searchKeysSingle) { - let key = searchKeysSingle[arrayId]; - if (bidRequest.userId.hasOwnProperty(key)) { - if (typeof (bidRequest.userId[key]) == 'string') { - ret[key] = bidRequest.userId[key]; - } else if (typeof (bidRequest.userId[key]) == 'object') { - logError(`WARNING: findAllUserIds had to use first key in user object to get value for bid.userId key: ${key}. Prebid adapter should be updated.`); - ret[key] = bidRequest.userId[key][Object.keys(bidRequest.userId[key])[0]]; // cannot use Object.values - } else { - logError(`failed to get string key value for userId : ${key}`); - } - } - } - let lipbid = deepAccess(bidRequest.userId, 'lipb.lipbid'); - if (lipbid) { - ret['lipb'] = {'lipbid': lipbid}; - } - let id5id = deepAccess(bidRequest.userId, 'id5id.uid'); - if (id5id) { - ret['id5id'] = id5id; - } - let parrableId = deepAccess(bidRequest.userId, 'parrableId.eid'); - if (parrableId) { - ret['parrableId'] = parrableId; - } - let sharedid = deepAccess(bidRequest.userId, 'sharedid.id'); - if (sharedid) { - ret['sharedid'] = sharedid; - } + findAllUserIdsFromEids(bidRequest) { + let ret = {}; + if (!bidRequest.hasOwnProperty('userIdAsEids')) { + logInfo('findAllUserIdsFromEids - no bidRequest.userIdAsEids object was found on the bid!'); + this.tryGetPubCidFromOldLocation(ret, bidRequest); // legacy + return ret; + } + for (let obj of bidRequest.userIdAsEids) { + ret[obj.source] = deepAccess(obj, 'uids.0.id'); } + this.tryGetPubCidFromOldLocation(ret, bidRequest); // legacy + return ret; + }, + tryGetPubCidFromOldLocation(ret, bidRequest) { if (!ret.hasOwnProperty('pubcid')) { let pubcid = deepAccess(bidRequest, 'crumbs.pubcid'); if (pubcid) { - ret['pubcid'] = pubcid; // if built with old pubCommonId module + ret['pubcid.org'] = pubcid; // if built with old pubCommonId module (use the new eid key) } } - return ret; }, getPlacementId(bidRequest) { return (bidRequest.params.placementId).toString(); @@ -709,7 +779,7 @@ export const spec = { let arr = this.getGetParametersAsObject(); if (arr.hasOwnProperty(whitelabelPrefix + 'storedrequest')) { if (this.isValidPlacementId(arr[whitelabelPrefix + 'storedrequest'])) { - logInfo(`using GET ${whitelabelPrefix}storedrequest ` + arr[whitelabelPrefix + 'storedrequest'] + ' to replace placementId'); + logInfo(`using GET ${whitelabelPrefix}storedrequest=` + arr[whitelabelPrefix + 'storedrequest'] + ' to replace placementId'); return arr[whitelabelPrefix + 'storedrequest']; } else { logError(`GET ${whitelabelPrefix}storedrequest FAILED VALIDATION - will not use it`); @@ -718,33 +788,14 @@ export const spec = { return null; }, getGetParametersAsObject() { - let parsed = parseUrl(this.getRefererInfo().location); + let parsed = parseUrl(getRefererInfo().location); logInfo('getGetParametersAsObject found:', parsed.search); return parsed.search; }, - getRefererInfo() { - if (getRefererInfo().hasOwnProperty('location')) { - logInfo('FOUND location on getRefererInfo OK (prebid >= 7); will use getRefererInfo for location & page'); - return getRefererInfo(); - } else { - logInfo('DID NOT FIND location on getRefererInfo (prebid < 7); will use legacy code that ALWAYS worked reliably to get location & page ;-)'); - try { - return { - page: top.location.href, - location: top.location.href - }; - } catch (e) { - return { - page: window.location.href, - location: window.location.href - }; - } - } - }, blockTheRequest() { let ozRequest = this.getWhitelabelConfigItem('ozone.oz_request'); - if (typeof ozRequest == 'boolean' && !ozRequest) { - logWarn(`Will not allow auction : ${this.propertyBag.whitelabel.keyPrefix}_request is set to false`); + if (ozRequest === false) { + logWarn(`Will not allow the auction : ${this.propertyBag.whitelabel.keyPrefix}_request is set to false`); return true; } return false; @@ -767,7 +818,7 @@ export const spec = { return ret; }, _unpackVideoConfigIntoIABformat(ret, objConfig) { - let arrVideoKeysAllowed = ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', 'api', 'companiontype']; + let arrVideoKeysAllowed = ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'plcmt', 'linearity', 'skip', 'skipmin', 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', 'api', 'companiontype']; for (const key in objConfig) { var found = false; arrVideoKeysAllowed.forEach(function(arg) { @@ -781,7 +832,7 @@ export const spec = { } } if (objConfig.hasOwnProperty('ext') && typeof objConfig.ext === 'object') { - if (objConfig.hasOwnProperty('ext')) { + if (ret.hasOwnProperty('ext')) { ret.ext = mergeDeep(ret.ext, objConfig.ext); } else { ret.ext = objConfig.ext; @@ -795,16 +846,14 @@ export const spec = { return objRet; }, _addVideoDefaults(objRet, objConfig, addIfMissing) { - let context = deepAccess(objConfig, 'context'); - if (context === 'outstream') { - objRet.placement = 3; - } else if (context === 'instream') { - objRet.placement = 1; + let placementValue = this.getVideoPlacementValue(deepAccess(objConfig, 'context')); + if (placementValue) { + objRet.placement = placementValue; } let skippable = deepAccess(objConfig, 'skippable', null); if (skippable == null) { if (addIfMissing && !objRet.hasOwnProperty('skip')) { - objRet.skip = skippable ? 1 : 0; + objRet.skip = 0; } } else { objRet.skip = skippable ? 1 : 0; @@ -834,7 +883,9 @@ export const spec = { params: bid.params, price: bid.price, transactionId: bid.transactionId, - ttl: bid.ttl + ttl: bid.ttl, + ortb2: deepAccess(bid, 'ortb2'), + ortb2Imp: deepAccess(bid, 'ortb2Imp'), }; if (bid.hasOwnProperty('floorData')) { logObj.floorData = bid.floorData; @@ -843,7 +894,7 @@ export const spec = { } }; export function injectAdIdsIntoAllBidResponses(seatbid) { - logInfo('injectAdIdsIntoAllBidResponses', seatbid); + logInfo('injectAdIdsIntoAllBidResponses', deepClone(seatbid)); for (let i = 0; i < seatbid.length; i++) { let sb = seatbid[i]; for (let j = 0; j < sb.bid.length; j++) { @@ -853,8 +904,8 @@ export function injectAdIdsIntoAllBidResponses(seatbid) { return seatbid; } export function checkDeepArray(Arr) { - if (Array.isArray(Arr)) { - if (Array.isArray(Arr[0])) { + if (isArray(Arr)) { + if (isArray(Arr[0])) { return Arr[0]; } else { return Arr; @@ -895,7 +946,7 @@ export function ozoneGetWinnerForRequestBid(requestBidId, serverResponseSeatBid) } return {'seat': winningSeat, 'bid': thisBidWinner}; } -export function ozoneGetAllBidsForBidId(matchBidId, serverResponseSeatBid) { +export function ozoneGetAllBidsForBidId(matchBidId, serverResponseSeatBid, defaultWidth, defaultHeight) { let objBids = {}; for (let j = 0; j < serverResponseSeatBid.length; j++) { let theseBids = serverResponseSeatBid[j].bid; @@ -904,10 +955,11 @@ export function ozoneGetAllBidsForBidId(matchBidId, serverResponseSeatBid) { if (theseBids[k].impid === matchBidId) { if (objBids.hasOwnProperty(thisSeat)) { // > 1 bid for an adunit from a bidder - only use the one with the highest bid if (objBids[thisSeat]['price'] < theseBids[k].price) { - objBids[thisSeat] = theseBids[k]; + objBids[thisSeat] = ozoneAddStandardProperties(theseBids[k], defaultWidth, defaultHeight); } } else { objBids[thisSeat] = theseBids[k]; + objBids[thisSeat] = ozoneAddStandardProperties(theseBids[k], defaultWidth, defaultHeight); } } } @@ -984,12 +1036,12 @@ export function getWidthAndHeightFromVideoObject(objVideo) { logInfo('getWidthAndHeightFromVideoObject found nested array inside playerSize.', playerSize[0]); playerSize = playerSize[0]; if (typeof playerSize[0] !== 'number' && typeof playerSize[0] !== 'string') { - logInfo('getWidthAndHeightFromVideoObject found non-number/string type inside the INNER array in playerSize. This is totally wrong - cannot continue.', playerSize[0]); + logError('getWidthAndHeightFromVideoObject found non-number/string type inside the INNER array in playerSize. This is totally wrong - cannot continue.', playerSize[0]); return null; } } if (playerSize.length !== 2) { - logInfo('getWidthAndHeightFromVideoObject found playerSize with length of ' + playerSize.length + '. This is totally wrong - cannot continue.'); + logError('getWidthAndHeightFromVideoObject found playerSize with length of ' + playerSize.length + '. This is totally wrong - cannot continue.'); return null; } return ({'w': playerSize[0], 'h': playerSize[1]}); @@ -1022,7 +1074,7 @@ function getPlayerSizeFromObject(objVideo) { } function newRenderer(adUnitCode, rendererOptions = {}) { let isLoaded = window.ozoneVideo; - logInfo(`newRenderer going to set loaded to ${isLoaded ? 'true' : 'false'}`); + logInfo(`newRenderer will set loaded to ${isLoaded ? 'true' : 'false'}`); const renderer = Renderer.install({ url: spec.getRendererUrl(), config: rendererOptions, @@ -1032,16 +1084,15 @@ function newRenderer(adUnitCode, rendererOptions = {}) { try { renderer.setRender(outstreamRender); } catch (err) { - logError('Prebid Error when calling setRender on renderer', renderer, err); + logError('Prebid Error calling renderer.setRender', renderer, err); } logInfo('returning renderer object'); return renderer; } function outstreamRender(bid) { - logInfo('outstreamRender called. Going to push the call to window.ozoneVideo.outstreamRender(bid) bid = (first static, then reference)'); - logInfo(JSON.parse(JSON.stringify(spec.getLoggableBidObject(bid)))); + logInfo('outstreamRender got', deepClone(spec.getLoggableBidObject(bid))); bid.renderer.push(() => { - logInfo('Going to execute window.ozoneVideo.outstreamRender'); + logInfo('outstreamRender: Going to execute window.ozoneVideo.outstreamRender'); window.ozoneVideo.outstreamRender(bid); }); } diff --git a/modules/paapi.js b/modules/paapi.js index 9122ecce1a0..94e720039f8 100644 --- a/modules/paapi.js +++ b/modules/paapi.js @@ -2,15 +2,27 @@ * Collect PAAPI component auction configs from bid adapters and make them available through `pbjs.getPAAPIConfig()` */ import {config} from '../src/config.js'; -import {getHook, module} from '../src/hook.js'; -import {deepSetValue, logInfo, logWarn, mergeDeep, parseSizesInput} from '../src/utils.js'; +import {getHook, hook, module} from '../src/hook.js'; +import { + deepAccess, + deepEqual, + deepSetValue, + logError, + logInfo, + logWarn, + mergeDeep, + sizesToSizeTuples +} from '../src/utils.js'; import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; import * as events from '../src/events.js'; import {EVENTS} from '../src/constants.js'; import {currencyCompare} from '../libraries/currencyUtils/currency.js'; -import {maximum, minimum} from '../src/utils/reducers.js'; -import {auctionManager} from '../src/auctionManager.js'; +import {keyCompare, maximum, minimum} from '../src/utils/reducers.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {auctionStore} from '../libraries/weakStore/weakStore.js'; +import {adapterMetrics, guardTids} from '../src/adapters/bidderFactory.js'; +import {defer, PbPromise} from '../src/utils/promise.js'; +import {auctionManager} from '../src/auctionManager.js'; const MODULE = 'PAAPI'; @@ -19,32 +31,31 @@ const USED = new WeakSet(); export function registerSubmodule(submod) { submodules.push(submod); - submod.init && submod.init({getPAAPIConfig}); + submod.init && submod.init({ + getPAAPIConfig, + expandFilters + }); } module('paapi', registerSubmodule); -function auctionConfigs() { - const store = new WeakMap(); - return function (auctionId, init = {}) { - const auction = auctionManager.index.getAuction({auctionId}); - if (auction == null) return; - if (!store.has(auction)) { - store.set(auction, init); - } - return store.get(auction); - }; -} +/* auction configs as returned by getPAAPIConfigs */ +const configsForAuction = auctionStore(); + +/* auction configs returned by adapters, but waiting for end-of-auction signals before they're added to configsForAuction */ +const pendingConfigsForAuction = auctionStore(); + +/* igb returned by adapters, waiting for end-of-auction signals before they're merged into configForAuctions */ +const pendingBuyersForAuction = auctionStore(); + +/* for auction configs that were generated in parallel with auctions (and contain promises), their resolve/reject methods */ +const deferredConfigsForAuction = auctionStore(); -const pendingForAuction = auctionConfigs(); -const configsForAuction = auctionConfigs(); let latestAuctionForAdUnit = {}; let moduleConfig = {}; -['paapi', 'fledgeForGpt'].forEach(ns => { - config.getConfig(ns, config => { - init(config[ns], ns); - }); +config.getConfig('paapi', config => { + init(config.paapi); }); export function reset() { @@ -52,22 +63,66 @@ export function reset() { latestAuctionForAdUnit = {}; } -export function init(cfg, configNamespace) { - if (configNamespace !== 'paapi') { - logWarn(`'${configNamespace}' configuration options will be renamed to 'paapi'; consider using setConfig({paapi: [...]}) instead`); - } +export function init(cfg) { if (cfg && cfg.enabled === true) { + if (!moduleConfig.enabled) { + attachHandlers(); + } moduleConfig = cfg; logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} runAdAuction)`, cfg); } else { + if (moduleConfig.enabled) { + detachHandlers(); + } moduleConfig = {}; logInfo(`${MODULE} disabled`, cfg); } } -getHook('addComponentAuction').before(addComponentAuctionHook); -getHook('makeBidRequests').after(markForFledge); -events.on(EVENTS.AUCTION_END, onAuctionEnd); +function attachHandlers() { + getHook('addPaapiConfig').before(addPaapiConfigHook); + getHook('makeBidRequests').before(addPaapiData); + getHook('makeBidRequests').after(markForFledge); + getHook('processBidderRequests').before(parallelPaapiProcessing, 9); + // resolve params before parallel processing + getHook('processBidderRequests').before(buildPAAPIParams, 10); + getHook('processBidderRequests').before(adAuctionHeadersHook); + events.on(EVENTS.AUCTION_INIT, onAuctionInit); + events.on(EVENTS.AUCTION_END, onAuctionEnd); +} + +function detachHandlers() { + getHook('addPaapiConfig').getHooks({hook: addPaapiConfigHook}).remove(); + getHook('makeBidRequests').getHooks({hook: addPaapiData}).remove(); + getHook('makeBidRequests').getHooks({hook: markForFledge}).remove(); + getHook('processBidderRequests').getHooks({hook: parallelPaapiProcessing}).remove(); + getHook('processBidderRequests').getHooks({hook: buildPAAPIParams}).remove(); + getHook('processBidderRequests').getHooks({hook: adAuctionHeadersHook}).remove(); + events.off(EVENTS.AUCTION_INIT, onAuctionInit); + events.off(EVENTS.AUCTION_END, onAuctionEnd); +} + +export function adAuctionHeadersHook(next, spec, bids, bidderRequest, ajax, ...args) { + if (bidderRequest.paapi?.enabled) { + ajax = ((orig) => { + return function (url, callback, data, options) { + options = options ?? {}; + options.adAuctionHeaders = options.adAuctionHeaders ?? true; + return orig.call(this, url, callback, data, options); + } + })(ajax); + } + return next.call(this, spec, bids, bidderRequest, ajax, ...args); +} + +function getStaticSignals(adUnit = {}) { + const cfg = {}; + const requestedSize = getRequestedSize(adUnit); + if (requestedSize) { + cfg.requestedSize = requestedSize; + } + return cfg; +} function getSlotSignals(bidsReceived = [], bidRequests = []) { let bidfloor, bidfloorcur; @@ -89,89 +144,281 @@ function getSlotSignals(bidsReceived = [], bidRequests = []) { return cfg; } +export function buyersToAuctionConfigs(igbRequests, merge = mergeBuyers, config = moduleConfig?.componentSeller ?? {}, partitioners = { + compact: (igbRequests) => partitionBuyers(igbRequests.map(req => req[1])).map(part => [{}, part]), + expand: partitionBuyersByBidder +}) { + if (!config.auctionConfig) { + logWarn(MODULE, 'Cannot use IG buyers: paapi.componentSeller.auctionConfig not set', igbRequests.map(req => req[1])); + return []; + } + const partition = partitioners[config.separateAuctions ? 'expand' : 'compact']; + return partition(igbRequests) + .map(([request, igbs]) => { + const auctionConfig = mergeDeep(merge(igbs), config.auctionConfig); + auctionConfig.auctionSignals = setFPD(auctionConfig.auctionSignals || {}, request); + return [request, auctionConfig]; + }); +} + function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adUnits}) { - const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || []) + const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || []); const allReqs = bidderRequests?.flatMap(br => br.bids); - const paapiConfigs = {}; + const paapiConfigs = configsForAuction(auctionId); (adUnitCodes || []).forEach(au => { - paapiConfigs[au] = null; + if (!paapiConfigs.hasOwnProperty(au)) { + paapiConfigs[au] = null; + } !latestAuctionForAdUnit.hasOwnProperty(au) && (latestAuctionForAdUnit[au] = null); }); - Object.entries(pendingForAuction(auctionId) || {}).forEach(([adUnitCode, auctionConfigs]) => { - const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; - const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); - paapiConfigs[adUnitCode] = { - ...slotSignals, - componentAuctions: auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg)) - }; - // TODO: need to flesh out size treatment: - // - which size should the paapi auction pick? (this uses the first one defined) - // - should we signal it to SSPs, and how? - // - what should we do if adapters pick a different one? - // - what does size mean for video and native? - const size = parseSizesInput(adUnitsByCode[adUnitCode]?.mediaTypes?.banner?.sizes)?.[0]?.split('x'); - if (size) { - paapiConfigs[adUnitCode].requestedSize = { - width: size[0], - height: size[1], - }; + + const pendingConfigs = pendingConfigsForAuction(auctionId); + const pendingBuyers = pendingBuyersForAuction(auctionId); + + if (pendingConfigs && pendingBuyers) { + Object.entries(pendingBuyers).forEach(([adUnitCode, igbRequests]) => { + buyersToAuctionConfigs(igbRequests).forEach(([{bidder}, auctionConfig]) => append(pendingConfigs, adUnitCode, {id: getComponentSellerConfigId(bidder), config: auctionConfig})) + }) + } + + const deferredConfigs = deferredConfigsForAuction(auctionId); + + const adUnitsWithConfigs = Array.from(new Set(Object.keys(pendingConfigs).concat(Object.keys(deferredConfigs)))); + const signals = Object.fromEntries( + adUnitsWithConfigs.map(adUnitCode => { + latestAuctionForAdUnit[adUnitCode] = auctionId; + const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; + return [adUnitCode, { + ...getStaticSignals(adUnitsByCode[adUnitCode]), + ...getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)) + }] + }) + ) + + const configsById = {}; + Object.entries(pendingConfigs || {}).forEach(([adUnitCode, auctionConfigs]) => { + auctionConfigs.forEach(({id, config}) => append(configsById, id, { + adUnitCode, + config: mergeDeep({}, signals[adUnitCode], config) + })); + }); + + function resolveSignals(signals, deferrals) { + Object.entries(deferrals).forEach(([signal, {resolve, default: defaultValue}]) => { + let value = signals.hasOwnProperty(signal) ? signals[signal] : null; + if (value == null && defaultValue == null) { + value = undefined; + } else if (typeof defaultValue === 'object' && typeof value === 'object') { + value = mergeDeep({}, defaultValue, value); + } else { + value = value ?? defaultValue + } + resolve(value); + }) + } + + Object.entries(deferredConfigs).forEach(([adUnitCode, {top, components}]) => { + resolveSignals(signals[adUnitCode], top); + Object.entries(components).forEach(([configId, {deferrals}]) => { + const matchingConfigs = configsById.hasOwnProperty(configId) ? configsById[configId] : []; + if (matchingConfigs.length > 1) { + logWarn(`Received multiple PAAPI configs for the same bidder and seller (${configId}), active PAAPI auctions will only see the first`); + } + const {config} = matchingConfigs.shift() ?? {config: {...signals[adUnitCode]}} + resolveSignals(config, deferrals); + }) + }); + + const newConfigs = Object.values(configsById).flatMap(configs => configs); + const hasDeferredConfigs = Object.keys(deferredConfigs).length > 0; + + if (moduleConfig.parallel && hasDeferredConfigs && newConfigs.length > 0) { + logError(`Received PAAPI configs after PAAPI auctions were already started in parallel with their contextual auction`, newConfigs) + } + + newConfigs.forEach(({adUnitCode, config}) => { + if (paapiConfigs[adUnitCode] == null) { + paapiConfigs[adUnitCode] = { + ...signals[adUnitCode], + componentAuctions: [] + } } - latestAuctionForAdUnit[adUnitCode] = auctionId; + paapiConfigs[adUnitCode].componentAuctions.push(mergeDeep({}, signals[adUnitCode], config)); }); - configsForAuction(auctionId, paapiConfigs); - submodules.forEach(submod => submod.onAuctionConfig?.( - auctionId, - paapiConfigs, - (adUnitCode) => paapiConfigs[adUnitCode] != null && USED.add(paapiConfigs[adUnitCode])) - ); + + if (!moduleConfig.parallel || !hasDeferredConfigs) { + submodules.forEach(submod => submod.onAuctionConfig?.(auctionId, paapiConfigs)); + } } -function setFPDSignals(auctionConfig, fpd) { - auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals); +function append(target, key, value) { + !target.hasOwnProperty(key) && (target[key] = []); + target[key].push(value); } -export function addComponentAuctionHook(next, request, componentAuctionConfig) { - if (getFledgeConfig().enabled) { - const {adUnitCode, auctionId, ortb2, ortb2Imp} = request; - const configs = pendingForAuction(auctionId); - if (configs != null) { - setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp}); - !configs.hasOwnProperty(adUnitCode) && (configs[adUnitCode] = []); - configs[adUnitCode].push(componentAuctionConfig); - } else { - logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig); +function setFPD(target, {ortb2, ortb2Imp}) { + ortb2 != null && deepSetValue(target, 'prebid.ortb2', mergeDeep({}, ortb2, target.prebid?.ortb2)); + ortb2Imp != null && deepSetValue(target, 'prebid.ortb2Imp', mergeDeep({}, ortb2Imp, target.prebid?.ortb2Imp)); + return target; +} + +function getConfigId(bidderCode, seller) { + return `${bidderCode}::${seller}`; +} + +function getComponentSellerConfigId(bidderCode) { + return moduleConfig.componentSeller.separateAuctions ? `igb::${bidderCode}` : 'igb'; +} + +export function addPaapiConfigHook(next, request, paapiConfig) { + if (getFledgeConfig(config.getCurrentBidder()).enabled) { + const {adUnitCode, auctionId, bidder} = request; + + + function storePendingData(store, data) { + const target = store(auctionId); + if (target != null) { + append(target, adUnitCode, data) + } else { + logWarn(MODULE, `Received PAAPI config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, data); + } + } + + const {config, igb} = paapiConfig; + if (config) { + config.auctionSignals = setFPD(config.auctionSignals || {}, request); + const pbs = config.perBuyerSignals = config.perBuyerSignals ?? {}; + (config.interestGroupBuyers || []).forEach(buyer => { + pbs[buyer] = setFPD(pbs[buyer] ?? {}, request); + }) + storePendingData(pendingConfigsForAuction, {id: getConfigId(bidder, config.seller), config}); + } + if (igb && checkOrigin(igb)) { + igb.pbs = setFPD(igb.pbs || {}, request); + storePendingData(pendingBuyersForAuction, [request, igb]) + } + } + next(request, paapiConfig); +} + +export const IGB_TO_CONFIG = { + cur: 'perBuyerCurrencies', + pbs: 'perBuyerSignals', + ps: 'perBuyerPrioritySignals', + maxbid: 'auctionSignals.prebid.perBuyerMaxbid', +} + +function checkOrigin(igb) { + if (igb.origin) return true; + logWarn('PAAPI buyer does not specify origin and will be ignored', igb); +} + +/** + * Convert a list of InterestGroupBuyer (igb) objects into a partial auction config. + * https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/Protected%20Audience%20Support.md + */ +export function mergeBuyers(igbs) { + const buyers = new Set(); + return Object.assign( + igbs.reduce((config, igb) => { + if (checkOrigin(igb)) { + if (!buyers.has(igb.origin)) { + buyers.add(igb.origin); + Object.entries(IGB_TO_CONFIG).forEach(([igbField, configField]) => { + if (igb[igbField] != null) { + const entry = deepAccess(config, configField) || {} + entry[igb.origin] = igb[igbField]; + deepSetValue(config, configField, entry); + } + }); + } else { + logWarn(MODULE, `Duplicate buyer: ${igb.origin}. All but the first will be ignored`, igbs); + } + } + return config; + }, {}), + { + interestGroupBuyers: Array.from(buyers.keys()) + } + ); +} + +/** + * Partition a list of InterestGroupBuyer (igb) object into sets that can each be merged into a single auction. + * If the same buyer (origin) appears more than once, it will be split across different partition unless the igb objects + * are identical. + */ +export function partitionBuyers(igbs) { + return igbs.reduce((partitions, igb) => { + if (checkOrigin(igb)) { + let partition = partitions.find(part => !part.hasOwnProperty(igb.origin) || deepEqual(part[igb.origin], igb)); + if (!partition) { + partition = {}; + partitions.push(partition); + } + partition[igb.origin] = igb; } + return partitions; + }, []).map(part => Object.values(part)); +} + +export function partitionBuyersByBidder(igbRequests) { + const requests = {}; + const igbs = {}; + igbRequests.forEach(([request, igb]) => { + !requests.hasOwnProperty(request.bidder) && (requests[request.bidder] = request); + append(igbs, request.bidder, igb); + }) + return Object.entries(igbs).map(([bidder, igbs]) => [requests[bidder], igbs]) +} + +/** + * Expand PAAPI api filters into a map from ad unit code to auctionId. + * + * @param auctionId when specified, the result will have this as the value for each entry. + * when not specified, each ad unit will map to the latest auction that involved that ad unit. + * @param adUnitCode when specified, the result will contain only one entry (for this ad unit) or be empty (if this ad + * unit was never involved in an auction). + * when not specified, the result will contain an entry for every ad unit that was involved in any auction. + * @return {{[adUnitCode: string]: string}} + */ +function expandFilters({auctionId, adUnitCode} = {}) { + let adUnitCodes = []; + if (adUnitCode == null) { + adUnitCodes = Object.keys(latestAuctionForAdUnit); + } else if (latestAuctionForAdUnit.hasOwnProperty(adUnitCode)) { + adUnitCodes = [adUnitCode]; } - next(request, componentAuctionConfig); + return Object.fromEntries( + adUnitCodes.map(au => [au, auctionId ?? latestAuctionForAdUnit[au]]) + ); } /** * Get PAAPI auction configuration. * - * @param auctionId? optional auction filter; if omitted, the latest auction for each ad unit is used - * @param adUnitCode? optional ad unit filter - * @param includeBlanks if true, include null entries for ad units that match the given filters but do not have any available auction configs. - * @returns {{}} a map from ad unit code to auction config for the ad unit. + * @param {Object} [filters] - Filters object + * @param {string} [filters.auctionId] optional auction filter; if omitted, the latest auction for each ad unit is used + * @param {string} [filters.adUnitCode] optional ad unit filter + * @param {boolean} [includeBlanks=false] if true, include null entries for ad units that match the given filters but do not have any available auction configs. + * @returns {Object} a map from ad unit code to auction config for the ad unit. */ -export function getPAAPIConfig({auctionId, adUnitCode} = {}, includeBlanks = false) { +export function getPAAPIConfig(filters = {}, includeBlanks = false) { const output = {}; - const targetedAuctionConfigs = auctionId && configsForAuction(auctionId); - Object.keys((auctionId != null ? targetedAuctionConfigs : latestAuctionForAdUnit) ?? []).forEach(au => { - const latestAuctionId = latestAuctionForAdUnit[au]; - const auctionConfigs = targetedAuctionConfigs ?? (latestAuctionId && configsForAuction(latestAuctionId)); - if ((adUnitCode ?? au) === au) { - let candidate; - if (targetedAuctionConfigs?.hasOwnProperty(au)) { - candidate = targetedAuctionConfigs[au]; - } else if (auctionId == null && auctionConfigs?.hasOwnProperty(au)) { - candidate = auctionConfigs[au]; - } + Object.entries(expandFilters(filters)).forEach(([au, auctionId]) => { + const auctionConfigs = configsForAuction(auctionId); + if (auctionConfigs?.hasOwnProperty(au)) { + // ad unit was involved in a PAAPI auction + const candidate = auctionConfigs[au]; if (candidate && !USED.has(candidate)) { output[au] = candidate; USED.add(candidate); } else if (includeBlanks) { output[au] = null; } + } else if (auctionId == null && includeBlanks) { + // ad unit was involved in a non-PAAPI auction + output[au] = null; } }); return output; @@ -183,73 +430,380 @@ function isFledgeSupported() { return 'runAdAuction' in navigator && 'joinAdInterestGroup' in navigator; } -function getFledgeConfig() { - const bidder = config.getCurrentBidder(); - const useGlobalConfig = moduleConfig.enabled && (bidder == null || !moduleConfig.bidders?.length || moduleConfig.bidders?.includes(bidder)); +function getFledgeConfig(bidder) { + const enabled = moduleConfig.enabled && (bidder == null || !moduleConfig.bidders?.length || moduleConfig.bidders?.includes(bidder)); return { - enabled: config.getConfig('fledgeEnabled') ?? useGlobalConfig, - ae: config.getConfig('defaultForSlots') ?? (useGlobalConfig ? moduleConfig.defaultForSlots : undefined) + enabled, + ae: enabled ? moduleConfig.defaultForSlots : undefined }; } +/** + * Given an array of size tuples, return the one that should be used for PAAPI. + */ +export const getPAAPISize = hook('sync', function (sizes) { + sizes = sizes + ?.filter(([w, h]) => !(w === h && w <= 5)); + + if (sizes?.length) { + return sizes + .reduce(maximum(keyCompare(([w, h]) => w * h))); + } +}, 'getPAAPISize'); + +function getRequestedSize(adUnit) { + return adUnit.ortb2Imp?.ext?.paapi?.requestedSize || (() => { + const size = getPAAPISize(sizesToSizeTuples(adUnit.mediaTypes?.banner?.sizes)); + if (size) { + return { + width: size[0], + height: size[1] + }; + } + })(); +} + +export function addPaapiData(next, adUnits, ...args) { + if (isFledgeSupported() && moduleConfig.enabled) { + adUnits.forEach(adUnit => { + // https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/Protected%20Audience%20Support.md + const igsAe = adUnit.ortb2Imp?.ext?.igs != null + ? adUnit.ortb2Imp.ext.igs.ae || 1 + : null; + const extAe = adUnit.ortb2Imp?.ext?.ae; + if (igsAe !== extAe && igsAe != null && extAe != null) { + logWarn(MODULE, `Ad unit defines conflicting ortb2Imp.ext.ae and ortb2Imp.ext.igs, using the latter`, adUnit); + } + const ae = igsAe ?? extAe ?? moduleConfig.defaultForSlots; + if (ae) { + deepSetValue(adUnit, 'ortb2Imp.ext.ae', ae); + adUnit.ortb2Imp.ext.igs = Object.assign({ + ae: ae, + biddable: 1 + }, adUnit.ortb2Imp.ext.igs); + const requestedSize = getRequestedSize(adUnit); + if (requestedSize) { + deepSetValue(adUnit, 'ortb2Imp.ext.paapi.requestedSize', requestedSize); + } + adUnit.bids.forEach(bidReq => { + if (!getFledgeConfig(bidReq.bidder).enabled) { + deepSetValue(bidReq, 'ortb2Imp.ext.ae', 0); + bidReq.ortb2Imp.ext.igs = {ae: 0, biddable: 0}; + } + }) + } + }) + } + next(adUnits, ...args); +} + +export const NAVIGATOR_APIS = ['createAuctionNonce', 'getInterestGroupAdAuctionData']; + export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { bidderRequests.forEach((bidderReq) => { - config.runWithBidder(bidderReq.bidderCode, () => { - const {enabled, ae} = getFledgeConfig(); - Object.assign(bidderReq, {fledgeEnabled: enabled}); - bidderReq.bids.forEach(bidReq => { - deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? ae); - }); + const {enabled} = getFledgeConfig(bidderReq.bidderCode); + Object.assign(bidderReq, { + paapi: { + enabled, + componentSeller: !!moduleConfig.componentSeller?.auctionConfig + } }); + if (enabled) { + NAVIGATOR_APIS.forEach(method => { + bidderReq.paapi[method] = (...args) => new AsyncPAAPIParam(() => navigator[method](...args)) + }) + } }); } next(bidderRequests); } +export const ASYNC_SIGNALS = [ + 'auctionSignals', + 'sellerSignals', + 'perBuyerSignals', + 'perBuyerTimeouts', + 'directFromSellerSignals', + 'perBuyerCurrencies', + 'perBuyerCumulativeTimeouts', + 'serverResponse' +]; + +const validatePartialConfig = (() => { + const REQUIRED_SYNC_SIGNALS = [ + { + props: ['seller'], + validate: (val) => typeof val === 'string' + }, + { + props: ['interestGroupBuyers'], + validate: (val) => Array.isArray(val) && val.length > 0 + }, + { + props: ['decisionLogicURL', 'decisionLogicUrl'], + validate: (val) => typeof val === 'string' + } + ]; + + return function (config) { + const invalid = REQUIRED_SYNC_SIGNALS.find(({props, validate}) => props.every(prop => !config.hasOwnProperty(prop) || !config[prop] || !validate(config[prop]))); + if (invalid) { + logError(`Partial PAAPI config has missing or invalid property "${invalid.props[0]}"`, config) + return false; + } + return true; + } +})() + +function callAdapterApi(spec, method, bids, bidderRequest) { + const metrics = adapterMetrics(bidderRequest); + const tidGuard = guardTids(bidderRequest); + let result; + metrics.measureTime(method, () => { + try { + result = spec[method](bids.map(tidGuard.bidRequest), tidGuard.bidderRequest(bidderRequest)) + } catch (e) { + logError(`Error invoking "${method}":`, e); + } + }); + return result; +} + +/** + * Adapters can provide a `spec.buildPAAPIConfigs(validBidRequests, bidderRequest)` to be included in PAAPI auctions + * that can be started in parallel with contextual auctions. + * + * If PAAPI is enabled, and an adapter provides `buildPAAPIConfigs`, it is invoked just before `buildRequests`, + * and takes the same arguments. It should return an array of PAAPI configuration objects with the same format + * as in `interpretResponse` (`{bidId, config?, igb?}`). + * + * Everything returned by `buildPAAPIConfigs` is treated in the same way as if it was returned by `interpretResponse` - + * except for signals that can be provided asynchronously (cfr. `ASYNC_SIGNALS`), which are replaced by promises. + * When the (contextual) auction ends, the promises are resolved. + * + * If during the auction the adapter's `interpretResponse` returned matching configurations (same `bidId`, + * and a `config` with the same `seller`, or an `igb` with the same `origin`), the promises resolve to their contents. + * Otherwise, they resolve to the values provided by `buildPAAPIConfigs`, or an empty object if no value was provided. + * + * Promisified auction configs are available from `getPAAPIConfig` immediately after `requestBids`. + * If the `paapi.parallel` config flag is set, PAAPI submodules are also triggered at the same time + * (instead of when the auction ends). + */ +export function parallelPaapiProcessing(next, spec, bids, bidderRequest, ...args) { + function makeDeferrals(defaults = {}) { + let promises = {}; + const deferrals = Object.fromEntries(ASYNC_SIGNALS.map(signal => { + const def = defer({promiseFactory: (resolver) => new Promise(resolver)}); + def.default = defaults.hasOwnProperty(signal) ? defaults[signal] : null; + promises[signal] = def.promise; + return [signal, def] + })) + return [deferrals, promises]; + } + + const {auctionId, paapi: {enabled, componentSeller} = {}} = bidderRequest; + const auctionConfigs = configsForAuction(auctionId); + bids.map(bid => bid.adUnitCode).forEach(adUnitCode => { + latestAuctionForAdUnit[adUnitCode] = auctionId; + if (!auctionConfigs.hasOwnProperty(adUnitCode)) { + auctionConfigs[adUnitCode] = null; + } + }); + + if (enabled && spec.buildPAAPIConfigs) { + const partialConfigs = callAdapterApi(spec, 'buildPAAPIConfigs', bids, bidderRequest) + const requestsById = Object.fromEntries(bids.map(bid => [bid.bidId, bid])); + (partialConfigs ?? []).forEach(({bidId, config, igb}) => { + const bidRequest = requestsById.hasOwnProperty(bidId) && requestsById[bidId]; + if (!bidRequest) { + logError(`Received partial PAAPI config for unknown bidId`, {bidId, config}); + } else { + const adUnitCode = bidRequest.adUnitCode; + latestAuctionForAdUnit[adUnitCode] = auctionId; + const deferredConfigs = deferredConfigsForAuction(auctionId); + + const getDeferredConfig = () => { + if (!deferredConfigs.hasOwnProperty(adUnitCode)) { + const [deferrals, promises] = makeDeferrals(); + auctionConfigs[adUnitCode] = { + ...getStaticSignals(auctionManager.index.getAdUnit(bidRequest)), + ...promises, + componentAuctions: [] + } + deferredConfigs[adUnitCode] = { + top: deferrals, + components: {}, + auctionConfig: auctionConfigs[adUnitCode] + } + } + return deferredConfigs[adUnitCode]; + } + + if (config && validatePartialConfig(config)) { + const configId = getConfigId(bidRequest.bidder, config.seller); + const deferredConfig = getDeferredConfig(); + if (deferredConfig.components.hasOwnProperty(configId)) { + logWarn(`Received multiple PAAPI configs for the same bidder and seller; config will be ignored`, { + config, + bidder: bidRequest.bidder + }) + } else { + const [deferrals, promises] = makeDeferrals(config); + const auctionConfig = { + ...getStaticSignals(bidRequest), + ...config, + ...promises + } + deferredConfig.auctionConfig.componentAuctions.push(auctionConfig) + deferredConfig.components[configId] = {auctionConfig, deferrals}; + } + } + if (componentSeller && igb && checkOrigin(igb)) { + const configId = getComponentSellerConfigId(spec.code); + const deferredConfig = getDeferredConfig(); + const partialConfig = buyersToAuctionConfigs([[bidRequest, igb]])[0][1]; + if (deferredConfig.components.hasOwnProperty(configId)) { + const {auctionConfig, deferrals} = deferredConfig.components[configId]; + if (!auctionConfig.interestGroupBuyers.includes(igb.origin)) { + const immediate = {}; + Object.entries(partialConfig).forEach(([key, value]) => { + if (deferrals.hasOwnProperty(key)) { + mergeDeep(deferrals[key], {default: value}); + } else { + immediate[key] = value; + } + }) + mergeDeep(auctionConfig, immediate); + } else { + logWarn(`Received the same PAAPI buyer multiple times for the same PAAPI auction. Consider setting paapi.componentSeller.separateAuctions: true`, igb) + } + } else { + const [deferrals, promises] = makeDeferrals(partialConfig); + const auctionConfig = { + ...partialConfig, + ...getStaticSignals(bidRequest), + ...promises, + } + deferredConfig.components[configId] = {auctionConfig, deferrals}; + deferredConfig.auctionConfig.componentAuctions.push(auctionConfig); + } + } + } + }) + } + return next.call(this, spec, bids, bidderRequest, ...args); +} + +export class AsyncPAAPIParam { + constructor(resolve) { + this.resolve = resolve; + } +} + +export function buildPAAPIParams(next, spec, bids, bidderRequest, ...args) { + if (bidderRequest.paapi?.enabled && spec.paapiParameters) { + const params = callAdapterApi(spec, 'paapiParameters', bids, bidderRequest); + return PbPromise.all( + Object.entries(params ?? {}).map(([key, value]) => + value instanceof AsyncPAAPIParam + ? value.resolve().then(result => [key, result]) + : Promise.resolve([key, value])) + ).then(resolved => { + bidderRequest.paapi.params = Object.fromEntries(resolved); + }).catch(err => { + logError(`Could not resolve PAAPI parameters`, err); + }).then(() => { + next.call(this, spec, bids, bidderRequest, ...args); + }) + } else { + next.call(this, spec, bids, bidderRequest, ...args); + } +} + +export function onAuctionInit({auctionId}) { + if (moduleConfig.parallel) { + auctionManager.index.getAuction({auctionId}).requestsDone.then(() => { + if (Object.keys(deferredConfigsForAuction(auctionId)).length > 0) { + submodules.forEach(submod => submod.onAuctionConfig?.(auctionId, configsForAuction(auctionId))); + } + }) + } +} + export function setImpExtAe(imp, bidRequest, context) { - if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) { + if (!context.bidderRequest.paapi?.enabled) { delete imp.ext?.ae; + delete imp.ext?.igs; } } registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); -// to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up -// fledge response processing in two steps: first aggregate all the auction configs by their imp... +export function parseExtIgi(response, ortbResponse, context) { + paapiResponseParser( + (ortbResponse.ext?.igi || []).flatMap(igi => { + return (igi?.igs || []).map(igs => { + if (igs.impid !== igi.impid && igs.impid != null && igi.impid != null) { + logWarn(MODULE, 'ORTB response ext.igi.igs.impid conflicts with parent\'s impid', igi); + } + return { + config: igs.config, + impid: igs.impid ?? igi.impid + } + }).concat((igi?.igb || []).map(igb => ({ + igb, + impid: igi.impid + }))) + }), + response, + context + ) +} -export function parseExtPrebidFledge(response, ortbResponse, context) { - (ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []).forEach((cfg) => { - const impCtx = context.impContext[cfg.impid]; +function paapiResponseParser(configs, response, context) { + configs.forEach((config) => { + const impCtx = context.impContext[config.impid]; if (!impCtx?.imp?.ext?.ae) { - logWarn('Received fledge auction configuration for an impression that was not in the request or did not ask for it', cfg, impCtx?.imp); + logWarn(MODULE, 'Received auction configuration for an impression that was not in the request or did not ask for it', config, impCtx?.imp); } else { - impCtx.fledgeConfigs = impCtx.fledgeConfigs || []; - impCtx.fledgeConfigs.push(cfg); + impCtx.paapiConfigs = impCtx.paapiConfigs || []; + impCtx.paapiConfigs.push(config); } }); } +// to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up +// fledge response processing in two steps: first aggregate all the auction configs by their imp... + +export function parseExtPrebidFledge(response, ortbResponse, context) { + paapiResponseParser( + (ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []), + response, + context + ) +} + registerOrtbProcessor({type: RESPONSE, name: 'extPrebidFledge', fn: parseExtPrebidFledge, dialects: [PBS]}); +registerOrtbProcessor({type: RESPONSE, name: 'extIgiIgs', fn: parseExtIgi}); // ...then, make them available in the adapter's response. This is the client side version, for which the // interpretResponse api is {fledgeAuctionConfigs: [{bidId, config}]} -export function setResponseFledgeConfigs(response, ortbResponse, context) { +export function setResponsePaapiConfigs(response, ortbResponse, context) { const configs = Object.values(context.impContext) - .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({ + .flatMap((impCtx) => (impCtx.paapiConfigs || []).map(cfg => ({ bidId: impCtx.bidRequest.bidId, - config: cfg.config + ...cfg }))); if (configs.length > 0) { - response.fledgeAuctionConfigs = configs; + response.paapi = configs; } } registerOrtbProcessor({ type: RESPONSE, - name: 'fledgeAuctionConfigs', + name: 'paapiConfigs', priority: -1, - fn: setResponseFledgeConfigs, - dialects: [PBS] + fn: setResponsePaapiConfigs, }); diff --git a/modules/paapiForGpt.js b/modules/paapiForGpt.js new file mode 100644 index 00000000000..363c83ada03 --- /dev/null +++ b/modules/paapiForGpt.js @@ -0,0 +1,166 @@ +/** + * GPT-specific slot configuration logic for PAAPI. + */ +import {getHook, submodule} from '../src/hook.js'; +import {deepAccess, logInfo, logWarn, sizeTupleToSizeString} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; + +import {keyCompare} from '../src/utils/reducers.js'; +import {getGPTSlotsForAdUnits, targeting} from '../src/targeting.js'; + +const MODULE = 'paapiForGpt'; + +let getPAAPIConfig; + +config.getConfig('paapi', (cfg) => { + if (deepAccess(cfg, 'paapi.gpt.configWithTargeting', true)) { + logInfo(MODULE, 'enabling PAAPI configuration with setTargetingForGPTAsync') + targeting.setTargetingForGPT.before(setTargetingHook); + } else { + targeting.setTargetingForGPT.getHooks({hook: setTargetingHook}).remove(); + } +}); + +export function setTargetingHookFactory(setPaapiConfig = getGlobal().setPAAPIConfigForGPT) { + return function(next, adUnit, customSlotMatching) { + const adUnitCodes = Array.isArray(adUnit) ? adUnit : [adUnit] + adUnitCodes + .map(adUnitCode => adUnitCode == null ? undefined : {adUnitCode}) + .forEach(filters => setPaapiConfig(filters, customSlotMatching)) + next(adUnit, customSlotMatching); + } +} + +export function slotConfigurator() { + const PREVIOUSLY_SET = {}; + return function setComponentAuction(adUnitCode, gptSlots, auctionConfigs, reset = true) { + if (gptSlots.length > 0) { + let previous = PREVIOUSLY_SET[adUnitCode] ?? {}; + let configsBySeller = Object.fromEntries(auctionConfigs.map(cfg => [cfg.seller, cfg])); + const sellers = Object.keys(configsBySeller); + if (reset) { + configsBySeller = Object.assign(previous, configsBySeller); + previous = Object.fromEntries(sellers.map(seller => [seller, null])); + } else { + sellers.forEach(seller => { + previous[seller] = null; + }); + } + Object.keys(previous).length ? PREVIOUSLY_SET[adUnitCode] = previous : delete PREVIOUSLY_SET[adUnitCode]; + const componentAuction = Object.entries(configsBySeller) + .map(([configKey, auctionConfig]) => ({configKey, auctionConfig})); + if (componentAuction.length > 0) { + gptSlots.forEach(gptSlot => { + gptSlot.setConfig({componentAuction}); + logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); + // reference https://developers.google.com/publisher-tag/reference#googletag.config.ComponentAuctionConfig + }); + } + } else if (auctionConfigs.length > 0) { + logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); + } + }; +} + +const setComponentAuction = slotConfigurator(); + +export const getPAAPISizeHook = (() => { + /* + https://github.com/google/ads-privacy/tree/master/proposals/fledge-multiple-seller-testing#faq + https://support.google.com/admanager/answer/1100453?hl=en + + Ignore any placeholder sizes, where placeholder is defined as a square creative with a side of <= 5 pixels + Look if there are any sizes that are part of the set of supported ad sizes defined here. If there are, choose the largest supported size by area (width * height) + For clarity, the set of supported ad sizes includes all of the ad sizes listed under “Top-performing ad sizes”, “Other supported ad sizes”, and “Regional ad sizes”. + If not, choose the largest remaining size (i.e. that isn’t in the list of supported ad sizes) by area (width * height) + */ + const SUPPORTED_SIZES = [ + [728, 90], + [336, 280], + [300, 250], + [300, 50], + [160, 600], + [1024, 768], + [970, 250], + [970, 90], + [768, 1024], + [480, 320], + [468, 60], + [320, 480], + [320, 100], + [320, 50], + [300, 600], + [300, 100], + [250, 250], + [234, 60], + [200, 200], + [180, 150], + [125, 125], + [120, 600], + [120, 240], + [120, 60], + [88, 31], + [980, 120], + [980, 90], + [950, 90], + [930, 180], + [750, 300], + [750, 200], + [750, 100], + [580, 400], + [250, 360], + [240, 400], + ].sort(keyCompare(([w, h]) => -(w * h))) + .map(size => [size, sizeTupleToSizeString(size)]); + + return function(next, sizes) { + if (sizes?.length) { + const sizeStrings = new Set(sizes.map(sizeTupleToSizeString)); + const preferredSize = SUPPORTED_SIZES.find(([_, sizeStr]) => sizeStrings.has(sizeStr)); + if (preferredSize) { + next.bail(preferredSize[0]); + return; + } + } + next(sizes); + } +})(); + +export function setPAAPIConfigFactory( + getConfig = (filters) => getPAAPIConfig(filters, true), + setGptConfig = setComponentAuction, + getSlots = getGPTSlotsForAdUnits) { + /** + * Configure GPT slots with PAAPI auction configs. + * `filters` are the same filters accepted by `pbjs.getPAAPIConfig`; + */ + return function(filters = {}, customSlotMatching) { + let some = false; + const cfg = getConfig(filters) || {}; + const auToSlots = getSlots(Object.keys(cfg), customSlotMatching); + + Object.entries(cfg).forEach(([au, config]) => { + if (config != null) { + some = true; + } + setGptConfig(au, auToSlots[au], config?.componentAuctions || [], true); + }) + if (!some) { + logInfo(`${MODULE}: No component auctions available to set`); + } + } +} +/** + * Configure GPT slots with PAAPI component auctions. Accepts the same filter arguments as `pbjs.getPAAPIConfig`. + */ +getGlobal().setPAAPIConfigForGPT = setPAAPIConfigFactory(); +const setTargetingHook = setTargetingHookFactory(); + +submodule('paapi', { + name: 'gpt', + init(params) { + getPAAPIConfig = params.getPAAPIConfig; + getHook('getPAAPISize').before(getPAAPISizeHook); + } +}); diff --git a/modules/fledgeForGpt.md b/modules/paapiForGpt.md similarity index 55% rename from modules/fledgeForGpt.md rename to modules/paapiForGpt.md index 28f44da6459..31cde2e268d 100644 --- a/modules/fledgeForGpt.md +++ b/modules/paapiForGpt.md @@ -1,22 +1,22 @@ # Overview -This module allows Prebid.js to support FLEDGE by integrating it with GPT's [experimental FLEDGE +This module allows Prebid.js to support PAAPI by integrating it with GPT's [experimental PAAPI support](https://github.com/google/ads-privacy/tree/master/proposals/fledge-multiple-seller-testing). -To learn more about FLEDGE in general, go [here](https://github.com/WICG/turtledove/blob/main/FLEDGE.md). +To learn more about PAAPI in general, go [here](https://github.com/WICG/turtledove/blob/main/PAAPI.md). -This document covers the steps necessary for publishers to enable FLEDGE on their inventory. It also describes -the changes Bid Adapters need to implement in order to support FLEDGE. +This document covers the steps necessary for publishers to enable PAAPI on their inventory. It also describes +the changes Bid Adapters need to implement in order to support PAAPI. ## Publisher Integration -Publishers wishing to enable FLEDGE support must do two things. First, they must compile Prebid.js with support for this module. -This is accomplished by adding the `fledgeForGpt` module to the list of modules they are already using: +Publishers wishing to enable PAAPI support must do two things. First, they must compile Prebid.js with support for this module. +This is accomplished by adding the `paapiForGpt` module to the list of modules they are already using: ``` -gulp build --modules=fledgeForGpt,... +gulp build --modules=paapiForGpt,... ``` -Second, they must enable FLEDGE in their Prebid.js configuration. -This is done through module level configuration, but to provide a high degree of flexiblity for testing, FLEDGE settings also exist at the bidder level and slot level. +Second, they must enable PAAPI in their Prebid.js configuration. +This is done through module level configuration, but to provide a high degree of flexiblity for testing, PAAPI settings also exist the slot level. ### Module Configuration This module exposes the following settings: @@ -27,14 +27,13 @@ This module exposes the following settings: |bidders | Array[String] |Optional list of bidders |Defaults to all bidders | |defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1 | -As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module and configure `defaultForSlots` to be `1` (meaning _Client-side auction_). -using the `setConfig` method of Prebid.js. Optionally, a list of -bidders to apply these settings to may be provided: +As noted above, PAAPI support is disabled by default. To enable it, set the `enabled` value to `true` for this module and configure `defaultForSlots` to be `1` (meaning _Client-side auction_). +using the `setConfig` method of Prebid.js. Optionally, a list of bidders to apply these settings to may be provided: ```js pbjs.que.push(function() { pbjs.setConfig({ - fledgeForGpt: { + paapi: { enabled: true, bidders: ['openx', 'rtbhouse'], defaultForSlots: 1 @@ -43,35 +42,14 @@ pbjs.que.push(function() { }); ``` -### Bidder Configuration -This module adds the following setting for bidders: - -|Name |Type |Description |Notes | -| :------------ | :------------ | :------------ |:------------ | -| fledgeEnabled | Boolean | Enable/disable a bidder to participate in FLEDGE | Defaults to `false` | -|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1| - -Individual bidders may be further included or excluded here using the `setBidderConfig` method -of Prebid.js: - -```js -pbjs.setBidderConfig({ - bidders: ["openx"], - config: { - fledgeEnabled: true, - defaultForSlots: 1 - } -}); -``` - ### AdUnit Configuration -All adunits can be opted-in to FLEDGE in the global config via the `defaultForSlots` parameter. +All adunits can be opted-in to PAAPI in the global config via the `defaultForSlots` parameter. If needed, adunits can be configured individually by setting an attribute of the `ortb2Imp` object for that adunit. This attribute will take precedence over `defaultForSlots` setting. |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | -| ortb2Imp.ext.ae | Integer | Auction Environment: 1 indicates FLEDGE eligible, 0 indicates it is not | Absence indicates this is not FLEDGE eligible | +| ortb2Imp.ext.ae | Integer | Auction Environment: 1 indicates PAAPI eligible, 0 indicates it is not | Absence indicates this is not PAAPI eligible | The `ae` field stands for Auction Environment and was chosen to be consistent with the field that GAM passes to bidders in their Open Bidding and Exchange Bidding APIs. More details on that can be found @@ -91,31 +69,31 @@ pbjs.addAdUnits({ ``` ## Bid Adapter Integration -Chrome has enabled a two-tier auction in FLEDGE. This allows multiple sellers (frequently SSPs) to act on behalf of the publisher with +Chrome has enabled a two-tier auction in PAAPI. This allows multiple sellers (frequently SSPs) to act on behalf of the publisher with a single entity serving as the final decision maker. In their [current approach](https://github.com/google/ads-privacy/tree/master/proposals/fledge-multiple-seller-testing), GPT has opted to run the final auction layer while allowing other SSPs/sellers to participate as -[Component Auctions](https://github.com/WICG/turtledove/blob/main/FLEDGE.md#21-initiating-an-on-device-auction) which feed their -bids to the final layer. To learn more about Component Auctions, go [here](https://github.com/WICG/turtledove/blob/main/FLEDGE.md#24-scoring-bids-in-component-auctions). +[Component Auctions](https://github.com/WICG/turtledove/blob/main/PAAPI.md#21-initiating-an-on-device-auction) which feed their +bids to the final layer. To learn more about Component Auctions, go [here](https://github.com/WICG/turtledove/blob/main/PAAPI.md#24-scoring-bids-in-component-auctions). -The FLEDGE auction, including Component Auctions, are configured via an `AuctionConfig` object that defines the parameters of the auction for a given -seller. This module enables FLEDGE support by allowing bid adaptors to return `AuctionConfig` objects in addition to bids. If a bid adaptor returns an +The PAAPI auction, including Component Auctions, are configured via an `AuctionConfig` object that defines the parameters of the auction for a given +seller. This module enables PAAPI support by allowing bid adaptors to return `AuctionConfig` objects in addition to bids. If a bid adaptor returns an `AuctionConfig` object, Prebid.js will register it with the appropriate GPT ad slot so the bidder can participate as a Component Auction in the overall -FLEDGE auction for that slot. More details on the GPT API can be found [here](https://developers.google.com/publisher-tag/reference#googletag.config.componentauctionconfig). +PAAPI auction for that slot. More details on the GPT API can be found [here](https://developers.google.com/publisher-tag/reference#googletag.config.componentauctionconfig). -Modifying a bid adapter to support FLEDGE is a straightforward process and consists of the following steps: -1. Detecting when a bid request is FLEDGE eligible +Modifying a bid adapter to support PAAPI is a straightforward process and consists of the following steps: +1. Detecting when a bid request is PAAPI eligible 2. Responding with AuctionConfig -FLEDGE eligibility is made available to bid adapters through the `bidderRequest.fledgeEnabled` field. +PAAPI eligibility is made available to bid adapters through the `bidderRequest.paapi.enabled` field. The [`bidderRequest`](https://docs.prebid.org/dev-docs/bidder-adaptor.html#bidderrequest-parameters) object is passed to the [`buildRequests`](https://docs.prebid.org/dev-docs/bidder-adaptor.html#building-the-request) method of an adapter. Bid adapters -who wish to participate should read this flag and pass it to their server. FLEDGE eligibility depends on a number of parameters: +who wish to participate should read this flag and pass it to their server. PAAPI eligibility depends on a number of parameters: 1. Chrome enablement 2. Publisher participatipon in the [Origin Trial](https://developer.chrome.com/docs/privacy-sandbox/unified-origin-trial/#configure) 3. Publisher Prebid.js configuration (detailed above) -When a bid request is FLEDGE enabled, a bid adapter can return a tuple consisting of bids and AuctionConfig objects rather than just a list of bids: +When a bid request is PAAPI enabled, a bid adapter can return a tuple consisting of bids and AuctionConfig objects rather than just a list of bids: ```js function interpretResponse(resp, req) { @@ -138,8 +116,8 @@ An AuctionConfig must be associated with an adunit and auction, and this is acco `validBidRequests` array passed to the `buildRequests` function - see [here](https://docs.prebid.org/dev-docs/bidder-adaptor.html#ad-unit-params-in-the-validbidrequests-array) for more details. This means that the AuctionConfig objects returned from `interpretResponse` must contain a `bidId` field whose value corresponds to the request it should be associated with. This may raise the question: why isn't the AuctionConfig object returned as part of the bid? The -answer is that it's possible to participate in the FLEDGE auction without returning a contextual bid. +answer is that it's possible to participate in the PAAPI auction without returning a contextual bid. An example of this can be seen in the OpenX OpenRTB bid adapter [here](https://github.com/prebid/Prebid.js/blob/master/modules/openxOrtbBidAdapter.js#L327). -Other than the addition of the `bidId` field, the AuctionConfig object should adhere to the requirements set forth in FLEDGE. The details of creating an AuctionConfig object are beyond the scope of this document. +Other than the addition of the `bidId` field, the AuctionConfig object should adhere to the requirements set forth in PAAPI. The details of creating an AuctionConfig object are beyond the scope of this document. diff --git a/modules/pairIdSystem.js b/modules/pairIdSystem.js index dbff4c6a402..778857bae1c 100644 --- a/modules/pairIdSystem.js +++ b/modules/pairIdSystem.js @@ -50,9 +50,11 @@ export const pairIdSubmodule = { return value && Array.isArray(value) ? {'pairId': value} : undefined }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @returns {id: string | undefined } + * Performs action to obtain ID and return a value in the callback's response argument. + * @function getId + * @param {Object} config - The configuration object. + * @param {Object} config.params - The parameters from the configuration. + * @returns {{id: string[] | undefined}} The obtained IDs or undefined if no IDs are found. */ getId(config) { const pairIdsString = pairIdFromLocalStorage(PAIR_ID_KEY) || pairIdFromCookie(PAIR_ID_KEY) @@ -67,13 +69,28 @@ export const pairIdSubmodule = { const configParams = (config && config.params) || {}; if (configParams && configParams.liveramp) { - let LRStorageLocation = configParams.liveramp.storageKey || DEFAULT_LIVERAMP_PAIR_ID_KEY - const liverampValue = pairIdFromLocalStorage(LRStorageLocation) || pairIdFromCookie(LRStorageLocation) - try { - const obj = JSON.parse(atob(liverampValue)); - ids = ids.concat(obj.envelope); - } catch (error) { - logInfo(error) + let LRStorageLocation = configParams.liveramp.storageKey || DEFAULT_LIVERAMP_PAIR_ID_KEY; + const liverampValue = pairIdFromLocalStorage(LRStorageLocation) || pairIdFromCookie(LRStorageLocation); + + if (liverampValue) { + try { + const parsedValue = atob(liverampValue); + if (parsedValue) { + const obj = JSON.parse(parsedValue); + + if (obj && typeof obj === 'object' && obj.envelope) { + ids = ids.concat(obj.envelope); + } else { + logInfo('Pairid: Parsed object is not valid or does not contain envelope'); + } + } else { + logInfo('Pairid: Decoded value is empty'); + } + } catch (error) { + logInfo('Pairid: Error parsing JSON: ', error); + } + } else { + logInfo('Pairid: liverampValue for pairId from storage is empty or null'); } } diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js deleted file mode 100644 index 5651bdf0434..00000000000 --- a/modules/parrableIdSystem.js +++ /dev/null @@ -1,416 +0,0 @@ -/** - * This module adds Parrable to the User ID module - * The {@link module:modules/userId} module is required - * @module modules/parrableIdSystem - * @requires module:modules/userId - */ - -// ci trigger: 1 - -import { - contains, - deepClone, - inIframe, - isEmpty, - isPlainObject, - logError, - logWarn, - pick, - timestamp -} from '../src/utils.js'; -import {find} from '../src/polyfill.js'; -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js'; -import {getRefererInfo} from '../src/refererDetection.js'; -import {uspDataHandler} from '../src/adapterManager.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {MODULE_TYPE_UID} from '../src/activities/modules.js'; - -/** - * @typedef {import('../modules/userId/index.js').Submodule} Submodule - * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig - * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData - */ - -const PARRABLE_URL = 'https://h.parrable.com/prebid'; -const PARRABLE_COOKIE_NAME = '_parrable_id'; -const PARRABLE_GVLID = 928; -const LEGACY_ID_COOKIE_NAME = '_parrable_eid'; -const LEGACY_OPTOUT_COOKIE_NAME = '_parrable_optout'; -const ONE_YEAR_MS = 364 * 24 * 60 * 60 * 1000; -const EXPIRE_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:00 GMT'; -const MODULE_NAME = 'parrableId'; - -const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); - -function getExpirationDate() { - const oneYearFromNow = new Date(timestamp() + ONE_YEAR_MS); - return oneYearFromNow.toGMTString(); -} - -function deserializeParrableId(parrableIdStr) { - const parrableId = {}; - const values = parrableIdStr.split(','); - - values.forEach(function(value) { - const pair = value.split(':'); - if (pair[0] === 'ccpaOptout' || pair[0] === 'ibaOptout') { // unpack a value of 0 or 1 as boolean - parrableId[pair[0]] = Boolean(+pair[1]); - } else if (!isNaN(pair[1])) { // convert to number if is a number - parrableId[pair[0]] = +pair[1] - } else { - parrableId[pair[0]] = pair[1] - } - }); - - return parrableId; -} - -function serializeParrableId(parrableIdAndParams) { - let components = []; - - if (parrableIdAndParams.eid) { - components.push('eid:' + parrableIdAndParams.eid); - } - if (parrableIdAndParams.ibaOptout) { - components.push('ibaOptout:1'); - } - if (parrableIdAndParams.ccpaOptout) { - components.push('ccpaOptout:1'); - } - if (parrableIdAndParams.tpcSupport !== undefined) { - const tpcSupportComponent = parrableIdAndParams.tpcSupport === true ? 'tpc:1' : 'tpc:0'; - const tpcUntil = `tpcUntil:${parrableIdAndParams.tpcUntil}`; - components.push(tpcSupportComponent); - components.push(tpcUntil); - } - if (parrableIdAndParams.filteredUntil) { - components.push(`filteredUntil:${parrableIdAndParams.filteredUntil}`); - components.push(`filterHits:${parrableIdAndParams.filterHits}`); - } - - return components.join(','); -} - -function isValidConfig(configParams) { - if (!configParams) { - logError('User ID - parrableId submodule requires configParams'); - return false; - } - if (!configParams.partners && !configParams.partner) { - logError('User ID - parrableId submodule requires partner list'); - return false; - } - if (configParams.storage) { - logWarn('User ID - parrableId submodule does not require a storage config'); - } - return true; -} - -function encodeBase64UrlSafe(base64) { - const ENC = { - '+': '-', - '/': '_', - '=': '.' - }; - return base64.replace(/[+/=]/g, (m) => ENC[m]); -} - -function readCookie() { - const parrableIdStr = storage.getCookie(PARRABLE_COOKIE_NAME); - if (parrableIdStr) { - const parsedCookie = deserializeParrableId(decodeURIComponent(parrableIdStr)); - const { tpc, tpcUntil, filteredUntil, filterHits, ...parrableId } = parsedCookie; - let { eid, ibaOptout, ccpaOptout, ...params } = parsedCookie; - - if ((Date.now() / 1000) >= tpcUntil) { - params.tpc = undefined; - } - - if ((Date.now() / 1000) < filteredUntil) { - params.shouldFilter = true; - params.filteredUntil = filteredUntil; - } else { - params.shouldFilter = false; - params.filterHits = filterHits; - } - return { parrableId, params }; - } - return null; -} - -function writeCookie(parrableIdAndParams) { - if (parrableIdAndParams) { - const parrableIdStr = encodeURIComponent(serializeParrableId(parrableIdAndParams)); - storage.setCookie(PARRABLE_COOKIE_NAME, parrableIdStr, getExpirationDate(), 'lax'); - } -} - -function readLegacyCookies() { - const eid = storage.getCookie(LEGACY_ID_COOKIE_NAME); - const ibaOptout = (storage.getCookie(LEGACY_OPTOUT_COOKIE_NAME) === 'true'); - if (eid || ibaOptout) { - const parrableId = {}; - if (eid) { - parrableId.eid = eid; - } - if (ibaOptout) { - parrableId.ibaOptout = ibaOptout; - } - return parrableId; - } - return null; -} - -function migrateLegacyCookies(parrableId) { - if (parrableId) { - writeCookie(parrableId); - if (parrableId.eid) { - storage.setCookie(LEGACY_ID_COOKIE_NAME, '', EXPIRE_COOKIE_DATE); - } - if (parrableId.ibaOptout) { - storage.setCookie(LEGACY_OPTOUT_COOKIE_NAME, '', EXPIRE_COOKIE_DATE); - } - } -} - -function shouldFilterImpression(configParams, parrableId) { - const config = configParams.timezoneFilter; - - if (!config) { - return false; - } - - if (parrableId) { - return false; - } - - const offset = (new Date()).getTimezoneOffset() / 60; - const zone = Intl.DateTimeFormat().resolvedOptions().timeZone; - - function isZoneListed(list, zone) { - // IE does not provide a timeZone in IANA format so zone will be empty - const zoneLowercase = zone && zone.toLowerCase(); - return !!(list && zone && find(list, zn => zn.toLowerCase() === zoneLowercase)); - } - - function isAllowed() { - if (isEmpty(config.allowedZones) && - isEmpty(config.allowedOffsets)) { - return true; - } - if (isZoneListed(config.allowedZones, zone)) { - return true; - } - if (contains(config.allowedOffsets, offset)) { - return true; - } - return false; - } - - function isBlocked() { - if (isEmpty(config.blockedZones) && - isEmpty(config.blockedOffsets)) { - return false; - } - if (isZoneListed(config.blockedZones, zone)) { - return true; - } - if (contains(config.blockedOffsets, offset)) { - return true; - } - return false; - } - - return isBlocked() || !isAllowed(); -} - -function epochFromTtl(ttl) { - return Math.floor((Date.now() / 1000) + ttl); -} - -function incrementFilterHits(parrableId, params) { - params.filterHits += 1; - writeCookie({ ...parrableId, ...params }) -} - -function fetchId(configParams, gdprConsentData) { - if (!isValidConfig(configParams)) return; - - let { parrableId, params } = readCookie() || {}; - if (!parrableId) { - parrableId = readLegacyCookies(); - migrateLegacyCookies(parrableId); - } - - if (shouldFilterImpression(configParams, parrableId)) { - return null; - } - - const eid = parrableId ? parrableId.eid : null; - const refererInfo = getRefererInfo(); - const tpcSupport = params ? params.tpc : null; - const shouldFilter = params ? params.shouldFilter : null; - const uspString = uspDataHandler.getConsentData(); - const gdprApplies = (gdprConsentData && typeof gdprConsentData.gdprApplies === 'boolean' && gdprConsentData.gdprApplies); - const gdprConsentString = (gdprConsentData && gdprApplies && gdprConsentData.consentString) || ''; - const partners = configParams.partners || configParams.partner; - const trackers = typeof partners === 'string' - ? partners.split(',') - : partners; - - const data = { - eid, - trackers, - url: refererInfo.page, - prebidVersion: '$prebid.version$', - isIframe: inIframe(), - tpcSupport - }; - - if (shouldFilter === false) { - data.filterHits = params.filterHits; - } - - const searchParams = { - data: encodeBase64UrlSafe(btoa(JSON.stringify(data))), - gdpr: gdprApplies ? 1 : 0, - _rand: Math.random() - }; - - if (uspString) { - searchParams.us_privacy = uspString; - } - - if (gdprApplies) { - searchParams.gdpr_consent = gdprConsentString; - } - - const options = { - method: 'GET', - withCredentials: true - }; - - const callback = function (cb) { - const callbacks = { - success: response => { - let newParrableId = parrableId ? deepClone(parrableId) : {}; - let newParams = {}; - if (response) { - try { - let responseObj = JSON.parse(response); - if (responseObj) { - if (responseObj.ccpaOptout !== true) { - newParrableId.eid = responseObj.eid; - } else { - newParrableId.eid = null; - newParrableId.ccpaOptout = true; - } - if (responseObj.ibaOptout === true) { - newParrableId.ibaOptout = true; - } - if (responseObj.tpcSupport !== undefined) { - newParams.tpcSupport = responseObj.tpcSupport; - newParams.tpcUntil = epochFromTtl(responseObj.tpcSupportTtl); - } - if (responseObj.filterTtl) { - newParams.filteredUntil = epochFromTtl(responseObj.filterTtl); - newParams.filterHits = 0; - } - } - } catch (error) { - logError(error); - cb(); - } - writeCookie({ ...newParrableId, ...newParams }); - cb(newParrableId); - } else { - logError('parrableId: ID fetch returned an empty result'); - cb(); - } - }, - error: error => { - logError(`parrableId: ID fetch encountered an error`, error); - cb(); - } - }; - - if (shouldFilter) { - incrementFilterHits(parrableId, params); - } else { - ajax(PARRABLE_URL, callbacks, searchParams, options); - } - }; - - return { - callback, - id: parrableId - }; -} - -/** @type {Submodule} */ -export const parrableIdSubmodule = { - /** - * used to link submodule with config - * @type {string} - */ - name: MODULE_NAME, - /** - * Global Vendor List ID - * @type {number} - */ - gvlid: PARRABLE_GVLID, - - /** - * decode the stored id value for passing to bid requests - * @function - * @param {ParrableId} parrableId - * @return {(Object|undefined} - */ - decode(parrableId) { - if (parrableId && isPlainObject(parrableId)) { - return { parrableId }; - } - return undefined; - }, - - /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] - * @returns {function(callback:function), id:ParrableId} - */ - getId(config, gdprConsentData, currentStoredId) { - const configParams = (config && config.params) || {}; - return fetchId(configParams, gdprConsentData); - }, - eids: { - 'parrableId': { - source: 'parrable.com', - atype: 1, - getValue: function(parrableId) { - if (parrableId.eid) { - return parrableId.eid; - } - if (parrableId.ccpaOptout) { - // If the EID was suppressed due to a non consenting ccpa optout then - // we still wish to provide this as a reason to the adapters - return ''; - } - return null; - }, - getUidExt: function(parrableId) { - const extendedData = pick(parrableId, [ - 'ibaOptout', - 'ccpaOptout' - ]); - if (Object.keys(extendedData).length) { - return extendedData; - } - } - }, - }, -}; - -submodule('userId', parrableIdSubmodule); diff --git a/modules/performaxBidAdapter.js b/modules/performaxBidAdapter.js new file mode 100644 index 00000000000..a765c4d9d78 --- /dev/null +++ b/modules/performaxBidAdapter.js @@ -0,0 +1,77 @@ +import { deepSetValue, deepAccess } 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 BIDDER_CODE = 'performax'; +const BIDDER_SHORT_CODE = 'px'; +const GVLID = 732 +const ENDPOINT = 'https://dale.performax.cz/ortb' +export const converter = ortbConverter({ + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'tagid', bidRequest.params.tagid); + return imp; + }, + + bidResponse(buildBidResponse, bid, context) { + context.netRevenue = deepAccess(bid, 'netRevenue'); + context.mediaType = deepAccess(bid, 'mediaType'); + context.currency = deepAccess(bid, 'currency'); + + return buildBidResponse(bid, context) + }, + + context: { + ttl: 360, + } +}) + +export const spec = { + code: BIDDER_CODE, + aliases: [BIDDER_SHORT_CODE], + gvlid: GVLID, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!bid.params.tagid; + }, + + buildRequests: function (bidRequests, bidderRequest) { + let data = converter.toORTB({bidderRequest, bidRequests}) + return [{ + method: 'POST', + url: ENDPOINT, + options: {'contentType': 'application/json'}, + data: data + }] + }, + + interpretResponse: function (bidderResponse, request) { + if (!bidderResponse.body) return []; + const response = bidderResponse.body + const data = { + + seatbid: response.seatbid.map(seatbid => ({ + seat: seatbid.seat, + bid: seatbid.bid.map(bid => ({ + impid: bid.imp_id, + w: bid.w, + h: bid.h, + requestId: request.data.id, + price: bid.price, + currency: response.cur, + adm: bid.adm, + crid: bid.id, + netRevenue: true, + mediaType: BANNER, + })) + })) + }; + return converter.fromORTB({ response: data, request: request.data }).bids + }, + +} + +registerBidder(spec); diff --git a/modules/performaxBidAdapter.md b/modules/performaxBidAdapter.md new file mode 100644 index 00000000000..8b5b702a8e6 --- /dev/null +++ b/modules/performaxBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: Performax Bid Adapter +Module Type: Bidder Adapter +Maintainer: development@performax.cz +``` + +# Description + +Connects to Performax exchange for bids. + +Performax bid adapter supports Banner. + + +# Sample Banner Ad Unit: For Publishers + +```javascript + var adUnits = [ + { + code: 'performax-div', + mediaTypes: { + banner: {sizes: [[300, 300]]}, + }, + bids: [ + { + bidder: "performax", + params: { + tagid: "sample" // required + } + } + ] + }, + ]; +``` + diff --git a/modules/permutiveIdentityManagerIdSystem.js b/modules/permutiveIdentityManagerIdSystem.js new file mode 100644 index 00000000000..5dc12d44edb --- /dev/null +++ b/modules/permutiveIdentityManagerIdSystem.js @@ -0,0 +1,151 @@ +import {MODULE_TYPE_UID} from '../src/activities/modules.js' +import {submodule} from '../src/hook.js' +import {getStorageManager} from '../src/storageManager.js' +import {prefixLog, safeJSONParse} from '../src/utils.js' +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const MODULE_NAME = 'permutiveIdentityManagerId' +const PERMUTIVE_ID_DATA_STORAGE_KEY = 'permutive-prebid-id' + +const ID5_DOMAIN = 'id5-sync.com' +const LIVERAMP_DOMAIN = 'liveramp.com' +const UID_DOMAIN = 'uidapi.com' + +const PRIMARY_IDS = ['id5id', 'idl_env', 'uid2'] + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}) + +const logger = prefixLog('[PermutiveID]') + +const readFromSdkLocalStorage = () => { + const data = safeJSONParse(storage.getDataFromLocalStorage(PERMUTIVE_ID_DATA_STORAGE_KEY)) + const id = {} + if (data && typeof data === 'object' && 'providers' in data && typeof data.providers === 'object') { + const now = Date.now() + for (const [idName, value] of Object.entries(data.providers)) { + if (PRIMARY_IDS.includes(idName) && value.userId) { + if (!value.expiryTime || value.expiryTime > now) { + id[idName] = value.userId + } + } + } + } + return id +} + +/** + * Catch and log errors + * @param {function} fn - Function to safely evaluate + */ +function makeSafe (fn) { + try { + return fn() + } catch (e) { + logger.logError(e) + } +} + +const waitAndRetrieveFromSdk = (timeoutMs) => + new Promise( + resolve => { + const fallback = setTimeout(() => { + logger.logInfo('timeout expired waiting for SDK - attempting read from local storage again') + resolve(readFromSdkLocalStorage()) + }, timeoutMs) + return window?.permutive?.ready(() => makeSafe(() => { + logger.logInfo('Permutive SDK is ready') + const onReady = makeSafe(() => window.permutive.addons.identity_manager.prebid.onReady) + if (typeof onReady === 'function') { + onReady((ids) => { + logger.logInfo('Permutive SDK has provided ids') + resolve(ids) + clearTimeout(fallback) + }) + } else { + logger.logError('Permutive SDK initialised but identity manager prebid api not present') + } + })) + } + ) + +/** @type {Submodule} */ +export const permutiveIdentityManagerIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * decode the stored id value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @param {SubmoduleConfig|undefined} config + * @returns {(Object|undefined)} + */ + decode(value, config) { + return value + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function getId + * @param {SubmoduleConfig} submoduleConfig + * @param {ConsentData} consentData + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ + getId(submoduleConfig, consentData, cacheIdObj) { + const id = readFromSdkLocalStorage() + if (Object.entries(id).length > 0) { + logger.logInfo('found id in sdk storage') + return { id } + } else if ('params' in submoduleConfig && submoduleConfig.params.ajaxTimeout) { + logger.logInfo('failed to find id in sdk storage - waiting for sdk') + // Is ajaxTimeout an appropriate timeout to use here? + return { callback: (done) => waitAndRetrieveFromSdk(submoduleConfig.params.ajaxTimeout).then(done) } + } else { + logger.logInfo('failed to find id in sdk storage and no wait time specified') + } + }, + + primaryIds: PRIMARY_IDS, + + eids: { + 'id5id': { + getValue: function (data) { + return data.uid + }, + source: ID5_DOMAIN, + atype: 1, + getUidExt: function (data) { + if (data.ext) { + return data.ext + } + } + }, + 'idl_env': { + source: LIVERAMP_DOMAIN, + atype: 3, + }, + 'uid2': { + source: UID_DOMAIN, + atype: 3, + getValue: function(data) { + return data.id + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext + } + } + } + } +} + +submodule('userId', permutiveIdentityManagerIdSubmodule) diff --git a/modules/permutiveIdentityManagerIdSystem.md b/modules/permutiveIdentityManagerIdSystem.md new file mode 100644 index 00000000000..ae249803d11 --- /dev/null +++ b/modules/permutiveIdentityManagerIdSystem.md @@ -0,0 +1,58 @@ +# Permutive Identity Manager + +This module supports [Permutive](https://permutive.com/) customers in using Permutive's Identity Manager functionality. + +To use this Prebid.js module it is assumed that the site includes Permutive's SDK, with Identity Manager configuration +enabled. See Permutive's user documentation for more information on Identity Manager. + +## Building Prebid.js with Permutive Identity Manager Support + +Prebid.js must be built with the `permutiveIdentityManagerIdSystem` module in order for Permutive's Identity Manager to be able to +activate relevant user identities to Prebid. + +To build Prebid.js with the `permutiveIdentityManagerIdSystem` module included: + +``` +gulp build --modules=userId,permutiveIdentityManagerIdSystem +``` + +## Prebid configuration + +There is minimal configuration required to be set on Prebid.js, since the bulk of the behaviour is managed through +Permutive's dashboard and SDK. + +It is recommended to keep the Prebid.js caching for this module short, since the mechanism by which Permutive's SDK +communicates with Prebid.js is effectively a local cache anyway. + +``` +pbjs.setConfig({ + ... + userSync: { + userIds: [ + { + name: 'permutiveIdentityManagerId', + params: { + ajaxTimeout: 90 + }, + storage: { + type: 'html5', + name: 'permutiveIdentityManagerId', + refreshInSeconds: 5 + } + } + ], + auctionDelay: 100 + }, + ... +}); +``` + +### ajaxTimeout + +By default this module will read IDs provided by the Permutive SDK from local storage when requested by prebid, and if +nothing is found, will not provide any identities. If a timeout is provided via the `ajaxTimeout` parameter, it will +instead wait for up to the specified number of milliseconds for Permutive's SDK to become available, and will retrieve +identities from the SDK directly if/when this happens. + +This value should be set to a value smaller than the `auctionDelay` set on the `userSync` configuration object, since +there is no point waiting longer than this as the auction will already have been triggered. \ No newline at end of file diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index c42a15d9197..bb2dff6189e 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -95,7 +95,8 @@ export function getModuleConfig(customModuleConfig) { /** * Sets ortb2 config for ac bidders * @param {Object} bidderOrtb2 - The ortb2 object for the all bidders - * @param {Object} customModuleConfig - Publisher config for module + * @param {Object} moduleConfig - Publisher config for module + * @param {Object} segmentData - Segment data grouped by bidder or type */ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const acBidders = deepAccess(moduleConfig, 'params.acBidders') @@ -129,13 +130,13 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { /** * Updates `user.data` object in existing bidder config with Permutive segments - * @param string bidder - The bidder + * @param {string} bidder - The bidder identifier * @param {Object} currConfig - Current bidder config - * @param {Object[]} transformationConfigs - array of objects with `id` and `config` properties, used to determine - * the transformations on user data to include the ORTB2 object * @param {string[]} segmentIDs - Permutive segment IDs * @param {string[]} sspSegmentIDs - Permutive SSP segment IDs * @param {Object} topics - Privacy Sandbox Topics, keyed by IAB taxonomy version (600, 601, etc.) + * @param {Object[]} transformationConfigs - array of objects with `id` and `config` properties, used to determine + * the transformations on user data to include the ORTB2 object * @param {Object} segmentData - The segments available for targeting * @return {Object} Merged ortb2 object */ @@ -270,7 +271,7 @@ function setSegments (reqBidsConfigObj, moduleConfig, segmentData) { */ function makeSafe (fn) { try { - fn() + return fn() } catch (e) { logError(e) } @@ -310,23 +311,71 @@ export function isPermutiveOnPage () { * @param {number} maxSegs - Maximum number of segments to be included * @return {Object} */ -export function getSegments (maxSegs) { - const legacySegs = readSegments('_psegs', []).map(Number).filter(seg => seg >= 1000000).map(String) - const _ppam = readSegments('_ppam', []) - const _pcrprs = readSegments('_pcrprs', []) - +export function getSegments(maxSegs) { const segments = { - ac: [..._pcrprs, ..._ppam, ...legacySegs], - ix: readSegments('_pindexs', []), - rubicon: readSegments('_prubicons', []), - appnexus: readSegments('_papns', []), - gam: readSegments('_pdfps', []), - ssp: readSegments('_pssps', { - cohorts: [], - ssps: [] + ac: + makeSafe(() => { + const legacySegs = + makeSafe(() => + readSegments('_psegs', []) + .map(Number) + .filter((seg) => seg >= 1000000) + .map(String), + ) || []; + const _ppam = makeSafe(() => readSegments('_ppam', []).map(String)) || []; + const _pcrprs = makeSafe(() => readSegments('_pcrprs', []).map(String)) || []; + + return [..._pcrprs, ..._ppam, ...legacySegs]; + }) || [], + + ix: + makeSafe(() => { + const _pindexs = readSegments('_pindexs', []); + return _pindexs.map(String); + }) || [], + + rubicon: + makeSafe(() => { + const _prubicons = readSegments('_prubicons', []); + return _prubicons.map(String); + }) || [], + + appnexus: + makeSafe(() => { + const _papns = readSegments('_papns', []); + return _papns.map(String); + }) || [], + + gam: + makeSafe(() => { + const _pdfps = readSegments('_pdfps', []); + return _pdfps.map(String); + }) || [], + + ssp: makeSafe(() => { + const _pssps = readSegments('_pssps', { + cohorts: [], + ssps: [], + }); + + return { + cohorts: makeSafe(() => _pssps.cohorts.map(String)) || [], + ssps: makeSafe(() => _pssps.ssps.map(String)) || [], + }; }), - topics: readSegments('_ppsts', {}), - } + + topics: + makeSafe(() => { + const _ppsts = readSegments('_ppsts', {}); + + const topics = {}; + for (const [k, value] of Object.entries(_ppsts)) { + topics[k] = makeSafe(() => value.map(String)) || []; + } + + return topics; + }) || {}, + }; for (const bidder in segments) { if (bidder === 'ssp') { @@ -342,7 +391,8 @@ export function getSegments (maxSegs) { } } - return segments + logger.logInfo(`Read segments`, segments) + return segments; } /** @@ -393,7 +443,7 @@ function iabSegmentId(permutiveSegmentId, iabIds) { * Pull the latest configuration and cohort information and update accordingly. * * @param reqBidsConfigObj - Bidder provided config for request - * @param customModuleConfig - Publisher provide config + * @param moduleConfig - Publisher provided config */ export function readAndSetCohorts(reqBidsConfigObj, moduleConfig) { const segmentData = getSegments(deepAccess(moduleConfig, 'params.maxSegs')) diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 9399dffab93..3cf3ed2b367 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -50,14 +50,15 @@ as well as enabling settings for specific use cases mentioned above (e.g. acbidd #### Context -Permutive is not listed as a TCF vendor as all data collection is on behalf of the publisher and based on consent the publisher has received from the user. -Rather than through the TCF framework, this consent is provided to Permutive when the user gives the relevant permissions on the publisher website which allow the Permutive SDK to run. -This means that if GDPR enforcement is configured _and_ the user consent isn’t given for Permutive to fire, no cohorts will populate. -As Prebid utilizes TCF vendor consent, for the Permutive RTD module to load, Permutive needs to be labeled within the Vendor Exceptions +While Permutive is listed as a TCF vendor (ID: 361), Permutive does not obtain consent directly from the TCF. As we act as a processor on behalf of our publishers consent is given to the Permutive SDK by the publisher, not by the [GDPR Consent Management Module](https://prebid-docs.atre.net/dev-docs/modules/consentManagement.html). + +This means that if GDPR enforcement is configured within the Permutive SDK _and_ the user consent isn’t given for Permutive to fire, no cohorts will populate. + +If you are also using the [TCF Control Module](https://docs.prebid.org/dev-docs/modules/tcfControl.html), in order to prevent Permutive from being blocked, it needs to be labeled within the Vendor Exceptions. #### Instructions -1. Publisher enables rules within Prebid GDPR module +1. Publisher enables rules within Prebid.js configuration. 2. Label Permutive as an exception, as shown below. ```javascript [ diff --git a/modules/pgamsspBidAdapter.js b/modules/pgamsspBidAdapter.js index fdc6bcf302f..36dbd1159cc 100644 --- a/modules/pgamsspBidAdapter.js +++ b/modules/pgamsspBidAdapter.js @@ -1,253 +1,21 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.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'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor, - eids: [] - }; - - if (bid.userId) { - getUserId(placement.eids, bid.userId.uid2?.id, 'uidapi.com'); - getUserId(placement.eids, bid.userId.id5id?.uid, 'id5-sync.com'); - } - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} -function getUserId(eids, id, source, uidExt) { - if (id) { - var uid = { id }; - if (uidExt) { - uid.ext = uidExt; - } - eids.push({ - source, - uids: [ uid ] - }); - } -} - export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - tmax: bidderRequest.timeout - }; - - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - - if (bidderRequest.gdprConsent) { - request.gdpr = { - consentString: bidderRequest.gdprConsent.consentString - }; - } - - if (bidderRequest.gppConsent) { - request.gpp = bidderRequest.gppConsent.gppString; - request.gpp_sid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - request.gpp = bidderRequest.ortb2.regs.gpp; - request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { - syncUrl += '&gpp=' + gppConsent.gppString; - syncUrl += '&gpp_sid=' + gppConsent.applicableSections.join(','); - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/pirIdSystem.js b/modules/pirIdSystem.js deleted file mode 100644 index 233176028d3..00000000000 --- a/modules/pirIdSystem.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * This module adds pirId to the User ID module - * The {@link module:modules/userId} module is required - * @module modules/pirId - * @requires module:modules/userId - */ - -import { MODULE_TYPE_UID } from '../src/activities/modules.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { submodule } from '../src/hook.js'; -import {domainOverrideToRootDomain} from '../libraries/domainOverrideToRootDomain/index.js'; - -/** - * @typedef {import('../modules/userId/index.js').Submodule} Submodule - * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse - */ - -const MODULE_NAME = 'pirId'; -const ID_TOKEN = 'WPxid'; -export const storage = getStorageManager({ moduleName: MODULE_NAME, moduleType: MODULE_TYPE_UID }); - -/** - * Reads the ID token from local storage or cookies. - * @returns {string|undefined} The ID token, or undefined if not found. - */ -export const readId = () => storage.getDataFromLocalStorage(ID_TOKEN) || storage.getCookie(ID_TOKEN); - -/** @type {Submodule} */ -export const pirIdSubmodule = { - name: MODULE_NAME, - gvlid: 676, - - /** - * decode the stored id value for passing to bid requests - * @function decode - * @param {string} value - * @returns {(Object|undefined)} - */ - decode(value) { - return typeof value === 'string' ? { 'pirId': value } : undefined; - }, - - /** - * performs action to obtain id and return a value - * @function - * @returns {(IdResponse|undefined)} - */ - getId() { - const pirIdToken = readId(); - - return pirIdToken ? { id: pirIdToken } : undefined; - }, - domainOverride: domainOverrideToRootDomain(storage, MODULE_NAME), - eids: { - 'pirId': { - source: 'pir.wp.pl', - atype: 1 - }, - }, -}; - -submodule('userId', pirIdSubmodule); diff --git a/modules/pirIdSystem.md b/modules/pirIdSystem.md deleted file mode 100644 index 913804f85c4..00000000000 --- a/modules/pirIdSystem.md +++ /dev/null @@ -1,27 +0,0 @@ -# Overview - -Module Name: pirIDSystem -Module Type: UserID Module -Maintainer: pawel.grudzien@grupawp.pl - -# Description - -User identification system for WPM - -### Prebid Params example - -``` -pbjs.setConfig({ - userSync: { - userIds: [{ - name: 'pirID', - storage: { - type: 'cookie', - name: 'pirIdToken', - expires: 7, - refreshInSeconds: 360 - }, - }] - } -}); -``` diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index 1c3f9b8da1a..c7ed1ec989d 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -5,7 +5,6 @@ import {config} from '../src/config.js'; import {find, includes} from '../src/polyfill.js'; import {deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; import {auctionManager} from '../src/auctionManager.js'; -import {getGlobal} from '../src/prebidGlobal.js'; import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; @@ -126,7 +125,7 @@ export const spec = { method: 'POST', options: {withCredentials: true}, data: { - v: getGlobal().version, + v: 'v' + '$prebid.version$', pageUrl: referer, bidId: bidRequest.bidId, // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 @@ -277,7 +276,7 @@ function bidToTag(bid) { } tag.keywords = getANKeywordParam(bid.ortb2, bid.params.keywords) - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + let gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { tag.gpid = gpid; } diff --git a/modules/playdigoBidAdapter.js b/modules/playdigoBidAdapter.js new file mode 100644 index 00000000000..5d65a91e1a7 --- /dev/null +++ b/modules/playdigoBidAdapter.js @@ -0,0 +1,21 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'playdigo'; +const GVLID = 1302; +const AD_URL = 'https://server.playdigo.com/pbjs'; +const SYNC_URL = 'https://cs.playdigo.com'; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/playdigoBidAdapter.md b/modules/playdigoBidAdapter.md new file mode 100644 index 00000000000..1c63cce79a1 --- /dev/null +++ b/modules/playdigoBidAdapter.md @@ -0,0 +1,78 @@ +# Overview + +``` +Module Name: Playdigo Bidder Adapter +Module Type: Playdigo Bidder Adapter +Maintainer: yr@playdigo.com +``` + +# Description + +One of the easiest way to gain access to Playdigo demand sources - Playdigo header bidding adapter. +Playdigo header bidding adapter connects with Playdigo demand sources to fetch bids for display placements + +# Test Parameters +``` + var adUnits = [ + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'playdigo', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'playdigo', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'playdigo', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/prebidServerBidAdapter/bidderConfig.js b/modules/prebidServerBidAdapter/bidderConfig.js new file mode 100644 index 00000000000..f6f4fb91389 --- /dev/null +++ b/modules/prebidServerBidAdapter/bidderConfig.js @@ -0,0 +1,161 @@ +import {mergeDeep, deepEqual, deepAccess, deepSetValue, deepClone} from '../../src/utils.js'; +import {ORTB_EIDS_PATHS} from '../../src/activities/redactor.js'; + +/** + * Perform a partial pre-merge of bidder config for PBS. + * + * Prebid.js and Prebid Server use different strategies for merging global and bidder-specific config; JS attemps to + * merge arrays (concatenating them, with some deduping, cfr. mergeDeep), while PBS only merges objects - + * a bidder-specific array will replace a global array. + * + * This returns bidder config (from `bidder`) where arrays are replaced with what you get from merging them with `global`, + * so that the result of merging in PBS is the same as in JS. + */ +export function getPBSBidderConfig({global, bidder}) { + return Object.fromEntries( + Object.entries(bidder).map(([bidderCode, bidderConfig]) => { + return [bidderCode, replaceArrays(bidderConfig, mergeDeep({}, global, bidderConfig))] + }) + ) +} + +function replaceArrays(config, mergedConfig) { + return Object.fromEntries( + Object.entries(config).map(([key, value]) => { + const mergedValue = mergedConfig[key]; + if (Array.isArray(value)) { + if (!deepEqual(value, mergedValue) && Array.isArray(mergedValue)) { + value = mergedValue; + } + } else if (value != null && typeof value === 'object') { + value = replaceArrays(value, mergedValue); + } + return [key, value]; + }) + ) +} + +/** + * Extract all EIDs from FPD. + * + * Returns {eids, conflicts}, where: + * + * - `eids` contains an object of the form `{eid, bidders}` for each unique EID object found anywhere in FPD; + * `bidders` is a list of all the bidders that refer to that specific EID object, or false if that EID object is defined globally. + * - `conflicts` is a set containing all EID sources that appear in multiple, otherwise different, EID objects. + */ +export function extractEids({global, bidder}) { + const entries = []; + const bySource = {}; + const conflicts = new Set() + + function getEntry(eid) { + let entry = entries.find((candidate) => deepEqual(candidate.eid, eid)); + if (entry == null) { + entry = {eid, bidders: new Set()} + entries.push(entry); + } + if (bySource[eid.source] == null) { + bySource[eid.source] = entry.eid; + } else if (entry.eid === eid) { + // if this is the first time we see this eid, but not the first time we see its source, we have a conflict + conflicts.add(eid.source); + } + return entry; + } + + ORTB_EIDS_PATHS.forEach(path => { + (deepAccess(global, path) || []).forEach(eid => { + getEntry(eid).bidders = false; + }); + }) + Object.entries(bidder).forEach(([bidderCode, bidderConfig]) => { + ORTB_EIDS_PATHS.forEach(path => { + (deepAccess(bidderConfig, path) || []).forEach(eid => { + const entry = getEntry(eid); + if (entry.bidders !== false) { + entry.bidders.add(bidderCode); + } + }) + }) + }) + return {eids: entries.map(({eid, bidders}) => ({eid, bidders: bidders && Array.from(bidders)})), conflicts}; +} + +/** + * Consolidate extracted EIDs to take advantage of PBS's eidpermissions feature: + * https://docs.prebid.org/prebid-server/endpoints/openrtb2/pbs-endpoint-auction.html#eid-permissions + * + * If different bidders have different EID configurations, in most cases we can avoid repeating it in each bidder's + * specific config. As long as there are no conflicts (different EID objects that refer to the same source constitute a conflict), + * the EID can be set as global, and eidpermissions can restrict its access only to specific bidders. + * + * Returns {global, bidder, permissions}, where: + * - `global` is a list of global EID objects (some of which may be restricted through `permissions` + * - `bidder` is a map from bidder code to EID objects that are specific to that bidder, and cannot be restricted through `permissions` + * - `permissions` is a list of EID permissions as expected by PBS. + */ +export function consolidateEids({eids, conflicts = new Set()}) { + const globalEntries = []; + const bidderEntries = []; + const byBidder = {}; + eids.forEach(eid => { + (eid.bidders === false ? globalEntries : bidderEntries).push(eid); + }); + bidderEntries.forEach(({eid, bidders}) => { + if (!conflicts.has(eid.source)) { + globalEntries.push({eid, bidders}) + } else { + bidders.forEach(bidderCode => { + (byBidder[bidderCode] = byBidder[bidderCode] || []).push(eid) + }) + } + }); + return { + global: globalEntries.map(({eid}) => eid), + permissions: globalEntries.filter(({bidders}) => bidders !== false).map(({eid, bidders}) => ({ + source: eid.source, + bidders + })), + bidder: byBidder + } +} + +function replaceEids({global, bidder}, requestedBidders) { + const consolidated = consolidateEids(extractEids({global, bidder})); + global = deepClone(global); + bidder = deepClone(bidder); + function removeEids(target) { + delete target?.user?.eids; + delete target?.user?.ext?.eids; + } + removeEids(global); + Object.values(bidder).forEach(removeEids); + if (consolidated.global.length) { + deepSetValue(global, 'user.ext.eids', consolidated.global); + } + if (requestedBidders?.length) { + consolidated.permissions.forEach((permission) => permission.bidders = permission.bidders.filter(bidder => requestedBidders.includes(bidder))); + } + if (consolidated.permissions.length) { + deepSetValue(global, 'ext.prebid.data.eidpermissions', consolidated.permissions); + } + Object.entries(consolidated.bidder).forEach(([bidderCode, bidderEids]) => { + if (bidderEids.length) { + deepSetValue(bidder[bidderCode], 'user.ext.eids', bidderEids); + } + }) + return {global, bidder} +} + +export function premergeFpd(ortb2Fragments, requestedBidders) { + if (ortb2Fragments == null || Object.keys(ortb2Fragments.bidder || {}).length === 0) { + return ortb2Fragments; + } else { + ortb2Fragments = replaceEids(ortb2Fragments, requestedBidders); + return { + ...ortb2Fragments, + bidder: getPBSBidderConfig(ortb2Fragments) + }; + } +} diff --git a/modules/prebidServerBidAdapter/config.js b/modules/prebidServerBidAdapter/config.js index 87274504f64..4a5ac1d8564 100644 --- a/modules/prebidServerBidAdapter/config.js +++ b/modules/prebidServerBidAdapter/config.js @@ -11,7 +11,7 @@ export const S2S_VENDORS = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' }, - timeout: 1000 + maxTimeout: 1000 }, 'rubicon': { adapter: 'prebidServer', @@ -24,20 +24,7 @@ export const S2S_VENDORS = { p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', }, - timeout: 500 - }, - 'openx': { - adapter: 'prebidServer', - enabled: true, - endpoint: { - p1Consent: 'https://prebid.openx.net/openrtb2/auction', - noP1Consent: 'https://prebid.openx.net/openrtb2/auction' - }, - syncEndpoint: { - p1Consent: 'https://prebid.openx.net/cookie_sync', - noP1Consent: 'https://prebid.openx.net/cookie_sync' - }, - timeout: 1000 + maxTimeout: 500 }, 'openwrap': { adapter: 'prebidServer', @@ -46,6 +33,6 @@ export const S2S_VENDORS = { p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' }, - timeout: 500 + maxTimeout: 500 } } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 037119d44a2..be9158fe047 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -1,10 +1,8 @@ import Adapter from '../../src/adapter.js'; import { - deepAccess, deepClone, flatten, generateUUID, - getPrebidInternal, insertUserSyncIframe, isNumber, isPlainObject, @@ -16,16 +14,16 @@ import { triggerPixel, uniques, } from '../../src/utils.js'; -import { EVENTS, REJECTION_REASON, S2S } from '../../src/constants.js'; +import {EVENTS, REJECTION_REASON, S2S} from '../../src/constants.js'; import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js'; import {config} from '../../src/config.js'; -import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js'; +import {addPaapiConfig, isValid} from '../../src/adapters/bidderFactory.js'; import * as events from '../../src/events.js'; import {includes} from '../../src/polyfill.js'; import {S2S_VENDORS} from './config.js'; import {ajax} from '../../src/ajax.js'; import {hook} from '../../src/hook.js'; -import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../../src/utils/gdpr.js'; import {buildPBSRequest, interpretPBSResponse} from './ortbConverter.js'; import {useMetrics} from '../../src/utils/perfMetrics.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; @@ -37,8 +35,6 @@ const TYPE = S2S.SRC; let _syncCount = 0; let _s2sConfigs; -let eidPermissions; - /** * @typedef {Object} AdapterOptions * @summary s2sConfig parameter that adds arguments to resulting OpenRTB payload that goes to Prebid Server @@ -82,6 +78,7 @@ let eidPermissions; * @property {string} [syncEndpoint] endpoint URL for syncing cookies * @property {Object} [extPrebid] properties will be merged into request.ext.prebid * @property {Object} [ortbNative] base value for imp.native.request + * @property {Number} [maxTimeout] */ /** @@ -89,7 +86,6 @@ let eidPermissions; */ export const s2sDefaultConfig = { bidders: Object.freeze([]), - timeout: 1000, syncTimeout: 1000, maxBids: 1, adapter: 'prebidServer', @@ -100,7 +96,8 @@ export const s2sDefaultConfig = { eventtrackers: [ {event: 1, methods: [1, 2]} ], - } + }, + maxTimeout: 1500 }; config.setDefaults({ @@ -108,45 +105,45 @@ config.setDefaults({ }); /** - * @param {S2SConfig} option + * @param {S2SConfig} s2sConfig * @return {boolean} */ -function updateConfigDefaultVendor(option) { - if (option.defaultVendor) { - let vendor = option.defaultVendor; - let optionKeys = Object.keys(option); +function updateConfigDefaults(s2sConfig) { + if (s2sConfig.defaultVendor) { + let vendor = s2sConfig.defaultVendor; + let optionKeys = Object.keys(s2sConfig); if (S2S_VENDORS[vendor]) { // vendor keys will be set if either: the key was not specified by user // or if the user did not set their own distinct value (ie using the system default) to override the vendor Object.keys(S2S_VENDORS[vendor]).forEach((vendorKey) => { - if (s2sDefaultConfig[vendorKey] === option[vendorKey] || !includes(optionKeys, vendorKey)) { - option[vendorKey] = S2S_VENDORS[vendor][vendorKey]; + if (s2sDefaultConfig[vendorKey] === s2sConfig[vendorKey] || !includes(optionKeys, vendorKey)) { + s2sConfig[vendorKey] = S2S_VENDORS[vendor][vendorKey]; } }); } else { logError('Incorrect or unavailable prebid server default vendor option: ' + vendor); return false; } + } else { + if (s2sConfig.adapter == null) { + s2sConfig.adapter = 'prebidServer'; + } } - // this is how we can know if user / defaultVendor has set it, or if we should default to false - return option.enabled = typeof option.enabled === 'boolean' ? option.enabled : false; + return true; } /** - * @param {S2SConfig} option + * @param {S2SConfig} s2sConfig * @return {boolean} */ -function validateConfigRequiredProps(option) { - const keys = Object.keys(option); - if (['accountId', 'endpoint'].filter(key => { - if (!includes(keys, key)) { +function validateConfigRequiredProps(s2sConfig) { + for (const key of ['accountId', 'endpoint']) { + if (s2sConfig[key] == null) { logError(key + ' missing in server to server config'); - return true; + return false; } - return false; - }).length > 0) { - return false; } + return true; } // temporary change to modify the s2sConfig for new format used for endpoint URLs; @@ -167,40 +164,43 @@ function formatUrlParams(option) { }); } -/** - * @param {(S2SConfig[]|S2SConfig)} options - */ -function setS2sConfig(options) { +export function validateConfig(options) { if (!options) { return; } - const normalizedOptions = Array.isArray(options) ? options : [options]; - - const activeBidders = []; - const optionsValid = normalizedOptions.every((option, i, array) => { - formatUrlParams(options); - const updateSuccess = updateConfigDefaultVendor(option); - if (updateSuccess !== false) { - const valid = validateConfigRequiredProps(option); - if (valid !== false) { - if (Array.isArray(option['bidders'])) { - array[i]['bidders'] = option['bidders'].filter(bidder => { - if (activeBidders.indexOf(bidder) === -1) { - activeBidders.push(bidder); - return true; - } + options = Array.isArray(options) ? options : [options]; + const activeBidders = new Set(); + return options.filter(s2sConfig => { + formatUrlParams(s2sConfig); + if ( + updateConfigDefaults(s2sConfig) && + validateConfigRequiredProps(s2sConfig) && + s2sConfig.enabled + ) { + if (Array.isArray(s2sConfig.bidders)) { + s2sConfig.bidders = s2sConfig.bidders.filter(bidder => { + if (activeBidders.has(bidder)) { return false; - }); - } - return true; + } else { + activeBidders.add(bidder); + return true; + } + }) } + return true; + } else { + logWarn('prebidServer: s2s config is disabled', s2sConfig); } - logWarn('prebidServer: s2s config is disabled'); - return false; - }); + }) +} - if (optionsValid) { - return _s2sConfigs = normalizedOptions; +/** + * @param {(S2SConfig[]|S2SConfig)} options + */ +function setS2sConfig(options) { + options = validateConfig(options); + if (options.length) { + _s2sConfigs = options; } } getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); @@ -367,66 +367,13 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent, gppConsent) { }); } -/** - * map wurl to auction id and adId for use in the BID_WON event - */ -let wurlMap = {}; - -/** - * @param {string} auctionId - * @param {string} adId generated value set to bidObject.adId by bidderFactory Bid() - * @param {string} wurl events.winurl passed from prebidServer as wurl - */ -function addWurl(auctionId, adId, wurl) { - if ([auctionId, adId].every(isStr)) { - wurlMap[`${auctionId}${adId}`] = wurl; - } -} - -/** - * @param {string} auctionId - * @param {string} adId generated value set to bidObject.adId by bidderFactory Bid() - */ -function removeWurl(auctionId, adId) { - if ([auctionId, adId].every(isStr)) { - wurlMap[`${auctionId}${adId}`] = undefined; - } -} -/** - * @param {string} auctionId - * @param {string} adId generated value set to bidObject.adId by bidderFactory Bid() - * @return {(string|undefined)} events.winurl which was passed as wurl - */ -function getWurl(auctionId, adId) { - if ([auctionId, adId].every(isStr)) { - return wurlMap[`${auctionId}${adId}`]; - } -} - -/** - * remove all cached wurls - */ -export function resetWurlMap() { - wurlMap = {}; -} - -/** - * BID_WON event to request the wurl - * @param {Bid} bid the winning bid object - */ -function bidWonHandler(bid) { - const wurl = getWurl(bid.auctionId, bid.adId); - if (isStr(wurl)) { - logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`); - triggerPixel(wurl); - - // remove from wurl cache, since the wurl url was called - removeWurl(bid.auctionId, bid.adId); - } -} - function getMatchingConsentUrl(urlProp, gdprConsent) { - return hasPurpose1Consent(gdprConsent) ? urlProp.p1Consent : urlProp.noP1Consent; + const hasPurpose = hasPurpose1Consent(gdprConsent); + const url = hasPurpose ? urlProp.p1Consent : urlProp.noP1Consent + if (!url) { + logWarn('Missing matching consent URL when gdpr=' + hasPurpose); + } + return url; } function getConsentData(bidRequests) { @@ -447,7 +394,7 @@ export function PrebidServer() { /* Prebid executes this function when the page asks to send out bid requests */ baseAdapter.callBids = function(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { - const adapterMetrics = s2sBidRequest.metrics = useMetrics(deepAccess(bidRequests, '0.metrics')) + const adapterMetrics = s2sBidRequest.metrics = useMetrics(bidRequests?.[0]?.metrics) .newMetrics() .renameWith((n) => [`adapter.s2s.${n}`, `adapters.s2s.${s2sBidRequest.s2sConfig.defaultVendor}.${n}`]) done = adapterMetrics.startTiming('total').stopBefore(done); @@ -457,8 +404,9 @@ export function PrebidServer() { if (Array.isArray(_s2sConfigs)) { if (s2sBidRequest.s2sConfig && s2sBidRequest.s2sConfig.syncEndpoint && getMatchingConsentUrl(s2sBidRequest.s2sConfig.syncEndpoint, gdprConsent)) { + const s2sAliases = (s2sBidRequest.s2sConfig.extPrebid && s2sBidRequest.s2sConfig.extPrebid.aliases) ?? {}; let syncBidders = s2sBidRequest.s2sConfig.bidders - .map(bidder => adapterManager.aliasRegistry[bidder] || bidder) + .map(bidder => adapterManager.aliasRegistry[bidder] || s2sAliases[bidder] || bidder) .filter((bidder, index, array) => (array.indexOf(bidder) === index)); queueSync(syncBidders, gdprConsent, uspConsent, gppConsent, s2sBidRequest.s2sConfig); @@ -469,7 +417,8 @@ export function PrebidServer() { if (isValid) { bidRequests.forEach(bidderRequest => events.emit(EVENTS.BIDDER_DONE, bidderRequest)); } - if (shouldEmitNonbids(s2sBidRequest.s2sConfig, response)) { + const { seatNonBidData, atagData } = getAnalyticsFlags(s2sBidRequest.s2sConfig, response) + if (seatNonBidData) { events.emit(EVENTS.SEAT_NON_BID, { seatnonbid: response.ext.seatnonbid, auctionId: bidRequests[0].auctionId, @@ -478,11 +427,28 @@ export function PrebidServer() { adapterMetrics }); } + // pbs analytics event + if (seatNonBidData || atagData) { + const data = { + seatnonbid: seatNonBidData, + atag: atagData, + auctionId: bidRequests[0].auctionId, + requestedBidders, + response, + adapterMetrics + } + events.emit(EVENTS.PBS_ANALYTICS, data); + } done(false); doClientSideSyncs(requestedBidders, gdprConsent, uspConsent, gppConsent); }, onError(msg, error) { - logError(`Prebid server call failed: '${msg}'`, error); + const {p1Consent = '', noP1Consent = ''} = s2sBidRequest?.s2sConfig?.endpoint || {}; + if (p1Consent === noP1Consent) { + logError(`Prebid server call failed: '${msg}'. Endpoint: "${p1Consent}"}`, error); + } else { + logError(`Prebid server call failed: '${msg}'. Endpoints: p1Consent "${p1Consent}", noP1Consent "${noP1Consent}"}`, error); + } bidRequests.forEach(bidderRequest => events.emit(EVENTS.BIDDER_ERROR, { error, bidderRequest })); done(error.timedOut); }, @@ -495,24 +461,20 @@ export function PrebidServer() { } else { if (metrics.measureTime('addBidResponse.validate', () => isValid(adUnit, bid))) { addBidResponse(adUnit, bid); - if (bid.pbsWurl) { - addWurl(bid.auctionId, bid.adId, bid.pbsWurl); - } } else { addBidResponse.reject(adUnit, bid, REJECTION_REASON.INVALID); } } }, onFledge: (params) => { - addComponentAuction({auctionId: bidRequests[0].auctionId, ...params}, params.config); + config.runWithBidder(params.bidder, () => { + addPaapiConfig({auctionId: bidRequests[0].auctionId, ...params}, {config: params.config}); + }) } }) } }; - // Listen for bid won to call wurl - events.on(EVENTS.BID_WON, bidWonHandler); - return Object.assign(this, { callBids: baseAdapter.callBids, setBidderCode: baseAdapter.setBidderCode, @@ -530,7 +492,7 @@ export function PrebidServer() { * @param onError {function(String, {})} invoked on HTTP failure - with status message and XHR error * @param onBid {function({})} invoked once for each bid in the response - with the bid as returned by interpretResponse */ -export const processPBSRequest = hook('sync', function (s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid, onFledge}) { +export const processPBSRequest = hook('async', function (s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid, onFledge}) { let { gdprConsent } = getConsentData(bidRequests); const adUnits = deepClone(s2sBidRequest.ad_units); @@ -540,24 +502,28 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques .reduce(flatten, []) .filter(uniques); - const request = s2sBidRequest.metrics.measureTime('buildRequests', () => buildPBSRequest(s2sBidRequest, bidRequests, adUnits, requestedBidders, eidPermissions)); - const requestJson = request && JSON.stringify(request); - logInfo('BidRequest: ' + requestJson); - const endpointUrl = getMatchingConsentUrl(s2sBidRequest.s2sConfig.endpoint, gdprConsent); - if (request && requestJson && endpointUrl) { + const request = s2sBidRequest.metrics.measureTime('buildRequests', () => buildPBSRequest(s2sBidRequest, bidRequests, adUnits, requestedBidders)); + const requestData = { + endpointUrl: getMatchingConsentUrl(s2sBidRequest.s2sConfig.endpoint, gdprConsent), + requestJson: request && JSON.stringify(request), + customHeaders: s2sBidRequest?.s2sConfig?.customHeaders ?? {}, + }; + events.emit(EVENTS.BEFORE_PBS_HTTP, requestData) + logInfo('BidRequest: ' + requestData); + if (request && requestData.requestJson && requestData.endpointUrl) { const networkDone = s2sBidRequest.metrics.startTiming('net'); ajax( - endpointUrl, + requestData.endpointUrl, { success: function (response) { networkDone(); let result; try { result = JSON.parse(response); - const {bids, fledgeAuctionConfigs} = s2sBidRequest.metrics.measureTime('interpretResponse', () => interpretPBSResponse(result, request)); + const {bids, paapi} = s2sBidRequest.metrics.measureTime('interpretResponse', () => interpretPBSResponse(result, request)); bids.forEach(onBid); - if (fledgeAuctionConfigs) { - fledgeAuctionConfigs.forEach(onFledge); + if (paapi) { + paapi.forEach(onFledge); } } catch (error) { logError(error); @@ -574,11 +540,12 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques onError.apply(this, arguments); } }, - requestJson, + requestData.requestJson, { contentType: 'text/plain', withCredentials: true, - browsingTopics: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, s2sActivityParams(s2sBidRequest.s2sConfig)) + browsingTopics: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, s2sActivityParams(s2sBidRequest.s2sConfig)), + customHeaders: requestData.customHeaders } ); } else { @@ -586,18 +553,18 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques } }, 'processPBSRequest'); -function shouldEmitNonbids(s2sConfig, response) { - return s2sConfig?.extPrebid?.returnallbidstatus && response?.ext?.seatnonbid; +function getAnalyticsFlags(s2sConfig, response) { + return { + atagData: getAtagData(response), + seatNonBidData: getNonBidData(s2sConfig, response) + } +} +function getNonBidData(s2sConfig, response) { + return s2sConfig?.extPrebid?.returnallbidstatus ? response?.ext?.seatnonbid : undefined; } -/** - * Global setter that sets eids permissions for bidders - * This setter is to be used by userId module when included - * @param {Array} newEidPermissions - */ -function setEidPermissions(newEidPermissions) { - eidPermissions = newEidPermissions; +function getAtagData(response) { + return response?.ext?.prebid?.analytics?.tags; } -getPrebidInternal().setEidPermissions = setEidPermissions; adapterManager.registerBidAdapter(new PrebidServer(), 'prebidServer'); diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index e0f038767c2..99fbcf3d2bb 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -1,17 +1,7 @@ import {ortbConverter} from '../../libraries/ortbConverter/converter.js'; -import { - deepAccess, - deepSetValue, - getBidRequest, - getDefinedParams, - isArray, - logError, - logWarn, - mergeDeep, - timestamp -} from '../../src/utils.js'; +import {deepClone, deepSetValue, getBidRequest, logError, logWarn, mergeDeep, timestamp} from '../../src/utils.js'; import {config} from '../../src/config.js'; -import { STATUS, S2S } from '../../src/constants.js'; +import {S2S, STATUS} from '../../src/constants.js'; import {createBid} from '../../src/bidfactory.js'; import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js'; import {setImpBidParams} from '../../libraries/pbsExtensions/processors/params.js'; @@ -25,12 +15,27 @@ import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_TRANSMIT_TID} from '../../src/activities/activities.js'; import {currencyCompare} from '../../libraries/currencyUtils/currency.js'; import {minimum} from '../../src/utils/reducers.js'; +import {s2sDefaultConfig} from './index.js'; +import {premergeFpd} from './bidderConfig.js'; +import {ALL_MEDIATYPES, BANNER} from '../../src/mediaTypes.js'; const DEFAULT_S2S_TTL = 60; const DEFAULT_S2S_CURRENCY = 'USD'; const DEFAULT_S2S_NETREVENUE = true; const BIDDER_SPECIFIC_REQUEST_PROPS = new Set(['bidderCode', 'bidderRequestId', 'uniquePbsTid', 'bids', 'timeout']); +const getMinimumFloor = (() => { + const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); + return function(candidates) { + let min; + for (const candidate of candidates) { + if (candidate?.bidfloorcur == null || candidate?.bidfloor == null) return null; + min = min == null ? candidate : getMin(min, candidate); + } + return min; + } +})(); + const PBS_CONVERTER = ortbConverter({ processors: pbsExtensions, context: { @@ -46,7 +51,7 @@ const PBS_CONVERTER = ortbConverter({ } }); if (Object.values(SUPPORTED_MEDIA_TYPES).some(mtype => imp[mtype])) { - imp.secure = context.s2sBidRequest.s2sConfig.secure; + imp.secure = proxyBidRequest.ortb2Imp?.secure ?? 1; return imp; } }, @@ -54,10 +59,11 @@ const PBS_CONVERTER = ortbConverter({ if (!imps.length) { logError('Request to Prebid Server rejected due to invalid media type(s) in adUnit.'); } else { - let {s2sBidRequest, requestedBidders, eidPermissions} = context; + let {s2sBidRequest} = context; const request = buildRequest(imps, proxyBidderRequest, context); - request.tmax = s2sBidRequest.s2sConfig.timeout; + request.tmax = s2sBidRequest.s2sConfig.timeout ?? Math.min(s2sBidRequest.requestBidsTimeout * 0.75, s2sBidRequest.s2sConfig.maxTimeout ?? s2sDefaultConfig.maxTimeout); + request.ext.tmaxmax = request.ext.tmaxmax || s2sBidRequest.requestBidsTimeout; [request.app, request.dooh, request.site].forEach(section => { if (section && !section.publisher?.id) { @@ -65,16 +71,6 @@ const PBS_CONVERTER = ortbConverter({ } }) - if (isArray(eidPermissions) && eidPermissions.length > 0) { - if (requestedBidders && isArray(requestedBidders)) { - eidPermissions = eidPermissions.map(p => ({ - ...p, - bidders: p.bidders.filter(bidder => requestedBidders.includes(bidder)) - })) - } - deepSetValue(request, 'ext.prebid.data.eidpermissions', eidPermissions); - } - if (!context.transmitTids) { deepSetValue(request, 'ext.prebid.createtids', false); } @@ -120,7 +116,10 @@ const PBS_CONVERTER = ortbConverter({ transactionId: context.adUnit.transactionId, adUnitId: context.adUnit.adUnitId, auctionId: context.bidderRequest.auctionId, - }), bidResponse), + }), bidResponse, { + deferRendering: !!context.adUnit.deferBilling, + deferBilling: !!context.adUnit.deferBilling + }), adUnit: context.adUnit.code }; }, @@ -140,24 +139,39 @@ const PBS_CONVERTER = ortbConverter({ } } }, + // for bid floors, we pass each bidRequest associated with this imp through normal bidfloor/extBidfloor processing, + // and aggregate all of them into a single, minimum floor to put in the request bidfloor(orig, imp, proxyBidRequest, context) { - // for bid floors, we pass each bidRequest associated with this imp through normal bidfloor processing, - // and aggregate all of them into a single, minimum floor to put in the request - const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); - let min; - for (const req of context.actualBidRequests.values()) { - const floor = {}; - orig(floor, req, context); - // if any bid does not have a valid floor, do not attempt to send any to PBS - if (floor.bidfloorcur == null || floor.bidfloor == null) { - min = null; - break; + const min = getMinimumFloor((function * () { + for (const req of context.actualBidRequests.values()) { + const floor = {}; + orig(floor, req, context); + yield floor; } - min = min == null ? floor : getMin(min, floor); - } + })()) if (min != null) { Object.assign(imp, min); } + }, + extBidfloor(orig, imp, proxyBidRequest, context) { + function setExtFloor(target, minFloor) { + if (minFloor != null) { + deepSetValue(target, 'ext.bidfloor', minFloor.bidfloor); + deepSetValue(target, 'ext.bidfloorcur', minFloor.bidfloorcur); + } + } + const imps = Array.from(context.actualBidRequests.values()) + .map(request => { + const requestImp = deepClone(imp); + orig(requestImp, request, context); + return requestImp; + }); + Object.values(ALL_MEDIATYPES).forEach(mediaType => { + setExtFloor(imp[mediaType], getMinimumFloor(imps.map(imp => imp[mediaType]?.ext))) + }); + (imp[BANNER]?.format || []).forEach((format, i) => { + setExtFloor(format, getMinimumFloor(imps.map(imp => imp[BANNER].format[i]?.ext))) + }) } }, [REQUEST]: { @@ -197,10 +211,8 @@ const PBS_CONVERTER = ortbConverter({ context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context)); }, sourceExtSchain(orig, ortbRequest, proxyBidderRequest, context) { - // pass schains in ext.prebid.schains, with the most commonly used one in source.ext.schain - let mainChain; - - let chains = (deepAccess(ortbRequest, 'ext.prebid.schains') || []); + // pass schains in ext.prebid.schains + let chains = ortbRequest?.ext?.prebid?.schains || []; const chainBidders = new Set(chains.flatMap((item) => item.bidders)); chains = Object.values( @@ -209,7 +221,7 @@ const PBS_CONVERTER = ortbConverter({ .filter((req) => !chainBidders.has(req.bidderCode)) // schain defined in s2sConfig.extPrebid takes precedence .map((req) => ({ bidders: [req.bidderCode], - schain: deepAccess(req, 'bids.0.schain') + schain: req?.bids?.[0]?.schain }))) .filter(({bidders, schain}) => bidders?.length > 0 && schain) .reduce((chains, {bidders, schain}) => { @@ -218,17 +230,10 @@ const PBS_CONVERTER = ortbConverter({ chains[key] = {bidders: new Set(), schain}; } bidders.forEach((bidder) => chains[key].bidders.add(bidder)); - if (mainChain == null || chains[key].bidders.size > mainChain.bidders.size) { - mainChain = chains[key] - } return chains; }, {}) ).map(({bidders, schain}) => ({bidders: Array.from(bidders), schain})); - if (mainChain != null) { - deepSetValue(ortbRequest, 'source.ext.schain', mainChain.schain); - } - if (chains.length) { deepSetValue(ortbRequest, 'ext.prebid.schains', chains); } @@ -239,27 +244,28 @@ const PBS_CONVERTER = ortbConverter({ // override to process each request context.actualBidderRequests.forEach(req => orig(response, ortbResponse, {...context, bidderRequest: req, bidRequests: req.bids})); }, - fledgeAuctionConfigs(orig, response, ortbResponse, context) { + paapiConfigs(orig, response, ortbResponse, context) { const configs = Object.values(context.impContext) - .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => { + .flatMap((impCtx) => (impCtx.paapiConfigs || []).map(cfg => { const bidderReq = impCtx.actualBidderRequests.find(br => br.bidderCode === cfg.bidder); const bidReq = impCtx.actualBidRequests.get(cfg.bidder); return { adUnitCode: impCtx.adUnit.code, ortb2: bidderReq?.ortb2, ortb2Imp: bidReq?.ortb2Imp, + bidder: cfg.bidder, config: cfg.config }; })); if (configs.length > 0) { - response.fledgeAuctionConfigs = configs; + response.paapi = configs; } } } }, }); -export function buildPBSRequest(s2sBidRequest, bidderRequests, adUnits, requestedBidders, eidPermissions) { +export function buildPBSRequest(s2sBidRequest, bidderRequests, adUnits, requestedBidders) { const requestTimestamp = timestamp(); const impIds = new Set(); const proxyBidRequests = []; @@ -301,14 +307,15 @@ export function buildPBSRequest(s2sBidRequest, bidderRequests, adUnits, requeste proxyBidRequests.push({ ...adUnit, adUnitCode: adUnit.code, - ...getDefinedParams(actualBidRequests.values().next().value || {}, ['userId', 'userIdAsEids', 'schain']), pbsData: {impId, actualBidRequests, adUnit}, }); }); const proxyBidderRequest = { ...Object.fromEntries(Object.entries(bidderRequests[0]).filter(([k]) => !BIDDER_SPECIFIC_REQUEST_PROPS.has(k))), - fledgeEnabled: bidderRequests.some(req => req.fledgeEnabled) + paapi: { + enabled: bidderRequests.some(br => br.paapi?.enabled) + } } return PBS_CONVERTER.toORTB({ @@ -318,10 +325,12 @@ export function buildPBSRequest(s2sBidRequest, bidderRequests, adUnits, requeste currency: config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY, ttl: s2sBidRequest.s2sConfig.defaultTtl || DEFAULT_S2S_TTL, requestTimestamp, - s2sBidRequest, + s2sBidRequest: { + ...s2sBidRequest, + ortb2Fragments: premergeFpd(s2sBidRequest.ortb2Fragments, requestedBidders) + }, requestedBidders, actualBidderRequests: bidderRequests, - eidPermissions, nativeRequest: s2sBidRequest.s2sConfig.ortbNative, getRedactor, transmitTids: isActivityAllowed(ACTIVITY_TRANSMIT_TID, s2sParams), diff --git a/modules/prebidmanagerAnalyticsAdapter.md b/modules/prebidmanagerAnalyticsAdapter.md deleted file mode 100644 index 030e79b406f..00000000000 --- a/modules/prebidmanagerAnalyticsAdapter.md +++ /dev/null @@ -1,9 +0,0 @@ -# Overview - -Module Name: Prebid Manager Analytics Adapter -Module Type: Analytics Adapter -Maintainer: admin@prebidmanager.com - -# Description - -Analytics adapter for Prebid Manager. Contact admin@prebidmanager.com for information. diff --git a/modules/precisoBidAdapter.js b/modules/precisoBidAdapter.js index 9125f6f3911..880d1bf4b1c 100644 --- a/modules/precisoBidAdapter.js +++ b/modules/precisoBidAdapter.js @@ -1,212 +1,66 @@ -import { logMessage, isFn, deepAccess, logInfo } from '../src/utils.js'; +import { logInfo } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - -const BIDDER_CODE = 'preciso'; -const AD_URL = 'https://ssp-bidder.mndtrk.com/bid_request/openrtb'; -const URL_SYNC = 'https://ck.2trk.info/rtb/user/usersync.aspx?'; -const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { buildBidResponse, buildRequests, onBidWon } from '../libraries/precisoUtils/bidUtils.js'; +import { buildUserSyncs } from '../libraries/precisoUtils/bidUtilsCommon.js'; + +const BIDDER__CODE = 'preciso'; +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: BIDDER__CODE }); +const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE]; const GVLID = 874; -let userId = 'NA'; +let precisoId = 'NA'; +let sharedId = 'NA'; + +const endpoint = 'https://ssp-bidder.2trk.info/bid_request/openrtb'; +let syncEndpoint = 'https://ck.2trk.info/rtb/user/usersync.aspx?'; export const spec = { - code: BIDDER_CODE, + code: BIDDER__CODE, supportedMediaTypes: SUPPORTED_MEDIA_TYPES, gvlid: GVLID, isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(bid.params.publisherId) && bid.params.host == 'prebid'); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - // userId = validBidRequests[0].userId.pubcid; - let winTop = window; - let location; - var offset = new Date().getTimezoneOffset(); - logInfo('timezone ' + offset); - var city = Intl.DateTimeFormat().resolvedOptions().timeZone; - logInfo('location test' + city) - - const countryCode = getCountryCodeByTimezone(city); - logInfo(`The country code for ${city} is ${countryCode}`); - - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - - let request = { - id: validBidRequests[0].bidderRequestId, - - imp: validBidRequests.map(request => { - const { bidId, sizes, mediaType, ortb2 } = request - const item = { - id: bidId, - region: request.params.region, - traffic: mediaType, - bidFloor: getBidFloor(request), - ortb2: ortb2 - - } - - if (request.mediaTypes.banner) { - item.banner = { - format: (request.mediaTypes.banner.sizes || sizes).map(size => { - return { w: size[0], h: size[1] } - }), - } - } - - if (request.schain) { - item.schain = request.schain; - } - - if (request.floorData) { - item.bidFloor = request.floorData.floorMin; - } - return item - }), - auctionId: validBidRequests[0].auctionId, - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language : '', - geo: navigator.geolocation.getCurrentPosition(position => { - const { latitude, longitude } = position.coords; - return { - latitude: latitude, - longitude: longitude - } - // Show a map centered at latitude / longitude. - }) || { utcoffset: new Date().getTimezoneOffset() }, - city: city, - 'host': location.host, - 'page': location.pathname, - 'coppa': config.getConfig('coppa') === true ? 1 : 0 - // userId: validBidRequests[0].userId - }; - - request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent - } - if (bidderRequest.gppConsent) { - request.gpp = bidderRequest.gppConsent; + sharedId = storage.getDataFromLocalStorage('_sharedid') || storage.getCookie('_sharedid'); + let precisoBid = true; + const preCall = 'https://ssp-usersync.mndtrk.com/getUUID?sharedId=' + sharedId; + precisoId = storage.getDataFromLocalStorage('_pre|id'); + if (Object.is(precisoId, 'NA') || Object.is(precisoId, null) || Object.is(precisoId, undefined)) { + if (!bid.precisoBid) { + precisoBid = false; + getapi(preCall); } } - return { - method: 'POST', - url: AD_URL, - data: request, - - }; + return Boolean(bid.bidId && bid.params && bid.params.publisherId && precisoBid); }, - - interpretResponse: function (serverResponse) { - const response = serverResponse.body - - const bids = [] - - response.seatbid.forEach(seat => { - seat.bid.forEach(bid => { - bids.push({ - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - creativeId: bid.crid, - ad: bid.adm, - currency: 'USD', - netRevenue: true, - ttl: 300, - meta: { - advertiserDomains: bid.adomain || [], - }, - }) - }) - }) - - return bids - }, - - getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { - let syncs = []; - let { gdprApplies, consentString = '' } = gdprConsent; - - if (serverResponses.length > 0) { - logInfo('preciso bidadapter getusersync serverResponses:' + serverResponses.toString); - } - if (syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: `${URL_SYNC}id=${userId}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=4` - }); - } else { - syncs.push({ - type: 'image', - url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=2` - }); - } - - return syncs + buildRequests: buildRequests(endpoint), + interpretResponse: buildBidResponse, + onBidWon, + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + syncEndpoint = syncEndpoint + 'id=' + sharedId; + return buildUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, syncEndpoint); } - }; -function getCountryCodeByTimezone(city) { +registerBidder(spec); + +async function getapi(url) { try { - const now = new Date(); - const options = { - timeZone: city, - timeZoneName: 'long', - }; - const [timeZoneName] = new Intl.DateTimeFormat('en-US', options) - .formatToParts(now) - .filter((part) => part.type === 'timeZoneName'); + const response = await fetch(url); + var data = await response.json(); + + const dataMap = new Map(Object.entries(data)); + const uuidValue = dataMap.get('UUID'); - if (timeZoneName) { - // Extract the country code from the timezone name - const parts = timeZoneName.value.split('-'); - if (parts.length >= 2) { - return parts[1]; + if (!Object.is(uuidValue, null) && !Object.is(uuidValue, undefined)) { + if (storage.localStorageIsEnabled()) { + storage.setDataInLocalStorage('_pre|id', uuidValue); } } + return data; } catch (error) { - // Handle errors, such as an invalid timezone city - logInfo(error); + logInfo('Error in preciso precall' + error); } - - // Handle the case where the city is not found or an error occurred - return 'Unknown'; } - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidFloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - -registerBidder(spec); diff --git a/modules/precisoBidAdapter.md b/modules/precisoBidAdapter.md index b1fb0d062da..97521f195d8 100644 --- a/modules/precisoBidAdapter.md +++ b/modules/precisoBidAdapter.md @@ -14,9 +14,9 @@ Module that connects to preciso' demand sources | Name | Scope | Description | Example | | :------------ | :------- | :------------------------ | :------------------- | -| `region` | required (for prebid.js) | region | "prebid-eu" | -| `publisherId` | required (for prebid-server) | partner ID | "1901" | -| `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | +| `region` | optional (for prebid.js) | 3 letter country code | "USA" | +| `publisherId` | required (for prebid-server) | partner ID provided by preciso | PreTest_0001 | +| `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native | "banner" | # Test Parameters ``` @@ -25,7 +25,35 @@ Module that connects to preciso' demand sources { code: 'placementId_0', mediaTypes: { - native: {} + native: { + ortb: { + assets: [ + { + id: 3, + required: 1, + img: { + type: 3, + w: 300, + h: 250 + } + }, + { + id: 1, + required: 1, + title: { + len: 800 + } + }, + { + id: 4, + required: 0, + data: { + type: 1 + } + } + ] + } + } }, bids: [ { @@ -33,7 +61,7 @@ Module that connects to preciso' demand sources params: { host: 'prebid', publisherId: '0', - region: 'prebid-eu', + region: 'USA', traffic: 'native' } } @@ -53,32 +81,11 @@ Module that connects to preciso' demand sources params: { host: 'prebid', publisherId: '0', - region: 'prebid-eu', + region: 'USA', traffic: 'banner' } } ] - }, - // Will return test vast xml. All video params are stored under placement in publishers UI - { - code: 'placementId_0', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'instream' - } - }, - bids: [ - { - bidder: 'preciso', - params: { - host: 'prebid', - publisherId: '0', - region: 'prebid-eu', - traffic: 'video' - } - } - ] } ]; ``` \ No newline at end of file diff --git a/modules/previousAuctionInfo/index.js b/modules/previousAuctionInfo/index.js new file mode 100644 index 00000000000..3846a46812a --- /dev/null +++ b/modules/previousAuctionInfo/index.js @@ -0,0 +1,134 @@ +import {on as onEvent, off as offEvent} from '../../src/events.js'; +import { EVENTS } from '../../src/constants.js'; +import { config } from '../../src/config.js'; +import {deepSetValue} from '../../src/utils.js'; +import {startAuction} from '../../src/prebid.js'; +export const CONFIG_NS = 'previousAuctionInfo'; +export let previousAuctionInfoEnabled = false; +let enabledBidders = []; +let maxQueueLength = 10; +let handlersAttached = false; + +export let auctionState = {}; + +export function resetPreviousAuctionInfo() { + previousAuctionInfoEnabled = false; + enabledBidders = []; + auctionState = {}; + deinitHandlers(); +} + +function initPreviousAuctionInfo() { + config.getConfig('previousAuctionInfo', ({[CONFIG_NS]: config = {}}) => { + if (!config?.enabled) { + resetPreviousAuctionInfo(); + return; + } + + if (config?.bidders) { enabledBidders = config.bidders; } + if (config?.maxQueueLength) { maxQueueLength = config.maxQueueLength; } + + previousAuctionInfoEnabled = true; + initHandlers(); + }); +} + +export const initHandlers = () => { + if (!handlersAttached) { + onEvent(EVENTS.AUCTION_END, onAuctionEndHandler); + onEvent(EVENTS.BID_WON, onBidWonHandler); + startAuction.before(startAuctionHook); + handlersAttached = true; + } +}; + +const deinitHandlers = () => { + if (handlersAttached) { + offEvent(EVENTS.AUCTION_END, onAuctionEndHandler); + offEvent(EVENTS.BID_WON, onBidWonHandler); + startAuction.getHooks({hook: startAuctionHook}).remove(); + handlersAttached = false; + } +} + +export const onAuctionEndHandler = (auctionDetails) => { + try { + const receivedBidsMap = {}; + const rejectedBidsMap = {}; + const highestBidsByAdUnitCode = {}; + + if (auctionDetails.bidsReceived?.length) { + auctionDetails.bidsReceived.forEach((bid) => { + receivedBidsMap[bid.requestId] = bid; + if (!highestBidsByAdUnitCode[bid.adUnitCode] || bid.cpm > highestBidsByAdUnitCode[bid.adUnitCode].cpm) { + highestBidsByAdUnitCode[bid.adUnitCode] = bid; + } + }); + } + + if (auctionDetails.bidsRejected?.length) { + auctionDetails.bidsRejected.forEach(bidRejected => { + rejectedBidsMap[bidRejected.requestId] = bidRejected; + }); + } + + if (auctionDetails.bidderRequests?.length) { + auctionDetails.bidderRequests.forEach(bidderRequest => { + const enabledBidder = enabledBidders.length === 0 || enabledBidders.find(bidderCode => bidderCode === bidderRequest.bidderCode); + + if (enabledBidder) { + auctionState[bidderRequest.bidderCode] = auctionState[bidderRequest.bidderCode] || []; + + bidderRequest.bids.forEach(bid => { + const previousAuctionInfoPayload = { + bidderRequestId: bidderRequest.bidderRequestId, + bidId: bid.bidId, + rendered: 0, + source: 'pbjs', + adUnitCode: bid.adUnitCode, + highestBidCpm: highestBidsByAdUnitCode[bid.adUnitCode]?.cpm || null, + highestBidCurrency: highestBidsByAdUnitCode[bid.adUnitCode]?.currency || null, + bidderCpm: receivedBidsMap[bid.bidId]?.cpm || null, + bidderOriginalCpm: receivedBidsMap[bid.bidId]?.originalCpm || null, + bidderCurrency: receivedBidsMap[bid.bidId]?.currency || null, + bidderOriginalCurrency: receivedBidsMap[bid.bidId]?.originalCurrency || null, + rejectionReason: rejectedBidsMap[bid.bidId]?.rejectionReason || null, + timestamp: auctionDetails.timestamp, + transactionId: bid.transactionId, // this field gets removed before injecting previous auction info into the bid stream + } + + if (auctionState[bidderRequest.bidderCode].length >= maxQueueLength) { + auctionState[bidderRequest.bidderCode].shift(); + } + + auctionState[bidderRequest.bidderCode].push(previousAuctionInfoPayload); + }); + } + }); + } + } catch (error) {} +} + +export const onBidWonHandler = (winningBid) => { + const winningTid = winningBid.transactionId; + + Object.values(auctionState).flat().forEach(prevAuctPayload => { + if (prevAuctPayload.transactionId === winningTid) { + prevAuctPayload.rendered = 1; + } + }); +}; + +export function startAuctionHook(next, req) { + const bidders = enabledBidders.length ? enabledBidders : Object.keys(auctionState); + bidders + .filter(bidder => auctionState[bidder]?.length) + .forEach(bidder => { + auctionState[bidder].forEach(payload => { delete payload.transactionId }); + deepSetValue(req.ortb2Fragments, `bidder.${bidder}.ext.prebid.previousauctioninfo`, auctionState[bidder]); + delete auctionState[bidder]; + }) + next.call(this, req); +} + +initPreviousAuctionInfo(); diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 5df8f938c3d..ab499ce7698 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -3,7 +3,6 @@ import { deepAccess, deepClone, deepSetValue, - generateUUID, getParameterByName, isNumber, logError, @@ -13,7 +12,8 @@ import { parseGPTSingleSizeArray, parseUrl, pick, - deepEqual + deepEqual, + generateUUID } from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {config} from '../src/config.js'; @@ -30,6 +30,8 @@ import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.j import {adjustCpm} from '../src/utils/cpm.js'; import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; import {convertCurrency} from '../libraries/currencyUtils/currency.js'; +import { timeoutQueue } from '../libraries/timeoutQueue/timeoutQueue.js'; +import {ALL_MEDIATYPES, BANNER} from '../src/mediaTypes.js'; export const FLOOR_SKIPPED_REASON = { NOT_FOUND: 'not_found', @@ -72,7 +74,7 @@ let _floorsConfig = {}; /** * @summary If a auction is to be delayed by an ongoing fetch we hold it here until it can be resumed */ -let _delayedAuctions = []; +const _delayedAuctions = timeoutQueue(); /** * @summary Each auction can have differing floors data depending on execution time or per adunit setup @@ -263,13 +265,20 @@ export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: // pub provided inverse function takes precedence, otherwise do old adjustment stuff const inverseFunction = bidderSettings.get(bidRequest.bidder, 'inverseBidAdjustment'); if (inverseFunction) { - floorInfo.matchingFloor = inverseFunction(floorInfo.matchingFloor, bidRequest); + const definedParams = Object.fromEntries( + Object.entries(requestParams).filter(([key, val]) => val !== '*' && ['mediaType', 'size'].includes(key)) + ); + floorInfo.matchingFloor = inverseFunction(floorInfo.matchingFloor, bidRequest, definedParams); } else { let cpmAdjustment = getBiddersCpmAdjustment(floorInfo.matchingFloor, null, bidRequest); floorInfo.matchingFloor = cpmAdjustment ? calculateAdjustedFloor(floorInfo.matchingFloor, cpmAdjustment) : floorInfo.matchingFloor; } } + if (floorInfo.floorRuleValue === null) { + return null; + } + if (floorInfo.matchingFloor) { return { floor: roundUp(floorInfo.matchingFloor, 4), @@ -436,17 +445,11 @@ export function createFloorsDataForAuction(adUnits, auctionId) { * @summary This is the function which will be called to exit our module and continue the auction. */ export function continueAuction(hookConfig) { - // only run if hasExited if (!hookConfig.hasExited) { - // if this current auction is still fetching, remove it from the _delayedAuctions - _delayedAuctions = _delayedAuctions.filter(auctionConfig => auctionConfig.timer !== hookConfig.timer); - // We need to know the auctionId at this time. So we will use the passed in one or generate and set it ourselves hookConfig.reqBidsConfigObj.auctionId = hookConfig.reqBidsConfigObj.auctionId || generateUUID(); - // now we do what we need to with adUnits and save the data object to be used for getFloor and enforcement calls _floorDataForAuction[hookConfig.reqBidsConfigObj.auctionId] = createFloorsDataForAuction(hookConfig.reqBidsConfigObj.adUnits || getGlobal().adUnits, hookConfig.reqBidsConfigObj.auctionId); - hookConfig.nextFn.apply(hookConfig.context, [hookConfig.reqBidsConfigObj]); hookConfig.hasExited = true; } @@ -467,7 +470,7 @@ function isValidRule(key, floor, numFields, delimiter) { if (typeof key !== 'string' || key.split(delimiter).length !== numFields) { return false; } - return typeof floor === 'number'; + return typeof floor === 'number' || floor === null; } function validateRules(floorsData, numFields, delimiter) { @@ -577,36 +580,22 @@ export const requestBidsHook = timedAuctionHook('priceFloors', function requestB reqBidsConfigObj, context: this, nextFn: fn, - haveExited: false, + hasExited: false, timer: null }; // If auction delay > 0 AND we are fetching -> Then wait until it finishes if (_floorsConfig.auctionDelay > 0 && fetching) { - hookConfig.timer = setTimeout(() => { + _delayedAuctions.submit(_floorsConfig.auctionDelay, () => continueAuction(hookConfig), () => { logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction`); _floorsConfig.fetchStatus = 'timeout'; continueAuction(hookConfig); - }, _floorsConfig.auctionDelay); - _delayedAuctions.push(hookConfig); + }); } else { continueAuction(hookConfig); } }); -/** - * @summary If an auction was queued to be delayed (waiting for a fetch) then this function will resume - * those delayed auctions when delay is hit or success return or fail return - */ -function resumeDelayedAuctions() { - _delayedAuctions.forEach(auctionConfig => { - // clear the timeout - clearTimeout(auctionConfig.timer); - continueAuction(auctionConfig); - }); - _delayedAuctions = []; -} - /** * This function handles the ajax response which comes from the user set URL to fetch floors data from * @param {object} fetchResponse The floors data response which came back from the url configured in config.floors @@ -631,7 +620,7 @@ export function handleFetchResponse(fetchResponse) { } // if any auctions are waiting for fetch to finish, we need to continue them! - resumeDelayedAuctions(); + _delayedAuctions.resume(); } function handleFetchError(status) { @@ -640,7 +629,7 @@ function handleFetchError(status) { logError(`${MODULE_NAME}: Fetch errored with: `, status); // if any auctions are waiting for fetch to finish, we need to continue them! - resumeDelayedAuctions(); + _delayedAuctions.resume(); } /** @@ -816,32 +805,72 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a config.getConfig('floors', config => handleSetFloorsConfig(config.floors)); -/** - * Sets bidfloor and bidfloorcur for ORTB imp objects - */ -export function setOrtbImpBidFloor(imp, bidRequest, context) { +function tryGetFloor(bidRequest, {currency = config.getConfig('currency.adServerCurrency') || 'USD', mediaType = '*', size = '*'}, fn) { if (typeof bidRequest.getFloor === 'function') { - let currency, floor; + let floor; try { - ({currency, floor} = bidRequest.getFloor({ - currency: context.currency || config.getConfig('currency.adServerCurrency') || 'USD', - mediaType: context.mediaType || '*', - size: '*' - })); + floor = bidRequest.getFloor({ + currency, + mediaType, + size + }) || {}; } catch (e) { logWarn('Cannot compute floor for bid', bidRequest); return; } - floor = parseFloat(floor); - if (currency != null && floor != null && !isNaN(floor)) { - Object.assign(imp, { - bidfloor: floor, - bidfloorcur: currency - }); + floor.floor = parseFloat(floor.floor); + if (floor.currency != null && floor.floor && !isNaN(floor.floor)) { + fn(floor.floor, floor.currency); } } } +/** + * Sets bidfloor and bidfloorcur for ORTB imp objects + */ +export function setOrtbImpBidFloor(imp, bidRequest, context) { + tryGetFloor(bidRequest, { + currency: context.currency, + mediaType: context.mediaType || '*', + size: '*' + }, (bidfloor, bidfloorcur) => { + Object.assign(imp, { + bidfloor, + bidfloorcur + }); + }) +} + +/** + * Set per-mediatype and per-format bidfloor + */ +export function setGranularBidfloors(imp, bidRequest, context) { + function setIfDifferent(bidfloor, bidfloorcur) { + if (bidfloor !== imp.bidfloor || bidfloorcur !== imp.bidfloorcur) { + deepSetValue(this, 'ext.bidfloor', bidfloor); + deepSetValue(this, 'ext.bidfloorcur', bidfloorcur); + } + } + + Object.values(ALL_MEDIATYPES) + .filter(mediaType => imp[mediaType] != null) + .forEach(mediaType => { + tryGetFloor(bidRequest, { + currency: imp.bidfloorcur || context?.currency, + mediaType + }, setIfDifferent.bind(imp[mediaType])) + }); + (imp[BANNER]?.format || []) + .filter(({w, h}) => w != null && h != null) + .forEach(format => { + tryGetFloor(bidRequest, { + currency: imp.bidfloorcur || context?.currency, + mediaType: BANNER, + size: [format.w, format.h] + }, setIfDifferent.bind(format)) + }) +} + export function setImpExtPrebidFloors(imp, bidRequest, context) { // logic below relates to https://github.com/prebid/Prebid.js/issues/8749 and does the following: // 1. check client-side floors (ref bidfloor/bidfloorcur & ortb2Imp floorMin/floorMinCur (if present)) @@ -882,5 +911,7 @@ export function setOrtbExtPrebidFloors(ortbRequest, bidderRequest, context) { } registerOrtbProcessor({type: IMP, name: 'bidfloor', fn: setOrtbImpBidFloor}); +// granular floors should be set after both "normal" bidfloors and mediaypes +registerOrtbProcessor({type: IMP, name: 'extBidfloor', fn: setGranularBidfloors, priority: -10}) registerOrtbProcessor({type: IMP, name: 'extPrebidFloors', fn: setImpExtPrebidFloors, dialects: [PBS], priority: -1}); registerOrtbProcessor({type: REQUEST, name: 'extPrebidFloors', fn: setOrtbExtPrebidFloors, dialects: [PBS]}); diff --git a/modules/prismaBidAdapter.js b/modules/prismaBidAdapter.js index b42c4b8af3f..9f7d37dcebe 100644 --- a/modules/prismaBidAdapter.js +++ b/modules/prismaBidAdapter.js @@ -3,6 +3,7 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; +import {getConnectionType} from '../libraries/connectionInfo/connectionUtils.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -20,33 +21,6 @@ const METRICS_TRACKER_URL = 'https://prisma.nexx360.io/track-imp'; const GVLID = 965; -function getConnectionType() { - const connection = navigator.connection || navigator.webkitConnection; - if (!connection) { - return 0; - } - switch (connection.type) { - case 'ethernet': - return 1; - case 'wifi': - return 2; - case 'cellular': - switch (connection.effectiveType) { - case 'slow-2g': - case '2g': - return 4; - case '3g': - return 5; - case '4g': - return 6; - default: - return 3; - } - default: - return 0; - } -} - export const spec = { code: BIDDER_CODE, gvlid: GVLID, diff --git a/modules/programmaticaBidAdapter.js b/modules/programmaticaBidAdapter.js index 7d52e305189..aeca74120d6 100644 --- a/modules/programmaticaBidAdapter.js +++ b/modules/programmaticaBidAdapter.js @@ -1,6 +1,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { hasPurpose1Consent } from '../src/utils/gdpr.js'; import { deepAccess, parseSizesInput, isArray } from '../src/utils.js'; const BIDDER_CODE = 'programmatica'; diff --git a/modules/pstudioBidAdapter.js b/modules/pstudioBidAdapter.js index 77a11ac58c6..cc9310e174b 100644 --- a/modules/pstudioBidAdapter.js +++ b/modules/pstudioBidAdapter.js @@ -6,16 +6,14 @@ import { isNumber, generateUUID, isEmpty, - isFn, - isPlainObject, } from '../src/utils.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'pstudio'; const ENDPOINT = 'https://exchange.pstudio.tadex.id/prebid-bid' const TIME_TO_LIVE = 300; -// in case that the publisher limits number of user syncs, thisse syncs will be discarded from the end of the list -// so more improtant syncing calls should be at the start of the list +// in case that the publisher limits number of user syncs, these syncs will be discarded from the end of the list +// so more important syncing calls should be at the start of the list const USER_SYNCS = [ // PARTNER_UID is a partner user id { @@ -40,6 +38,7 @@ const VIDEO_PARAMS = [ 'protocols', 'startdelay', 'placement', + 'plcmt', 'skip', 'skipafter', 'minbitrate', @@ -58,7 +57,7 @@ export const spec = { isBidRequestValid: function (bid) { const params = bid.params || {}; - return !!params.pubid && !!params.floorPrice && isVideoRequestValid(bid); + return !!params.pubid && !!params.adtagid && isVideoRequestValid(bid); }, buildRequests: function (validBidRequests, bidderRequest) { @@ -144,7 +143,7 @@ function buildRequestData(bid, bidderRequest) { function buildBaseObject(bid, bidderRequest) { const firstPartyData = prepareFirstPartyData(bidderRequest.ortb2); - const { pubid, bcat, badv, bapp } = bid.params; + const { pubid, adtagid, bcat, badv, bapp } = bid.params; const { userId } = bid; const uid2Token = userId?.uid2?.id; @@ -167,8 +166,7 @@ function buildBaseObject(bid, bidderRequest) { return { id: bid.bidId, pubid, - floor_price: getBidFloor(bid), - adtagid: bid.adUnitCode, + adtagid: adtagid, ...(bcat && { bcat }), ...(badv && { badv }), ...(bapp && { bapp }), @@ -416,7 +414,7 @@ function validateSizes(sizes) { ); } -function getBidFloor(bid) { +/* function getBidFloor(bid) { if (!isFn(bid.getFloor)) { return bid.params.floorPrice ? bid.params.floorPrice : null; } @@ -430,6 +428,6 @@ function getBidFloor(bid) { return floor.floor; } return null; -} +} */ registerBidder(spec); diff --git a/modules/pstudioBidAdapter.md b/modules/pstudioBidAdapter.md index 0c8c6927f43..a4b3e098cfc 100644 --- a/modules/pstudioBidAdapter.md +++ b/modules/pstudioBidAdapter.md @@ -31,7 +31,8 @@ var adUnits = [ params: { // id of test publisher pubid: '22430f9d-9610-432c-aabe-6134256f11af', - floorPrice: 1.25, + // id of test adtag id + adtagid: '6f3173b9-5623-4a4f-8c62-2b1d24ceb4e6', }, }, ], @@ -53,7 +54,8 @@ var adUnits = [ params: { // id of test publisher pubid: '22430f9d-9610-432c-aabe-6134256f11af', - floorPrice: 1.25, + // id of test adtag id + adtagid: '097c601f-ad09-495b-b70b-d9cf6f1edbc1', }, }, ], @@ -79,7 +81,7 @@ var adUnits = [ bidder: 'pstudio', params: { pubid: '22430f9d-9610-432c-aabe-6134256f11af', // required - floorPrice: 1.15, // required + adtagid: 'b9be4c35-3c12-4fa9-96ba-34b90276208c', // required bcat: ['IAB1-1', 'IAB1-3'], // optional badv: ['nike.com'], // optional bapp: ['com.foo.mygame'], // optional @@ -121,7 +123,7 @@ var videoAdUnits = [ bidder: 'pstudio', params: { pubid: '22430f9d-9610-432c-aabe-6134256f11af', - floorPrice: 1.25, + adtagid: '46e348cf-b79d-43e5-81bc-5954cdf15d7e', badv: ['adidas.com'], }, }, diff --git a/modules/pubCircleBidAdapter.js b/modules/pubCircleBidAdapter.js index 54224fd0403..c63b80b819c 100644 --- a/modules/pubCircleBidAdapter.js +++ b/modules/pubCircleBidAdapter.js @@ -1,231 +1,19 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'pubcircle'; const AD_URL = 'https://ml.pubcircle.ai/pbjs'; const SYNC_URL = 'https://cs.pubcircle.ai'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - placement.placementId = placementId; - placement.type = 'publisher'; - - if (bid.userId) { - getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); - getUserId(placement.eids, bid.userId.lotamePanoramaId, 'lotame.com'); - getUserId(placement.eids, bid.userId.idx, 'idx.lat'); - getUserId(placement.eids, bid.userId.idl_env, 'liveramp.com'); - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - -function getUserId(eids, id, source, uidExt) { - if (id) { - var uid = { id }; - if (uidExt) { - uid.ext = uidExt; - } - eids.push({ - source, - uids: [ uid ] - }); - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && params.placementId); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - }, - - onBidViewable: function (bid) { - // to do : we need to implement js tag to fire pixel with viewability counter - } + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/pubgeniusBidAdapter.js b/modules/pubgeniusBidAdapter.js index d92a9352cee..19260e65e60 100644 --- a/modules/pubgeniusBidAdapter.js +++ b/modules/pubgeniusBidAdapter.js @@ -155,7 +155,7 @@ function buildVideoParams(videoMediaType, videoParams) { 'maxduration', 'protocols', 'startdelay', - 'placement', + 'plcmt', 'skip', 'skipafter', 'minbitrate', @@ -166,17 +166,6 @@ function buildVideoParams(videoMediaType, videoParams) { 'linearity', ]); - switch (videoMediaType.context) { - case 'instream': - params.placement = 1; - break; - case 'outstream': - params.placement = 2; - break; - default: - break; - } - if (videoMediaType.playerSize) { params.w = videoMediaType.playerSize[0][0]; params.h = videoMediaType.playerSize[0][1]; @@ -205,7 +194,7 @@ function buildImp(bid) { mediaType: bid.mediaTypes.banner ? 'banner' : 'video', size: '*', currency: 'USD', - }); + }) || {}; if (floor) { imp.bidfloor = floor; @@ -301,8 +290,7 @@ function isValidBanner(banner) { function isValidVideo(videoMediaType, videoParams) { const params = buildVideoParams(videoMediaType, videoParams); - return !!(params.placement && - isValidSize([params.w, params.h]) && + return !!(isValidSize([params.w, params.h]) && params.mimes && params.mimes.length && isArrayOfNums(params.protocols) && params.protocols.length); } diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index e8eb90cd02a..875f6592c04 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -9,7 +9,6 @@ import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import { parseUrl, buildUrl, logError } from '../src/utils.js'; -import {uspDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; /** @@ -39,9 +38,9 @@ function publinkIdUrl(params, consentData, storedId) { mpv: '$prebid.version$', }; - if (consentData) { - url.search.gdpr = (consentData.gdprApplies) ? 1 : 0; - url.search.gdpr_consent = consentData.consentString; + if (consentData?.gdpr) { + url.search.gdpr = (consentData.gdpr.gdprApplies) ? 1 : 0; + url.search.gdpr_consent = consentData.gdpr.consentString; } if (params) { @@ -60,7 +59,7 @@ function publinkIdUrl(params, consentData, storedId) { url.search.publink = storedId; } - const usPrivacyString = uspDataHandler.getConsentData(); + const usPrivacyString = consentData?.usp; if (usPrivacyString && typeof usPrivacyString === 'string') { url.search.us_privacy = usPrivacyString; } diff --git a/modules/publirBidAdapter.js b/modules/publirBidAdapter.js index 432e123458c..2cf55aa86cb 100644 --- a/modules/publirBidAdapter.js +++ b/modules/publirBidAdapter.js @@ -2,32 +2,26 @@ import { logWarn, logInfo, isArray, - isFn, deepAccess, - isEmpty, - contains, timestamp, triggerPixel, - getDNT, - getBidIdParameter } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; import { ajax } from '../src/ajax.js'; +import { + generateBidsParams, + generateGeneralParams, +} from '../libraries/riseUtils/index.js'; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'publir'; const ADAPTER_VERSION = '1.0.0'; const TTL = 360; const CURRENCY = 'USD'; -const DEFAULT_SELLER_ENDPOINT = 'https://prebid.publir.com/publirPrebidEndPoint'; +const BASE_URL = 'https://prebid.publir.com/publirPrebidEndPoint'; const DEFAULT_IMPS_ENDPOINT = 'https://prebidimpst.publir.com/publirPrebidImpressionTracker'; -const SUPPORTED_SYNC_METHODS = { - IFRAME: 'iframe', - PIXEL: 'pixel' -} export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const spec = { @@ -40,25 +34,26 @@ export const spec = { logWarn('pubId is a mandatory param for Publir adapter'); return false; } - return true; }, buildRequests: function (validBidRequests, bidderRequest) { - const reqObj = {}; + const combinedRequestsObject = {}; + const generalObject = validBidRequests[0]; - reqObj.params = generatePubGeneralsParams(generalObject, bidderRequest); - reqObj.bids = generatePubBidParams(validBidRequests, bidderRequest); - reqObj.bids.timestamp = timestamp(); + combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest, ADAPTER_VERSION); + combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); + combinedRequestsObject.bids.timestamp = timestamp(); + let options = { withCredentials: false }; return { method: 'POST', - url: DEFAULT_SELLER_ENDPOINT, - data: reqObj, + url: BASE_URL, + data: combinedRequestsObject, options - } + }; }, interpretResponse: function ({ body }) { const bidResponses = []; @@ -109,20 +104,20 @@ export const spec = { const syncs = []; for (const response of serverResponses) { if (response.body && response.body.params) { - if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { + if (syncOptions.iframeEnabled && deepAccess(response, 'body.params.userSyncURL')) { syncs.push({ type: 'iframe', - url: response.body.params.userSyncURL + url: deepAccess(response, 'body.params.userSyncURL') }); } - if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { + if (syncOptions.pixelEnabled && isArray(deepAccess(response, 'body.params.userSyncPixels'))) { const pixels = response.body.params.userSyncPixels.map(pixel => { return { type: 'image', url: pixel - } - }) - syncs.push(...pixels) + }; + }); + syncs.push(...pixels); } } } @@ -141,251 +136,3 @@ export const spec = { }; registerBidder(spec); - -/** - * Get floor price - * @param bid {bid} - * @returns {Number} - */ -function getFloor(bid, mediaType) { - if (!isFn(bid.getFloor)) { - return 0; - } - let floorResult = bid.getFloor({ - currency: CURRENCY, - mediaType: mediaType, - size: '*' - }); - return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; -} - -/** - * Get the the ad sizes array from the bid - * @param bid {bid} - * @returns {Array} - */ -function getSizesArray(bid, mediaType) { - let sizesArray = [] - - if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { - sizesArray = bid.mediaTypes[mediaType].sizes; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - sizesArray = bid.sizes; - } - - return sizesArray; -} - -/** - * Get schain string value - * @param schainObject {Object} - * @returns {string} - */ -function getSupplyChain(schainObject) { - if (isEmpty(schainObject)) { - return ''; - } - let scStr = `${schainObject.ver},${schainObject.complete}`; - schainObject.nodes.forEach((node) => { - scStr += '!'; - scStr += `${getEncodedValIfNotEmpty(node.asi)},`; - scStr += `${getEncodedValIfNotEmpty(node.sid)},`; - scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; - scStr += `${getEncodedValIfNotEmpty(node.rid)},`; - scStr += `${getEncodedValIfNotEmpty(node.name)},`; - scStr += `${getEncodedValIfNotEmpty(node.domain)}`; - }); - return scStr; -} - -/** - * Get encoded node value - * @param val {string} - * @returns {string} - */ -function getEncodedValIfNotEmpty(val) { - return !isEmpty(val) ? encodeURIComponent(val) : ''; -} - -function getAllowedSyncMethod(filterSettings, bidderCode) { - const iframeConfigsToCheck = ['all', 'iframe']; - const pixelConfigToCheck = 'image'; - if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { - return SUPPORTED_SYNC_METHODS.IFRAME; - } - if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { - return SUPPORTED_SYNC_METHODS.PIXEL; - } -} - -/** - * Check if sync rule is supported - * @param syncRule {Object} - * @param bidderCode {string} - * @returns {boolean} - */ -function isSyncMethodAllowed(syncRule, bidderCode) { - if (!syncRule) { - return false; - } - const isInclude = syncRule.filter === 'include'; - const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; - return isInclude && contains(bidders, bidderCode); -} - -/** - * get device type - * @param ua {ua} - * @returns {string} - */ -function getDeviceType(ua) { - if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(ua.toLowerCase())) { - return '5'; - } - if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(ua.toLowerCase())) { - return '4'; - } - if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i.test(ua.toLowerCase())) { - return '3'; - } - return '1'; -} - -function generatePubBidParams(validBidRequests, bidderRequest) { - const bidsArray = []; - - if (validBidRequests.length) { - validBidRequests.forEach(bid => { - bidsArray.push(generateBidParameters(bid, bidderRequest)); - }); - } - return bidsArray; -} - -/** - * Generate bid specific parameters - * @param {bid} bid - * @param {bidderRequest} bidderRequest - * @returns {Object} bid specific params object - */ -function generateBidParameters(bid, bidderRequest) { - const { params } = bid; - const mediaType = isBanner(bid) ? BANNER : VIDEO; - const sizesArray = getSizesArray(bid, mediaType); - - // fix floor price in case of NAN - if (isNaN(params.floorPrice)) { - params.floorPrice = 0; - } - - const bidObject = { - mediaType, - adUnitCode: getBidIdParameter('adUnitCode', bid), - sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), - bidId: getBidIdParameter('bidId', bid), - bidderRequestId: getBidIdParameter('bidderRequestId', bid), - loop: getBidIdParameter('bidderRequestsCount', bid), - transactionId: bid.ortb2Imp?.ext?.tid, - coppa: 0 - }; - - const pubId = params.pubId; - if (pubId) { - bidObject.pubId = pubId; - } - - const sua = deepAccess(bid, `ortb2.device.sua`); - if (sua) { - bidObject.sua = sua; - } - - const coppa = deepAccess(bid, `ortb2.regs.coppa`) - if (coppa) { - bidObject.coppa = 1; - } - - return bidObject; -} - -function isBanner(bid) { - return bid.mediaTypes && bid.mediaTypes.banner; -} - -function getLocalStorage(cookieObjName) { - if (storage.localStorageIsEnabled()) { - const lstData = storage.getDataFromLocalStorage(cookieObjName); - return lstData; - } - return ''; -} - -/** - * Generate params that are common between all bids - * @param generalObject - * @param {bidderRequest} bidderRequest - * @returns {object} the common params object - */ -function generatePubGeneralsParams(generalObject, bidderRequest) { - const domain = bidderRequest.refererInfo; - const { syncEnabled, filterSettings } = config.getConfig('userSync') || {}; - const { bidderCode } = bidderRequest; - const generalBidParams = generalObject.params; - const timeout = bidderRequest.timeout; - const generalParams = { - wrapper_type: 'prebidjs', - wrapper_vendor: '$$PREBID_GLOBAL$$', - wrapper_version: '$prebid.version$', - adapter_version: ADAPTER_VERSION, - auction_start: bidderRequest.auctionStart, - publisher_id: generalBidParams.pubId, - publisher_name: domain, - site_domain: domain, - dnt: getDNT() ? 1 : 0, - device_type: getDeviceType(navigator.userAgent), - ua: navigator.userAgent, - is_wrapper: !!generalBidParams.isWrapper, - session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), - tmax: timeout, - user_cookie: getLocalStorage('_publir_prebid_creative') - }; - - const userIdsParam = getBidIdParameter('userId', generalObject); - if (userIdsParam) { - generalParams.userIds = JSON.stringify(userIdsParam); - } - - const ortb2Metadata = bidderRequest.ortb2 || {}; - if (ortb2Metadata.site) { - generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); - } - if (ortb2Metadata.user) { - generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); - } - - if (syncEnabled) { - const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); - if (allowedSyncMethod) { - generalParams.cs_method = allowedSyncMethod; - } - } - - if (bidderRequest.uspConsent) { - generalParams.us_privacy = bidderRequest.uspConsent; - } - - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; - generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - - if (generalObject.schain) { - generalParams.schain = getSupplyChain(generalObject.schain); - } - - if (bidderRequest && bidderRequest.refererInfo) { - generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); - } - - return generalParams; -} diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index 9e1fa49fef2..7c52d59e3eb 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -17,9 +17,10 @@ const FLOOR_VALUES = { TIMEOUT: 'timeout' }; -/// /////////// CONSTANTS ////////////// +/// /////////// CONSTANTS /////////////// const ADAPTER_CODE = 'pubmatic'; const VENDOR_OPENWRAP = 'openwrap'; +const DISPLAY_MANAGER = 'Prebid.js'; const SEND_TIMEOUT = 2000; const END_POINT_HOST = 'https://t.pubmatic.com/'; const END_POINT_BID_LOGGER = END_POINT_HOST + 'wl?'; @@ -49,6 +50,22 @@ const MEDIATYPE = { NATIVE: 2 } +// TODO : Remove - Once BM calculation moves to Server Side +const BROWSER_MAP = [ + { value: /(firefox)\/([\w\.]+)/i, key: 12 }, // Firefox + { value: /\b(?:crios)\/([\w\.]+)/i, key: 1 }, // Chrome for iOS + { value: /edg(?:e|ios|a)?\/([\w\.]+)/i, key: 2 }, // Edge + { value: /(opera|opr)(?:.+version\/|[\/ ]+)([\w\.]+)/i, key: 3 }, // Opera + { value: /(?:ms|\()(ie) ([\w\.]+)|(?:trident\/[\w\.]+)/i, key: 4 }, // Internet Explorer + { value: /fxios\/([-\w\.]+)/i, key: 5 }, // Firefox for iOS + { value: /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w\.]+);)/i, key: 6 }, // Facebook In-App Browser + { value: / wv\).+(chrome)\/([\w\.]+)/i, key: 7 }, // Chrome WebView + { value: /droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i, key: 8 }, // Android Browser + { value: /(chrome|chromium|crios)\/v?([\w\.]+)/i, key: 9 }, // Chrome + { value: /version\/([\w\.\,]+) .*mobile\/\w+ (safari)/i, key: 10 }, // Safari Mobile + { value: /version\/([\w(\.|\,)]+) .*(mobile ?safari|safari)/i, key: 11 }, // Safari +]; + /// /////////// VARIABLES ////////////// let publisherId = DEFAULT_PUBLISHER_ID; // int: mandatory let profileId = DEFAULT_PROFILE_ID; // int: optional @@ -203,6 +220,17 @@ function getDevicePlatform() { return deviceType; } +// TODO : Remove - Once BM calculation moves to Server Side +function getBrowserType() { + const userAgent = navigator?.userAgent; + let browserIndex = userAgent == null ? -1 : 0; + + if (userAgent) { + browserIndex = BROWSER_MAP.find(({ value }) => value.test(userAgent))?.key || 0; + } + return browserIndex; +} + function getValueForKgpv(bid, adUnitId) { if (bid.params && bid.params.regexPattern) { return bid.params.regexPattern; @@ -281,7 +309,32 @@ function isOWPubmaticBid(adapterName) { }) } -function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { +function getFloorsCommonField (floorData) { + if (!floorData) return; + const { location, fetchStatus, floorProvider, modelVersion } = floorData; + return { + ffs: { + [FLOOR_VALUES.SUCCESS]: 1, + [FLOOR_VALUES.ERROR]: 2, + [FLOOR_VALUES.TIMEOUT]: 4, + undefined: 0 + }[fetchStatus], + fsrc: { + [FLOOR_VALUES.FETCH]: 2, + [FLOOR_VALUES.NO_DATA]: 0, + [FLOOR_VALUES.AD_UNIT]: 1, + [FLOOR_VALUES.SET_CONFIG]: 1 + }[location], + fp: floorProvider, + mv: modelVersion + } +} + +function getFloorType(floorResponseData) { + return floorResponseData ? (floorResponseData.enforcements.enforceJS == false ? 0 : 1) : undefined; +} + +function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid, e) { highestBid = (highestBid && highestBid.length > 0) ? highestBid[0] : null; return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { adUnit.bids[bidId].forEach(function(bid) { @@ -290,6 +343,16 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { return; } const pg = window.parseFloat(Number(bid.bidResponse?.adserverTargeting?.hb_pb || bid.bidResponse?.adserverTargeting?.pwtpb).toFixed(BID_PRECISION)); + + const prebidBidsReceived = e?.bidsReceived; + if (isArray(prebidBidsReceived) && prebidBidsReceived.length > 0) { + prebidBidsReceived.forEach(function(iBid) { + if (iBid.adId === bid.adId) { + bid.bidderCode = iBid.bidderCode; + } + }); + } + partnerBids.push({ 'pn': adapterName, 'bc': bid.bidderCode || bid.bidder, @@ -315,6 +378,7 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, 'frv': bid.bidResponse ? bid.bidResponse.floorData?.floorRuleValue : undefined, + 'fv': bid.bidResponse ? bid.bidResponse.floorData?.floorValue : undefined, 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined, 'pb': pg || undefined }); @@ -364,19 +428,19 @@ function getFloorFetchStatus(floorData) { function executeBidsLoggerCall(e, highestCpmBids) { let auctionId = e.auctionId; - let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; + let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId]?.referer || ''; let auctionCache = cache.auctions[auctionId]; let wiid = auctionCache?.wiid || auctionId; let floorData = auctionCache?.floorData; - let floorFetchStatus = getFloorFetchStatus(auctionCache?.floorData); + let floorFetchStatus = getFloorFetchStatus(floorData); let outputObj = { s: [] }; let pixelURL = END_POINT_BID_LOGGER; - if (!auctionCache) { - return; - } + const country = e.bidderRequests?.length > 0 + ? e.bidderRequests.find(bidder => bidder?.bidderCode === ADAPTER_CODE)?.ortb2?.user?.ext?.ctr || '' + : ''; - if (auctionCache.sent) { + if (!auctionCache || auctionCache.sent) { return; } @@ -391,11 +455,27 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj['pdvid'] = '' + profileVersionId; outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = getTgId(); - outputObj['pbv'] = getGlobal()?.version || '-1'; - - if (floorData && floorFetchStatus) { - outputObj['fmv'] = floorData.floorRequestData ? floorData.floorRequestData.modelVersion || undefined : undefined; - outputObj['ft'] = floorData.floorResponseData ? (floorData.floorResponseData.enforcements.enforceJS == false ? 0 : 1) : undefined; + outputObj['dm'] = DISPLAY_MANAGER; + outputObj['dmv'] = '$prebid.version$' || '-1'; + outputObj['bm'] = getBrowserType(); + outputObj['ctr'] = country || ''; + + if (floorData) { + const floorRootValues = getFloorsCommonField(floorData?.floorRequestData); + if (floorRootValues) { + const { ffs, fsrc, fp, mv } = floorRootValues; + if (floorData?.floorRequestData) { + outputObj['ffs'] = ffs; + outputObj['fsrc'] = fsrc; + outputObj['fp'] = fp; + } + if (floorFetchStatus) { + outputObj['fmv'] = mv || undefined; + } + } + if (floorFetchStatus) { + outputObj['ft'] = getFloorType(floorData?.floorResponseData); + } } outputObj.s = Object.keys(auctionCache.adUnitCodes).reduce(function(slotsArray, adUnitId) { @@ -407,26 +487,10 @@ function executeBidsLoggerCall(e, highestCpmBids) { 'au': origAdUnit.owAdUnitId || getGptSlotInfoForAdUnitCode(adUnitId)?.gptSlot || adUnitId, 'mt': getAdUnitAdFormats(origAdUnit), 'sz': getSizesForAdUnit(adUnit, adUnitId), - 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)), + 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId), e), 'fskp': floorData && floorFetchStatus ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, 'sid': generateUUID() }; - if (floorData?.floorRequestData) { - const { location, fetchStatus, floorProvider } = floorData?.floorRequestData; - slotObject.ffs = { - [FLOOR_VALUES.SUCCESS]: 1, - [FLOOR_VALUES.ERROR]: 2, - [FLOOR_VALUES.TIMEOUT]: 4, - undefined: 0 - }[fetchStatus]; - slotObject.fsrc = { - [FLOOR_VALUES.FETCH]: 2, - [FLOOR_VALUES.NO_DATA]: 2, - [FLOOR_VALUES.AD_UNIT]: 1, - [FLOOR_VALUES.SET_CONFIG]: 1 - }[location]; - slotObject.fp = floorProvider; - } slotsArray.push(slotObject); return slotsArray; }, []); @@ -446,8 +510,8 @@ function executeBidsLoggerCall(e, highestCpmBids) { } function executeBidWonLoggerCall(auctionId, adUnitId) { - const winningBidId = cache.auctions[auctionId].adUnitCodes[adUnitId].bidWon; - const winningBids = cache.auctions[auctionId].adUnitCodes[adUnitId].bids[winningBidId]; + const winningBidId = cache.auctions[auctionId]?.adUnitCodes[adUnitId]?.bidWon; + const winningBids = cache.auctions[auctionId]?.adUnitCodes[adUnitId]?.bids[winningBidId]; if (!winningBids) { logWarn(LOG_PRE_FIX + 'Could not find winningBids for : ', auctionId); return; @@ -455,26 +519,26 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { let winningBid = winningBids[0]; if (winningBids.length > 1) { - winningBid = winningBids.filter(bid => bid.adId === cache.auctions[auctionId].adUnitCodes[adUnitId].bidWonAdId)[0]; + winningBid = winningBids.filter(bid => bid.adId === cache.auctions[auctionId]?.adUnitCodes[adUnitId]?.bidWonAdId)[0]; } const adapterName = getAdapterNameForAlias(winningBid.adapterCode || winningBid.bidder); if (isOWPubmaticBid(adapterName) && isS2SBidder(winningBid.bidder)) { return; } - let origAdUnit = getAdUnit(cache.auctions[auctionId].origAdUnits, adUnitId) || {}; + let origAdUnit = getAdUnit(cache.auctions[auctionId]?.origAdUnits, adUnitId) || {}; let owAdUnitId = origAdUnit.owAdUnitId || getGptSlotInfoForAdUnitCode(adUnitId)?.gptSlot || adUnitId; let auctionCache = cache.auctions[auctionId]; - let floorData = auctionCache.floorData; + let floorData = auctionCache?.floorData; let wiid = cache.auctions[auctionId]?.wiid || auctionId; - let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; + let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId]?.referer || ''; let adv = winningBid.bidResponse ? getAdDomain(winningBid.bidResponse) || undefined : undefined; let fskp = floorData ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined; let pg = window.parseFloat(Number(winningBid?.bidResponse?.adserverTargeting?.hb_pb || winningBid?.bidResponse?.adserverTargeting?.pwtpb)) || undefined; let pixelURL = END_POINT_WIN_BID_LOGGER; pixelURL += 'pubid=' + publisherId; - pixelURL += '&purl=' + enc(config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''); + pixelURL += '&purl=' + enc(config.getConfig('pageUrl') || cache.auctions[auctionId]?.referer || ''); pixelURL += '&tst=' + Math.round((new window.Date()).getTime() / 1000); pixelURL += '&iid=' + enc(wiid); pixelURL += '&bidid=' + enc(winningBidId); @@ -484,12 +548,18 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += '&au=' + enc(owAdUnitId); pixelURL += '&pn=' + enc(adapterName); pixelURL += '&bc=' + enc(winningBid.bidderCode || winningBid.bidder); - pixelURL += '&en=' + enc(winningBid.bidResponse.bidPriceUSD); - pixelURL += '&eg=' + enc(winningBid.bidResponse.bidGrossCpmUSD); + pixelURL += '&en=' + enc(winningBid.bidResponse?.bidPriceUSD); + pixelURL += '&eg=' + enc(winningBid.bidResponse?.bidGrossCpmUSD); pixelURL += '&kgpv=' + enc(getValueForKgpv(winningBid, adUnitId)); + pixelURL += '&dm=' + enc(DISPLAY_MANAGER); + pixelURL += '&dmv=' + enc('$prebid.version$' || '-1'); pixelURL += '&origbidid=' + enc(winningBid?.bidResponse?.partnerImpId || winningBid?.bidResponse?.prebidBidId || winningBid.bidId); pixelURL += '&di=' + enc(winningBid?.bidResponse?.dealId || OPEN_AUCTION_DEAL_ID); - pixelURL += '&pb=' + enc(pg); + const ds = winningBid.bidResponse?.meta ? getMetadata(winningBid.bidResponse.meta)?.ds : undefined; + if (ds) { + pixelURL += '&ds=' + enc(ds); + } + pg && (pixelURL += '&pb=' + enc(pg)); pixelURL += '&plt=' + enc(getDevicePlatform()); pixelURL += '&psz=' + enc((winningBid?.bidResponse?.dimensions?.width || '0') + 'x' + @@ -499,6 +569,27 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += '&orig=' + enc(getDomainFromUrl(referrer)); pixelURL += '&ss=' + enc(isS2SBidder(winningBid.bidder)); (fskp != undefined) && (pixelURL += '&fskp=' + enc(fskp)); + if (floorData) { + const floorRootValues = getFloorsCommonField(floorData.floorRequestData); + if (floorRootValues) { + const { ffs, fsrc, fp, mv } = floorRootValues || {}; + const params = { ffs, fsrc, fp, fmv: mv }; + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + pixelURL += `&${key}=${enc(value)}`; + } + }); + } + const floorType = getFloorType(floorData.floorResponseData); + if (floorType !== undefined) { + pixelURL += '&ft=' + enc(floorType); + } + const floorRuleValue = winningBid?.bidResponse?.floorData?.floorRuleValue; + (floorRuleValue !== undefined) && (pixelURL += '&frv=' + enc(floorRuleValue)); + + const floorValue = winningBid?.bidResponse?.floorData?.floorValue; + (floorValue !== undefined) && (pixelURL += '&fv=' + enc(floorValue)); + } pixelURL += '&af=' + enc(winningBid.bidResponse ? (winningBid.bidResponse.mediaType || undefined) : undefined); ajax( @@ -559,7 +650,8 @@ function bidResponseHandler(args) { logWarn(LOG_PRE_FIX + 'Got null requestId in bidResponseHandler'); return; } - let bid = cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId][0]; + let requestId = args.originalRequestId || args.requestId; + let bid = cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[requestId][0]; if (!bid) { logError(LOG_PRE_FIX + 'Could not find associated bid request for bid response with requestId: ', args.requestId); return; @@ -567,7 +659,9 @@ function bidResponseHandler(args) { if ((bid.bidder && args.bidderCode && bid.bidder !== args.bidderCode) || (bid.bidder === args.bidderCode && bid.status === SUCCESS)) { bid = copyRequiredBidDetails(args); - cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId].push(bid); + cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[requestId].push(bid); + } else if (args.originalRequestId) { + bid.bidId = args.requestId; } if (args.floorData) { @@ -598,7 +692,7 @@ function bidRejectedHandler(args) { function bidderDoneHandler(args) { cache.auctions[args.auctionId].bidderDonePendingCount--; args.bids.forEach(bid => { - let cachedBid = cache.auctions[bid.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId || bid.requestId]; + let cachedBid = cache.auctions[bid.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId || bid.originalRequestId || bid.requestId]; if (typeof bid.serverResponseTimeMs !== 'undefined') { cachedBid.serverLatencyTimeMs = bid.serverResponseTimeMs; } @@ -613,7 +707,7 @@ function bidderDoneHandler(args) { function bidWonHandler(args) { let auctionCache = cache.auctions[args.auctionId]; - auctionCache.adUnitCodes[args.adUnitCode].bidWon = args.requestId; + auctionCache.adUnitCodes[args.adUnitCode].bidWon = args.originalRequestId || args.requestId; auctionCache.adUnitCodes[args.adUnitCode].bidWonAdId = args.adId; executeBidWonLoggerCall(args.auctionId, args.adUnitCode); } @@ -631,7 +725,7 @@ function bidTimeoutHandler(args) { // db = 0 and t = 1 means bidder did respond with a bid but post timeout args.forEach(badBid => { let auctionCache = cache.auctions[badBid.auctionId]; - let bid = auctionCache.adUnitCodes[badBid.adUnitCode].bids[ badBid.bidId || badBid.requestId ][0]; + let bid = auctionCache.adUnitCodes[badBid.adUnitCode].bids[ badBid.bidId || badBid.originalRequestId || badBid.requestId ][0]; if (bid) { bid.status = ERROR; bid.error = { diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 7ff86c5c46f..a3c97682d5b 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -1,10 +1,11 @@ -import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, uniques, isPlainObject, isInteger, generateUUID } from '../src/utils.js'; +import { logWarn, isStr, isArray, deepAccess, deepSetValue, isBoolean, isInteger, logInfo, logError, deepClone, uniques, generateUUID, isPlainObject, isFn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE, ADPOD } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { bidderSettings } from '../src/bidderSettings.js'; -import { NATIVE_IMAGE_TYPES, NATIVE_KEYS_THAT_ARE_NOT_ASSETS, NATIVE_KEYS, NATIVE_ASSET_TYPES } from '../src/constants.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { NATIVE_ASSET_TYPES, NATIVE_IMAGE_TYPES, PREBID_NATIVE_DATA_KEYS_TO_ORTB, NATIVE_KEYS_THAT_ARE_NOT_ASSETS, NATIVE_KEYS } from '../src/constants.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -22,1018 +23,623 @@ const AUCTION_TYPE = 1; const UNDEFINED = undefined; const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; -const PREBID_NATIVE_HELP_LINK = 'http://prebid.org/dev-docs/show-native-ads.html'; const PUBLICATION = 'pubmatic'; // Your publication on Blue Billywig, potentially with environment (e.g. publication.bbvms.com or publication.test.bbvms.com) const RENDERER_URL = 'https://pubmatic.bbvms.com/r/'.concat('$RENDERER', '.js'); // URL of the renderer application -const MSG_VIDEO_PLACEMENT_MISSING = 'Video.Placement param missing'; - +const MSG_VIDEO_PLCMT_MISSING = 'Video.plcmt param missing'; +const PREBID_NATIVE_DATA_KEY_VALUES = Object.values(PREBID_NATIVE_DATA_KEYS_TO_ORTB); +const DEFAULT_TTL = 360; const CUSTOM_PARAMS = { 'kadpageurl': '', // Custom page url 'gender': '', // User gender 'yob': '', // User year of birth 'lat': '', // User location - Latitude 'lon': '', // User Location - Longitude - 'wiid': '', // OpenWrap Wrapper Impression ID - 'profId': '', // OpenWrap Legacy: Profile ID - 'verId': '' // OpenWrap Legacy: version ID + 'wiid': '' // OpenWrap Wrapper Impression ID }; -const DATA_TYPES = { - 'NUMBER': 'number', - 'STRING': 'string', - 'BOOLEAN': 'boolean', - 'ARRAY': 'array', - 'OBJECT': 'object' -}; -const VIDEO_CUSTOM_PARAMS = { - 'mimes': DATA_TYPES.ARRAY, - 'minduration': DATA_TYPES.NUMBER, - 'maxduration': DATA_TYPES.NUMBER, - 'startdelay': DATA_TYPES.NUMBER, - 'playbackmethod': DATA_TYPES.ARRAY, - 'api': DATA_TYPES.ARRAY, - 'protocols': DATA_TYPES.ARRAY, - 'w': DATA_TYPES.NUMBER, - 'h': DATA_TYPES.NUMBER, - 'battr': DATA_TYPES.ARRAY, - 'linearity': DATA_TYPES.NUMBER, - 'placement': DATA_TYPES.NUMBER, - 'plcmt': DATA_TYPES.NUMBER, - 'minbitrate': DATA_TYPES.NUMBER, - 'maxbitrate': DATA_TYPES.NUMBER, - 'skip': DATA_TYPES.NUMBER -} -const NATIVE_ASSET_IMAGE_TYPE = { - 'ICON': 1, - 'IMAGE': 3 -} - -const NET_REVENUE = true; -const dealChannelValues = { +const dealChannel = { 1: 'PMP', 5: 'PREF', 6: 'PMPG' }; -// BB stands for Blue BillyWig -const BB_RENDERER = { - bootstrapPlayer: function(bid) { - const config = { - code: bid.adUnitCode, - }; - - if (bid.vastXml) config.vastXml = bid.vastXml; - else if (bid.vastUrl) config.vastUrl = bid.vastUrl; - - if (!bid.vastXml && !bid.vastUrl) { - logWarn(`${LOG_WARN_PREFIX}: No vastXml or vastUrl on bid, bailing...`); - return; - } - - const rendererId = BB_RENDERER.getRendererId(PUBLICATION, bid.rendererCode); - - const ele = document.getElementById(bid.adUnitCode); // NB convention - - let renderer; +const MEDIATYPE_TTL = { + 'banner': 360, + 'video': 1800, + 'native': 1800 +}; - for (let rendererIndex = 0; rendererIndex < window.bluebillywig.renderers.length; rendererIndex++) { - if (window.bluebillywig.renderers[rendererIndex]._id === rendererId) { - renderer = window.bluebillywig.renderers[rendererIndex]; - break; - } - } +let conf = {}; +let blockedIabCategories = []; +let allowedIabCategories = []; +let pubId = 0; +export let cpmAdjustment; - if (renderer) renderer.bootstrap(config, ele); - else logWarn(`${LOG_WARN_PREFIX}: Couldn't find a renderer with ${rendererId}`); +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: DEFAULT_TTL }, - newRenderer: function(rendererCode, adUnitCode) { - var rendererUrl = RENDERER_URL.replace('$RENDERER', rendererCode); - const renderer = Renderer.install({ - url: rendererUrl, - loaded: false, - adUnitCode + imp(buildImp, bidRequest, context) { + const { kadfloor, currency, adSlot = '', deals, dctr, pmzoneid, hashedKey } = bidRequest.params; + const { adUnitCode, mediaTypes, rtd } = bidRequest; + const imp = buildImp(bidRequest, context); + if (deals) addPMPDeals(imp, deals); + if (dctr) addDealCustomTargetings(imp, dctr); + if (rtd?.jwplayer) addJWPlayerSegmentData(imp, rtd.jwplayer); + imp.bidfloor = _parseSlotParam('kadfloor', kadfloor); + imp.bidfloorcur = currency ? _parseSlotParam('currency', currency) : DEFAULT_CURRENCY; + setFloorInImp(imp, bidRequest); + if (imp.hasOwnProperty('banner')) updateBannerImp(imp.banner, adSlot); + if (imp.hasOwnProperty('video')) updateVideoImp(imp.video, mediaTypes?.video, adUnitCode, imp); + if (imp.hasOwnProperty('native')) updateNativeImp(imp, mediaTypes?.native); + // Check if the imp object does not have banner, video, or native + if (!imp.hasOwnProperty('banner') && !imp.hasOwnProperty('video') && !imp.hasOwnProperty('native')) { + return null; + } + if (pmzoneid) imp.ext.pmZoneId = pmzoneid; + setImpTagId(imp, adSlot.trim(), hashedKey); + setImpFields(imp); + // check for battr data types + ['banner', 'video', 'native'].forEach(key => { + if (imp[key]?.battr && !Array.isArray(imp[key].battr)) { + delete imp[key].battr; + } }); - - try { - renderer.setRender(BB_RENDERER.outstreamRender); - } catch (err) { - logWarn(`${LOG_WARN_PREFIX}: Error tying to setRender on renderer`, err); - } - - return renderer; + return imp; }, - outstreamRender: function(bid) { - bid.renderer.push(function() { BB_RENDERER.bootstrapPlayer(bid) }); + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + if (blockedIabCategories.length || request.bcat) { + const validatedBCategories = validateBlockedCategories([...(blockedIabCategories || []), ...(request.bcat || [])]); + if (validatedBCategories.length) request.bcat = validatedBCategories; + } + if (allowedIabCategories.length || request.acat) { + const validatedACategories = validateAllowedCategories([...(allowedIabCategories || []), ...(request.acat || [])]); + if (validatedACategories.length) request.acat = validatedACategories; + } + reqLevelParams(request); + updateUserSiteDevice(request, context?.bidRequests); + addExtenstionParams(request); + const marketPlaceEnabled = bidderRequest?.bidderCode + ? bidderSettings.get(bidderRequest.bidderCode, 'allowAlternateBidderCodes') : undefined; + if (marketPlaceEnabled) updateRequestExt(request, bidderRequest); + return request; }, - getRendererId: function(pub, renderer) { - return `${pub}-${renderer}`; // NB convention! - } -}; - -const MEDIATYPE = [ - BANNER, - VIDEO, - NATIVE -] - -let publisherId = 0; -let isInvalidNativeRequest = false; -let biddersList = ['pubmatic']; -const allBiddersList = ['all']; - -export function _getDomainFromURL(url) { - let anchor = document.createElement('a'); - anchor.href = url; - return anchor.hostname; -} - -function _parseSlotParam(paramName, paramValue) { - if (!isStr(paramValue)) { - paramValue && logWarn(LOG_WARN_PREFIX + 'Ignoring param key: ' + paramName + ', expects string-value, found ' + typeof paramValue); - return UNDEFINED; - } - - switch (paramName) { - case 'pmzoneid': - return paramValue.split(',').slice(0, 50).map(id => id.trim()).join(); - case 'kadfloor': - return parseFloat(paramValue) || UNDEFINED; - case 'lat': - return parseFloat(paramValue) || UNDEFINED; - case 'lon': - return parseFloat(paramValue) || UNDEFINED; - case 'yob': - return parseInt(paramValue) || UNDEFINED; - default: - return paramValue; - } -} - -function _cleanSlot(slotName) { - if (isStr(slotName)) { - return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); - } - if (slotName) { - logWarn(BIDDER_CODE + ': adSlot must be a string. Ignoring adSlot'); - } - return ''; -} - -function _parseAdSlot(bid) { - bid.params.adUnit = ''; - bid.params.adUnitIndex = '0'; - bid.params.width = 0; - bid.params.height = 0; - bid.params.adSlot = _cleanSlot(bid.params.adSlot); - - var slot = bid.params.adSlot; - var splits = slot.split(':'); - - slot = splits[0]; - if (splits.length == 2) { - bid.params.adUnitIndex = splits[1]; - } - // check if size is mentioned in sizes array. in that case do not check for @ in adslot - splits = slot.split('@'); - bid.params.adUnit = splits[0]; - if (splits.length > 1) { - // i.e size is specified in adslot, so consider that and ignore sizes array - splits = splits[1].split('x'); - if (splits.length != 2) { - logWarn(LOG_WARN_PREFIX + 'AdSlot Error: adSlot not in required format'); - return; - } - bid.params.width = parseInt(splits[0], 10); - bid.params.height = parseInt(splits[1], 10); - } else if (bid.hasOwnProperty('mediaTypes') && - bid.mediaTypes.hasOwnProperty(BANNER) && - bid.mediaTypes.banner.hasOwnProperty('sizes')) { - var i = 0; - var sizeArray = []; - for (;i < bid.mediaTypes.banner.sizes.length; i++) { - if (bid.mediaTypes.banner.sizes[i].length === 2) { // sizes[i].length will not be 2 in case where size is set as fluid, we want to skip that entry - sizeArray.push(bid.mediaTypes.banner.sizes[i]); + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + if (bidResponse.meta) bidResponse.meta.mediaType = bidResponse.mediaType; + updateResponseWithCustomFields(bidResponse, bid, context); + const { mediaType, playerWidth, playerHeight } = bidResponse; + const { params, adUnitCode, mediaTypes } = context?.bidRequest; + if (mediaType === VIDEO) { + if (!bidResponse.width) bidResponse.width = playerWidth; + if (!bidResponse.height) bidResponse.height = playerHeight; + const { context, maxduration } = mediaTypes[mediaType]; + if (context === 'outstream' && params.outstreamAU && adUnitCode) { + bidResponse.rendererCode = params.outstreamAU; + bidResponse.renderer = BB_RENDERER.newRenderer(bidResponse.rendererCode, adUnitCode); + } + assignDealTier(bidResponse, context, maxduration); + } + if (mediaType === NATIVE && bid.adm) { + try { + const adm = JSON.parse(bid.adm.replace(/\\/g, '')); + bidResponse.native = { ortb: { ...adm.native } }; + } catch (ex) { + logWarn(`${LOG_WARN_PREFIX}Error: Cannot parse native response for ad response: ${bid.adm}`); + return; } + bidResponse.width = bid.w || DEFAULT_WIDTH; + bidResponse.height = bid.h || DEFAULT_HEIGHT; } - bid.mediaTypes.banner.sizes = sizeArray; - if (bid.mediaTypes.banner.sizes.length >= 1) { - // set the first size in sizes array in bid.params.width and bid.params.height. These will be sent as primary size. - // The rest of the sizes will be sent in format array. - bid.params.width = bid.mediaTypes.banner.sizes[0][0]; - bid.params.height = bid.mediaTypes.banner.sizes[0][1]; - bid.mediaTypes.banner.sizes = bid.mediaTypes.banner.sizes.splice(1, bid.mediaTypes.banner.sizes.length - 1); + return bidResponse; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + return buildResponse(bidResponses, ortbResponse, context); + }, + overrides: { + imp: { + bidfloor: false, + extBidfloor: false + }, + bidResponse: { + native: false } } -} +}); -function _initConf(refererInfo) { - return { - // TODO: do the fallbacks make sense here? - pageURL: refererInfo?.page || window.location.href, - refURL: refererInfo?.ref || window.document.referrer - }; -} +export function _calculateBidCpmAdjustment(bid) { + if (!bid) return; -function _handleCustomParams(params, conf) { - if (!conf.kadpageurl) { - conf.kadpageurl = conf.pageURL; - } + const { originalCurrency, currency, cpm, originalCpm, meta } = bid; + const convertedCpm = originalCurrency !== currency && isFn(bid.getCpmInNewCurrency) + ? bid.getCpmInNewCurrency(originalCurrency) + : cpm; - var key, value, entry; - for (key in CUSTOM_PARAMS) { - if (CUSTOM_PARAMS.hasOwnProperty(key)) { - value = params[key]; - if (value) { - entry = CUSTOM_PARAMS[key]; - - if (typeof entry === 'object') { - // will be used in future when we want to process a custom param before using - // 'keyname': {f: function() {}} - value = entry.f(value, conf); - } + const mediaType = bid.mediaType; + const metaMediaType = meta?.mediaType; - if (isStr(value)) { - conf[key] = value; - } else { - logWarn(LOG_WARN_PREFIX + 'Ignoring param : ' + key + ' with value : ' + CUSTOM_PARAMS[key] + ', expects string-value, found ' + typeof value); - } - } - } - } - return conf; -} + cpmAdjustment = cpmAdjustment || { + currency, + originalCurrency, + adjustment: [] + }; -export function getDeviceConnectionType() { - let connection = window.navigator && (window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection); - switch (connection?.effectiveType) { - case 'ethernet': - return 1; - case 'wifi': - return 2; - case 'slow-2g': - case '2g': - return 4; - case '3g': - return 5; - case '4g': - return 6; - default: - return 0; - } -} + const adjustmentValue = Number(((originalCpm - convertedCpm) / originalCpm).toFixed(2)); -function _createOrtbTemplate(conf) { - return { - id: '' + new Date().getTime(), - at: AUCTION_TYPE, - cur: [DEFAULT_CURRENCY], - imp: [], - site: { - page: conf.pageURL, - ref: conf.refURL, - publisher: {} - }, - 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: getDeviceConnectionType() - }, - user: {}, - ext: {} + const adjustmentEntry = { + cpmAdjustment: adjustmentValue, + mediaType, + metaMediaType, + cpm: convertedCpm, + originalCpm }; + + const existingIndex = cpmAdjustment?.adjustment?.findIndex( + (entry) => entry?.mediaType === mediaType && entry?.metaMediaType === metaMediaType + ); + + existingIndex !== -1 + ? cpmAdjustment.adjustment.splice(existingIndex, 1, adjustmentEntry) + : cpmAdjustment.adjustment.push(adjustmentEntry); } -function _checkParamDataType(key, value, datatype) { - var errMsg = 'Ignoring param key: ' + key + ', expects ' + datatype + ', found ' + typeof value; - var functionToExecute; - switch (datatype) { - case DATA_TYPES.BOOLEAN: - functionToExecute = isBoolean; - break; - case DATA_TYPES.NUMBER: - functionToExecute = isNumber; - break; - case DATA_TYPES.STRING: - functionToExecute = isStr; - break; - case DATA_TYPES.ARRAY: - functionToExecute = isArray; - break; +const handleImageProperties = asset => { + const imgProps = {}; + if (asset.aspect_ratios && isArray(asset.aspect_ratios) && asset.aspect_ratios.length) { + const { min_width: minWidth, min_height: minHeight } = asset.aspect_ratios[0]; + if (isInteger(minWidth) && isInteger(minHeight)) { + imgProps.wmin = minWidth; + imgProps.hmin = minHeight; + } + // eslint-disable-next-line camelcase + imgProps.ext = { aspectratios: asset.aspect_ratios.filter(({ ratio_width, ratio_height }) => ratio_width && ratio_height).map(({ ratio_width, ratio_height }) => `${ratio_width}:${ratio_height}`) }; } - if (functionToExecute(value)) { - return value; + imgProps.w = asset.w || asset.width; + imgProps.h = asset.h || asset.height; + if (asset.sizes && asset.sizes.length === 2 && isInteger(asset.sizes[0]) && isInteger(asset.sizes[1])) { + imgProps.w = asset.sizes[0]; + imgProps.h = asset.sizes[1]; + delete imgProps.wmin; + delete imgProps.hmin; } - logWarn(LOG_WARN_PREFIX + errMsg); - return UNDEFINED; + asset.ext && (imgProps.ext = asset.ext); + asset.mimes && (imgProps.mimes = asset.mimes); + return imgProps; } -// TODO delete this code when removing native 1.1 support -const PREBID_NATIVE_DATA_KEYS_TO_ORTB = { - 'desc': 'desc', - 'desc2': 'desc2', - 'body': 'desc', - 'body2': 'desc2', - 'sponsoredBy': 'sponsored', - 'cta': 'ctatext', - 'rating': 'rating', - 'address': 'address', - 'downloads': 'downloads', - 'likes': 'likes', - 'phone': 'phone', - 'price': 'price', - 'salePrice': 'saleprice', - 'displayUrl': 'displayurl', - 'saleprice': 'saleprice', - 'displayurl': 'displayurl' -}; - -const PREBID_NATIVE_DATA_KEY_VALUES = Object.values(PREBID_NATIVE_DATA_KEYS_TO_ORTB); - -// TODO remove this function when the support for 1.1 is removed -/** - * Copy of the function toOrtbNativeRequest from core native.js to handle the title len/length - * and ext and mimes parameters from legacy assets. - * @param {object} legacyNativeAssets - * @returns an OpenRTB format of the same bid request - */ -export function toOrtbNativeRequest(legacyNativeAssets) { - if (!legacyNativeAssets && !isPlainObject(legacyNativeAssets)) { - logWarn(`${LOG_WARN_PREFIX}: Native assets object is empty or not an object: ${legacyNativeAssets}`); - isInvalidNativeRequest = true; - return; - } - const ortb = { - ver: '1.2', - assets: [] - }; +const toOrtbNativeRequest = legacyNativeAssets => { + const ortb = { ver: '1.2', assets: [] }; for (let key in legacyNativeAssets) { - // skip conversion for non-asset keys if (NATIVE_KEYS_THAT_ARE_NOT_ASSETS.includes(key)) continue; if (!NATIVE_KEYS.hasOwnProperty(key) && !PREBID_NATIVE_DATA_KEY_VALUES.includes(key)) { - logWarn(`${LOG_WARN_PREFIX}: Unrecognized native asset code: ${key}. Asset will be ignored.`); + logWarn(`${LOG_WARN_PREFIX}: Unrecognized asset: ${key}. Ignored.`); continue; } const asset = legacyNativeAssets[key]; - let required = 0; - if (asset.required && isBoolean(asset.required)) { - required = Number(asset.required); - } - const ortbAsset = { - id: ortb.assets.length, - required - }; - // data cases + const required = asset.required && isBoolean(asset.required) ? 1 : 0; + const ortbAsset = { id: ortb.assets.length, required }; + if (key in PREBID_NATIVE_DATA_KEYS_TO_ORTB) { - ortbAsset.data = { - type: NATIVE_ASSET_TYPES[PREBID_NATIVE_DATA_KEYS_TO_ORTB[key]] - } - if (asset.len || asset.length) { - ortbAsset.data.len = asset.len || asset.length; - } - if (asset.ext) { - ortbAsset.data.ext = asset.ext; - } - // icon or image case + ortbAsset.data = { type: NATIVE_ASSET_TYPES[PREBID_NATIVE_DATA_KEYS_TO_ORTB[key]], ...asset.len && { len: asset.len }, ...asset.ext && { ext: asset.ext } }; } else if (key === 'icon' || key === 'image') { ortbAsset.img = { type: key === 'icon' ? NATIVE_IMAGE_TYPES.ICON : NATIVE_IMAGE_TYPES.MAIN, - } - // if min_width and min_height are defined in aspect_ratio, they are preferred - if (asset.aspect_ratios) { - if (!isArray(asset.aspect_ratios)) { - logWarn(`${LOG_WARN_PREFIX}: image.aspect_ratios was passed, but it's not a an array: ${asset.aspect_ratios}`); - } else if (!asset.aspect_ratios.length) { - logWarn(`${LOG_WARN_PREFIX}: image.aspect_ratios was passed, but it's empty: ${asset.aspect_ratios}`); - } else { - const { min_width: minWidth, min_height: minHeight } = asset.aspect_ratios[0]; - if (!isInteger(minWidth) || !isInteger(minHeight)) { - logWarn(`${LOG_WARN_PREFIX}: image.aspect_ratios min_width or min_height are invalid: ${minWidth}, ${minHeight}`); - } else { - ortbAsset.img.wmin = minWidth; - ortbAsset.img.hmin = minHeight; - } - const aspectRatios = asset.aspect_ratios - .filter((ar) => ar.ratio_width && ar.ratio_height) - .map(ratio => `${ratio.ratio_width}:${ratio.ratio_height}`); - if (aspectRatios.length > 0) { - ortbAsset.img.ext = { - aspectratios: aspectRatios - } - } - } - } - - ortbAsset.img.w = asset.w || asset.width; - ortbAsset.img.h = asset.h || asset.height; - ortbAsset.img.wmin = asset.wmin || asset.minimumWidth || (asset.minsizes ? asset.minsizes[0] : UNDEFINED); - ortbAsset.img.hmin = asset.hmin || asset.minimumHeight || (asset.minsizes ? asset.minsizes[1] : UNDEFINED); - - // if asset.sizes exist, by OpenRTB spec we should remove wmin and hmin - if (asset.sizes) { - if (asset.sizes.length !== 2 || !isInteger(asset.sizes[0]) || !isInteger(asset.sizes[1])) { - logWarn(`${LOG_WARN_PREFIX}: image.sizes was passed, but its value is not an array of integers: ${asset.sizes}`); - } else { - logInfo(`${LOG_WARN_PREFIX}: if asset.sizes exist, by OpenRTB spec we should remove wmin and hmin`); - ortbAsset.img.w = asset.sizes[0]; - ortbAsset.img.h = asset.sizes[1]; - delete ortbAsset.img.hmin; - delete ortbAsset.img.wmin; - } - } - asset.ext && (ortbAsset.img.ext = asset.ext); - asset.mimes && (ortbAsset.img.mimes = asset.mimes); - // title case + ...handleImageProperties(asset) + }; } else if (key === 'title') { - ortbAsset.title = { - // in openRTB, len is required for titles, while in legacy prebid was not. - // for this reason, if len is missing in legacy prebid, we're adding a default value of 140. - len: asset.len || asset.length || 140 - } - asset.ext && (ortbAsset.title.ext = asset.ext); - // all extensions to the native bid request are passed as is + ortbAsset.title = { len: asset.len || 140, ...asset.ext && { ext: asset.ext } }; } else if (key === 'ext') { ortbAsset.ext = asset; - // in `ext` case, required field is not needed delete ortbAsset.required; } ortb.assets.push(ortbAsset); } - - if (ortb.assets.length < 1) { - logWarn(`${LOG_WARN_PREFIX}: Could not find any valid asset`); - isInvalidNativeRequest = true; - return; - } - return ortb; } -// TODO delete this code when removing native 1.1 support - -function _createNativeRequest(params) { - var nativeRequestObject; - - // TODO delete this code when removing native 1.1 support - if (!params.ortb) { // legacy assets definition found - nativeRequestObject = toOrtbNativeRequest(params); - } else { // ortb assets definition found - params = params.ortb; - // TODO delete this code when removing native 1.1 support - nativeRequestObject = { ver: '1.2', ...params, assets: [] }; - const { assets } = params; - - const isValidAsset = (asset) => asset.title || asset.img || asset.data || asset.video; - - if (assets.length < 1 || !assets.some(asset => isValidAsset(asset))) { - logWarn(`${LOG_WARN_PREFIX}: Native assets object is empty or contains some invalid object`); - isInvalidNativeRequest = true; - return nativeRequestObject; - } - assets.forEach(asset => { - var assetObj = asset; - if (assetObj.img) { - if (assetObj.img.type == NATIVE_ASSET_IMAGE_TYPE.IMAGE) { - assetObj.w = assetObj.w || assetObj.width || (assetObj.sizes ? assetObj.sizes[0] : UNDEFINED); - assetObj.h = assetObj.h || assetObj.height || (assetObj.sizes ? assetObj.sizes[1] : UNDEFINED); - assetObj.wmin = assetObj.wmin || assetObj.minimumWidth || (assetObj.minsizes ? assetObj.minsizes[0] : UNDEFINED); - assetObj.hmin = assetObj.hmin || assetObj.minimumHeight || (assetObj.minsizes ? assetObj.minsizes[1] : UNDEFINED); - } else if (assetObj.img.type == NATIVE_ASSET_IMAGE_TYPE.ICON) { - assetObj.w = assetObj.w || assetObj.width || (assetObj.sizes ? assetObj.sizes[0] : UNDEFINED); - assetObj.h = assetObj.h || assetObj.height || (assetObj.sizes ? assetObj.sizes[1] : UNDEFINED); - } - } +const setImpFields = imp => { + imp.displaymanager ||= 'Prebid.js'; + imp.displaymanagerver ||= '$prebid.version$'; + const gptAdSlot = imp.ext?.data?.adserver?.adslot; + if (gptAdSlot) imp.ext.dfp_ad_unit_code = gptAdSlot; + // Delete ext.data in case of no-adserver + if (imp.ext?.data && Object.keys(imp.ext.data).length === 0) delete imp.ext.data +} - if (assetObj && assetObj.id !== undefined && isValidAsset(assetObj)) { - nativeRequestObject.assets.push(assetObj); - } +function removeGranularFloor(imp, mediaTypes) { + mediaTypes.forEach(mt => { + if (imp[mt]?.ext && imp[mt].ext.bidfloor === imp.bidfloor && imp[mt].ext.bidfloorcur === imp.bidfloorcur) { + delete imp[mt].ext; } - ); - } - return nativeRequestObject; + }) } -function _createBannerRequest(bid) { - var sizes = bid.mediaTypes.banner.sizes; - var format = []; - var bannerObj; - if (sizes !== UNDEFINED && isArray(sizes)) { - bannerObj = {}; - if (!bid.params.width && !bid.params.height) { - if (sizes.length === 0) { - // i.e. since bid.params does not have width or height, and length of sizes is 0, need to ignore this banner imp - bannerObj = UNDEFINED; - logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); - return bannerObj; - } else { - bannerObj.w = parseInt(sizes[0][0], 10); - bannerObj.h = parseInt(sizes[0][1], 10); - sizes = sizes.splice(1, sizes.length - 1); - } - } else { - bannerObj.w = bid.params.width; - bannerObj.h = bid.params.height; - } - if (sizes.length > 0) { - format = []; - sizes.forEach(function (size) { - if (size.length > 1) { - format.push({ w: size[0], h: size[1] }); +const setFloorInImp = (imp, bid) => { + let bidFloor = -1; + let requestedMediatypes = Object.keys(bid.mediaTypes); + let isMultiFormatRequest = requestedMediatypes.length > 1 + if (typeof bid.getFloor === 'function' && !config.getConfig('pubmatic.disableFloors')) { + [BANNER, VIDEO, NATIVE].forEach(mediaType => { + if (!imp.hasOwnProperty(mediaType)) return; + + const sizes = (mediaType === 'banner' + ? imp[mediaType]?.format?.map(({ w, h }) => [w, h]) + : ['*']) || ['*']; + + sizes.forEach(size => { + const floorInfo = bid.getFloor({ currency: imp.bidfloorcur, mediaType, size }); + logInfo(LOG_WARN_PREFIX, 'floor from floor module returned for mediatype:', mediaType, ' and size:', size, ' is: currency', floorInfo.currency, 'floor', floorInfo.floor); + + if (isPlainObject(floorInfo) && floorInfo?.currency === imp.bidfloorcur && !isNaN(parseInt(floorInfo.floor))) { + const mediaTypeFloor = parseFloat(floorInfo.floor); + if (isMultiFormatRequest && mediaType !== BANNER) { + logInfo(LOG_WARN_PREFIX, 'floor from floor module returned for mediatype:', mediaType, 'is : ', mediaTypeFloor, 'with currency :', imp.bidfloorcur); + imp[mediaType]['ext'] = {'bidfloor': mediaTypeFloor, 'bidfloorcur': imp.bidfloorcur}; + } + logInfo(LOG_WARN_PREFIX, 'floor from floor module:', mediaTypeFloor, 'previous floor value', bidFloor, 'Min:', Math.min(mediaTypeFloor, bidFloor)); + bidFloor = bidFloor === -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor); + logInfo(LOG_WARN_PREFIX, 'new floor value:', bidFloor); } }); - if (format.length > 0) { - bannerObj.format = format; + if (isMultiFormatRequest && mediaType === BANNER) { + imp[mediaType]['ext'] = {'bidfloor': bidFloor, 'bidfloorcur': imp.bidfloorcur}; } - } - bannerObj.pos = 0; - bannerObj.topframe = inIframe() ? 0 : 1; - } else { - logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); - bannerObj = UNDEFINED; + }); + } + // Determine the highest value between imp.bidfloor and the floor from the floor module. + // Since we're using Math.max, it's safe if no floor is returned from the floor module, as bidFloor defaults to -1. + if (imp.bidfloor) { + logInfo(LOG_WARN_PREFIX, 'Comparing floors:', 'from floor module:', bidFloor, 'impObj.bidfloor:', imp.bidfloor, 'Max:', Math.max(bidFloor, imp.bidfloor)); + bidFloor = Math.max(bidFloor, imp.bidfloor); } - return bannerObj; -} -export function checkVideoPlacement(videoData, adUnitCode) { - // Check for video.placement property. If property is missing display log message. - if (FEATURES.VIDEO && !deepAccess(videoData, 'placement')) { - logWarn(MSG_VIDEO_PLACEMENT_MISSING + ' for ' + adUnitCode); - }; + // Set imp.bidfloor only if bidFloor is greater than 0. + imp.bidfloor = (bidFloor > 0) ? bidFloor : UNDEFINED; + logInfo(LOG_WARN_PREFIX, 'Updated imp.bidfloor:', imp.bidfloor); + // remove granular floor if impression level floor is same as granular + if (isMultiFormatRequest) removeGranularFloor(imp, requestedMediatypes); } -function _createVideoRequest(bid) { - var videoData = mergeDeep(deepAccess(bid.mediaTypes, 'video'), bid.params.video); - var videoObj; - - if (FEATURES.VIDEO && videoData !== UNDEFINED) { - videoObj = {}; - checkVideoPlacement(videoData, bid.adUnitCode); - for (var key in VIDEO_CUSTOM_PARAMS) { - if (videoData.hasOwnProperty(key)) { - videoObj[key] = _checkParamDataType(key, videoData[key], VIDEO_CUSTOM_PARAMS[key]); - } - } - // read playersize and assign to h and w. - if (isArray(bid.mediaTypes.video.playerSize[0])) { - videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0][0], 10); - videoObj.h = parseInt(bid.mediaTypes.video.playerSize[0][1], 10); - } else if (isNumber(bid.mediaTypes.video.playerSize[0])) { - videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0], 10); - videoObj.h = parseInt(bid.mediaTypes.video.playerSize[1], 10); - } +const updateBannerImp = (bannerObj, adSlot) => { + let slot = adSlot.split(':'); + let splits = slot[0]?.split('@'); + splits = splits?.length == 2 ? splits[1].split('x') : splits.length == 3 ? splits[2].split('x') : []; + const primarySize = bannerObj.format[0]; + if (splits.length !== 2 || (parseInt(splits[0]) == 0 && parseInt(splits[1]) == 0)) { + bannerObj.w = primarySize.w; + bannerObj.h = primarySize.h; } else { - videoObj = UNDEFINED; - logWarn(LOG_WARN_PREFIX + 'Error: Video config params missing for adunit: ' + bid.params.adUnit + ' with mediaType set as video. Ignoring video impression in the adunit.'); + bannerObj.w = parseInt(splits[0]); + bannerObj.h = parseInt(splits[1]); } - return videoObj; + + bannerObj.format = bannerObj.format.filter( + (item) => !(item.w === bannerObj.w && item.h === bannerObj.h) + ); + bannerObj.pos ??= 0; } -// support for PMP deals -function _addPMPDealsInImpression(impObj, bid) { - if (bid.params.deals) { - if (isArray(bid.params.deals)) { - bid.params.deals.forEach(function(dealId) { - if (isStr(dealId) && dealId.length > 3) { - if (!impObj.pmp) { - impObj.pmp = { private_auction: 0, deals: [] }; - } - impObj.pmp.deals.push({ id: dealId }); - } else { - logWarn(LOG_WARN_PREFIX + 'Error: deal-id present in array bid.params.deals should be a strings with more than 3 charaters length, deal-id ignored: ' + dealId); - } - }); - } else { - logWarn(LOG_WARN_PREFIX + 'Error: bid.params.deals should be an array of strings.'); - } - } +const setImpTagId = (imp, adSlot, hashedKey) => { + const splits = adSlot.split(':')[0].split('@'); + imp.tagid = hashedKey || splits[0]; } -function _addDealCustomTargetings(imp, bid) { - var dctr = ''; - var dctrLen; - if (bid.params.dctr) { - dctr = bid.params.dctr; - if (isStr(dctr) && dctr.length > 0) { - var arr = dctr.split('|'); - dctr = ''; - arr.forEach(val => { - dctr += (val.length > 0) ? (val.trim() + '|') : ''; - }); - dctrLen = dctr.length; - if (dctr.substring(dctrLen, dctrLen - 1) === '|') { - dctr = dctr.substring(0, dctrLen - 1); - } - imp.ext['key_val'] = dctr.trim(); +const updateNativeImp = (imp, nativeParams) => { + if (!nativeParams?.ortb) { + imp.native.request = JSON.stringify(toOrtbNativeRequest(nativeParams)); + } + if (nativeParams?.ortb) { + let nativeConfig = JSON.parse(imp.native.request); + const { assets } = nativeConfig; + if (!assets?.some(asset => asset.title || asset.img || asset.data || asset.video)) { + logWarn(`${LOG_WARN_PREFIX}: Native assets object is empty or contains invalid objects`); + delete imp.native; } else { - logWarn(LOG_WARN_PREFIX + 'Ignoring param : dctr with value : ' + dctr + ', expects string-value, found empty or non-string value'); + imp.native.request = JSON.stringify({ ver: '1.2', ...nativeConfig }); } } } -function _addJWPlayerSegmentData(imp, bid) { - var jwSegData = (bid.rtd && bid.rtd.jwplayer && bid.rtd.jwplayer.targeting) || undefined; - var jwPlayerData = ''; - const jwMark = 'jw-'; - - if (jwSegData === undefined || jwSegData === '' || !jwSegData.hasOwnProperty('segments')) return; - - var maxLength = jwSegData.segments.length; - - jwPlayerData += jwMark + 'id=' + jwSegData.content.id; // add the content id first - - for (var i = 0; i < maxLength; i++) { - jwPlayerData += '|' + jwMark + jwSegData.segments[i] + '=1'; +const updateVideoImp = (videoImp, videoParams, adUnitCode, imp) => { + if (!deepAccess(videoParams, 'plcmt')) { + logWarn(MSG_VIDEO_PLCMT_MISSING + ' for ' + adUnitCode); + }; + if (!videoParams || (!videoImp.w && !videoImp.h)) { + delete imp.video; + logWarn(`${LOG_WARN_PREFIX}Error: Missing ${!videoParams ? 'video config params' : 'video size params (playersize or w&h)'} for adunit: ${adUnitCode} with mediaType set as video. Ignoring video impression in the adunit.`); } - - var ext; - - ext = imp.ext; - ext && ext.key_val === undefined ? ext.key_val = jwPlayerData : ext.key_val += '|' + jwPlayerData; } -function _createImpressionObject(bid, bidderRequest) { - var impObj = {}; - var bannerObj; - var videoObj; - var nativeObj = {}; - var sizes = bid.hasOwnProperty('sizes') ? bid.sizes : []; - var mediaTypes = ''; - var format = []; - var isFledgeEnabled = bidderRequest?.fledgeEnabled; - - impObj = { - id: bid.bidId, - tagid: bid.params.adUnit || undefined, - bidfloor: _parseSlotParam('kadfloor', bid.params.kadfloor), - secure: 1, - ext: { - pmZoneId: _parseSlotParam('pmzoneid', bid.params.pmzoneid) - }, - bidfloorcur: bid.params.currency ? _parseSlotParam('currency', bid.params.currency) : DEFAULT_CURRENCY - }; +const addJWPlayerSegmentData = (imp, jwplayer) => { + const jwSegData = jwplayer?.targeting; + if (!jwSegData || !jwSegData.segments?.length) return; + const jwMark = 'jw-'; + const contentId = `${jwMark}id=${jwSegData.content.id}`; + const segmentData = jwSegData.segments.map(segment => `${jwMark}${segment}=1`).join('|'); + const jwPlayerData = `${contentId}|${segmentData}`; + imp.ext = imp.ext || {}; + imp.ext.key_val = imp.ext.key_val ? `${imp.ext.key_val}|${jwPlayerData}` : jwPlayerData; +}; - _addPMPDealsInImpression(impObj, bid); - _addDealCustomTargetings(impObj, bid); - _addJWPlayerSegmentData(impObj, bid); - if (bid.hasOwnProperty('mediaTypes')) { - for (mediaTypes in bid.mediaTypes) { - switch (mediaTypes) { - case BANNER: - bannerObj = _createBannerRequest(bid); - if (bannerObj !== UNDEFINED) { - impObj.banner = bannerObj; - } - break; - case NATIVE: - // TODO uncomment below line when removing native 1.1 support - // nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeOrtbRequest)); - // TODO delete below line when removing native 1.1 support - nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeParams)); - if (!isInvalidNativeRequest) { - impObj.native = nativeObj; - } else { - logWarn(LOG_WARN_PREFIX + 'Error: Error in Native adunit ' + bid.params.adUnit + '. Ignoring the adunit. Refer to ' + PREBID_NATIVE_HELP_LINK + ' for more details.'); - isInvalidNativeRequest = false; - } - break; - case FEATURES.VIDEO && VIDEO: - videoObj = _createVideoRequest(bid); - if (videoObj !== UNDEFINED) { - impObj.video = videoObj; - } - break; - } - } +const addDealCustomTargetings = (imp, dctr) => { + if (isStr(dctr) && dctr.length > 0) { + const arr = dctr.split('|').filter(val => val.trim().length > 0); + dctr = arr.map(val => val.trim()).join('|'); + imp.ext['key_val'] = dctr; } else { - // mediaTypes is not present, so this is a banner only impression - // this part of code is required for older testcases with no 'mediaTypes' to run succesfully. - bannerObj = { - pos: 0, - w: bid.params.width, - h: bid.params.height, - topframe: inIframe() ? 0 : 1 - }; - if (isArray(sizes) && sizes.length > 1) { - sizes = sizes.splice(1, sizes.length - 1); - sizes.forEach(size => { - format.push({ - w: size[0], - h: size[1] - }); - }); - bannerObj.format = format; - } - impObj.banner = bannerObj; + logWarn(LOG_WARN_PREFIX + 'Ignoring param : dctr with value : ' + dctr + ', expects string-value, found empty or non-string value'); } - - _addImpressionFPD(impObj, bid); - - _addFloorFromFloorModule(impObj, bid); - - _addFledgeflag(impObj, bid, isFledgeEnabled) - - return impObj.hasOwnProperty(BANNER) || - impObj.hasOwnProperty(NATIVE) || - (FEATURES.VIDEO && impObj.hasOwnProperty(VIDEO)) ? impObj : UNDEFINED; } -function _addFledgeflag(impObj, bid, isFledgeEnabled) { - if (isFledgeEnabled) { - impObj.ext = impObj.ext || {}; - if (bid?.ortb2Imp?.ext?.ae !== undefined) { - impObj.ext.ae = bid.ortb2Imp.ext.ae; - } - } else { - if (impObj.ext?.ae) { - delete impObj.ext.ae; - } +const addPMPDeals = (imp, deals) => { + if (!isArray(deals)) { + logWarn(`${LOG_WARN_PREFIX}Error: bid.params.deals should be an array of strings.`); + return; } -} - -function _addImpressionFPD(imp, bid) { - const ortb2 = {...deepAccess(bid, 'ortb2Imp.ext.data')}; - Object.keys(ortb2).forEach(prop => { - /** - * Prebid AdSlot - * @type {(string|undefined)} - */ - if (prop === 'pbadslot') { - if (typeof ortb2[prop] === 'string' && ortb2[prop]) deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]); - } else if (prop === 'adserver') { - /** - * Copy GAM AdUnit and Name to imp - */ - ['name', 'adslot'].forEach(name => { - /** @type {(string|undefined)} */ - const value = deepAccess(ortb2, `adserver.${name}`); - if (typeof value === 'string' && value) { - deepSetValue(imp, `ext.data.adserver.${name.toLowerCase()}`, value); - // copy GAM ad unit id as imp[].ext.dfp_ad_unit_code - if (name === 'adslot') { - deepSetValue(imp, `ext.dfp_ad_unit_code`, value); - } - } - }); + deals.forEach(deal => { + if (typeof deal === 'string' && deal.length > 3) { + if (!imp.pmp) { + imp.pmp = { private_auction: 0, deals: [] }; + } + imp.pmp.deals.push({ id: deal }); } else { - deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); + logWarn(`${LOG_WARN_PREFIX}Error: deal-id present in array bid.params.deals should be a string with more than 3 characters length, deal-id ignored: ${deal}`); } }); - - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); - gpid && deepSetValue(imp, `ext.gpid`, gpid); } -function _addFloorFromFloorModule(impObj, bid) { - let bidFloor = -1; - // get lowest floor from floorModule - if (typeof bid.getFloor === 'function' && !config.getConfig('pubmatic.disableFloors')) { - [BANNER, VIDEO, NATIVE].forEach(mediaType => { - if (impObj.hasOwnProperty(mediaType)) { - let sizesArray = []; - - if (mediaType === 'banner') { - if (impObj[mediaType].w && impObj[mediaType].h) { - sizesArray.push([impObj[mediaType].w, impObj[mediaType].h]); - } - if (isArray(impObj[mediaType].format)) { - impObj[mediaType].format.forEach(size => sizesArray.push([size.w, size.h])); - } - } +const updateRequestExt = (req, bidderRequest) => { + const allBiddersList = ['all']; + let allowedBiddersList = bidderSettings.get(bidderRequest.bidderCode, 'allowedAlternateBidderCodes'); + const biddersList = isArray(allowedBiddersList) + ? allowedBiddersList.map(val => val.trim().toLowerCase()).filter(uniques) + : allBiddersList; + req.ext.marketplace = { + allowedbidders: (biddersList.includes('*') || biddersList.includes('all')) ? allBiddersList : [...new Set(['pubmatic', ...biddersList.filter(val => val && val.trim())])] + } +} - if (sizesArray.length === 0) { - sizesArray.push('*') - } +const reqLevelParams = (req) => { + deepSetValue(req, 'at', AUCTION_TYPE); + deepSetValue(req, 'cur', [DEFAULT_CURRENCY]); + req.test = window.location.href.includes('pubmaticTest=true') ? 1 : undefined; + if (req.source && !Object.keys(req.source).length) delete req.source; + if (req.app?.publisher) req.app.publisher.id = pubId; +}; - sizesArray.forEach(size => { - let floorInfo = bid.getFloor({ currency: impObj.bidfloorcur, mediaType: mediaType, size: size }); - logInfo(LOG_WARN_PREFIX, 'floor from floor module returned for mediatype:', mediaType, ' and size:', size, ' is: currency', floorInfo.currency, 'floor', floorInfo.floor); - if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidfloorcur && !isNaN(parseInt(floorInfo.floor))) { - let mediaTypeFloor = parseFloat(floorInfo.floor); - logInfo(LOG_WARN_PREFIX, 'floor from floor module:', mediaTypeFloor, 'previous floor value', bidFloor, 'Min:', Math.min(mediaTypeFloor, bidFloor)); - if (bidFloor === -1) { - bidFloor = mediaTypeFloor; - } else { - bidFloor = Math.min(mediaTypeFloor, bidFloor) - } - logInfo(LOG_WARN_PREFIX, 'new floor value:', bidFloor); - } - }); - } - }); - } - // get highest from impObj.bidfllor and floor from floor module - // as we are using Math.max, it is ok if we have not got any floor from floorModule, then value of bidFloor will be -1 - if (impObj.bidfloor) { - logInfo(LOG_WARN_PREFIX, 'floor from floor module:', bidFloor, 'impObj.bidfloor', impObj.bidfloor, 'Max:', Math.max(bidFloor, impObj.bidfloor)); - bidFloor = Math.max(bidFloor, impObj.bidfloor) +const updateUserSiteDevice = (req, bidRequest) => { + const { gender, yob, pubId, refURL, kadpageurl } = conf; + const { user } = req; + if (req.device) { + Object.assign(req.device, { js: 1, connectiontype: getConnectionType() }); } + req.user = { + ...req.user, + gender: user?.gender || gender?.trim() || UNDEFINED, + yob: user?.yob || _parseSlotParam('yob', yob) + }; - // assign value only if bidFloor is > 0 - impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : UNDEFINED); - logInfo(LOG_WARN_PREFIX, 'new impObj.bidfloor value:', impObj.bidfloor); -} + // start - IH eids for Prebid + const userIdAsEids = deepAccess(bidRequest, '0.userIdAsEids'); + if (bidRequest.length && userIdAsEids?.length && !req.user.ext?.eids) { + req.user.ext = req.user.ext || {}; + req.user.ext.eids = userIdAsEids; + } // end - IH eids for Prebid -function _handleEids(payload, validBidRequests) { - let bidUserIdAsEids = deepAccess(validBidRequests, '0.userIdAsEids'); - if (isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) { - deepSetValue(payload, 'user.eids', bidUserIdAsEids); + if (req.site?.publisher) { + req.site.ref = req.site.ref || refURL; + req.site.publisher.id = pubId?.trim(); } -} - -function _checkMediaType(bid, newBid) { - // Create a regex here to check the strings - if (bid.ext && bid.ext['bidtype'] != undefined) { - newBid.mediaType = MEDIATYPE[bid.ext.bidtype]; - } else { - logInfo(LOG_WARN_PREFIX + 'bid.ext.bidtype does not exist, checking alternatively for mediaType'); - var adm = bid.adm; - var admStr = ''; - var videoRegex = new RegExp(/VAST\s+version/); - if (adm.indexOf('span class="PubAPIAd"') >= 0) { - newBid.mediaType = BANNER; - } else if (FEATURES.VIDEO && videoRegex.test(adm)) { - newBid.mediaType = VIDEO; - } else { - try { - admStr = JSON.parse(adm.replace(/\\/g, '')); - if (admStr && admStr.native) { - newBid.mediaType = NATIVE; - } - } catch (e) { - logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + adm); - } - } + // if kadpageurl present then update site.page url with kadpageurl + if (req.site?.page && kadpageurl) req.site.page = kadpageurl.trim(); + // Check if geo information is present in device or user object + if (req.device.geo && !req.user.geo) { + req.user.geo = req.device.geo; + } else if (req.user.geo && !req.device.geo) { + req.device.geo = req.user.geo; } } -function _parseNativeResponse(bid, newBid) { - if (bid.hasOwnProperty('adm')) { - var adm = ''; - try { - adm = JSON.parse(bid.adm.replace(/\\/g, '')); - } catch (ex) { - logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + newBid.adm); - return; - } - newBid.native = { - ortb: { ...adm.native } - }; - newBid.mediaType = NATIVE; - if (!newBid.width) { - newBid.width = DEFAULT_WIDTH; - } - if (!newBid.height) { - newBid.height = DEFAULT_HEIGHT; - } +const updateResponseWithCustomFields = (res, bid, ctx) => { + const { ortbRequest, seatbid } = ctx; + res.referrer = ortbRequest.site?.ref || ''; + res.sspID = res.partnerImpId = bid.id || ''; + res.ad = bid.adm; + res.pm_dspid = bid.ext?.dspid ? bid.ext.dspid : null; + res.pm_seat = seatbid.seat; + if (!res.creativeId) res.creativeId = bid.id; + if (res.ttl == DEFAULT_TTL) res.ttl = MEDIATYPE_TTL[res.mediaType]; + if (bid.dealid) { + res.dealChannel = bid.ext?.deal_channel ? dealChannel[bid.ext.deal_channel] || null : 'PMP'; } -} - -function _blockedIabCategoriesValidation(payload, blockedIabCategories) { - blockedIabCategories = blockedIabCategories - .filter(function(category) { - if (typeof category === 'string') { // only strings - return true; - } else { - logWarn(LOG_WARN_PREFIX + 'bcat: Each category should be a string, ignoring category: ' + category); - return false; - } - }) - .map(category => category.trim()) // trim all - .filter(function(category, index, arr) { // more than 3 charaters length - if (category.length > 3) { - return arr.indexOf(category) === index; // unique value only - } else { - logWarn(LOG_WARN_PREFIX + 'bcat: Each category should have a value of a length of more than 3 characters, ignoring category: ' + category) - } - }); - if (blockedIabCategories.length > 0) { - logWarn(LOG_WARN_PREFIX + 'bcat: Selected: ', blockedIabCategories); - payload.bcat = blockedIabCategories; + if (seatbid.ext?.buyid) { + res.adserverTargeting = { 'hb_buyid_pubmatic': seatbid.ext.buyid } } -} - -function _allowedIabCategoriesValidation(payload, allowedIabCategories) { - allowedIabCategories = allowedIabCategories - .filter(function(category) { - if (typeof category === 'string') { // returns only strings - return true; - } else { - logWarn(LOG_WARN_PREFIX + 'acat: Each category should be a string, ignoring category: ' + category); - return false; - } - }) - .map(category => category.trim()) // trim all categories - .filter((category, index, arr) => arr.indexOf(category) === index); // return unique values only - - if (allowedIabCategories.length > 0) { - logWarn(LOG_WARN_PREFIX + 'acat: Selected: ', allowedIabCategories); - payload.ext.acat = allowedIabCategories; + if (bid.ext?.marketplace) { + res.bidderCode = bid.ext.marketplace; } -} -function _assignRenderer(newBid, request) { - let bidParams, context, adUnitCode; - if (request.bidderRequest && request.bidderRequest.bids) { - for (let bidderRequestBidsIndex = 0; bidderRequestBidsIndex < request.bidderRequest.bids.length; bidderRequestBidsIndex++) { - if (request.bidderRequest.bids[bidderRequestBidsIndex].bidId === newBid.requestId) { - bidParams = request.bidderRequest.bids[bidderRequestBidsIndex].params; - - if (FEATURES.VIDEO) { - context = request.bidderRequest.bids[bidderRequestBidsIndex].mediaTypes[VIDEO].context; - } - adUnitCode = request.bidderRequest.bids[bidderRequestBidsIndex].adUnitCode; - } - } - if (context && context === 'outstream' && bidParams && bidParams.outstreamAU && adUnitCode) { - newBid.rendererCode = bidParams.outstreamAU; - newBid.renderer = BB_RENDERER.newRenderer(newBid.rendererCode, adUnitCode); - } + // add meta fields + // NOTE: We will not recieve below fields from the translator response also not sure on what will be the key names for these in the response, + // when we needed we can add it back. + // New fields added, assignee fields name may change + // if (bid.ext.networkName) res.meta.networkName = bid.ext.networkName; + // if (bid.ext.advertiserName) res.meta.advertiserName = bid.ext.advertiserName; + // if (bid.ext.agencyName) res.meta.agencyName = bid.ext.agencyName; + // if (bid.ext.brandName) res.meta.brandName = bid.ext.brandName; + if (bid.ext) { + const { dspid, dchain, dsa, ibv } = bid.ext; + if (dspid) res.meta.networkId = res.meta.demandSource = dspid; + if (dchain) res.meta.dchain = dchain; + if (dsa && Object.keys(dsa).length) res.meta.dsa = dsa; + if (ibv) { + res.ext = res.ext || {}; + res.ext['ibv'] = ibv; + res.meta.mediaType = VIDEO; + } + } + + const advid = seatbid.seat || bid.ext?.advid; + if (advid) res.meta.advertiserId = res.meta.agencyId = res.meta.buyerId = advid; + + if (isNonEmptyArray(bid.adomain)) { + res.meta.clickUrl = res.meta.brandId = bid.adomain[0]; + } +} + +const addExtenstionParams = (req) => { + const { profId, verId, wiid, transactionId } = conf; + req.ext = { + epoch: new Date().getTime(), // Sending epoch timestamp in request.ext object + wrapper: { + profile: profId ? parseInt(profId) : undefined, + version: verId ? parseInt(verId) : undefined, + wiid: wiid, + wv: '$$REPO_AND_VERSION$$', + transactionId, + wp: 'pbjs' + }, + cpmAdjustment: cpmAdjustment } } /** * In case of adpod video context, assign prebiddealpriority to the dealtier property of adpod-video bid, * so that adpod module can set the hb_pb_cat_dur targetting key. - * @param {*} newBid * @param {*} bid - * @param {*} request + * @param {*} context + * @param {*} maxduration * @returns */ -export function assignDealTier(newBid, bid, request) { +const assignDealTier = (bid, context, maxduration) => { if (!bid?.ext?.prebiddealpriority || !FEATURES.VIDEO) return; - const bidRequest = getBidRequest(newBid.requestId, [request.bidderRequest]); - const videoObj = deepAccess(bidRequest, 'mediaTypes.video'); - if (videoObj?.context != ADPOD) return; + if (context != ADPOD) return; - const duration = bid?.ext?.video?.duration || videoObj?.maxduration; + const duration = bid?.ext?.video?.duration || maxduration; // if (!duration) return; - newBid.video = { + bid.video = { context: ADPOD, durationSeconds: duration, dealTier: bid.ext.prebiddealpriority }; } -function isNonEmptyArray(test) { - if (isArray(test) === true) { - if (test.length > 0) { - return true; - } - } - return false; +const validateAllowedCategories = (acat) => { + return [...new Set( + acat + .filter(item => { + if (typeof item === 'string') { + return true; + } else { + logWarn(LOG_WARN_PREFIX + 'acat: Each category should be a string, ignoring category: ' + item); + } + }) + .map(item => item.trim()) + )]; +}; + +const validateBlockedCategories = (bcats) => { + bcats = bcats.map(item => typeof item === 'string' ? item.trim() : item); + const droppedCategories = bcats.filter(item => typeof item !== 'string' || item.length < 3); + logWarn(LOG_WARN_PREFIX + 'bcat: Each category must be a string with a length greater than 3, ignoring ' + droppedCategories); + return [...new Set(bcats.filter(item => typeof item === 'string' && item.length >= 3))]; } -/** - * Prepare meta object to pass as params - * @param {*} br : bidResponse - * @param {*} bid : bids - */ -export function prepareMetaObject(br, bid, seat) { - br.meta = {}; +const getConnectionType = () => { + let connection = window.navigator && (window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection); + const types = { ethernet: 1, wifi: 2, 'slow-2g': 4, '2g': 4, '3g': 5, '4g': 6 }; + return types[connection?.effectiveType] || 0; +} - if (bid.ext && bid.ext.dspid) { - br.meta.networkId = bid.ext.dspid; - br.meta.demandSource = bid.ext.dspid; - } +// BB stands for Blue BillyWig +const BB_RENDERER = { + bootstrapPlayer: function(bid) { + const config = { + code: bid.adUnitCode, + vastXml: bid.vastXml || null, + vastUrl: bid.vastUrl || null, + }; - // NOTE: We will not recieve below fields from the translator response also not sure on what will be the key names for these in the response, - // when we needed we can add it back. - // New fields added, assignee fields name may change - // if (bid.ext.networkName) br.meta.networkName = bid.ext.networkName; - // if (bid.ext.advertiserName) br.meta.advertiserName = bid.ext.advertiserName; - // if (bid.ext.agencyName) br.meta.agencyName = bid.ext.agencyName; - // if (bid.ext.brandName) br.meta.brandName = bid.ext.brandName; - if (bid.ext && bid.ext.dchain) { - br.meta.dchain = bid.ext.dchain; - } + if (!config.vastXml && !config.vastUrl) { + logWarn(`${LOG_WARN_PREFIX}: No vastXml or vastUrl on bid, bailing...`); + return; + } - const advid = seat || (bid.ext && bid.ext.advid); - if (advid) { - br.meta.advertiserId = advid; - br.meta.agencyId = advid; - br.meta.buyerId = advid; - } + const rendererId = BB_RENDERER.getRendererId(PUBLICATION, bid.rendererCode); + const ele = document.getElementById(bid.adUnitCode); // NB convention - if (bid.adomain && isNonEmptyArray(bid.adomain)) { - br.meta.advertiserDomains = bid.adomain; - br.meta.clickUrl = bid.adomain[0]; - br.meta.brandId = bid.adomain[0]; + const renderer = window.bluebillywig.renderers.find(r => r._id === rendererId); + if (renderer) renderer.bootstrap(config, ele); + else logWarn(`${LOG_WARN_PREFIX}: Couldn't find a renderer with ${rendererId}`); + }, + + newRenderer: function(rendererCode, adUnitCode) { + const rendererUrl = RENDERER_URL.replace('$RENDERER', rendererCode); + const renderer = Renderer.install({ url: rendererUrl, loaded: false, adUnitCode }); + try { + renderer.setRender(BB_RENDERER.outstreamRender); + } catch (err) { + logWarn(`${LOG_WARN_PREFIX}: Error tying to setRender on renderer`, err); + } + return renderer; + }, + + outstreamRender: function(bid) { + bid.renderer.push(() => BB_RENDERER.bootstrapPlayer(bid)); + }, + + getRendererId: function(pub, renderer) { + return `${pub}-${renderer}`; // NB convention! } +}; - if (bid.cat && isNonEmptyArray(bid.cat)) { - br.meta.secondaryCatIds = bid.cat; - br.meta.primaryCatId = bid.cat[0]; +function _parseSlotParam(paramName, paramValue) { + if (!isStr(paramValue)) { + paramValue && logWarn(LOG_WARN_PREFIX + 'Ignoring param key: ' + paramName + ', expects string-value, found ' + typeof paramValue); + return UNDEFINED; } - if (bid.ext && bid.ext.dsa && Object.keys(bid.ext.dsa).length) { - br.meta.dsa = bid.ext.dsa; + const parsers = { + pmzoneid: () => paramValue.split(',').slice(0, 50).map(id => id.trim()).join(), + kadfloor: () => parseFloat(paramValue), + lat: () => parseFloat(paramValue), + lon: () => parseFloat(paramValue), + yob: () => parseInt(paramValue) + }; + return parsers[paramName]?.() || paramValue; +} + +function isNonEmptyArray(test) { + if (isArray(test) === true) { + if (test.length > 0) { + return true; + } } + return false; } +const getPublisherId = (bids) => + Array.isArray(bids) && bids.length > 0 + ? bids.find(bid => bid.params?.publisherId?.trim())?.params.publisherId || null + : null; + +const _handleCustomParams = (params, conf) => { + Object.keys(CUSTOM_PARAMS).forEach(key => { + const value = params[key]; + if (value) { + if (isStr(value)) { + conf[key] = value; + } else { + logWarn(`${LOG_WARN_PREFIX}Ignoring param: ${key} with value: ${CUSTOM_PARAMS[key]}, expects string value, found ${typeof value}`); + } + } + }); + return conf; +}; + export const spec = { code: BIDDER_CODE, gvlid: 76, @@ -1045,48 +651,38 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: bid => { - if (bid && bid.params) { - if (!isStr(bid.params.publisherId)) { - logWarn(LOG_WARN_PREFIX + 'Error: publisherId is mandatory and cannot be numeric (wrap it in quotes in your config). Call to OpenBid will not be sent for ad unit: ' + JSON.stringify(bid)); + if (!(bid && bid.params)) return false; + const { publisherId } = bid.params; + const mediaTypes = bid.mediaTypes || {}; + const videoMediaTypes = mediaTypes[VIDEO] || {}; + if (!isStr(publisherId)) { + logWarn(LOG_WARN_PREFIX + 'Error: publisherId is mandatory and cannot be numeric (wrap it in quotes in your config). Call to OpenBid will not be sent for ad unit: ' + JSON.stringify(bid)); + return false; + } + if (FEATURES.VIDEO && mediaTypes.hasOwnProperty(VIDEO)) { + // bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array + const mediaTypesVideoMimes = deepAccess(bid, 'mediaTypes.video.mimes'); + const paramsVideoMimes = deepAccess(bid, 'params.video.mimes'); + if (!isNonEmptyArray(mediaTypesVideoMimes) && !isNonEmptyArray(paramsVideoMimes)) { + logWarn(LOG_WARN_PREFIX + 'Error: For video ads, bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array. Call to OpenBid will not be sent for ad unit:' + JSON.stringify(bid)); return false; } - // video ad validation - if (FEATURES.VIDEO && bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { - // bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array - let mediaTypesVideoMimes = deepAccess(bid.mediaTypes, 'video.mimes'); - let paramsVideoMimes = deepAccess(bid, 'params.video.mimes'); - if (isNonEmptyArray(mediaTypesVideoMimes) === false && isNonEmptyArray(paramsVideoMimes) === false) { - logWarn(LOG_WARN_PREFIX + 'Error: For video ads, bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array. Call to OpenBid will not be sent for ad unit:' + JSON.stringify(bid)); - return false; - } - - if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { - logError(`${LOG_WARN_PREFIX}: no context specified in bid. Rejecting bid: `, bid); - return false; - } - - if (bid.mediaTypes[VIDEO].context === 'outstream' && - !isStr(bid.params.outstreamAU) && - !bid.hasOwnProperty('renderer') && - !bid.mediaTypes[VIDEO].hasOwnProperty('renderer')) { - // we are here since outstream ad-unit is provided without outstreamAU and renderer - // so it is not a valid video ad-unit - // but it may be valid banner or native ad-unit - // so if mediaType banner or Native is present then we will remove media-type video and return true - - if (bid.mediaTypes.hasOwnProperty(BANNER) || bid.mediaTypes.hasOwnProperty(NATIVE)) { - delete bid.mediaTypes[VIDEO]; - logWarn(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting mediatype Video of bid: `, bid); - return true; - } else { - logError(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting bid: `, bid); - return false; - } + if (!videoMediaTypes.context) { + logError(`${LOG_WARN_PREFIX}: No context specified in bid. Rejecting bid: `, bid); + return false; + } + if (videoMediaTypes.context === 'outstream' && !isStr(bid.params.outstreamAU) && +!bid.renderer && !videoMediaTypes.renderer) { + if (mediaTypes.hasOwnProperty(BANNER) || mediaTypes.hasOwnProperty(NATIVE)) { + delete mediaTypes[VIDEO]; + logWarn(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting mediatype Video of bid: `, bid); + return true; } + logError(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting bid: `, bid); + return false; } - return true; } - return false; + return true; }, /** @@ -1096,234 +692,43 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - // validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - var refererInfo; - if (bidderRequest && bidderRequest.refererInfo) { - refererInfo = bidderRequest.refererInfo; + const { page, ref } = bidderRequest?.refererInfo || {}; + const { publisherId, profId, verId } = bidderRequest?.bids?.[0]?.params || {}; + pubId = publisherId?.trim() || getPublisherId(bidderRequest?.bids)?.trim(); + const wiid = generateUUID(); + let bid; + blockedIabCategories = []; + allowedIabCategories = []; + conf = { + pageURL: page || window.location.href, + refURL: ref || window.document.referrer, + pubId, + kadpageurl: page || window.location.href, + profId: profId, + verId: verId } - var conf = _initConf(refererInfo); - var payload = _createOrtbTemplate(conf); - var bidCurrency = ''; - var dctrArr = []; - var bid; - var blockedIabCategories = []; - var allowedIabCategories = []; - var wiid = generateUUID(); - validBidRequests.forEach(originalBid => { originalBid.params.wiid = originalBid.params.wiid || bidderRequest.auctionId || wiid; bid = deepClone(originalBid); - bid.params.adSlot = bid.params.adSlot || ''; - _parseAdSlot(bid); - if ((bid.mediaTypes && bid.mediaTypes.hasOwnProperty('video')) || bid.params.hasOwnProperty('video')) { - // Nothing to do - } else { - // If we have a native mediaType configured alongside banner, its ok if the banner size is not set in width and height - // The corresponding banner imp object will not be generated, but we still want the native object to be sent, hence the following check - if (!(bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(NATIVE)) && bid.params.width === 0 && bid.params.height === 0) { - logWarn(LOG_WARN_PREFIX + 'Skipping the non-standard adslot: ', bid.params.adSlot, JSON.stringify(bid)); - return; - } - } - conf.pubId = conf.pubId || bid.params.publisherId; - conf = _handleCustomParams(bid.params, conf); + _handleCustomParams(bid.params, conf); conf.transactionId = bid.ortb2Imp?.ext?.tid; - if (bidCurrency === '') { - bidCurrency = bid.params.currency || UNDEFINED; - } else if (bid.params.hasOwnProperty('currency') && bidCurrency !== bid.params.currency) { - logWarn(LOG_WARN_PREFIX + 'Currency specifier ignored. Only one currency permitted.'); - } - bid.params.currency = bidCurrency; - // check if dctr is added to more than 1 adunit - if (bid.params.hasOwnProperty('dctr') && isStr(bid.params.dctr)) { - dctrArr.push(bid.params.dctr); - } - if (bid.params.hasOwnProperty('bcat') && isArray(bid.params.bcat)) { - blockedIabCategories = blockedIabCategories.concat(bid.params.bcat); - } - if (bid.params.hasOwnProperty('acat') && isArray(bid.params.acat)) { - allowedIabCategories = allowedIabCategories.concat(bid.params.acat); - } - var impObj = _createImpressionObject(bid, bidderRequest); - if (impObj) { - payload.imp.push(impObj); - } - }); - - if (payload.imp.length == 0) { - return; - } - - payload.site.publisher.id = conf.pubId.trim(); - publisherId = conf.pubId.trim(); - payload.ext.wrapper = {}; - payload.ext.wrapper.profile = parseInt(conf.profId) || UNDEFINED; - payload.ext.wrapper.version = parseInt(conf.verId) || UNDEFINED; - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - payload.ext.wrapper.wiid = conf.wiid || bidderRequest.auctionId; - // eslint-disable-next-line no-undef - payload.ext.wrapper.wv = $$REPO_AND_VERSION$$; - payload.ext.wrapper.transactionId = conf.transactionId; - payload.ext.wrapper.wp = 'pbjs'; - const allowAlternateBidder = bidderRequest ? bidderSettings.get(bidderRequest.bidderCode, 'allowAlternateBidderCodes') : undefined; - if (allowAlternateBidder !== undefined) { - payload.ext.marketplace = {}; - if (bidderRequest && allowAlternateBidder == true) { - let allowedBiddersList = bidderSettings.get(bidderRequest.bidderCode, 'allowedAlternateBidderCodes'); - if (isArray(allowedBiddersList)) { - allowedBiddersList = allowedBiddersList.map(val => val.trim().toLowerCase()).filter(val => !!val).filter(uniques); - biddersList = allowedBiddersList.includes('*') ? allBiddersList : [...biddersList, ...allowedBiddersList]; - } else { - biddersList = allBiddersList; - } - } - payload.ext.marketplace.allowedbidders = biddersList.filter(uniques); - } - - payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); - payload.user.geo = {}; - // TODO: fix lat and long to only come from request object, not params - payload.user.yob = _parseSlotParam('yob', conf.yob); - payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); - payload.site.domain = _getDomainFromURL(payload.site.page); - - // add the content object from config in request - if (typeof config.getConfig('content') === 'object') { - payload.site.content = config.getConfig('content'); - } - - // merge the device from config.getConfig('device') - if (typeof config.getConfig('device') === 'object') { - payload.device = Object.assign(payload.device, config.getConfig('device')); - } - - // update device.language to ISO-639-1-alpha-2 (2 character language) - payload.device.language = payload.device.language && payload.device.language.split('-')[0]; - - // passing transactionId in source.tid - deepSetValue(payload, 'source.tid', bidderRequest?.ortb2?.source?.tid); - - // test bids - if (window.location.href.indexOf('pubmaticTest=true') !== -1) { - payload.test = 1; - } - - // adding schain object - if (validBidRequests[0].schain) { - deepSetValue(payload, 'source.ext.schain', validBidRequests[0].schain); - } - - // Attaching GDPR Consent Params - if (bidderRequest && bidderRequest.gdprConsent) { - deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); - } - - // CCPA - if (bidderRequest && bidderRequest.uspConsent) { - deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - // Attaching GPP Consent Params - if (bidderRequest?.gppConsent?.gppString) { - deepSetValue(payload, 'regs.gpp', bidderRequest.gppConsent.gppString); - deepSetValue(payload, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); - } else if (bidderRequest?.ortb2?.regs?.gpp) { - deepSetValue(payload, 'regs.gpp', bidderRequest.ortb2.regs.gpp); - deepSetValue(payload, 'regs.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); - } - - // coppa compliance - if (config.getConfig('coppa') === true) { - deepSetValue(payload, 'regs.coppa', 1); - } - - // dsa - if (bidderRequest?.ortb2?.regs?.ext?.dsa) { - deepSetValue(payload, 'regs.ext.dsa', bidderRequest.ortb2.regs.ext.dsa); - } - - _handleEids(payload, validBidRequests); - - // First Party Data - const commonFpd = (bidderRequest && bidderRequest.ortb2) || {}; - const { user, device, site, bcat, badv } = commonFpd; - if (site) { - const { page, domain, ref } = payload.site; - mergeDeep(payload, {site: site}); - payload.site.page = page; - payload.site.domain = domain; - payload.site.ref = ref; - } - if (user) { - mergeDeep(payload, {user: user}); - } - if (badv) { - mergeDeep(payload, {badv: badv}); - } - if (bcat) { - blockedIabCategories = blockedIabCategories.concat(bcat); - } - // check if fpd ortb2 contains device property with sua object - if (device?.sua) { - payload.device.sua = device?.sua; - } - - if (device?.ext?.cdep) { - deepSetValue(payload, 'device.ext.cdep', device.ext.cdep); - } - - if (user?.geo && device?.geo) { - payload.device.geo = { ...payload.device.geo, ...device.geo }; - payload.user.geo = { ...payload.user.geo, ...user.geo }; - } else { - if (user?.geo || device?.geo) { - payload.user.geo = payload.device.geo = (user?.geo ? { ...payload.user.geo, ...user.geo } : { ...payload.user.geo, ...device.geo }); + const { bcat, acat } = bid.params; + if (bcat) { + blockedIabCategories = blockedIabCategories.concat(bcat); } - } - - if (commonFpd.ext?.prebid?.bidderparams?.[bidderRequest.bidderCode]?.acat) { - const acatParams = commonFpd.ext.prebid.bidderparams[bidderRequest.bidderCode].acat; - _allowedIabCategoriesValidation(payload, acatParams); - } else if (allowedIabCategories.length) { - _allowedIabCategoriesValidation(payload, allowedIabCategories); - } - _blockedIabCategoriesValidation(payload, blockedIabCategories); - - // Check if bidderRequest has timeout property if present send timeout as tmax value to translator request - // bidderRequest has timeout property if publisher sets during calling requestBids function from page - // if not bidderRequest contains global value set by Prebid - if (bidderRequest?.timeout) { - payload.tmax = bidderRequest.timeout; - } else { - payload.tmax = window?.PWT?.versionDetails?.timeout; - } - - // Sending epoch timestamp in request.ext object - payload.ext.epoch = new Date().getTime(); - - // Note: Do not move this block up - // if site object is set in Prebid config then we need to copy required fields from site into app and unset the site object - if (typeof config.getConfig('app') === 'object') { - payload.app = config.getConfig('app'); - // not copying domain from site as it is a derived value from page - payload.app.publisher = payload.site.publisher; - payload.app.ext = payload.site.ext || UNDEFINED; - // We will also need to pass content object in app.content if app object is also set into the config; - // BUT do not use content object from config if content object is present in app as app.content - if (typeof payload.app.content !== 'object') { - payload.app.content = payload.site.content || UNDEFINED; + if (acat) { + allowedIabCategories = allowedIabCategories.concat(acat); } - delete payload.site; - } + }) + const data = converter.toORTB({ validBidRequests, bidderRequest }); - return { + let serverRequest = { method: 'POST', url: ENDPOINT, - data: JSON.stringify(payload), + data: data, bidderRequest: bidderRequest }; + return data?.imp?.length ? serverRequest : null; }, /** @@ -1333,121 +738,39 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: (response, request) => { - const bidResponses = []; - var respCur = DEFAULT_CURRENCY; - let parsedRequest = JSON.parse(request.data); - let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : ''; - try { - if (response.body && response.body.seatbid && isArray(response.body.seatbid)) { - // Supporting multiple bid responses for same adSize - respCur = response.body.cur || respCur; - response.body.seatbid.forEach(seatbidder => { - seatbidder.bid && - isArray(seatbidder.bid) && - seatbidder.bid.forEach(bid => { - let newBid = { - requestId: bid.impid, - cpm: parseFloat((bid.price || 0).toFixed(2)), - width: bid.w, - height: bid.h, - creativeId: bid.crid || bid.id, - dealId: bid.dealid, - currency: respCur, - netRevenue: NET_REVENUE, - ttl: 300, - referrer: parsedReferrer, - ad: bid.adm, - pm_seat: seatbidder.seat || null, - pm_dspid: bid.ext && bid.ext.dspid ? bid.ext.dspid : null, - partnerImpId: bid.id || '' // partner impression Id - }; - if (parsedRequest.imp && parsedRequest.imp.length > 0) { - parsedRequest.imp.forEach(req => { - if (bid.impid === req.id) { - _checkMediaType(bid, newBid); - switch (newBid.mediaType) { - case BANNER: - break; - case FEATURES.VIDEO && VIDEO: - newBid.width = bid.hasOwnProperty('w') ? bid.w : req.video.w; - newBid.height = bid.hasOwnProperty('h') ? bid.h : req.video.h; - newBid.vastXml = bid.adm; - _assignRenderer(newBid, request); - assignDealTier(newBid, bid, request); - break; - case NATIVE: - _parseNativeResponse(bid, newBid); - break; - } - } - }); - } - if (bid.ext && bid.ext.deal_channel) { - newBid['dealChannel'] = dealChannelValues[bid.ext.deal_channel] || null; - } - - prepareMetaObject(newBid, bid, seatbidder.seat); - - // adserverTargeting - if (seatbidder.ext && seatbidder.ext.buyid) { - newBid.adserverTargeting = { - 'hb_buyid_pubmatic': seatbidder.ext.buyid - }; - } - - // if from the server-response the bid.ext.marketplace is set then - // submit the bid to Prebid as marketplace name - if (bid.ext && !!bid.ext.marketplace) { - newBid.bidderCode = bid.ext.marketplace; - } - - bidResponses.push(newBid); - }); - }); - } - let fledgeAuctionConfigs = deepAccess(response.body, 'ext.fledge_auction_configs'); - if (fledgeAuctionConfigs) { - fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { - return { - bidId, - config: Object.assign({ - auctionSignals: {}, - }, cfg) - } - }); - return { - bids: bidResponses, - fledgeAuctionConfigs, - } - } - } catch (error) { - logError(error); - } - - return bidResponses; + const { bids } = converter.fromORTB({ response: response.body, request: request.data }); + const fledgeAuctionConfigs = deepAccess(response.body, 'ext.fledge_auction_configs'); + if (fledgeAuctionConfigs) { + return { + bids, + paapi: Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => ({ + bidId, + config: { auctionSignals: {}, ...cfg } + })) + }; + } + return bids; }, /** * Register User Sync. */ getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { - let syncurl = '' + publisherId; + let syncurl = pubId; // Attaching GDPR Consent Params in UserSync url if (gdprConsent) { - syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + syncurl += `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(gdprConsent.consentString || '')}`; } // CCPA if (uspConsent) { - syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); + syncurl += `&us_privacy=${encodeURIComponent(uspConsent)}`; } // GPP Consent if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { - syncurl += '&gpp=' + encodeURIComponent(gppConsent.gppString); - syncurl += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + syncurl += `&gpp=${encodeURIComponent(gppConsent.gppString)}&gpp_sid=${encodeURIComponent(gppConsent.applicableSections.join(','))}`; } // coppa compliance @@ -1455,17 +778,13 @@ export const spec = { syncurl += '&coppa=1'; } - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: USER_SYNC_URL_IFRAME + syncurl - }]; - } else { - return [{ - type: 'image', - url: USER_SYNC_URL_IMAGE + syncurl - }]; - } + const type = syncOptions.iframeEnabled ? 'iframe' : 'image'; + const url = (type === 'iframe' ? USER_SYNC_URL_IFRAME : USER_SYNC_URL_IMAGE) + syncurl; + return [{ type, url }]; + }, + + onBidWon: (bid) => { + _calculateBidCpmAdjustment(bid); } }; diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md index baf58177505..6fe84d81350 100644 --- a/modules/pubmaticBidAdapter.md +++ b/modules/pubmaticBidAdapter.md @@ -70,6 +70,7 @@ var adVideoAdUnits = [ protocols: [ 2, 3 ], // optional battr: [ 13, 14 ], // optional linearity: 1, // optional + plcmt: 1, // optional placement: 2, // optional minbitrate: 10, // optional maxbitrate: 10 // optional @@ -169,6 +170,7 @@ var adUnits = [ protocols: [ 2, 3 ], // optional battr: [ 13, 14 ], // optional linearity: 1, // optional + plcmt: 1, // optional placement: 2, // optional minbitrate: 10, // optional maxbitrate: 10 // optional @@ -187,7 +189,12 @@ PubMatic recommends the UserSync configuration below. Without it, the PubMatic pbjs.setConfig({ userSync: { iframeEnabled: true, - enabledBidders: ['pubmatic'], + filterSettings: { + iframe: { + bidders: '*', // '*' represents all bidders + filter: 'include' + } + }, syncDelay: 6000 }}); diff --git a/modules/pubmaticIdSystem.js b/modules/pubmaticIdSystem.js new file mode 100644 index 00000000000..801277f122b --- /dev/null +++ b/modules/pubmaticIdSystem.js @@ -0,0 +1,156 @@ +import { logInfo, logError, isNumber, isStr, isEmptyStr } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { uspDataHandler, coppaDataHandler, gppDataHandler } from '../src/adapterManager.js'; + +const MODULE_NAME = 'pubmaticId'; +const GVLID = 76; +export const STORAGE_NAME = 'pubmaticId'; +const STORAGE_EXPIRES = 30; // days +const STORAGE_REFRESH_IN_SECONDS = 24 * 3600; // 24 Hours +const LOG_PREFIX = 'PubMatic User ID: '; +const VERSION = '1'; +const API_URL = 'https://image6.pubmatic.com/AdServer/UCookieSetPug?oid=5&p='; + +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); + +function generateQueryStringParams(config, consentData) { + const uspString = uspDataHandler.getConsentData(); + const coppaValue = coppaDataHandler.getCoppa(); + const gppConsent = gppDataHandler.getConsentData(); + + const params = { + publisherId: config.params.publisherId, + gdpr: (consentData && consentData?.gdprApplies) ? 1 : 0, + gdpr_consent: consentData && consentData?.consentString ? encodeURIComponent(consentData.consentString) : '', + src: 'pbjs_uid', + ver: VERSION, + coppa: Number(coppaValue), + us_privacy: uspString ? encodeURIComponent(uspString) : '', + gpp: gppConsent?.gppString ? encodeURIComponent(gppConsent.gppString) : '', + gpp_sid: gppConsent?.applicableSections?.length ? encodeURIComponent(gppConsent.applicableSections.join(',')) : '' + }; + + return params; +} + +function buildUrl(config, consentData) { + let baseUrl = `${API_URL}${config.params.publisherId}`; + const params = generateQueryStringParams(config, consentData); + + Object.keys(params).forEach((key) => { + baseUrl += `&${key}=${params[key]}`; + }); + + return baseUrl; +} + +function deleteFromAllStorages(key) { + const cKeys = [key, `${key}_cst`, `${key}_last`, `${key}_exp`]; + cKeys.forEach((cKey) => { + if (storage.getCookie(cKey)) { + storage.setCookie(cKey, '', new Date(0).toUTCString()); + } + }); + + const lsKeys = [key, `${key}_cst`, `${key}_last`, `${key}_exp`]; + lsKeys.forEach((lsKey) => { + if (storage.getDataFromLocalStorage(lsKey)) { + storage.removeDataFromLocalStorage(lsKey); + } + }); +} + +function getSuccessAndErrorHandler(callback) { + return { + success: (response) => { + let responseObj; + + try { + responseObj = JSON.parse(response); + logInfo(LOG_PREFIX + 'response received from the server', responseObj); + } catch (error) {} + + if (responseObj && isStr(responseObj.id) && !isEmptyStr(responseObj.id)) { + callback(responseObj); + } else { + deleteFromAllStorages(STORAGE_NAME); + callback(); + } + }, + error: (error) => { + deleteFromAllStorages(STORAGE_NAME); + logError(LOG_PREFIX + 'getId fetch encountered an error', error); + callback(); + } + }; +} + +function hasRequiredConfig(config) { + if (!config || !config.storage || !config.params) { + logError(LOG_PREFIX + 'config.storage and config.params should be passed.'); + return false; + } + + if (!isNumber(config.params.publisherId)) { + logError(LOG_PREFIX + 'config.params.publisherId (int) should be provided.'); + return false; + } + + if (config.storage.name !== STORAGE_NAME) { + logError(LOG_PREFIX + `config.storage.name should be '${STORAGE_NAME}'.`); + return false; + } + + if (config.storage.expires !== STORAGE_EXPIRES) { + logError(LOG_PREFIX + `config.storage.expires should be ${STORAGE_EXPIRES}.`); + return false; + } + + if (config.storage.refreshInSeconds !== STORAGE_REFRESH_IN_SECONDS) { + logError(LOG_PREFIX + `config.storage.refreshInSeconds should be ${STORAGE_REFRESH_IN_SECONDS}.`); + return false; + } + + return true; +} + +export const pubmaticIdSubmodule = { + name: MODULE_NAME, + gvlid: GVLID, + decode(value) { + if (isStr(value.id) && !isEmptyStr(value.id)) { + return { pubmaticId: value.id }; + } + return undefined; + }, + getId(config, consentData) { + if (!hasRequiredConfig(config)) { + return undefined; + } + + const resp = (callback) => { + logInfo(LOG_PREFIX + 'requesting an ID from the server'); + const url = buildUrl(config, consentData); + ajax(url, getSuccessAndErrorHandler(callback), null, { + method: 'GET', + withCredentials: true, + }); + }; + + return { callback: resp }; + }, + eids: { + 'pubmaticId': { + source: 'esp.pubmatic.com', + atype: 1, + getValue: (data) => { + return data; + } + }, + } +}; + +submodule('userId', pubmaticIdSubmodule); diff --git a/modules/pubmaticIdSystem.md b/modules/pubmaticIdSystem.md new file mode 100644 index 00000000000..ecd523dcf40 --- /dev/null +++ b/modules/pubmaticIdSystem.md @@ -0,0 +1,51 @@ +# PubMatic ID + +### Prebid Configuration + +You can configure this submodule in your `userSync.userIds[]` configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "pubmaticId", + params: { + publisherId: 123456 + }, + storage: { + name: "pubmaticId", + type: "cookie&html5", + expires: 30, + refreshInSeconds: 86400 + }, + }, + ], + }, +}); +``` + +| Parameters under `userSync.userIds[]` | Scope | Type | Description | Example | +| ---| --- | --- | --- | --- | +| name | Required | String | Name for the PubMatic ID submodule | `"pubmaticId"` | | +| storage | Required | Object | Configures how to cache User IDs locally in the browser | See [storage settings](#storage-settings) | +| params | Required | Object | Parameters for the PubMatic ID submodule | See [params](#params) | + +### Storage Settings + +The following settings are available for the `storage` property in the `userSync.userIds[]` object: + +| Param name | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String| Name of the cookie or HTML5 local storage where the user ID will be stored | `"pubmaticId"` | +| type | Required | String | `"cookie&html5"` (preferred) or `"cookie"` or `"html5"` | `"cookie&html5"` | +| expires | Required (Must be `30`) | Number | How long (in days) the user ID information will be stored | `30` | +| refreshInSeconds | Required (Must be `86400`) | Number | The interval (in seconds) for refreshing the user ID | `86400` | + +### Params + +The following settings are available in the `params` property in `userSync.userIds[]` object: + +| Param name | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| publisherId | Required | Number | Publisher ID provided by PubMatic | `123456` | diff --git a/modules/pubmaticRtdProvider.js b/modules/pubmaticRtdProvider.js new file mode 100644 index 00000000000..80447e94274 --- /dev/null +++ b/modules/pubmaticRtdProvider.js @@ -0,0 +1,284 @@ +import { submodule } from '../src/hook.js'; +import { logError, isStr, isPlainObject, isEmpty, isFn, mergeDeep } from '../src/utils.js'; +import { config as conf } from '../src/config.js'; +import { getDeviceType as fetchDeviceType, getOS } from '../libraries/userAgentUtils/index.js'; +import { getLowEntropySUA } from '../src/fpd/sua.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +/** + * This RTD module has a dependency on the priceFloors module. + * We utilize the continueAuction function from the priceFloors module to incorporate price floors data into the current auction. + */ +import { continueAuction } from './priceFloors.js'; // eslint-disable-line prebid/validate-imports + +const CONSTANTS = Object.freeze({ + SUBMODULE_NAME: 'pubmatic', + REAL_TIME_MODULE: 'realTimeData', + LOG_PRE_FIX: 'PubMatic-Rtd-Provider: ', + UTM: 'utm_', + UTM_VALUES: { + TRUE: '1', + FALSE: '0' + }, + TIME_OF_DAY_VALUES: { + MORNING: 'morning', + AFTERNOON: 'afternoon', + EVENING: 'evening', + NIGHT: 'night', + }, + ENDPOINTS: { + BASEURL: 'https://ads.pubmatic.com/AdServer/js/pwt', + FLOORS: 'floors.json', + CONFIGS: 'config.json' + } +}); + +const BROWSER_REGEX_MAP = [ + { regex: /\b(?:crios)\/([\w\.]+)/i, id: 1 }, // Chrome for iOS + { regex: /(edg|edge)(?:e|ios|a)?(?:\/([\w\.]+))?/i, id: 2 }, // Edge + { regex: /(opera|opr)(?:.+version\/|[\/ ]+)([\w\.]+)/i, id: 3 }, // Opera + { regex: /(?:ms|\()(ie) ([\w\.]+)|(?:trident\/[\w\.]+)/i, id: 4 }, // Internet Explorer + { regex: /fxios\/([-\w\.]+)/i, id: 5 }, // Firefox for iOS + { regex: /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w\.]+);)/i, id: 6 }, // Facebook In-App Browser + { regex: / wv\).+(chrome)\/([\w\.]+)/i, id: 7 }, // Chrome WebView + { regex: /droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i, id: 8 }, // Android Browser + { regex: /(chrome|crios)(?:\/v?([\w\.]+))?\b/i, id: 9 }, // Chrome + { regex: /version\/([\w\.\,]+) .*mobile\/\w+ (safari)/i, id: 10 }, // Safari Mobile + { regex: /version\/([\w(\.|\,)]+) .*(mobile ?safari|safari)/i, id: 11 }, // Safari + { regex: /(firefox)\/([\w\.]+)/i, id: 12 } // Firefox +]; + +export const defaultValueTemplate = { + currency: 'USD', + skipRate: 0, + schema: { + fields: ['mediaType', 'size'] + } +}; + +let initTime; +let _fetchFloorRulesPromise = null; let _fetchConfigPromise = null; +export let configMerged; +// configMerged is a reference to the function that can resolve configMergedPromise whenever we want +let configMergedPromise = new Promise((resolve) => { configMerged = resolve; }); +export let _country; + +// Waits for a given promise to resolve within a timeout +export function withTimeout(promise, ms) { + let timeout; + const timeoutPromise = new Promise((resolve) => { + timeout = setTimeout(() => resolve(undefined), ms); + }); + + return Promise.race([promise.finally(() => clearTimeout(timeout)), timeoutPromise]); +} + +// Utility Functions +export const getCurrentTimeOfDay = () => { + const currentHour = new Date().getHours(); + + return currentHour < 5 ? CONSTANTS.TIME_OF_DAY_VALUES.NIGHT + : currentHour < 12 ? CONSTANTS.TIME_OF_DAY_VALUES.MORNING + : currentHour < 17 ? CONSTANTS.TIME_OF_DAY_VALUES.AFTERNOON + : currentHour < 19 ? CONSTANTS.TIME_OF_DAY_VALUES.EVENING + : CONSTANTS.TIME_OF_DAY_VALUES.NIGHT; +} + +export const getBrowserType = () => { + const brandName = getLowEntropySUA()?.browsers + ?.map(b => b.brand.toLowerCase()) + .join(' ') || ''; + const browserMatch = brandName ? BROWSER_REGEX_MAP.find(({ regex }) => regex.test(brandName)) : -1; + + if (browserMatch?.id) return browserMatch.id.toString(); + + const userAgent = navigator?.userAgent; + let browserIndex = userAgent == null ? -1 : 0; + + if (userAgent) { + browserIndex = BROWSER_REGEX_MAP.find(({ regex }) => regex.test(userAgent))?.id || 0; + } + return browserIndex.toString(); +} + +// Getter Functions +export const getOs = () => getOS().toString(); +export const getDeviceType = () => fetchDeviceType().toString(); +export const getCountry = () => _country; +export const getUtm = () => { + const url = new URL(window.location?.href); + const urlParams = new URLSearchParams(url?.search); + return urlParams && urlParams.toString().includes(CONSTANTS.UTM) ? CONSTANTS.UTM_VALUES.TRUE : CONSTANTS.UTM_VALUES.FALSE; +} + +export const getFloorsConfig = (floorsData, profileConfigs) => { + if (!isPlainObject(profileConfigs) || isEmpty(profileConfigs)) { + logError(`${CONSTANTS.LOG_PRE_FIX} profileConfigs is not an object or is empty`); + return undefined; + } + + // Floor configs from adunit / setconfig + const defaultFloorConfig = conf.getConfig('floors') ?? {}; + if (defaultFloorConfig?.endpoint) { + delete defaultFloorConfig.endpoint; + } + // Plugin data from profile + const dynamicFloors = profileConfigs?.plugins?.dynamicFloors; + + // If plugin disabled or config not present, return undefined + if (!dynamicFloors?.enabled || !dynamicFloors?.config) { + return undefined; + } + + let config = { ...dynamicFloors.config }; + + // default values provided by publisher on profile + const defaultValues = config.defaultValues ?? {}; + // If floorsData is not present, use default values + const finalFloorsData = floorsData ?? { ...defaultValueTemplate, values: { ...defaultValues } }; + + delete config.defaultValues; + // If skiprate is provided in configs, overwrite the value in finalFloorsData + (config.skipRate !== undefined) && (finalFloorsData.skipRate = config.skipRate); + + // merge default configs from page, configs + return { + floors: { + ...defaultFloorConfig, + ...config, + data: finalFloorsData, + additionalSchemaFields: { + deviceType: getDeviceType, + timeOfDay: getCurrentTimeOfDay, + browser: getBrowserType, + os: getOs, + utm: getUtm, + country: getCountry, + }, + }, + }; +}; + +export const fetchData = async (publisherId, profileId, type) => { + try { + const endpoint = CONSTANTS.ENDPOINTS[type]; + const baseURL = (type == 'FLOORS') ? `${CONSTANTS.ENDPOINTS.BASEURL}/floors` : CONSTANTS.ENDPOINTS.BASEURL; + const url = `${baseURL}/${publisherId}/${profileId}/${endpoint}`; + const response = await fetch(url); + + if (!response.ok) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error while fetching ${type}: Not ok`); + return; + } + + if (type === "FLOORS") { + const cc = response.headers?.get('country_code'); + _country = cc ? cc.split(',')?.map(code => code.trim())[0] : undefined; + } + + return await response.json(); + } catch (error) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error while fetching ${type}:`, error); + } +}; + +/** + * Initialize the Pubmatic RTD Module. + * @param {Object} config + * @param {Object} _userConsent + * @returns {boolean} + */ +const init = (config, _userConsent) => { + initTime = Date.now(); // Capture the initialization time + const { publisherId, profileId } = config?.params || {}; + + if (!publisherId || !isStr(publisherId) || !profileId || !isStr(profileId)) { + logError( + `${CONSTANTS.LOG_PRE_FIX} ${!publisherId ? 'Missing publisher Id.' + : !isStr(publisherId) ? 'Publisher Id should be a string.' + : !profileId ? 'Missing profile Id.' + : 'Profile Id should be a string.' + }` + ); + return false; + } + + if (!isFn(continueAuction)) { + logError(`${CONSTANTS.LOG_PRE_FIX} continueAuction is not a function. Please ensure to add priceFloors module.`); + return false; + } + + _fetchFloorRulesPromise = fetchData(publisherId, profileId, "FLOORS"); + _fetchConfigPromise = fetchData(publisherId, profileId, "CONFIGS"); + + _fetchConfigPromise.then(async (profileConfigs) => { + const auctionDelay = conf.getConfig('realTimeData').auctionDelay; + const maxWaitTime = 0.8 * auctionDelay; + + const elapsedTime = Date.now() - initTime; + const remainingTime = Math.max(maxWaitTime - elapsedTime, 0); + const floorsData = await withTimeout(_fetchFloorRulesPromise, remainingTime); + + const floorsConfig = getFloorsConfig(floorsData, profileConfigs); + floorsConfig && conf.setConfig(floorsConfig); + configMerged(); + }); + + return true; +}; + +/** + * @param {Object} reqBidsConfigObj + * @param {function} callback + * @param {Object} config + * @param {Object} userConsent + */ +const getBidRequestData = (reqBidsConfigObj, callback) => { + configMergedPromise.then(() => { + const hookConfig = { + reqBidsConfigObj, + context: this, + nextFn: () => true, + haveExited: false, + timer: null + }; + continueAuction(hookConfig); + if (_country) { + const ortb2 = { + user: { + ext: { + ctr: _country, + } + } + } + + mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { + [CONSTANTS.SUBMODULE_NAME]: ortb2 + }); + } + callback(); + }).catch((error) => { + logError(CONSTANTS.LOG_PRE_FIX, 'Error in updating floors :', error); + callback(); + }); +} + +/** @type {RtdSubmodule} */ +export const pubmaticSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: CONSTANTS.SUBMODULE_NAME, + init, + getBidRequestData, +}; + +export const registerSubModule = () => { + submodule(CONSTANTS.REAL_TIME_MODULE, pubmaticSubmodule); +} + +registerSubModule(); diff --git a/modules/pubmaticRtdProvider.md b/modules/pubmaticRtdProvider.md new file mode 100644 index 00000000000..c4c273de6fb --- /dev/null +++ b/modules/pubmaticRtdProvider.md @@ -0,0 +1,72 @@ +## Overview + +- Module Name: PubMatic RTD Provider +- Module Type: RTD Adapter +- Maintainer: header-bidding@pubmatic.com + +## Description + +The PubMatic RTD module fetches pricing floor data and updates the Price Floors Module based on user's context in real-time as per Price Floors Modules Floor Data Provider Interface guidelines [Dynamic Floor Data Provider](https://docs.prebid.org/dev-docs/modules/floors.html#floor-data-provider-interface). + +## Usage + +Step 1: Contact PubMatic to get a publisher ID and create your first profile. + +Step 2: Integrate the PubMatic Analytics Adapter (see Prebid Analytics modules) as well as the Price Floors module. + +Step 3: Prepare the base Prebid file. + +For example: + +To compile the Price Floors, PubMatic RTD module and PubMatic Analytics Adapter into your Prebid build: + +```shell +gulp build --modules=priceFloors,rtdModule,pubmaticRtdProvider,pubmaticAnalyticsAdapter +``` + +{: .alert.alert-info :} +Note: The PubMatic RTD module is dependent on the global real-time data module : `rtdModule`, price floor module : `priceFloors` and PubMatic Analytics Adapter : `pubmaticAnalyticsAdapter`. + +Step 4: Set configuration and enable PubMatic RTD Module using pbjs.setConfig. + +## Configuration + +This module is configured as part of the `realTimeData.dataProviders`. We recommend setting `auctionDelay` to at least 250 ms and make sure `waitForIt` is set to `true` for the `pubmatic` RTD provider. + +```js +const AUCTION_DELAY = 250; +pbjs.setConfig({ + // rest of the config + ..., + realTimeData: { + auctionDelay: AUCTION_DELAY, + dataProviders: [ + { + name: "pubmatic", + waitForIt: true, + params: { + publisherId: ``, // please contact PubMatic to get a publisherId for yourself + profileId: ``, // please contact PubMatic to get a profileId for yourself + }, + }, + ], + }, + // rest of the config + ..., +}); +``` + +## Parameters + +| Name | Type | Description | Default | +| :----------------- | :------ | :------------------------------------------------------------- | :------------------------- | +| name | String | Name of the real-time data module | Always `pubmatic` | +| waitForIt | Boolean | Should be `true` if an `auctionDelay` is defined (mandatory) | `false` | +| params | Object | | | +| params.publisherId | String | Publisher ID | | +| params.profileId | String | Profile ID | | + + +## What Should Change in the Bid Request? + +There are no direct changes in the bid request due to our RTD module, but floor configuration will be set using the price floors module. These changes will be reflected in adunit bids or bidder requests as floor data. \ No newline at end of file diff --git a/modules/pubriseBidAdapter.js b/modules/pubriseBidAdapter.js new file mode 100644 index 00000000000..646546329db --- /dev/null +++ b/modules/pubriseBidAdapter.js @@ -0,0 +1,19 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'pubrise'; +const AD_URL = 'https://backend.pubrise.ai/pbjs'; +const SYNC_URL = 'https://sync.pubrise.ai'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/pubriseBidAdapter.md b/modules/pubriseBidAdapter.md new file mode 100755 index 00000000000..4c130954392 --- /dev/null +++ b/modules/pubriseBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Pubrise Bidder Adapter +Module Type: Pubrise Bidder Adapter +Maintainer: prebid@pubrise.ai +``` + +# Description + +Connects to Pubrise exchange for bids. +Pubrise 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: 'pubrise', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pubrise', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pubrise', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/pubwiseAnalyticsAdapter.js b/modules/pubwiseAnalyticsAdapter.js index 00d8e3ccb6a..bf6be03422e 100644 --- a/modules/pubwiseAnalyticsAdapter.js +++ b/modules/pubwiseAnalyticsAdapter.js @@ -46,7 +46,6 @@ let sessTimeoutName = 'sess_timeout'; function enrichWithSessionInfo(dataBag) { try { - // eslint-disable-next-line // console.log(sessionData); dataBag['session_id'] = sessionData.sessionId; dataBag['activation_id'] = sessionData.activationId; @@ -315,7 +314,7 @@ pubwiseAnalytics.ensureSession = function () { } else if (sessionId != null) { sessionData.sessionId = sessionId; } - // eslint-disable-next-line + // console.log('ensured session'); extendUserSessionTimeout(); }; diff --git a/modules/pubwiseBidAdapter.js b/modules/pubwiseBidAdapter.js index eca0c971050..f77cb9a47d1 100644 --- a/modules/pubwiseBidAdapter.js +++ b/modules/pubwiseBidAdapter.js @@ -1,5 +1,5 @@ -import { _each, isBoolean, isEmptyStr, isNumber, isStr, deepClone, isArray, deepSetValue, inIframe, mergeDeep, deepAccess, logMessage, logInfo, logWarn, logError } from '../src/utils.js'; +import { _each, isBoolean, isEmptyStr, 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'; import { config } from '../src/config.js'; @@ -63,6 +63,7 @@ const VIDEO_CUSTOM_PARAMS = { 'battr': DATA_TYPES.ARRAY, 'linearity': DATA_TYPES.NUMBER, 'placement': DATA_TYPES.NUMBER, + 'plcmt': DATA_TYPES.NUMBER, 'minbitrate': DATA_TYPES.NUMBER, 'maxbitrate': DATA_TYPES.NUMBER, 'skip': DATA_TYPES.NUMBER @@ -666,7 +667,7 @@ function _addFloorFromFloorModule(impObj, bid) { [BANNER, VIDEO, NATIVE].forEach(mediaType => { if (impObj.hasOwnProperty(mediaType)) { let floorInfo = bid.getFloor({ currency: impObj.bidFloorCur, mediaType: mediaType, size: '*' }); - if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidFloorCur && !isNaN(parseInt(floorInfo.floor))) { + if (isPlainObject(floorInfo) && floorInfo.currency === impObj.bidFloorCur && !isNaN(parseInt(floorInfo.floor))) { let mediaTypeFloor = parseFloat(floorInfo.floor); bidFloor = (bidFloor == -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor)) } @@ -862,19 +863,19 @@ function _logMessage(textValue, objectValue) { logMessage(LOG_PREFIX + textValue, objectValue); } -// eslint-disable-next-line no-unused-vars + function _logInfo(textValue, objectValue) { objectValue = objectValue || ''; logInfo(LOG_PREFIX + textValue, objectValue); } -// eslint-disable-next-line no-unused-vars + function _logWarn(textValue, objectValue) { objectValue = objectValue || ''; logWarn(LOG_PREFIX + textValue, objectValue); } -// eslint-disable-next-line no-unused-vars + function _logError(textValue, objectValue) { objectValue = objectValue || ''; logError(LOG_PREFIX + textValue, objectValue); diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index d4a7ec70a70..b2f9af247f6 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -1,208 +1,354 @@ -import { deepAccess, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import { + getGptSlotInfoForAdUnitCode, getGptSlotForAdUnitCode +} from '../libraries/gptUtils/gptUtils.js'; +import { getDeviceType, getBrowser, getOS } from '../libraries/userAgentUtils/index.js'; +import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js'; import adapterManager from '../src/adapterManager.js'; +import { sendBeacon } from '../src/ajax.js' import { EVENTS } from '../src/constants.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { + deepAccess, parseSizesInput, getWindowLocation, buildUrl, cyrb53Hash +} from '../src/utils.js'; + +let initOptions; const emptyUrl = ''; const analyticsType = 'endpoint'; -const pubxaiAnalyticsVersion = 'v1.2.0'; +const adapterCode = 'pubxai'; +const pubxaiAnalyticsVersion = 'v2.1.0'; const defaultHost = 'api.pbxai.com'; const auctionPath = '/analytics/auction'; const winningBidPath = '/analytics/bidwon'; +const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: adapterCode }) -let initOptions; -let auctionTimestamp; -let auctionCache = []; -let events = { - bids: [], - floorDetail: {}, - pageDetail: {}, - deviceDetail: {} -}; - -function getStorage() { - try { - return window.top['sessionStorage']; - } catch (e) { - return null; +/** + * The sendCache is a global cache object which tracks the pending sends + * back to pubx.ai. The data may be removed from this cache, post send. + */ +export const sendCache = new Proxy( + {}, + { + get: (target, name) => { + if (!target.hasOwnProperty(name)) { + target[name] = []; + } + return target[name]; + }, } -} +); -var pubxaiAnalyticsAdapter = Object.assign(adapter( +/** + * auctionCache is a global cache object which stores all auction histories + * for the session. When getting a key from the auction cache, any + * information already known about the auction or associated data (floor + * data configured by prebid, browser data, user data etc) is added to + * the cache automatically. + */ +export const auctionCache = new Proxy( + {}, { - emptyUrl, - analyticsType - }), { - track({ eventType, args }) { - if (typeof args !== 'undefined') { - if (eventType === EVENTS.BID_TIMEOUT) { - args.forEach(item => { mapBidResponse(item, 'timeout'); }); - } else if (eventType === EVENTS.AUCTION_INIT) { - events.auctionInit = args; - events.floorDetail = {}; - events.bids = []; - const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData'); - if (typeof floorData !== 'undefined') { - Object.assign(events.floorDetail, floorData); - } - auctionTimestamp = args.timestamp; - } else if (eventType === EVENTS.BID_RESPONSE) { - mapBidResponse(args, 'response'); - } else if (eventType === EVENTS.BID_WON) { - send({ - winningBid: mapBidResponse(args, 'bidwon') - }, 'bidwon'); + get: (target, name) => { + if (!target.hasOwnProperty(name)) { + target[name] = { + bids: [], + auctionDetail: { + refreshRank: Object.keys(target).length, + auctionId: name, + }, + floorDetail: {}, + pageDetail: { + host: getWindowLocation().host, + path: getWindowLocation().pathname, + search: getWindowLocation().search, + }, + deviceDetail: { + platform: navigator.platform, + deviceType: getDeviceType(), + deviceOS: getOS(), + browser: getBrowser(), + }, + userDetail: { + userIdTypes: Object.keys(getGlobal().getUserIds?.() || {}), + }, + consentDetail: { + consentTypes: Object.keys(getGlobal().getConsentMetadata?.() || {}), + }, + pmacDetail: JSON.parse(storage.getDataFromLocalStorage('pubx:pmac')) || {}, // {auction_1: {floor:0.23,maxBid:0.34,bidCount:3},auction_2:{floor:0.13,maxBid:0.14,bidCount:2} + extraData: JSON.parse(storage.getDataFromLocalStorage('pubx:extraData')) || {}, + initOptions: { + ...initOptions, + auctionId: name, // back-compat + }, + sendAs: [], + }; } - } - if (eventType === EVENTS.AUCTION_END) { - send(events, 'auctionEnd'); - } + return target[name]; + }, } -}); +); -function mapBidResponse(bidResponse, status) { - if (typeof bidResponse !== 'undefined') { - let bid = { - adUnitCode: bidResponse.adUnitCode, - gptSlotCode: getGptSlotInfoForAdUnitCode(bidResponse.adUnitCode).gptSlot || null, - auctionId: bidResponse.auctionId, - bidderCode: bidResponse.bidder, - cpm: bidResponse.cpm, - creativeId: bidResponse.creativeId, - currency: bidResponse.currency, - floorData: bidResponse.floorData, - mediaType: bidResponse.mediaType, - netRevenue: bidResponse.netRevenue, - requestTimestamp: bidResponse.requestTimestamp, - responseTimestamp: bidResponse.responseTimestamp, - status: bidResponse.status, - statusMessage: bidResponse.statusMessage, - timeToRespond: bidResponse.timeToRespond, - transactionId: bidResponse.transactionId - }; - if (status !== 'bidwon') { - Object.assign(bid, { - bidId: status === 'timeout' ? bidResponse.bidId : bidResponse.requestId, - renderStatus: status === 'timeout' ? 3 : 2, - sizes: parseSizesInput(bidResponse.size).toString(), +/** + * Fetch extra ad server data for a specific ad slot (bid) + * @param {object} bid an output from extractBid + * @returns {object} key value pairs from the adserver + */ +const getAdServerDataForBid = (bid) => { + const gptSlot = getGptSlotForAdUnitCode(bid); + if (gptSlot) { + return Object.fromEntries( + gptSlot + .getTargetingKeys() + .filter( + (key) => + key.startsWith('pubx-') || + (key.startsWith('hb_') && (key.match(/_/g) || []).length === 1) + ) + .map((key) => [key, gptSlot.getTargeting(key)]) + ); + } + return {}; // TODO: support more ad servers +}; + +/** + * extracts and derives valuable data from a prebid bidder bidResponse object + * @param {object} bidResponse a prebid bidder bidResponse (see + * https://docs.prebid.org/dev-docs/publisher-api-reference/getBidResponses.html) + * @returns {object} + */ +const extractBid = (bidResponse) => { + return { + adUnitCode: bidResponse.adUnitCode, + gptSlotCode: + getGptSlotInfoForAdUnitCode(bidResponse.adUnitCode).gptSlot || null, + auctionId: bidResponse.auctionId, + bidderCode: bidResponse.bidder, + cpm: bidResponse.cpm, + creativeId: bidResponse.creativeId, + dealId: bidResponse.dealId, + currency: bidResponse.currency, + floorData: bidResponse.floorData, + mediaType: bidResponse.mediaType, + netRevenue: bidResponse.netRevenue, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp, + status: bidResponse.status, + sizes: parseSizesInput(bidResponse.size).toString(), + statusMessage: bidResponse.statusMessage, + timeToRespond: bidResponse.timeToRespond, + transactionId: bidResponse.transactionId, + bidId: bidResponse.bidId || bidResponse.requestId, + placementId: bidResponse.params + ? deepAccess(bidResponse, 'params.0.placementId') + : null, + source: bidResponse.source || 'null', + }; +}; + +/** + * Track the events emitted by prebid and handle each case. See https://docs.prebid.org/dev-docs/publisher-api-reference/getEvents.html for more info + * @param {object} event the prebid event emmitted + * @param {string} event.eventType the type of the event + * @param {object} event.args the arguments of the emitted event + */ +const track = ({ eventType, args }) => { + switch (eventType) { + // handle invalid bids, and remove them from the adUnit cache + case EVENTS.BID_TIMEOUT: + args.map(extractBid).forEach((bid) => { + bid.bidType = 3; + auctionCache[bid.auctionId].bids.push(bid); + }); + break; + // handle valid bid responses and record them as part of an auction + case EVENTS.BID_RESPONSE: + const bid = Object.assign(extractBid(args), { bidType: 2 }); + auctionCache[bid.auctionId].bids.push(bid); + break; + case EVENTS.BID_REJECTED: + const rejectedBid = Object.assign(extractBid(args), { bidType: 1 }); + auctionCache[rejectedBid.auctionId].bids.push(rejectedBid); + break; + // capture extra information from the auction, and if there were no bids + // (and so no chance of a win) send the auction + case EVENTS.AUCTION_END: + Object.assign( + auctionCache[args.auctionId].floorDetail, + args.adUnits + .map((i) => i?.bids.length && i.bids[0]?.floorData) + .find((i) => i) || {} + ); + auctionCache[args.auctionId].deviceDetail.cdep = args.bidderRequests + .map((bidRequest) => bidRequest.ortb2?.device?.ext?.cdep) + .find((i) => i); + Object.assign(auctionCache[args.auctionId].auctionDetail, { + adUnitCodes: args.adUnits.map((i) => i.code), + timestamp: args.timestamp, }); - events.bids.push(bid); - } else { - Object.assign(bid, { - bidId: bidResponse.requestId, - floorProvider: events.floorDetail?.floorProvider || null, - floorFetchStatus: events.floorDetail?.fetchStatus || null, - floorLocation: events.floorDetail?.location || null, - floorModelVersion: events.floorDetail?.modelVersion || null, - floorSkipRate: events.floorDetail?.skipRate || 0, - isFloorSkipped: events.floorDetail?.skipped || false, + if ( + auctionCache[args.auctionId].bids.every((bid) => [1, 3].includes(bid.bidType)) + ) { + prepareSend(args.auctionId); + } + break; + // send the prebid winning bid back to pubx + case EVENTS.BID_WON: + const winningBid = extractBid(args); + const floorDetail = auctionCache[winningBid.auctionId].floorDetail; + Object.assign(winningBid, { + floorProvider: floorDetail?.floorProvider || null, + floorFetchStatus: floorDetail?.fetchStatus || null, + floorLocation: floorDetail?.location || null, + floorModelVersion: floorDetail?.modelVersion || null, + floorSkipRate: floorDetail?.skipRate || 0, + isFloorSkipped: floorDetail?.skipped || false, isWinningBid: true, - placementId: bidResponse.params ? deepAccess(bidResponse, 'params.0.placementId') : null, - renderedSize: bidResponse.size, - renderStatus: 4 + renderedSize: args.size, + bidType: 4, }); - return bid; - } - } -} - -export function getDeviceType() { - if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { - return 'tablet'; - } - if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { - return 'mobile'; + winningBid.adServerData = getAdServerDataForBid(winningBid); + auctionCache[winningBid.auctionId].winningBid = winningBid; + prepareSend(winningBid.auctionId); + break; + // do nothing + default: + break; } - return 'desktop'; -} - -export function getBrowser() { - if (/Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)) return 'Chrome'; - else if (navigator.userAgent.match('CriOS')) return 'Chrome'; - else if (/Firefox/.test(navigator.userAgent)) return 'Firefox'; - else if (/Edg/.test(navigator.userAgent)) return 'Microsoft Edge'; - else if (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)) return 'Safari'; - else if (/Trident/.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent)) return 'Internet Explorer'; - else return 'Others'; -} - -export function getOS() { - if (navigator.userAgent.indexOf('Android') != -1) return 'Android'; - if (navigator.userAgent.indexOf('like Mac') != -1) return 'iOS'; - if (navigator.userAgent.indexOf('Win') != -1) return 'Windows'; - if (navigator.userAgent.indexOf('Mac') != -1) return 'Macintosh'; - if (navigator.userAgent.indexOf('Linux') != -1) return 'Linux'; - if (navigator.appVersion.indexOf('X11') != -1) return 'Unix'; - return 'Others'; -} +}; -// add sampling rate -pubxaiAnalyticsAdapter.shouldFireEventRequest = function (samplingRate = 1) { - return (Math.floor((Math.random() * samplingRate + 1)) === parseInt(samplingRate)); +/** + * If true, send data back to pubxai + * @param {string} auctionId + * @param {number} samplingRate + * @returns {boolean} + */ +const shouldFireEventRequest = (auctionId, samplingRate = 1) => { + return parseInt(cyrb53Hash(auctionId)) % samplingRate === 0; }; -function send(data, status) { - if (pubxaiAnalyticsAdapter.shouldFireEventRequest(initOptions.samplingRate)) { - let location = getWindowLocation(); - const storage = getStorage(); - data.initOptions = initOptions; - data.pageDetail = {}; - Object.assign(data.pageDetail, { - host: location.host, - path: location.pathname, - search: location.search - }); - if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { - data.pageDetail.adUnits = data.auctionInit.adUnitCodes; - data.initOptions.auctionId = data.auctionInit.auctionId; - delete data.auctionInit; - - data.pmcDetail = {}; - Object.assign(data.pmcDetail, { - bidDensity: storage ? storage.getItem('pbx:dpbid') : null, - maxBid: storage ? storage.getItem('pbx:mxbid') : null, - auctionId: storage ? storage.getItem('pbx:aucid') : null, - }); +/** + * prepare the payload for sending auction data back to pubx.ai + * @param {string} auctionId the auction to send + */ +const prepareSend = (auctionId) => { + const auctionData = Object.assign({}, auctionCache[auctionId]); + if (!shouldFireEventRequest(auctionId, initOptions.samplingRate)) { + return; + } + [ + { + path: winningBidPath, + requiredKeys: [ + 'winningBid', + 'pageDetail', + 'deviceDetail', + 'floorDetail', + 'auctionDetail', + 'userDetail', + 'consentDetail', + 'pmacDetail', + 'extraData', + 'initOptions', + ], + eventType: 'win', + }, + { + path: auctionPath, + requiredKeys: [ + 'bids', + 'pageDetail', + 'deviceDetail', + 'floorDetail', + 'auctionDetail', + 'userDetail', + 'consentDetail', + 'pmacDetail', + 'extraData', + 'initOptions', + ], + eventType: 'auction', + }, + ].forEach(({ path, requiredKeys, eventType }) => { + const data = Object.fromEntries( + requiredKeys.map((key) => [key, auctionData[key]]) + ); + if ( + auctionCache[auctionId].sendAs.includes(eventType) || + !requiredKeys.every((key) => !!auctionData[key]) + ) { + return; } - data.deviceDetail = {}; - Object.assign(data.deviceDetail, { - platform: navigator.platform, - deviceType: getDeviceType(), - deviceOS: getOS(), - browser: getBrowser() - }); - - let pubxaiAnalyticsRequestUrl = buildUrl({ + const pubxaiAnalyticsRequestUrl = buildUrl({ protocol: 'https', - hostname: (initOptions && initOptions.hostName) || defaultHost, - pathname: status == 'bidwon' ? winningBidPath : auctionPath, + hostname: + (auctionData.initOptions && auctionData.initOptions.hostName) || + defaultHost, + pathname: path, search: { - auctionTimestamp: auctionTimestamp, + auctionTimestamp: auctionData.auctionDetail.timestamp, pubxaiAnalyticsVersion: pubxaiAnalyticsVersion, - prebidVersion: getGlobal().version + prebidVersion: '$prebid.version$', + pubxId: initOptions.pubxId, + }, + }); + sendCache[pubxaiAnalyticsRequestUrl].push(data); + auctionCache[auctionId].sendAs.push(eventType); + }); +}; + +const send = () => { + const toBlob = (d) => new Blob([JSON.stringify(d)], { type: 'text/json' }); + + Object.entries(sendCache).forEach(([requestUrl, events]) => { + let payloadStart = 0; + + events.forEach((event, index, arr) => { + const payload = arr.slice(payloadStart, index + 2); + const payloadTooLarge = toBlob(payload).size > 65536; + + if (payloadTooLarge || index + 1 === arr.length) { + sendBeacon( + requestUrl, + toBlob(payloadTooLarge ? payload.slice(0, -1) : payload) + ); + payloadStart = index; } }); - if (status == 'bidwon') { - ajax(pubxaiAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/json' }); - } else if (status == 'auctionEnd' && auctionCache.indexOf(data.initOptions.auctionId) === -1) { - ajax(pubxaiAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/json' }); - auctionCache.push(data.initOptions.auctionId); + + events.splice(0); + }); +}; + +// register event listener to send logs when user leaves page +if (document.visibilityState) { + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + send(); } - } + }); } -pubxaiAnalyticsAdapter.originEnableAnalytics = pubxaiAnalyticsAdapter.enableAnalytics; -pubxaiAnalyticsAdapter.enableAnalytics = function (config) { +// declare the analytics adapter +var pubxaiAnalyticsAdapter = Object.assign( + adapter({ + emptyUrl, + analyticsType, + }), + { track } +); + +pubxaiAnalyticsAdapter.originEnableAnalytics = + pubxaiAnalyticsAdapter.enableAnalytics; +pubxaiAnalyticsAdapter.enableAnalytics = (config) => { initOptions = config.options; pubxaiAnalyticsAdapter.originEnableAnalytics(config); }; adapterManager.registerAnalyticsAdapter({ adapter: pubxaiAnalyticsAdapter, - code: 'pubxai' + code: adapterCode, }); export default pubxaiAnalyticsAdapter; diff --git a/modules/pubxaiRtdProvider.js b/modules/pubxaiRtdProvider.js index b958856df00..4528b29cf11 100644 --- a/modules/pubxaiRtdProvider.js +++ b/modules/pubxaiRtdProvider.js @@ -2,6 +2,8 @@ import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { submodule } from '../src/hook.js'; import { deepAccess } from '../src/utils.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { getStorageManager } from '../src/storageManager.js'; /** * This RTD module has a dependency on the priceFloors module. * We utilize the createFloorsDataForAuction function from the priceFloors module to incorporate price floors data into the current auction. @@ -16,6 +18,7 @@ export const FloorsApiStatus = Object.freeze({ SUCCESS: 'SUCCESS', ERROR: 'ERROR', }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); export const FLOORS_EVENT_HANDLE = 'floorsApi'; export const FLOORS_END_POINT = 'https://floor.pbxai.com/'; export const FLOOR_PROVIDER = 'PubxFloorProvider'; @@ -80,31 +83,48 @@ export const setFloorsApiStatus = (status) => { export const getUrl = (provider) => { const { pubxId, endpoint } = deepAccess(provider, 'params'); - return `${endpoint || FLOORS_END_POINT}?pubxId=${pubxId}&page=${ - window.location.href - }`; + if (!endpoint) { + return null; // Indicate that no endpoint is provided + } + return `${endpoint || FLOORS_END_POINT}?pubxId=${pubxId}&page=${window.location.href}`; }; export const fetchFloorRules = async (provider) => { return new Promise((resolve, reject) => { setFloorsApiStatus(FloorsApiStatus.IN_PROGRESS); - ajax(getUrl(provider), { - success: (responseText, response) => { - try { - if (response && response.response) { - const floorsResponse = JSON.parse(response.response); - resolve(floorsResponse); - } else { - resolve(null); + const url = getUrl(provider); + if (url) { + // Fetch from remote endpoint + ajax(url, { + success: (responseText, response) => { + try { + if (response && response.response) { + const floorsResponse = JSON.parse(response.response); + resolve(floorsResponse); + } else { + resolve(null); + } + } catch (error) { + reject(error); } - } catch (error) { - reject(error); + }, + error: (responseText, response) => { + reject(response); + }, + }); + } else { + // Fetch from local storage + try { + const localData = storage.getDataFromSessionStorage('pubx:dynamicFloors') || window.__pubxDynamicFloors__; + if (localData) { + resolve(JSON.parse(localData)); + } else { + resolve(null); } - }, - error: (responseText, response) => { - reject(response); - }, - }); + } catch (error) { + reject(error); + } + } }); }; diff --git a/modules/pubxaiRtdProvider.md b/modules/pubxaiRtdProvider.md index d7d89857c62..1855ebbcda7 100644 --- a/modules/pubxaiRtdProvider.md +++ b/modules/pubxaiRtdProvider.md @@ -32,17 +32,17 @@ pbjs.setConfig({ ..., realTimeData: { auctionDelay: AUCTION_DELAY, - dataProviders: { + dataProviders: [{ name: "pubxai", waitForIt: true, params: { pubxId: ``, - endpoint: ``, // (optional) + endpoint: ``, floorMin: ``, // (optional) enforcement: ``, // (optional) data: `` // (optional) } - } + }] } // rest of the config ..., @@ -57,7 +57,7 @@ pbjs.setConfig({ | waitForIt | Boolean | Should be `true` if an `auctionDelay` is defined (optional) | `false` | | params | Object | | | | params.pubxId | String | Publisher ID | | -| params.endpoint | String | URL to retrieve floor data (optional) | `https://floor.pbxai.com/` | +| params.endpoint | String | URL to retrieve floor data | | | params.floorMin | Number | Minimum CPM floor (optional) | `None` | | params.enforcement | Object | Enforcement behavior within the Price Floors Module (optional) | `None` | | params.data | Object | Default floor data provided by pubX.ai (optional) | `None` | diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 516254b358b..50747616872 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -1,7 +1,6 @@ import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import {isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const DEFAULT_CURRENCY = 'USD'; const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; @@ -58,13 +57,6 @@ export const spec = { url: 'https://bh.contextweb.com/visitormatch/prebid' }]; } - }, - transformBidParams: function(params) { - return convertTypes({ - 'cf': 'string', - 'cp': 'number', - 'ct': 'number' - }, params); } }; diff --git a/modules/qortexRtdProvider.js b/modules/qortexRtdProvider.js index 88b4339b38e..8049f02be81 100644 --- a/modules/qortexRtdProvider.js +++ b/modules/qortexRtdProvider.js @@ -1,14 +1,23 @@ import { submodule } from '../src/hook.js'; -import { ajax } from '../src/ajax.js'; import { logWarn, mergeDeep, logMessage, generateUUID } from '../src/utils.js'; import { loadExternalScript } from '../src/adloader.js'; import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; -let requestUrl; -let bidderArray; -let impressionIds; -let currentSiteContext; +const QX_VERSION = { v: '1.0' } + +const qortexSessionInfo = {}; +const QX_IN_MESSAGE = { + BID_ENRICH_INITIALIZED: 'CX-BID-ENRICH-INITIALIZED', + DISPATCH_CONTEXT: 'DISPATCH-CONTEXT' +} +const QX_OUT_MESSAGE = { + AUCTION_END: 'AUCTION-END', + NO_CONTEXT: 'NO-CONTEXT', + RTD_INITIALIZED: 'RTD-INITIALIZED', + REQUEST_CONTEXT: 'REQUEST-CONTEXT' +} /** * Init if module configuration is valid @@ -21,11 +30,16 @@ function init (config) { return false; } else { initializeModuleData(config); + if (config?.params?.enableBidEnrichment) { + initializeBidEnrichment(); + } else { + logWarn('Bid Enrichment Function has been disabled in module configuration') + } + if (config?.params?.tagConfig) { + loadScriptTag(config) + } + return true; } - if (config?.params?.tagConfig) { - loadScriptTag(config) - } - return true; } /** @@ -34,64 +48,47 @@ function init (config) { * @param {Function} callback Called on completion */ function getBidRequestData (reqBidsConfig, callback) { - if (reqBidsConfig?.adUnits?.length > 0) { - getContext() - .then(contextData => { - setContextData(contextData) - addContextToRequests(reqBidsConfig) - callback(); - }) - .catch((e) => { - logWarn(e?.message); - callback(); - }); + if (reqBidsConfig?.adUnits?.length > 0 && shouldAllowBidEnrichment()) { + addContextToRequests(reqBidsConfig); } else { - logWarn('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfig)) - callback(); + logWarn('Module function is paused due to configuration \n Module Config: ' + JSON.stringify(reqBidsConfig)); } + callback(); } /** - * determines whether to send a request to context api and does so if necessary - * @returns {Promise} ortb Content object + * Processess auction end events for Qortex reporting + * @param {Object} data Auction end object */ -export function getContext () { - if (!currentSiteContext) { - logMessage('Requesting new context data'); - return new Promise((resolve, reject) => { - const callbacks = { - success(text, data) { - const result = data.status === 200 ? JSON.parse(data.response)?.content : null; - resolve(result); - }, - error(error) { - reject(new Error(error)); - } - } - ajax(requestUrl, callbacks) - }) - } else { - logMessage('Adding Content object from existing context data'); - return new Promise(resolve => resolve(currentSiteContext)); +function onAuctionEndEvent (data, config, t) { + logMessage('Auction ended: ', JSON.stringify(data)); + if (shouldAllowBidEnrichment()) { + if (!qortexSessionInfo.auctionsEnded) { + qortexSessionInfo.auctionsEnded = []; + } + qortexSessionInfo.auctionsEnded.push(JSON.stringify(data)); + postBidEnrichmentMessage(QX_OUT_MESSAGE.AUCTION_END, JSON.stringify(data)); } } /** * Updates bidder configs with the response from Qortex context services * @param {Object} reqBidsConfig Bid request configuration object - * @param {string[]} bidders Bidders specified in module's configuration */ export function addContextToRequests (reqBidsConfig) { - if (currentSiteContext === null) { + if (qortexSessionInfo.currentSiteContext === null) { logWarn('No context data received at this time'); + requestContextData(); } else { - const fragment = { site: {content: currentSiteContext} } - if (bidderArray?.length > 0) { - bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment})) - } else if (!bidderArray) { - mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment); - } else { - logWarn('Config contains an empty bidders array, unable to determine which bids to enrich'); + if (checkPercentageOutcome(qortexSessionInfo.groupConfig?.prebidBidEnrichmentPercentage)) { + const fragment = qortexSessionInfo.currentSiteContext + if (qortexSessionInfo.bidderArray?.length > 0) { + qortexSessionInfo.bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment})); + } else if (!qortexSessionInfo.bidderArray) { + mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment); + } else { + logWarn('Config contains an empty bidders array, unable to determine which bids to enrich'); + } } } } @@ -121,22 +118,43 @@ export function loadScriptTag(config) { switch (e?.detail?.type) { case 'qx-impression': const {uid} = e.detail; - if (!uid || impressionIds.has(uid)) { - logWarn(`received invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`) + if (!uid || qortexSessionInfo.impressionIds.has(uid)) { + logWarn(`Received invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`) return; } else { - logMessage('received billable event: qx-impression') - impressionIds.add(uid) + logMessage('Received billable event: qx-impression') + qortexSessionInfo.impressionIds.add(uid) billableEvent.transactionId = e.detail.uid; events.emit(EVENTS.BILLABLE_EVENT, billableEvent); break; } default: - logWarn(`received invalid billable event: ${e.detail?.type}`) + logWarn(`Received invalid billable event: ${e.detail?.type}`) } }) - loadExternalScript(src, code, undefined, undefined, attr); + loadExternalScript(src, MODULE_TYPE_RTD, code, undefined, undefined, attr); +} + +/** + * Request contextual data about page (after checking for allow) and begin listening for postMessages from publisher + */ +export function initializeBidEnrichment() { + if (shouldAllowBidEnrichment()) { + requestContextData() + } + addEventListener('message', windowPostMessageReceived); +} + +/** + * Call Qortex code on page for available contextual information about current environment + */ +export function requestContextData() { + if (qortexSessionInfo.currentSiteContext) { + logMessage('Context data already retrieved.'); + } else { + postBidEnrichmentMessage(QX_OUT_MESSAGE.REQUEST_CONTEXT); + } } /** @@ -144,22 +162,106 @@ export function loadScriptTag(config) { * @param {Object} config module config obtained during init */ export function initializeModuleData(config) { - const DEFAULT_API_URL = 'https://demand.qortex.ai'; - const {apiUrl, groupId, bidders} = config.params; - requestUrl = `${apiUrl || DEFAULT_API_URL}/api/v1/analyze/${groupId}/prebid`; - bidderArray = bidders; - impressionIds = new Set(); - currentSiteContext = null; + const {groupId, bidders, enableBidEnrichment} = config.params; + qortexSessionInfo.bidEnrichmentDisabled = enableBidEnrichment !== null ? !enableBidEnrichment : true; + qortexSessionInfo.bidderArray = bidders; + qortexSessionInfo.impressionIds = new Set(); + qortexSessionInfo.currentSiteContext = null; + qortexSessionInfo.sessionId = generateSessionId(); + qortexSessionInfo.groupId = groupId; + return qortexSessionInfo; } +/** + * Allows setting of contextual data + */ export function setContextData(value) { - currentSiteContext = value + qortexSessionInfo.currentSiteContext = value +} + +/** + * Allows setting of group configuration data + */ +export function setGroupConfigData(value) { + qortexSessionInfo.groupConfig = value +} + +/** + * Unique id generator creating an identifier through datetime and random number + * @returns {string} + */ +function generateSessionId() { + const randomInt = window.crypto.getRandomValues(new Uint32Array(1)); + const currentDateTime = Math.floor(Date.now() / 1000); + return 'QX' + randomInt.toString() + 'X' + currentDateTime.toString() +} + +/** + * Check for a random value to be above given percentage threshold + * @param {number} percentageValue 0-100 number for percentage check. + * @returns {Boolean} + */ +function checkPercentageOutcome(percentageValue) { + return (percentageValue ?? 0) > (Math.random() * 100); +} + +/** + * Check for allowing functionality of bid enrichment capabilities. + * @returns {Boolean} + */ +function shouldAllowBidEnrichment() { + if (qortexSessionInfo.bidEnrichmentDisabled) { + logWarn('Bid enrichment disabled at prebid config') + return false; + } + return true +} + +/** + * Passes message out to external page through postMessage method + * @param {string} msg message string to be passed to CX-BID-ENRICH target on current page + * @param {Object} data optional parameter object with additional data to send with post + */ +function postBidEnrichmentMessage(msg, data) { + window.postMessage({ + target: 'CX-BID-ENRICH', + message: msg, + params: data + }, window.location.protocol + '//' + window.location.host); + logMessage('Dispatching window postMessage: ' + msg); +} + +/** + * Receives messages passed through postMessage method to QORTEX-PREBIDJS-RTD-MODULE on current page + * @param {Object} evt data object holding Event information + */ +export function windowPostMessageReceived(evt) { + const data = evt.data; + if (typeof data.target !== 'undefined' && data.target === 'QORTEX-PREBIDJS-RTD-MODULE') { + if (shouldAllowBidEnrichment()) { + if (data.message === QX_IN_MESSAGE.BID_ENRICH_INITIALIZED) { + if (Boolean(data.params) && Boolean(data.params?.groupConfig)) { + setGroupConfigData(data.params.groupConfig); + } + postBidEnrichmentMessage(QX_OUT_MESSAGE.RTD_INITIALIZED, QX_VERSION); + if (qortexSessionInfo?.auctionsEnded?.length > 0) { + qortexSessionInfo.auctionsEnded.forEach(data => postBidEnrichmentMessage(QX_OUT_MESSAGE.AUCTION_END, data)); + } + requestContextData(); + } else if (data.message === QX_IN_MESSAGE.DISPATCH_CONTEXT) { + if (data.params?.context) { + setContextData(data.params.context); + } + } + } + } } export const qortexSubmodule = { name: 'qortex', init, - getBidRequestData + getBidRequestData, + onAuctionEndEvent } submodule('realTimeData', qortexSubmodule); diff --git a/modules/qortexRtdProvider.md b/modules/qortexRtdProvider.md index 312696068cd..027388c6f6d 100644 --- a/modules/qortexRtdProvider.md +++ b/modules/qortexRtdProvider.md @@ -12,7 +12,7 @@ Maintainer: mannese@qortex.ai The Qortex RTD module appends contextual segments to the bidding object based on the content of a page using the Qortex API. -Upon load, the Qortex context API will analyze the bidder page (video, text, image, etc.) and will return a [Content object](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=26). The module will then merge that object into the appropriate bidders' `ortb2.site.content`, which can be used by prebid adapters that use `site.content` data. +If the `Qortex Group Id` and module parameters provided during configuration is active, the Qortex context API will attempt to generate and return an object containing [Content data](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=26) using indexed data from provided page content. The module will then merge that object into the appropriate bidders' `ortb2` object, which can be used by prebid adapters that use RTB contextual data for bid requests. ## Build @@ -40,13 +40,8 @@ pbjs.setConfig({ params: { groupId: 'ABC123', //required bidders: ['qortex', 'adapter2'], //optional (see below) - tagConfig: { // optional, please reach out to your account manager for configuration reccommendation - videoContainer: 'string', - htmlContainer: 'string', - attachToTop: 'string', - esm6Mod: 'string', - continuousLoad: 'string' - } + enableBidEnrichment: true, //optional (see below) + tagConfig: { } // optional, please reach out to your account manager for configuration reccommendation } }] } @@ -63,7 +58,11 @@ pbjs.setConfig({ - If this parameter is omitted, the RTD module will default to updating `ortb2.site.content` on *all* bid adapters being used on the page +#### `enableBidEnrichment` - optional +- This optional parameter allows a publisher to opt-in to the features of the RTD module that use our API to enrich bids with first party data for contextuality. Enabling this feature will allow this module to interact with the Qortex AI contextuality server for indexing and analysis. Please use caution when adding this module to pages that may contain personal user data or proprietary information. + #### `tagConfig` - optional - This optional parameter is an object containing the config settings that could be usedto initialize the Qortex integration on your page. A preconfigured object for this step will be provided to you by the Qortex team. -- If this parameter is not present, the Qortex integration can still be configured and loaded manually on your page outside of prebid. The RTD module will continue to initialize and operate as normal. \ No newline at end of file +- If this parameter is not present, the Qortex integration can still be configured and loaded manually on your page outside of prebid. The RTD module will continue to initialize and operate as normal. + \ No newline at end of file diff --git a/modules/qtBidAdapter.js b/modules/qtBidAdapter.js new file mode 100644 index 00000000000..cbfd3586fe5 --- /dev/null +++ b/modules/qtBidAdapter.js @@ -0,0 +1,21 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'qt'; +const GVLID = 1331; +const AD_URL = 'https://endpoint1.qt.io/pbjs'; +const SYNC_URL = 'https://cs.qt.io'; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/qtBidAdapter.md b/modules/qtBidAdapter.md new file mode 100644 index 00000000000..6d505b38379 --- /dev/null +++ b/modules/qtBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: QT Bidder Adapter +Module Type: QT Bidder Adapter +Maintainer: octavian.cimpu@qt.io +``` + +# Description + +Connects to QT exchange for bids. +QT 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: 'qt', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'qt', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'qt', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 1ba23302367..ea907f0429c 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -49,7 +49,6 @@ function makeVideoImp(bid) { maxbitrate: video.maxbitrate, playbackmethod: video.playbackmethod, delivery: video.delivery, - placement: video.placement, api: video.api, w: video.w, h: video.h @@ -58,7 +57,7 @@ function makeVideoImp(bid) { return { video: videoCopy, placementCode: bid.placementCode, - bidFloor: bid.params.bidFloor || DEFAULT_BID_FLOOR + bidFloor: DEFAULT_BID_FLOOR }; } @@ -76,7 +75,7 @@ function makeBannerImp(bid) { }) }, placementCode: bid.placementCode, - bidFloor: bid.params.bidFloor || DEFAULT_BID_FLOOR + bidFloor: DEFAULT_BID_FLOOR }; } diff --git a/modules/r2b2AnalyticsAdapter.js b/modules/r2b2AnalyticsAdapter.js new file mode 100644 index 00000000000..aa909225c4d --- /dev/null +++ b/modules/r2b2AnalyticsAdapter.js @@ -0,0 +1,627 @@ +import {ajax} from '../src/ajax.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import {EVENTS} from '../src/constants.js'; +import adapterManager from '../src/adapterManager.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {isNumber, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {config} from '../src/config.js'; + +const ADAPTER_VERSION = '1.1.0'; +const ADAPTER_CODE = 'r2b2'; +const MODULE_NAME = 'R2B2 Analytics' +const GVLID = 1235; +const analyticsType = 'endpoint'; + +const DEFAULT_SERVER = 'log.r2b2.cz'; +const DEFAULT_EVENT_PATH = 'prebid/events'; +const DEFAULT_ERROR_PATH = 'error'; +const DEFAULT_PROTOCOL = 'https'; + +const ERROR_MAX = 10; +const BATCH_SIZE = 50; +const BATCH_DELAY = 500; +const MAX_CALL_DEPTH = 20; +const REPORTED_URL = getRefererInfo().page || getRefererInfo().topmostLocation || ''; + +const START_TIME = Date.now(); + +const CACHE_TTL = 300 * 1000; + +const EVENT_MAP = {}; +EVENT_MAP[EVENTS.NO_BID] = 'noBid'; +EVENT_MAP[EVENTS.AUCTION_INIT] = 'init'; +EVENT_MAP[EVENTS.BID_REQUESTED] = 'request'; +EVENT_MAP[EVENTS.BID_TIMEOUT] = 'timeout'; +EVENT_MAP[EVENTS.BID_RESPONSE] = 'response'; +EVENT_MAP[EVENTS.BID_REJECTED] = 'reject'; +EVENT_MAP[EVENTS.BIDDER_ERROR] = 'bidError'; +EVENT_MAP[EVENTS.BIDDER_DONE] = 'bidderDone'; +EVENT_MAP[EVENTS.AUCTION_END] = 'auction'; +EVENT_MAP[EVENTS.BID_WON] = 'bidWon'; +EVENT_MAP[EVENTS.SET_TARGETING] = 'targeting'; +EVENT_MAP[EVENTS.STALE_RENDER] = 'staleRender'; +EVENT_MAP[EVENTS.AD_RENDER_SUCCEEDED] = 'render'; +EVENT_MAP[EVENTS.AD_RENDER_FAILED] = 'renderFail'; +EVENT_MAP[EVENTS.BID_VIEWABLE] = 'view'; + +/* CONFIGURATION */ +let WEBSITE = 0; +let CONFIG_ID = 0; +let CONFIG_VERSION = 0; +let LOG_SERVER = DEFAULT_SERVER; + +/* CACHED DATA */ +let latestAuction = ''; +let previousAuction = ''; +let auctionCount = 0; +let auctionsData = {}; +let bidsData = {}; +let adServerCurrency = ''; + +let flushTimer; +let eventBuffer = []; +let errors = 0; + +let callDepth = 0; +function flushEvents () { + let events = { prebid: { e: eventBuffer, c: adServerCurrency } }; + eventBuffer = []; + callDepth++; + try { + // check for recursion in case reportEvents propagates error events + // and execution doesn't finish before BATCH_SIZE is reached again + if (callDepth >= MAX_CALL_DEPTH) { + if (callDepth === MAX_CALL_DEPTH) { + logError(`${MODULE_NAME}: Maximum call depth reached, discarding events`); + } + return; + } + // clear out old data only in state without recursion + if (callDepth === 1) { + clearCache(bidsData); + clearCache(auctionsData); + } + + reportEvents(events); + } finally { + callDepth--; + } +} + +function clearCache(cache) { + const now = Date.now(); + for (const [key, { t }] of Object.entries(cache)) { + if ((t + CACHE_TTL) < now) { + delete cache[key]; + } + } +} + +export function resetAnalyticAdapter() { + latestAuction = ''; + previousAuction = ''; + auctionCount = 0; + auctionsData = {}; + bidsData = {}; + adServerCurrency = ''; + clearTimeout(flushTimer); + eventBuffer = []; + errors = 0; + callDepth = 0; + + WEBSITE = 0; + CONFIG_ID = 0; + CONFIG_VERSION = 0; + LOG_SERVER = DEFAULT_SERVER; +} +function processEvent (event) { + // console.log('process event:', event); + // console.log(JSON.stringify(event)); + if (!event) { + return + } + eventBuffer.push(event); + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null + } + if (eventBuffer.length >= BATCH_SIZE) { + flushEvents(); + } else { + flushTimer = setTimeout(flushEvents, BATCH_DELAY); + } +} + +function processErrorParams(params) { + if (isPlainObject(params)) { + try { + return JSON.stringify(params); + } catch (e) { /* do nothing */ } + } + return null +} +function reportError (message, params) { + errors++; + if (errors > ERROR_MAX) return; + params = processErrorParams(params); + message = `[ANALYTICS-${ADAPTER_VERSION}] ${message}`; + const url = r2b2Analytics.getErrorUrl() + + `?d=${encodeURIComponent(WEBSITE)}` + + `&m=${encodeURIComponent(message)}` + + `&t=prebid` + + `&p=1` + + (params ? `&pr=${encodeURIComponent(params)}` : '') + + (CONFIG_ID ? `&conf=${encodeURIComponent(CONFIG_ID)}` : '') + + (CONFIG_VERSION ? `&conf_ver=${encodeURIComponent(CONFIG_VERSION)}` : '') + + `&u=${encodeURIComponent(REPORTED_URL)}`; + ajax(url, null, null, {}); +} +function reportEvents (events) { + try { + let data = 'events=' + JSON.stringify(events); + let url = r2b2Analytics.getUrl() + + `?v=${encodeURIComponent(ADAPTER_VERSION)}` + + `&hbDomain=${encodeURIComponent(WEBSITE)}` + + (CONFIG_ID ? `&conf=${encodeURIComponent(CONFIG_ID)}` : '') + + (CONFIG_VERSION ? `&conf_ver=${encodeURIComponent(CONFIG_VERSION)}` : '') + + `&u=${encodeURIComponent(REPORTED_URL)}`; + let headers = { + contentType: 'application/x-www-form-urlencoded' + } + data = data.replace(/&/g, '%26'); + ajax(url, null, data, headers); + } catch (e) { + const msg = `Error sending events - ${e.message}`; + logError(`${MODULE_NAME}: ${msg}`); + reportError(msg); + } +} + +function getStandardTargeting (obj) { + if (obj) { + return { + b: obj.hb_bidder || '', + sz: obj.hb_size || '', + pb: obj.hb_pb || '', + fmt: obj.hb_format || '' + } + } +} +function getEventTimestamps (eventName, auctionId) { + const timestamps = { + t: Date.now() - START_TIME + }; + if (!auctionId || !auctionsData[auctionId]) { + return timestamps + } + const auctionData = auctionsData[auctionId]; + + timestamps.to = auctionData.timeout; + timestamps.ts = auctionData.start - START_TIME; + if (auctionData.end) { + timestamps.te = auctionData.end - START_TIME; + } + if (eventName === EVENT_MAP[EVENTS.AUCTION_INIT] && auctionCount > 1) { + timestamps.tprev = auctionsData[previousAuction].start - START_TIME; + } + return timestamps +} + +function createEvent (name, data, auctionId) { + if (!auctionId || !auctionsData[auctionId]) { + reportError('No auction data when creating event', { + event: name, + auctionId: !!auctionId + }); + return null + } + if (auctionsData[auctionId] && auctionsData[auctionId].empty) { + return null + } + + data = data || {}; + data.ai = auctionId; + + return { + e: name, + d: data, + t: getEventTimestamps(name, auctionId) + } +} + +function createAuctionData (auction, empty) { + const auctionId = auction.auctionId; + previousAuction = latestAuction; + latestAuction = auctionId; + auctionCount++; + auctionsData[auctionId] = { + start: auction.timestamp, + end: auction.auctionEnd ? auction.auctionEnd : null, + timeout: auction.timeout, + empty: !!empty, + t: Date.now(), + }; +} +function handleAuctionInit (args) { + // console.log('auction init:', arguments); + createAuctionData(args); + const auctionId = args.auctionId; + const bidderRequests = args.bidderRequests || []; + const data = { + o: auctionCount, + u: bidderRequests.reduce((result, bidderRequest) => { + bidderRequest.bids.forEach((bid) => { + if (!result[bid.adUnitCode]) { + result[bid.adUnitCode] = [] + } + result[bid.adUnitCode].push(bid.bidder) + }); + return result + }, {}) + }; + const event = createEvent(EVENT_MAP[EVENTS.AUCTION_INIT], data, auctionId); + processEvent(event); +} +function handleBidRequested (args) { + // console.log('bid request:', arguments); + const data = { + b: args.bidderCode, + u: args.bids.reduce((result, bid) => { + if (!result[bid.adUnitCode]) { + result[bid.adUnitCode] = 1 + } else { + result[bid.adUnitCode]++ + } + return result + }, {}) + }; + const event = createEvent(EVENT_MAP[EVENTS.BID_REQUESTED], data, args.auctionId); + processEvent(event); +} +function handleBidTimeout (args) { + // console.log('bid timeout:', arguments); + const auctionId = args.length ? args[0].auctionId : null; + if (auctionId) { + let bidders = args.reduce((result, bid) => { + if (!result[bid.bidder]) { + result[bid.bidder] = {} + } + const bidderData = result[bid.bidder]; + if (!bidderData[bid.adUnitCode]) { + bidderData[bid.adUnitCode] = 1 + } else { + bidderData[bid.adUnitCode]++ + } + return result + }, {}); + + const data = { + b: bidders, + } + const event = createEvent(EVENT_MAP[EVENTS.BID_TIMEOUT], data, auctionId); + processEvent(event); + } +} +function handleNoBid (args) { + // console.log('no bid:', arguments); + const data = { + b: args.bidder, + u: args.adUnitCode + }; + const event = createEvent(EVENT_MAP[EVENTS.NO_BID], data, args.auctionId); + processEvent(event); +} +function handleBidResponse (args) { + // console.log('bid response:', arguments); + bidsData[args.adId] = { + id: args.requestId, + auctionId: args.auctionId, + t: Date.now(), + }; + const data = { + b: args.bidder, + u: args.adUnitCode, + p: args.cpm, + op: args.originalCpm, + c: args.currency, + oc: args.originalCurrency, + sz: args.size, + st: args.status, + rt: args.timeToRespond, + bi: args.requestId, + }; + const event = createEvent(EVENT_MAP[EVENTS.BID_RESPONSE], data, args.auctionId); + processEvent(event); +} +function handleBidRejected (args) { + // console.log('bid rejected:', arguments); + const data = { + b: args.bidder, + u: args.adUnitCode, + p: args.cpm, + c: args.currency, + r: args.rejectionReason, + bi: args.requestId, + }; + const event = createEvent(EVENT_MAP[EVENTS.BID_REJECTED], data, args.auctionId); + processEvent(event); +} +function handleBidderDone (args) { + // console.log('bidder done:', arguments); + const data = { + b: args.bidderCode + }; + const event = createEvent(EVENT_MAP[EVENTS.BIDDER_DONE], data, args.auctionId); + processEvent(event); +} +function getAuctionUnitsData (auctionObject) { + let unitsData = {}; + const {bidsReceived, bidsRejected} = auctionObject; + let _unitsDataBidReducer = function(data, bid, key) { + const {adUnitCode, bidder} = bid; + data[adUnitCode] = data[adUnitCode] || {}; + data[adUnitCode][key] = data[adUnitCode][key] || {}; + data[adUnitCode][key][bidder] = (data[adUnitCode][key][bidder] || 0) + 1; + return data + }; + unitsData = bidsReceived.reduce((data, bid) => { + if (!bid.cpm) return data; + return _unitsDataBidReducer(data, bid, 'b') + }, unitsData); + unitsData = bidsRejected.reduce((data, bid) => { + return _unitsDataBidReducer(data, bid, 'rj') + }, unitsData); + return unitsData +} +function handleEmptyAuction(auction) { + let auctionId = auction.auctionId; + if (!auctionsData[auctionId]) { + createAuctionData(auction, true); + } +} +function handleAuctionEnd (args) { + // console.log('auction end:', arguments); + if (!args.bidderRequests.length) { + handleEmptyAuction(args); + return + } + auctionsData[args.auctionId].end = args.auctionEnd; + let winningBids = getGlobal().getHighestCpmBids() || []; + if (winningBids.length === 0) { + winningBids = getGlobal().getAllWinningBids() || []; + } + const wins = []; + winningBids.forEach((bid) => { + if (bid.auctionId === args.auctionId) { + wins.push({ + b: bid.bidder, + u: bid.adUnitCode, + p: bid.cpm, + c: bid.currency, + sz: bid.size, + bi: bid.requestId, + }) + } + }); + const data = { + wins, + u: getAuctionUnitsData(args), + o: auctionCount, + bc: args.bidsReceived.length, + nbc: args.noBids.length, + rjc: args.bidsRejected.length, + brc: args.bidderRequests.reduce((count, bidderRequest) => { + const c = bidderRequest.bids.length || 0; + return count + c + }, 0) + }; + const event = createEvent(EVENT_MAP[EVENTS.AUCTION_END], data, args.auctionId); + processEvent(event); +} +function handleBidWon (args) { + // console.log('bid won:', arguments); + const data = { + b: args.bidder, + u: args.adUnitCode, + p: args.cpm, + op: args.originalCpm, + c: args.currency, + oc: args.originalCurrency, + sz: args.size, + mt: args.mediaType, + at: getStandardTargeting(args.adserverTargeting), + o: auctionCount, + bi: args.requestId, + }; + const event = createEvent(EVENT_MAP[EVENTS.BID_WON], data, args.auctionId); + processEvent(event); +} +function handleSetTargeting (args) { + // console.log('set targeting:', arguments); + let adId; + const filteredTargetings = {}; + Object.keys(args).forEach((unit) => { + if (Object.keys(args[unit]).length) { + if (!adId) { + adId = args[unit].hb_adid + } + filteredTargetings[unit] = getStandardTargeting(args[unit]); + } + }); + if (adId) { + const auctionId = bidsData[adId].auctionId; + const data = { + u: filteredTargetings + } + const event = createEvent(EVENT_MAP[EVENTS.SET_TARGETING], data, auctionId); + processEvent(event); + } +} +function handleStaleRender (args) { + // console.log('stale render:', arguments); + const data = { + b: args.bidder, + u: args.adUnitCode, + p: args.cpm, + c: args.currency, + bi: args.requestId, + }; + const event = createEvent(EVENT_MAP[EVENTS.STALE_RENDER], data, args.auctionId); + processEvent(event); +} +function handleRenderSuccess (args) { + // console.log('render success:', arguments); + const {bid} = args; + bidsData[bid.adId].renderTime = Date.now(); + const data = { + b: bid.bidder, + u: bid.adUnitCode, + p: bid.cpm, + c: bid.currency, + sz: bid.size, + mt: bid.mediaType, + bi: bid.requestId, + }; + const event = createEvent(EVENT_MAP[EVENTS.AD_RENDER_SUCCEEDED], data, bid.auctionId); + processEvent(event); +} +function handleRenderFailed (args) { + // console.log('render failed:', arguments); + const {bid, reason} = args; + const data = { + b: bid.bidder, + u: bid.adUnitCode, + p: bid.cpm, + c: bid.currency, + r: reason, + bi: bid.requestId, + }; + const event = createEvent(EVENT_MAP[EVENTS.AD_RENDER_FAILED], data, bid.auctionId); + processEvent(event); +} +function handleBidViewable (args) { + // console.log('bid viewable:', arguments); + const renderTime = bidsData[args.adId].renderTime; + const data = { + b: args.bidder, + u: args.adUnitCode, + rt: Date.now() - renderTime, + bi: args.requestId, + }; + const event = createEvent(EVENT_MAP[EVENTS.BID_VIEWABLE], data, args.auctionId); + processEvent(event); +} + +let baseAdapter = adapter({analyticsType}); +let r2b2Analytics = Object.assign({}, baseAdapter, { + getUrl() { + return `${DEFAULT_PROTOCOL}://${LOG_SERVER}/${DEFAULT_EVENT_PATH}` + }, + getErrorUrl() { + return `${DEFAULT_PROTOCOL}://${LOG_SERVER}/${DEFAULT_ERROR_PATH}` + }, + enableAnalytics(conf = {}) { + if (isPlainObject(conf.options)) { + const {domain, configId, configVer, server} = conf.options; + if (!domain || !isStr(domain)) { + logWarn(`${MODULE_NAME}: Mandatory parameter 'domain' not configured, analytics disabled`); + return + } + WEBSITE = domain + if (server) { + if (isStr(server)) { + LOG_SERVER = server + } else { + logWarn(`options.server must be a string`); + } + } + if (configId) { + if (isNumber(configId)) { + CONFIG_ID = configId + } else { + logWarn(`options.configId must be a number`); + } + } + if (configVer) { + if (isNumber(configVer)) { + CONFIG_VERSION = configVer + } else { + logWarn(`options.configVer must be a number`); + } + } + } + baseAdapter.enableAnalytics.call(this, conf); + }, + track(event) { + const {eventType, args} = event; + try { + if (!adServerCurrency) { + const currencyObj = config.getConfig('currency'); + adServerCurrency = (currencyObj && currencyObj.adServerCurrency) || 'USD'; + } + switch (eventType) { + case EVENTS.NO_BID: + handleNoBid(args) + break; + case EVENTS.AUCTION_INIT: + handleAuctionInit(args) + break; + case EVENTS.BID_REQUESTED: + handleBidRequested(args) + break; + case EVENTS.BID_TIMEOUT: + handleBidTimeout(args) + break; + case EVENTS.BID_RESPONSE: + handleBidResponse(args) + break; + case EVENTS.BID_REJECTED: + handleBidRejected(args) + break; + case EVENTS.BIDDER_DONE: + handleBidderDone(args) + break; + case EVENTS.AUCTION_END: + handleAuctionEnd(args) + break; + case EVENTS.BID_WON: + handleBidWon(args) + break; + case EVENTS.SET_TARGETING: + handleSetTargeting(args) + break; + case EVENTS.STALE_RENDER: + handleStaleRender(args) + break; + case EVENTS.AD_RENDER_SUCCEEDED: + handleRenderSuccess(args) + break; + case EVENTS.AD_RENDER_FAILED: + handleRenderFailed(args) + break; + case EVENTS.BID_VIEWABLE: + handleBidViewable(args) + break; + } + } catch (e) { + reportError(`${eventType} - ${e.message}`) + } + } +}); + +// save the base class function +r2b2Analytics.originEnableAnalytics = r2b2Analytics.enableAnalytics; + +// override enableAnalytics so we can get access to the config passed in from the page +r2b2Analytics.enableAnalytics = function (config) { + r2b2Analytics.originEnableAnalytics(config); // call the base class function +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: r2b2Analytics, + code: ADAPTER_CODE, + gvlid: GVLID +}); + +export default r2b2Analytics; diff --git a/modules/r2b2AnalyticsAdapter.md b/modules/r2b2AnalyticsAdapter.md new file mode 100644 index 00000000000..484339be106 --- /dev/null +++ b/modules/r2b2AnalyticsAdapter.md @@ -0,0 +1,32 @@ +# Overview + +``` +Module Name: R2B2 Analytics Adapter +Module Type: Analytics Adapter +Maintainer: dev@r2b2.cz +``` + +## Description + +The R2B2 Analytics Adapter enables data collection for analysis and reporting purposes. Access to collected data and the ability to start data collection require prior approval from R2B2. For approval, please contact our account team on partner@r2b2.io. + +## How to configure? + +``` +pbjs.enableAnalytics({ + provider: 'r2b2', + options: { + domain: 'example.com', + configId: 1, + configVer: 1 + } +}); +``` + +### Options + +| Name | Scope | Example | Type | Description | +|-----------|----------|-------------|----------|----------------------------------------------------------------| +| `domain` | required | example.com | `string` | R2B2 approved domain where data collection occurs | +| `configId` | optional | 1 | `int` | Identifier for different configurations under the same domain (e.g., 1 for mobile, 2 for desktop) | +| `configVer` | optional | 1 | `int` | Version number for configurations sharing the same `configId` | diff --git a/modules/raynRtdProvider.js b/modules/raynRtdProvider.js index d558c360c4a..ee3d18be381 100644 --- a/modules/raynRtdProvider.js +++ b/modules/raynRtdProvider.js @@ -14,6 +14,7 @@ import { deepAccess, deepSetValue, logError, logMessage, mergeDeep } from '../sr const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'rayn'; const RAYN_TCF_ID = 1220; +const RAYN_PERSONA_TAXONOMY_ID = 103015; const LOG_PREFIX = 'RaynJS: '; export const SEGMENTS_RESOLVER = 'rayn.io'; export const RAYN_LOCAL_STORAGE_KEY = 'rayn-segtax'; @@ -77,6 +78,32 @@ export function generateOrtbDataObject(segtax, segment, maxTier) { }; } +/** + * Create and return ORTB2 object with segtax and personaIds + * @param {number} segtax + * @param {Array} personaIds + * @return {Array} + */ +export function generatePersonaOrtbDataObject(segtax, personaIds) { + const segmentIds = []; + + try { + segmentIds.push(...personaIds.map((id) => { + return { id }; + })) + } catch (error) { + logError(LOG_PREFIX, error); + } + + return { + name: SEGMENTS_RESOLVER, + ext: { + segtax, + }, + segment: segmentIds, + }; +} + /** * Generates checksum * @param {string} url @@ -127,8 +154,14 @@ export function setSegmentsAsBidderOrtb2(bidConfig, bidders, integrationConfig, deepSetValue(raynOrtb2, 'site.content.data', raynContentData); } + const raynUserData = []; if (integrationConfig.iabAudienceCategories.v1_1.enabled && segments[4]) { - const raynUserData = [generateOrtbDataObject(4, segments[4], integrationConfig.iabAudienceCategories.v1_1.tier)]; + raynUserData.push(generateOrtbDataObject(4, segments[4], integrationConfig.iabAudienceCategories.v1_1.tier)); + } + if (segments[RAYN_PERSONA_TAXONOMY_ID]) { + raynUserData.push(generatePersonaOrtbDataObject(RAYN_PERSONA_TAXONOMY_ID, segments[RAYN_PERSONA_TAXONOMY_ID])); + } + if (raynUserData.length > 0) { deepSetValue(raynOrtb2, 'user.data', raynUserData); } @@ -163,8 +196,8 @@ function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) { segments[checksum] || (segments[4] && integrationConfig.iabAudienceCategories.v1_1.enabled && !integrationConfig.iabContentCategories.v2_2.enabled && - !integrationConfig.iabContentCategories.v3_0.enabled - ) + !integrationConfig.iabContentCategories.v3_0.enabled) || + segments[RAYN_PERSONA_TAXONOMY_ID] )) { logMessage(LOG_PREFIX, `Segtax data from localStorage: ${JSON.stringify(segments)}`); setSegmentsAsBidderOrtb2(reqBidsConfigObj, bidders, integrationConfig, segments, checksum); diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index 4ff51aeb43e..da3153c0b68 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -136,7 +136,7 @@ function impression(slot) { mediaType: 'native', size: '\*' }); - bidFloorFromModule = floorInfo.currency === 'USD' ? floorInfo.floor : undefined; + bidFloorFromModule = floorInfo?.currency === 'USD' ? floorInfo?.floor : undefined; } const imp = { id: slot.bidId, diff --git a/modules/rediadsBidAdapter.js b/modules/rediadsBidAdapter.js new file mode 100644 index 00000000000..7f96135aace --- /dev/null +++ b/modules/rediadsBidAdapter.js @@ -0,0 +1,110 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepSetValue, logWarn, logError } from '../src/utils.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'rediads'; +const ENDPOINT_URL = 'https://bidding.rediads.com/openrtb2/auction'; +const STAGING_ENDPOINT_URL = 'https://stagingbidding.rediads.com/openrtb2/auction'; +const DEFAULT_CURRENCY = 'USD'; +const LOG_PREFIX = 'Rediads: '; + +const MEDIA_TYPES = { + [BANNER]: 1, + [VIDEO]: 2, + [NATIVE]: 4, +}; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: DEFAULT_CURRENCY, + }, + bidResponse(buildBidResponse, bid, context) { + let mediaType = 'banner'; // Default media type + + if (bid.vastXml || bid.vastUrl || (bid.adm && bid.adm.startsWith(' { + const bidRequest = bidRequests[idx]; + if (bidRequest?.params?.slot) { + impression.tagid = bidRequest?.params?.slot; + } + }); + } catch (err) { + logError(`${LOG_PREFIX} encountered an error while building bid requests :: ${err}`) + } + + return [ + { + method: 'POST', + url: FINAL_ENDPOINT_URL, + data, + }, + ]; + }, + interpretResponse(response, request) { + let bids = []; + try { + bids = converter.fromORTB({ + response: response.body, + request: request.data, + }).bids + } catch (err) { + logError(`${LOG_PREFIX} encountered an error while processing bid responses :: ${err}`) + } + return bids; + }, +}; + +registerBidder(spec); diff --git a/modules/rediadsBidAdapter.md b/modules/rediadsBidAdapter.md new file mode 100644 index 00000000000..abc40664de2 --- /dev/null +++ b/modules/rediadsBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: RediAds Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@rediads.com +``` + +# Description + +Connect to RediAds exchange for bids. + +The RediAds adapter requires setup and approval. +Please reach out to support@rediads.com for more information. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: "rediads", + params: { + account_id: '123', + slot: '321', // optional + endpoint: 'https://bidding.rediads.com/openrtb2/auction' // optional, only to be used if rediads team provides one + } + } + ] + } + ]; +``` diff --git a/modules/redtramBidAdapter.js b/modules/redtramBidAdapter.js index e1dc0e2a148..bacc209f991 100644 --- a/modules/redtramBidAdapter.js +++ b/modules/redtramBidAdapter.js @@ -1,155 +1,21 @@ -import { - isFn, - isStr, - deepAccess, - getWindowTop, - triggerPixel -} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { bidWinReport, buildBidRequests, buildUserSyncs, interpretResponse, isBidRequestValid } from '../libraries/precisoUtils/bidUtilsCommon.js'; const BIDDER_CODE = 'redtram'; const AD_URL = 'https://prebid.redtram.com/pbjs'; const SYNC_URL = 'https://prebid.redtram.com/sync'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency || !bid.meta) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - default: - return false; - } -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidFloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], - - isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && bid.params.placementId); - }, - - buildRequests: (validBidRequests = [], bidderRequest) => { - const winTop = getWindowTop(); - const location = winTop.location; - const placements = []; - - const request = { - deviceWidth: winTop.screen.width, - deviceHeight: winTop.screen.height, - language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - host: location.host, - page: location.pathname, - placements: placements - }; - - if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } - if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent; - } - } - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - const placement = { - placementId: bid.params.placementId, - bidId: bid.bidId, - schain: bid.schain || {}, - bidfloor: getBidFloor(bid) - }; - - if (typeof bid.userId !== 'undefined') { - placement.userId = bid.userId; - } - - const mediaType = bid.mediaTypes; - - if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { - placement.sizes = mediaType[BANNER].sizes; - placement.adFormat = BANNER; - } - - placements.push(placement); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - + isBidRequestValid: isBidRequestValid, + buildRequests: buildBidRequests(AD_URL), + interpretResponse: interpretResponse, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; + return buildUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, SYNC_URL); }, - - onBidWon: (bid) => { - const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; - if (isStr(bid.nurl) && bid.nurl !== '') { - bid.nurl = bid.nurl.replace(/\${AUCTION_PRICE}/, cpm); - triggerPixel(bid.nurl); - } - } + onBidWon: bidWinReport }; registerBidder(spec); diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index a180d04cc71..69f9be8d107 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -19,7 +19,7 @@ import { isSlotMatchingAdUnitCode } from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'relaido'; const BIDDER_DOMAIN = 'api.relaido.jp'; -const ADAPTER_VERSION = '1.2.0'; +const ADAPTER_VERSION = '1.2.1'; const DEFAULT_TTL = 300; const UUID_KEY = 'relaido_uuid'; @@ -270,6 +270,7 @@ function outstreamRender(bid) { height: bid.height, vastXml: bid.vastXml, mediaType: bid.mediaType, + placementId: bid.placementId, }); }); } diff --git a/modules/relevatehealthBidAdapter.js b/modules/relevatehealthBidAdapter.js new file mode 100644 index 00000000000..560dbdeac3e --- /dev/null +++ b/modules/relevatehealthBidAdapter.js @@ -0,0 +1,144 @@ +import { formatResponse } from '../libraries/deepintentUtils/index.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { + BANNER +} from '../src/mediaTypes.js'; +import { + deepAccess, + generateUUID, + isArray, + logError +} from '../src/utils.js'; +const BIDDER_CODE = 'relevatehealth'; +const ENDPOINT_URL = 'https://rtb.relevate.health/prebid/relevate'; + +function buildRequests(bidRequests, bidderRequest) { + const requests = []; + // Loop through each bid request + bidRequests.forEach(bid => { + // Construct the bid request object + const request = { + id: generateUUID(), + placementId: bid.params.placement_id, + imp: [{ + id: bid.bidId, + banner: getBanner(bid), + bidfloor: getFloor(bid) + }], + site: getSite(bidderRequest), + user: buildUser(bid) + }; + // Get uspConsent from bidderRequest + if (bidderRequest && bidderRequest.uspConsent) { + request.us_privacy = bidderRequest.uspConsent; + } + // Get GPP Consent from bidderRequest + if (bidderRequest?.gppConsent?.gppString) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest?.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + // Get coppa compliance from bidderRequest + if (bidderRequest?.ortb2?.regs?.coppa) { + request.coppa = 1; + } + // Push the constructed bid request to the requests array + requests.push(request); + }); + // Return the array of bid requests + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(requests), + options: { + contentType: 'application/json', + } + }; +} +// Format the response as per the standards +function interpretResponse(bidResponse, bidRequest) { + let resp = []; + if (bidResponse && bidResponse.body) { + try { + let bids = bidResponse.body.seatbid && bidResponse.body.seatbid[0] ? bidResponse.body.seatbid[0].bid : []; + if (bids) { + bids.forEach(bidObj => { + let newBid = formatResponse(bidObj); + newBid.mediaType = BANNER; + resp.push(newBid); + }); + } + } catch (err) { + logError(err); + } + } + return resp; +} +// Function to check if Bid is valid +function isBidRequestValid(bid) { + return !!(bid.params.placement_id && bid.params.user_id); +} +// Function to get banner details +function getBanner(bid) { + if (deepAccess(bid, 'mediaTypes.banner')) { + // Fetch width and height from MediaTypes object, if not provided in bid params + if (deepAccess(bid, 'mediaTypes.banner.sizes') && !bid.params.height && !bid.params.width) { + let sizes = deepAccess(bid, 'mediaTypes.banner.sizes'); + if (isArray(sizes) && sizes.length > 0) { + return { + h: sizes[0][1], + w: sizes[0][0] + }; + } + } else { + return { + h: bid.params.height, + w: bid.params.width + }; + } + } +} +// Function to get bid_floor +function getFloor(bid) { + if (bid.params && bid.params.bid_floor) { + return bid.params.bid_floor; + } else { + return 0; + } +} +// Function to get site details +function getSite(bidderRequest) { + let site = {}; + if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { + site.name = bidderRequest.refererInfo.domain; + } else { + site.name = ''; + } + return site; +} +// Function to build the user object +function buildUser(bid) { + if (bid && bid.params) { + return { + id: bid.params.user_id && typeof bid.params.user_id == 'string' ? bid.params.user_id : '', + // TODO: commented out because of rule violations + buyeruid: '', // localStorage.getItem('adx_profile_guid') ? localStorage.getItem('adx_profile_guid') : '', + keywords: bid.params.keywords && typeof bid.params.keywords == 'string' ? bid.params.keywords : '', + customdata: bid.params.customdata && typeof bid.params.customdata == 'string' ? bid.params.customdata : '' + }; + } +} +// Export const spec +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: BANNER, + isBidRequestValid, + buildRequests, + interpretResponse +} + +registerBidder(spec); diff --git a/modules/relevatehealthBidAdapter.md b/modules/relevatehealthBidAdapter.md new file mode 100644 index 00000000000..432e4fcec02 --- /dev/null +++ b/modules/relevatehealthBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: relevatehealth Bidder Adapter +Module Type: Bidder Adapter +Maintainer: marketingops@relevatehealth.com +``` + +# Description + +relevatehealth currently supports the BANNER type ads through prebid js + +Module that connects to relevatehealth's demand sources. + +# Banner Test Request +``` + var adUnits = [ + { + code: 'banner-ad', + mediaTypes: { + banner: { + sizes: [[160, 600]], + } + } + bids: [ + { + bidder: 'relevatehealth', + params: { + placement_id: 110011, // Required parameter + user_id: '1111111' // Required parameter + width: 160, // Optional parameter + height: 600, // Optional parameter + domain: '', // Optional parameter + bid_floor: 0.5 // Optional parameter + } + } + ] + } + ]; +``` diff --git a/modules/resetdigitalBidAdapter.js b/modules/resetdigitalBidAdapter.js index 1fe4b4d750c..2110437f3ea 100644 --- a/modules/resetdigitalBidAdapter.js +++ b/modules/resetdigitalBidAdapter.js @@ -1,4 +1,4 @@ -import { timestamp, deepAccess, isStr, deepClone } from '../src/utils.js'; +import { timestamp, deepAccess, isStr, deepClone, isPlainObject } from '../src/utils.js'; import { getOrigin } from '../libraries/getOrigin/index.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -92,7 +92,7 @@ export const spec = { size: '*', }); if ( - typeof floorInfo === 'object' && + isPlainObject(floorInfo) && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor)) ) { @@ -101,10 +101,16 @@ export const spec = { } } - // get param kewords (if it exists) + // get param keywords (if it exists) let paramsKeywords = req.params.keywords - ? req.params.keywords.split(',') - : []; + + if (typeof req.params.keywords === 'string') { + paramsKeywords = req.params.keywords.split(','); + } else if (Array.isArray(req.params.keywords)) { + paramsKeywords = req.params.keywords; + } else { + paramsKeywords = []; + } // merge all keywords let keywords = ortb2KeywordsList .concat(paramsKeywords) diff --git a/modules/resetdigitalBidAdapter.md b/modules/resetdigitalBidAdapter.md index a368c7f5633..64b600a7cc0 100644 --- a/modules/resetdigitalBidAdapter.md +++ b/modules/resetdigitalBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: ResetDigital Bidder Adapter Module Type: Bidder Adapter -Maintainer: bruce@resetdigital.co +Maintainer: BidderSupport@resetdigital.co ``` # Description @@ -15,49 +15,58 @@ Video is supported but requires a publisher supplied renderer at this time. ## Web ``` - var adUnits = [ - { - code: 'your-div', - mediaTypes: { - banner: { - sizes: [[300,250]] - }, - - }, - bids: [ - { - bidder: "resetdigital", - params: { - pubId: "your-pub-id", - site_id: "your-site-id", - forceBid: true, - } - } - ] - } - ]; - - - var videoAdUnits = [ - { - code: 'your-div', - mediaTypes: { - video: { - playerSize: [640, 480] - }, - - }, - bids: [ - { - bidder: "resetdigital", - params: { - pubId: "your-pub-id", - site_id: "your-site-id", - forceBid: true, - } - } - ] - } - ]; +// Define the ad units for banner ads +var adUnits = [ + { + code: 'your-div', // Replace with the actual ad unit code + mediaTypes: { + banner: { + sizes: [[300, 250]] // Define the sizes for banner ads + } + }, + bids: [ + { + bidder: "resetdigital", + params: { + pubId: "your-pub-id", // (required) Replace with your publisher ID + site_id: "your-site-id", // Replace with your site ID + endpoint: 'https://ads.resetsrv.com', // (optional) Endpoint URL for the ad server + forceBid: true, // Optional parameter to force the bid + zoneId: { // (optional) Zone ID parameters + placementId: "", // Optional ID used for reports + deals: "", // Optional string of deal IDs, comma-separated + test: 1 // Set to 1 to force the bidder to respond with a creative + } + } + } + ] + } +]; +// Define the ad units for video ads +var videoAdUnits = [ + { + code: 'your-div', // Replace with the actual video ad unit code + mediaTypes: { + video: { + playerSize: [640, 480] // Define the player size for video ads + } + }, + bids: [ + { + bidder: "resetdigital", + params: { + pubId: "your-pub-id", // (required) Replace with your publisher ID + site_id: "your-site-id", // Replace with your site ID + forceBid: true, // Optional parameter to force the bid + zoneId: { // (optional) Zone ID parameters + placementId: "", // Optional ID used for reports + deals: "", // Optional string of deal IDs, comma-separated + test: 1 // Set to 1 to force the bidder to respond with a creative + } + } + } + ] + } +]; ``` diff --git a/modules/responsiveAdsBidAdapter.js b/modules/responsiveAdsBidAdapter.js new file mode 100644 index 00000000000..a33a52f5644 --- /dev/null +++ b/modules/responsiveAdsBidAdapter.js @@ -0,0 +1,80 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { + logMessage, + isSafeFrameWindow, + mergeDeep, + canAccessWindowTop, +} from '../src/utils.js'; + +const BIDDER_VERSION = '1.0'; +const BIDDER_CODE = 'responsiveads'; +const ENDPOINT_URL = 'https://ve60c4xzl9.execute-api.us-east-1.amazonaws.com/prod/prebidjs'; +const DEFAULT_CURRENCY = 'USD'; +const GVLID = 1189; + +const converter = ortbConverter({ + context: { + mediaType: BANNER, + netRevenue: true, + ttl: 300, + currency: DEFAULT_CURRENCY, + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + // add additional information we might need on the backend + mergeDeep(req, { + ext: { + prebid: { + adapterVersion: `${BIDDER_VERSION}`, + }, + }, + }); + return req; + }, +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + isBidRequestValid: function(bid) { + // validate the bid request + return !!(bid.params); + }, + buildRequests: function(bidRequests, bidderRequest) { + // we only want to bid if we are not in a safeframe + if (isSafeFrameWindow()) { + return null; + } + + // if we can't access top we don't want to bid + if (!canAccessWindowTop()) { + return null; + } + const data = converter.toORTB({ bidRequests, bidderRequest }); + return { + method: 'POST', + url: ENDPOINT_URL, + data: data, + options: { + contentType: 'application/json', + withCredentials: false + }, + bidderRequest + }; + }, + interpretResponse: function(response, request) { + const res = converter.fromORTB({ response: response.body, request: request.data }); + const bids = res.bids; + return bids; + }, + + onBidWon: (bid) => { + logMessage('onBidWon', bid); + } + +}; + +registerBidder(spec); diff --git a/modules/responsiveAdsBidAdapter.md b/modules/responsiveAdsBidAdapter.md new file mode 100644 index 00000000000..af3b59e80e3 --- /dev/null +++ b/modules/responsiveAdsBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +```markdown +Module Name: responsiveAdsBidAdapter +Module Type: Bidder Adapter +Maintainer: support@responsiveads.com +``` + + +# Description +Module that connects to ResponsiveAds Programmatic Fluid demand. + + +## Running the code +To view an example of the on page setup required: + +```bash +gulp serve-and-test --file test/spec/modules/responsiveAdsBidAdapter_spec.js +``` + +# Test Parameters +``` +var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'responsiveads', + params: {} + }] + +}]; +``` diff --git a/modules/retailspotBidAdapter.js b/modules/retailspotBidAdapter.js index 557dd617274..da8e46bec81 100644 --- a/modules/retailspotBidAdapter.js +++ b/modules/retailspotBidAdapter.js @@ -5,17 +5,21 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest */ const BIDDER_CODE = 'retailspot'; -const DEFAULT_SUBDOMAIN = 'ssp'; -const PREPROD_SUBDOMAIN = 'ssp-preprod'; -const HOST = 'retail-spot.io'; -const ENDPOINT = '/prebid'; -const DEV_URL = 'http://localhost:8090/prebid'; +const GVL_ID = 1319; + +const DEFAULT_SUBDOMAIN = 'hbapi'; +const PREPROD_SUBDOMAIN = 'hbapi-preprod'; +const HOST = 'retailspotads.com'; +const ENDPOINT = '/'; +const DEV_URL = 'http://localhost:3030/'; export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], aliases: ['rs'], // short code /** @@ -25,7 +29,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - const sizes = getSize(getSizeArray(bid)); + const sizes = getSize(bid); const sizeValid = sizes.width > 0 && sizes.height > 0; return deepAccess(bid, 'params.placement') && sizeValid; @@ -33,7 +37,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {BidRequests} - bidRequests.bids[] is an array of AdUnits and bids + * @param {BidRequest} bidRequests is an array of AdUnits and bids + * @param {BidderRequest} bidderRequest * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { @@ -96,40 +101,39 @@ export const spec = { } } -function getSizeArray(bid) { +/* Get parsed size from request size */ +function getSize(bid) { let inputSize = bid.sizes || []; - if (bid.mediaTypes && bid.mediaTypes.banner) { + if (bid.mediaTypes?.banner) { inputSize = bid.mediaTypes.banner.sizes || []; } - // handle size in bid.params in formats: [w, h] and [[w,h]]. - if (bid.params && Array.isArray(bid.params.size)) { + // Size can be [w, h] or array of sizes : [[w,h]]. + if (Array.isArray(bid.params?.size)) { inputSize = bid.params.size; if (!Array.isArray(inputSize[0])) { inputSize = [inputSize] } } - return parseSizesInput(inputSize); -} - -/* Get parsed size from request size */ -function getSize(sizesArray) { + const sizesArray = parseSizesInput(inputSize); const parsed = {}; - // the main requested size is the first one + + // Use the first size as the main requested one const size = sizesArray[0]; + // size is ready if (typeof size !== 'string') { return parsed; } - const parsedSize = size.toUpperCase().split('X'); + // size is given as string "wwwxhhh" or "www*hhh" + const parsedSize = size.includes('*') ? size.split('*') : size.toUpperCase().split('X'); const width = parseInt(parsedSize[0], 10); if (width) { parsed.width = width; } - const height = parseInt(parsedSize[1], 10); if (height) { parsed.height = height; diff --git a/modules/retailspotBidAdapter .md b/modules/retailspotBidAdapter.md similarity index 92% rename from modules/retailspotBidAdapter .md rename to modules/retailspotBidAdapter.md index a9b4cb4bec3..1f56f66fbc8 100644 --- a/modules/retailspotBidAdapter .md +++ b/modules/retailspotBidAdapter.md @@ -25,7 +25,7 @@ Banner and Video ad formats are supported. bids: [{ bidder: "retailspot", params: { - placement: "test-12345" + placement: "eq-609785-1856964-125234" } }] }; diff --git a/modules/revcontentBidAdapter.js b/modules/revcontentBidAdapter.js index f1d5521f780..ce04e3aa822 100644 --- a/modules/revcontentBidAdapter.js +++ b/modules/revcontentBidAdapter.js @@ -207,7 +207,7 @@ function buildImp(bid, id) { currency: 'USD', mediaType: '*', size: '*' - }).floor; + })?.floor; } else { bidfloor = deepAccess(bid, `params.bidfloor`) || 0.1; } diff --git a/modules/rewardedInterestIdSystem.js b/modules/rewardedInterestIdSystem.js new file mode 100644 index 00000000000..8cf514f372b --- /dev/null +++ b/modules/rewardedInterestIdSystem.js @@ -0,0 +1,142 @@ +/** + * This module adds rewarded interest ID to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/rewardedInterestIdSystem + * @requires module:modules/userId + */ + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +/** + * @typedef RewardedInterestApi + * @property {getApiVersion} getApiVersion + * @property {getIdentityToken} getIdentityToken + */ + +/** + * Retrieves the Rewarded Interest API version. + * @callback getApiVersion + * @return {string} + */ + +/** + * Retrieves the current identity token. + * @callback getIdentityToken + * @return {Promise} + */ + +import {submodule} from '../src/hook.js'; +import {logError} from '../src/utils.js'; + +export const MODULE_NAME = 'rewardedInterestId'; +export const SOURCE = 'rewardedinterest.com'; + +/** + * Get rewarded interest API + * @function + * @returns {RewardedInterestApi|undefined} + */ +export function getRewardedInterestApi() { + if (window.__riApi && window.__riApi.getIdentityToken) { + return window.__riApi; + } +} + +/** + * Wait while rewarded interest API to be set and execute the callback function + * @param {function} callback + */ +export function watchRewardedInterestApi(callback) { + Object.defineProperties(window, { + __rewardedInterestApi: { + value: undefined, + writable: true + }, + __riApi: { + get: () => { + return window.__rewardedInterestApi; + }, + set: value => { + window.__rewardedInterestApi = value; + callback(value); + }, + configurable: true, + } + }); +} + +/** + * Get rewarded interest ID from API and pass it to the callback function + * @param {RewardedInterestApi} rewardedInterestApi + * @param {function} callback User ID callbackCompleted + */ +export function getRewardedInterestId(rewardedInterestApi, callback) { + rewardedInterestApi.getIdentityToken().then(callback).catch(error => { + callback(); + logError(`${MODULE_NAME} module: ID fetch encountered an error`, error); + }); +} + +/** + * @param {function} callback User ID callbackCompleted + */ +export function apiNotAvailable(callback) { + callback(); + logError(`${MODULE_NAME} module: Rewarded Interest API not found`); +} + +/** @type {Submodule} */ +export const rewardedInterestIdSubmodule = { + /** + * Used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * Decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{rewardedInterestId: string}|undefined} + */ + decode(value) { + return value ? {[MODULE_NAME]: value} : undefined; + }, + + /** + * Performs action to obtain id and return a value in the callback's response argument + * @function + * @returns {IdResponse|undefined} + */ + getId() { + return { + callback: cb => { + const api = getRewardedInterestApi(); + if (api) { + getRewardedInterestId(api, cb); + } else if (document.readyState === 'complete') { + apiNotAvailable(cb); + } else { + watchRewardedInterestApi(api => getRewardedInterestId(api, cb)); + // Ensure that cb is called when API is not available + window.addEventListener('load', () => { + if (!getRewardedInterestApi()) { + apiNotAvailable(cb); + } + }) + } + }, + }; + }, + eids: { + [MODULE_NAME]: { + source: SOURCE, + atype: 3, + }, + }, +}; + +submodule('userId', rewardedInterestIdSubmodule); diff --git a/modules/rewardedInterestIdSystem.md b/modules/rewardedInterestIdSystem.md new file mode 100644 index 00000000000..8d12aa86e61 --- /dev/null +++ b/modules/rewardedInterestIdSystem.md @@ -0,0 +1,23 @@ +## Rewarded Interest User ID Submodule + +This module adds rewarded interest advertising token to the user ID module + +*Note: The storage config should be omitted + +### Prebid Params + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'rewardedInterestId', + }] + } +}); +``` + +## Parameter Descriptions for the `usersync` Configuration Section + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +|--------------------------------|----------|--------|--------------------------|------------------------| +| name | Required | String | The name of this module. | `"rewardedInterestId"` | diff --git a/modules/rhythmoneBidAdapter.js b/modules/rhythmoneBidAdapter.js index 749ab92c0dc..3ab8b79df81 100644 --- a/modules/rhythmoneBidAdapter.js +++ b/modules/rhythmoneBidAdapter.js @@ -161,7 +161,7 @@ function RhythmOneBidAdapter() { } }, at: 1, - tmax: 1000, + tmax: Math.min(1000, bidderRequest.timeout), regs: { ext: { gdpr: deepAccess(bidderRequest, 'gdprConsent.gdprApplies') ? Boolean(bidderRequest.gdprConsent.gdprApplies & 1) : false diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js old mode 100755 new mode 100644 index b63e31266fb..9bd395fc166 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -1,9 +1,9 @@ -import {deepAccess, isStr, triggerPixel} from '../src/utils.js'; +import {deepAccess, triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; const BIDDER_CODE = 'richaudience'; let REFERER = ''; @@ -11,7 +11,7 @@ let REFERER = ''; export const spec = { code: BIDDER_CODE, gvlid: 108, - aliases: ['ra'], + aliases: [{code: 'ra', gvlid: 108}], supportedMediaTypes: [BANNER, VIDEO], /*** @@ -36,8 +36,7 @@ export const spec = { ifa: bid.params.ifa, pid: bid.params.pid, supplyType: bid.params.supplyType, - currencyCode: config.getConfig('currency.adServerCurrency'), - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 + currencyCode: getCurrencyFromBidderRequest(bidderRequest), auctionId: bid.auctionId, bidId: bid.bidId, BidRequestsCount: bid.bidRequestsCount, @@ -45,22 +44,22 @@ export const spec = { bidderRequestId: bid.bidderRequestId, tagId: bid.adUnitCode, sizes: raiGetSizes(bid), - // TODO: is 'page' the right value here? referer: (typeof bidderRequest.refererInfo.page != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.page) : null), numIframes: (typeof bidderRequest.refererInfo.numIframes != 'undefined' ? bidderRequest.refererInfo.numIframes : null), transactionId: bid.ortb2Imp?.ext?.tid, timeout: bidderRequest.timeout || 600, - user: raiSetEids(bid), + eids: deepAccess(bid, 'userIdAsEids') ? bid.userIdAsEids : [], demand: raiGetDemandType(bid), videoData: raiGetVideoInfo(bid), scr_rsl: raiGetResolution(), cpuc: (typeof window.navigator != 'undefined' ? window.navigator.hardwareConcurrency : null), - kws: getAllOrtbKeywords(bidderRequest.ortb2, bid.params.keywords).join(','), + kws: bid.params.keywords, schain: bid.schain, - gpid: raiSetPbAdSlot(bid) + gpid: raiSetPbAdSlot(bid), + dsa: setDSA(bid), + userData: deepAccess(bid, 'ortb2.user.data') }; - // TODO: is 'page' the right value here? REFERER = (typeof bidderRequest.refererInfo.page != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.page) : null) payload.gdpr_consent = ''; @@ -119,7 +118,9 @@ export const spec = { netRevenue: response.netRevenue, currency: response.currency, ttl: response.ttl, - meta: response.adomain, + meta: { + advertiserDomains: [response.adomain[0]] + }, dealId: response.dealId }; @@ -154,7 +155,7 @@ export const spec = { * * @param {syncOptions} Publisher prebid configuration * @param {serverResponses} Response from the server - * @param {gdprConsent} GPDR consent object + * @param {gdprConsent} GDPR consent object * @returns {Array} */ getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { @@ -265,30 +266,6 @@ function raiGetVideoInfo(bid) { return videoData; } -function raiSetEids(bid) { - let eids = []; - - if (bid && bid.userId) { - raiSetUserId(bid, eids, 'id5-sync.com', deepAccess(bid, `userId.id5id.uid`)); - raiSetUserId(bid, eids, 'pubcommon', deepAccess(bid, `userId.pubcid`)); - raiSetUserId(bid, eids, 'criteo.com', deepAccess(bid, `userId.criteoId`)); - raiSetUserId(bid, eids, 'liveramp.com', deepAccess(bid, `userId.idl_env`)); - raiSetUserId(bid, eids, 'liveintent.com', deepAccess(bid, `userId.lipb.lipbid`)); - raiSetUserId(bid, eids, 'adserver.org', deepAccess(bid, `userId.tdid`)); - } - - return eids; -} - -function raiSetUserId(bid, eids, source, value) { - if (isStr(value)) { - eids.push({ - userId: value, - source: source - }); - } -} - function renderer(bid) { bid.renderer.push(() => { renderAd(bid) @@ -298,12 +275,6 @@ function renderer(bid) { function renderAd(bid) { let raOutstreamHBPassback = `${bid.vastXml}`; let raPlayerHB = { - config: bid.params[0].player != undefined ? { - end: bid.params[0].player.end != null ? bid.params[0].player.end : 'close', - init: bid.params[0].player.init != null ? bid.params[0].player.init : 'close', - skin: bid.params[0].player.skin != null ? bid.params[0].player.skin : 'light', - } : {end: 'close', init: 'close', skin: 'light'}, - pid: bid.params[0].pid, adUnit: bid.adUnitCode }; @@ -376,3 +347,8 @@ function raiGetTimeoutURL(data) { } return url } + +function setDSA(bid) { + let dsa = bid?.ortb2?.regs?.ext?.dsa ? bid?.ortb2?.regs?.ext?.dsa : null; + return dsa; +} diff --git a/modules/richaudienceBidAdapter.md b/modules/richaudienceBidAdapter.md index f888117b166..35298b8421d 100644 --- a/modules/richaudienceBidAdapter.md +++ b/modules/richaudienceBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Rich Audience Bidder Adapter Module Type: Bidder Adapter -Maintainer: cert@richaudience.com +Maintainer: integrations@richaudience.com ``` # Description diff --git a/modules/ringieraxelspringerBidAdapter.js b/modules/ringieraxelspringerBidAdapter.js index 0a741494f69..e6899c155d6 100644 --- a/modules/ringieraxelspringerBidAdapter.js +++ b/modules/ringieraxelspringerBidAdapter.js @@ -7,6 +7,7 @@ import { } from '../src/utils.js'; import { getAllOrtbKeywords } from '../libraries/keywords/keywords.js'; import { getAdUnitSizes } from '../libraries/sizeUtils/sizeUtils.js'; +import { BO_CSR_ONET } from '../libraries/paapiTools/buyerOrigins.js'; const BIDDER_CODE = 'ringieraxelspringer'; const VERSION = '1.0'; @@ -72,7 +73,7 @@ function parseNativeResponse(ad) { } const { click, Thirdpartyimpressiontracker, Thirdpartyimpressiontracker2, thirdPartyClickTracker2, imp, impression, impression1, impressionJs1, image, Image, title, leadtext, url, Calltoaction, Body, Headline, Thirdpartyclicktracker, adInfo, partner_logo: partnerLogo } = ad.data.fields; - + const { dsaurl, height, width, adclick } = ad.data.meta; const emsLink = ad.ems_link; const link = adclick + (url || click); @@ -211,9 +212,9 @@ const parseAuctionConfigs = (serverResponse, bidRequest) => { auctionConfigs.push({ 'bidId': bid.bidId, 'config': { - 'seller': 'https://csr.onet.pl', - 'decisionLogicUrl': `https://csr.onet.pl/${encodeURIComponent(bid.params.network)}/v1/protected-audience-api/decision-logic.js`, - 'interestGroupBuyers': ['https://csr.onet.pl'], + 'seller': BO_CSR_ONET, + 'decisionLogicUrl': `${BO_CSR_ONET}/${encodeURIComponent(bid.params.network)}/v1/protected-audience-api/decision-logic.js`, + 'interestGroupBuyers': [ BO_CSR_ONET ], 'auctionSignals': { 'params': bid.params, 'sizes': bid.sizes, @@ -266,7 +267,7 @@ export const spec = { const slotsQuery = getSlots(bidRequests); const contextQuery = getContextParams(bidRequests, bidderRequest); const gdprQuery = getGdprParams(bidderRequest); - const fledgeEligible = Boolean(bidderRequest && bidderRequest.fledgeEnabled); + const fledgeEligible = Boolean(bidderRequest?.paapi?.enabled); const network = bidRequests[0].params.network; const bidIds = bidRequests.map((bid) => ({ slot: bid.params.slot, @@ -294,7 +295,7 @@ export const spec = { if (fledgeAuctionConfigs) { // Return a tuple of bids and auctionConfigs. It is possible that bids could be null. - return {bids, fledgeAuctionConfigs}; + return {bids, paapi: fledgeAuctionConfigs}; } else { return bids; } diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index b176ab08aaf..a6970e959ce 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -1,45 +1,19 @@ -import { - logWarn, - logInfo, - isArray, - isFn, - deepAccess, - isEmpty, - contains, - timestamp, - triggerPixel, - isInteger, - getBidIdParameter -} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; - -const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; -const BIDDER_CODE = 'rise'; -const ADAPTER_VERSION = '6.0.0'; -const TTL = 360; -const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_GVLID = 1043; -const DEFAULT_SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; -const MODES = { - PRODUCTION: 'hb-multi', - TEST: 'hb-multi-test' -} -const SUPPORTED_SYNC_METHODS = { - IFRAME: 'iframe', - PIXEL: 'pixel' -} +import {makeBaseSpec} from '../libraries/riseUtils/index.js'; +import { + ALIASES, + BASE_URL, + BIDDER_CODE, + DEFAULT_GVLID, + MODES, +} from '../libraries/riseUtils/constants.js'; export const spec = { + ...makeBaseSpec(BASE_URL, MODES), code: BIDDER_CODE, - aliases: [ - { code: 'risexchange', gvlid: DEFAULT_GVLID }, - { code: 'openwebxchange', gvlid: 280 } - ], + aliases: ALIASES, gvlid: DEFAULT_GVLID, - version: ADAPTER_VERSION, - supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { if (!bidRequest.params) { logWarn('no params have been set to Rise adapter'); @@ -52,443 +26,7 @@ export const spec = { } return true; - }, - buildRequests: function (validBidRequests, bidderRequest) { - const combinedRequestsObject = {}; - - // use data from the first bid, to create the general params for all bids - const generalObject = validBidRequests[0]; - const testMode = generalObject.params.testMode; - const rtbDomain = generalObject.params.rtbDomain; - - combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); - combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); - - return { - method: 'POST', - url: getEndpoint(testMode, rtbDomain), - data: combinedRequestsObject - } - }, - interpretResponse: function ({body}) { - const bidResponses = []; - - if (body.bids) { - body.bids.forEach(adUnit => { - const bidResponse = { - requestId: adUnit.requestId, - cpm: adUnit.cpm, - currency: adUnit.currency || DEFAULT_CURRENCY, - width: adUnit.width, - height: adUnit.height, - ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, - netRevenue: adUnit.netRevenue || true, - nurl: adUnit.nurl, - mediaType: adUnit.mediaType, - meta: { - mediaType: adUnit.mediaType - } - }; - - if (adUnit.mediaType === VIDEO) { - bidResponse.vastXml = adUnit.vastXml; - } else if (adUnit.mediaType === BANNER) { - bidResponse.ad = adUnit.ad; - } - - if (adUnit.adomain && adUnit.adomain.length) { - bidResponse.meta.advertiserDomains = adUnit.adomain; - } - - bidResponses.push(bidResponse); - }); - } - - return bidResponses; - }, - getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; - for (const response of serverResponses) { - if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { - syncs.push({ - type: 'iframe', - url: response.body.params.userSyncURL - }); - } - if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { - const pixels = response.body.params.userSyncPixels.map(pixel => { - return { - type: 'image', - url: pixel - } - }) - syncs.push(...pixels) - } - } - return syncs; - }, - onBidWon: function (bid) { - if (bid == null) { - return; - } - - logInfo('onBidWon:', bid); - if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { - triggerPixel(bid.nurl); - } } }; registerBidder(spec); - -/** - * Get floor price - * @param bid {bid} - * @param mediaType {string} - * @returns {Number} - */ -function getFloor(bid, mediaType) { - if (!isFn(bid.getFloor)) { - return 0; - } - let floorResult = bid.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: mediaType, - size: '*' - }); - return floorResult.currency === DEFAULT_CURRENCY && floorResult.floor ? floorResult.floor : 0; -} - -/** - * Get the the ad sizes array from the bid - * @param bid {bid} - * @returns {Array} - */ -function getSizesArray(bid, mediaType) { - let sizesArray = [] - - if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { - sizesArray = bid.mediaTypes[mediaType].sizes; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - sizesArray = bid.sizes; - } - - return sizesArray; -} - -/** - * Get schain string value - * @param schainObject {Object} - * @returns {string} - */ -function getSupplyChain(schainObject) { - if (isEmpty(schainObject)) { - return ''; - } - let scStr = `${schainObject.ver},${schainObject.complete}`; - schainObject.nodes.forEach((node) => { - scStr += '!'; - scStr += `${getEncodedValIfNotEmpty(node.asi)},`; - scStr += `${getEncodedValIfNotEmpty(node.sid)},`; - scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; - scStr += `${getEncodedValIfNotEmpty(node.rid)},`; - scStr += `${getEncodedValIfNotEmpty(node.name)},`; - scStr += `${getEncodedValIfNotEmpty(node.domain)}`; - }); - return scStr; -} - -/** - * Get encoded node value - * @param val {string} - * @returns {string} - */ -function getEncodedValIfNotEmpty(val) { - return !isEmpty(val) ? encodeURIComponent(val) : ''; -} - -/** - * Get preferred user-sync method based on publisher configuration - * @param bidderCode {string} - * @returns {string} - */ -function getAllowedSyncMethod(filterSettings, bidderCode) { - const iframeConfigsToCheck = ['all', 'iframe']; - const pixelConfigToCheck = 'image'; - if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { - return SUPPORTED_SYNC_METHODS.IFRAME; - } - if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { - return SUPPORTED_SYNC_METHODS.PIXEL; - } -} - -/** - * Check if sync rule is supported - * @param syncRule {Object} - * @param bidderCode {string} - * @returns {boolean} - */ -function isSyncMethodAllowed(syncRule, bidderCode) { - if (!syncRule) { - return false; - } - const isInclude = syncRule.filter === 'include'; - const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; - return isInclude && contains(bidders, bidderCode); -} - -/** - * Get the seller endpoint - * @param testMode {boolean} - * @param rtbDomain {string} - * @returns {string} - */ -function getEndpoint(testMode, rtbDomain) { - const SELLER_ENDPOINT = rtbDomain ? `https://${rtbDomain}/` : DEFAULT_SELLER_ENDPOINT; - return testMode - ? SELLER_ENDPOINT + MODES.TEST - : SELLER_ENDPOINT + MODES.PRODUCTION; -} - -/** - * get device type - * @param uad {ua} - * @returns {string} - */ -function getDeviceType(ua) { - if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i - .test(ua.toLowerCase())) { - return '5'; - } - if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i - .test(ua.toLowerCase())) { - return '4'; - } - if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i - .test(ua.toLowerCase())) { - return '3'; - } - return '1'; -} - -function generateBidsParams(validBidRequests, bidderRequest) { - const bidsArray = []; - - if (validBidRequests.length) { - validBidRequests.forEach(bid => { - bidsArray.push(generateBidParameters(bid, bidderRequest)); - }); - } - - return bidsArray; -} - -/** - * Generate bid specific parameters - * @param {bid} bid - * @param {bidderRequest} bidderRequest - * @returns {Object} bid specific params object - */ -function generateBidParameters(bid, bidderRequest) { - const {params} = bid; - const mediaType = isBanner(bid) ? BANNER : VIDEO; - const sizesArray = getSizesArray(bid, mediaType); - // fix floor price in case of NAN - if (isNaN(params.floorPrice)) { - params.floorPrice = 0; - } - - const bidObject = { - mediaType, - adUnitCode: getBidIdParameter('adUnitCode', bid), - sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), - bidId: getBidIdParameter('bidId', bid), - bidderRequestId: getBidIdParameter('bidderRequestId', bid), - loop: getBidIdParameter('bidderRequestsCount', bid), - transactionId: bid.ortb2Imp?.ext?.tid, - coppa: 0, - }; - - const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); - if (pos) { - bidObject.pos = pos; - } - - const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); - if (gpid) { - bidObject.gpid = gpid; - } - - const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); - if (placementId) { - bidObject.placementId = placementId; - } - - const mimes = deepAccess(bid, `mediaTypes.${mediaType}.mimes`); - if (mimes) { - bidObject.mimes = mimes; - } - - const api = deepAccess(bid, `mediaTypes.${mediaType}.api`); - if (api) { - bidObject.api = api; - } - - const sua = deepAccess(bid, `ortb2.device.sua`); - if (sua) { - bidObject.sua = sua; - } - - const coppa = deepAccess(bid, `ortb2.regs.coppa`) - if (coppa) { - bidObject.coppa = 1; - } - - if (mediaType === VIDEO) { - const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); - let playbackMethodValue; - - // verify playbackMethod is of type integer array, or integer only. - if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { - // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation - playbackMethodValue = playbackMethod[0]; - } else if (isInteger(playbackMethod)) { - playbackMethodValue = playbackMethod; - } - - if (playbackMethodValue) { - bidObject.playbackMethod = playbackMethodValue; - } - - const placement = deepAccess(bid, `mediaTypes.video.placement`); - if (placement) { - bidObject.placement = placement; - } - - const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); - if (minDuration) { - bidObject.minDuration = minDuration; - } - - const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); - if (maxDuration) { - bidObject.maxDuration = maxDuration; - } - - const skip = deepAccess(bid, `mediaTypes.video.skip`); - if (skip) { - bidObject.skip = skip; - } - - const linearity = deepAccess(bid, `mediaTypes.video.linearity`); - if (linearity) { - bidObject.linearity = linearity; - } - - const protocols = deepAccess(bid, `mediaTypes.video.protocols`); - if (protocols) { - bidObject.protocols = protocols; - } - - const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); - if (plcmt) { - bidObject.plcmt = plcmt; - } - } - - return bidObject; -} - -function isBanner(bid) { - return bid.mediaTypes && bid.mediaTypes.banner; -} - -/** - * Generate params that are common between all bids - * @param {single bid object} generalObject - * @param {bidderRequest} bidderRequest - * @returns {object} the common params object - */ -function generateGeneralParams(generalObject, bidderRequest) { - const domain = window.location.hostname; - const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; - const {bidderCode} = bidderRequest; - const generalBidParams = generalObject.params; - const timeout = bidderRequest.timeout; - - // these params are snake_case instead of camelCase to allow backwards compatability on the server. - // in the future, these will be converted to camelCase to match our convention. - const generalParams = { - wrapper_type: 'prebidjs', - wrapper_vendor: '$$PREBID_GLOBAL$$', - wrapper_version: '$prebid.version$', - adapter_version: ADAPTER_VERSION, - auction_start: timestamp(), - publisher_id: generalBidParams.org, - publisher_name: domain, - site_domain: domain, - dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, - device_type: getDeviceType(navigator.userAgent), - ua: navigator.userAgent, - is_wrapper: !!generalBidParams.isWrapper, - session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), - tmax: timeout - }; - - const userIdsParam = getBidIdParameter('userId', generalObject); - if (userIdsParam) { - generalParams.userIds = JSON.stringify(userIdsParam); - } - - const ortb2Metadata = bidderRequest.ortb2 || {}; - if (ortb2Metadata.site) { - generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); - } - if (ortb2Metadata.user) { - generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); - } - - if (syncEnabled) { - const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); - if (allowedSyncMethod) { - generalParams.cs_method = allowedSyncMethod; - } - } - - if (bidderRequest.uspConsent) { - generalParams.us_privacy = bidderRequest.uspConsent; - } - - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; - generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - - if (bidderRequest && bidderRequest.gppConsent) { - generalParams.gpp = bidderRequest.gppConsent.gppString; - generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - generalParams.gpp = bidderRequest.ortb2.regs.gpp; - generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - - if (generalBidParams.ifa) { - generalParams.ifa = generalBidParams.ifa; - } - - if (generalObject.schain) { - generalParams.schain = getSupplyChain(generalObject.schain); - } - - if (bidderRequest && bidderRequest.refererInfo) { - // TODO: is 'ref' the right value here? - generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); - // TODO: does the fallback make sense here? - generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); - } - - return generalParams; -} diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md index 94d36a08510..2d355cd0cb6 100644 --- a/modules/riseBidAdapter.md +++ b/modules/riseBidAdapter.md @@ -13,7 +13,7 @@ Module that connects to Rise's demand sources. The Rise adapter requires setup and approval from the Rise. Please reach out to prebid-rise-engage@risecodes.com to create an Rise account. -The adapter supports Video(instream). +The adapter supports Video(instream), Banner, Native and multi-format bid requests. # Bid Parameters ## Video diff --git a/modules/rixengineBidAdapter.js b/modules/rixengineBidAdapter.js index 8ffdb55f09b..b0a419d4282 100644 --- a/modules/rixengineBidAdapter.js +++ b/modules/rixengineBidAdapter.js @@ -26,6 +26,11 @@ const converter = ortbConverter({ }); export const spec = { code: BIDDER_CODE, + // Register "algorix" as an alias, also with gvlid if needed + aliases: [{ + code: 'algorix', + gvlid: 1176 + }], supportedMediaTypes: [BANNER], isBidRequestValid: function (bid) { diff --git a/modules/robustaBidAdapter.js b/modules/robustaBidAdapter.js new file mode 100644 index 00000000000..91ce043c034 --- /dev/null +++ b/modules/robustaBidAdapter.js @@ -0,0 +1,88 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { deepSetValue } from '../src/utils.js'; +import { config } from '../src/config.js'; + +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; + } +}); + +const BIDDER_CODE = 'robusta'; +const VERSION = '1.0.0'; +const METHOD = 'POST'; +const DEFAULT_RTB_DOMAIN = 'pbjs.baristartb.com'; +const DEFAULT_SYNC_DOMAIN = 'sync.baristartb.com'; + +function isBidRequestValid(bidRequest) { + return !!bidRequest.params.lineItemId; +} + +function buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({ bidderRequest, bidRequests }); + const domain = config.getConfig('rtbDomain') || DEFAULT_RTB_DOMAIN; + + return [{ + method: METHOD, + url: `//${domain}/api/prebid`, + data: data, + options: { + withCredentials: false + } + }] +} + +function interpretResponse(response, request) { + const bids = converter.fromORTB({ response: response.body, request: request.data }); + + return bids; +} + +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + let syncParams = ''; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncParams = `gdpr_consent=${gdprConsent.consentString}`; + } + } + + const domain = config.getConfig('syncDomain') || DEFAULT_SYNC_DOMAIN; + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `//${domain}/api/sync?` + syncParams + }); + } + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `//${domain}/api/sync?` + syncParams + }); + } + return syncs; +} + +export const spec = { + code: BIDDER_CODE, + version: VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/robustaBidAdapter.md b/modules/robustaBidAdapter.md new file mode 100644 index 00000000000..14be3b2c40e --- /dev/null +++ b/modules/robustaBidAdapter.md @@ -0,0 +1,76 @@ +# Overview + +Module Name: Robusta Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@robustadigital.com + +# Description + +Connects to Robusta's demand sources to fetch bids. +Please use `robusta` as the bidder code. + +# Bid Params + +| Name | Scope | Description | Example | Type | +|------|--------|-------------|---------|------| +| lineItemId | Required | The Line Item ID | `'123'` | `string` | + +# Example Ad Unit Config + +```javascript +var adUnits = [ + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + } + }, + bids: [{ + bidder: 'robusta', + params: { + lineItemId: '323bfac4-a3cb-40e8-a3ae-e9832b35f969' + } + }] + } +]; +``` + +# User Sync + +Robusta bid adapter supports both iframe and image-based user syncing. Configuration example: + +```javascript +pbjs.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: ['robusta'], + filter: 'include' + }, + image: { + bidders: ['robusta'], + filter: 'include' + } + } + } +}); +``` + +# Additional Configuration + +The adapter supports custom RTB and sync domains through Prebid.js configuration: + +```javascript +pbjs.setBidderConfig({ + bidders: ['robusta'], + config: { + rtbDomain: 'custom.rtb.domain.com', // Optional: Override default RTB domain + syncDomain: 'custom.sync.domain.com' // Optional: Override default sync domain + } +}); +``` + +Default domains: +- RTB Domain: pbjs.baristartb.com +- Sync Domain: sync.baristartb.com \ No newline at end of file diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 1cd97696770..74a4df14f6f 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -5,6 +5,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {includes} from '../src/polyfill.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; import {config} from '../src/config.js'; +import { interpretNativeBid, OPENRTB } from '../libraries/precisoUtils/bidNativeUtils.js'; const BIDDER_CODE = 'rtbhouse'; const REGIONS = ['prebid-eu', 'prebid-us', 'prebid-asia']; @@ -24,29 +25,6 @@ const DSA_ATTRIBUTES = [ { name: 'datatopub', 'min': 0, 'max': 2 } ]; -// Codes defined by OpenRTB Native Ads 1.1 specification -export const OPENRTB = { - NATIVE: { - IMAGE_TYPE: { - ICON: 1, - MAIN: 3, - }, - ASSET_ID: { - TITLE: 1, - IMAGE: 2, - ICON: 3, - BODY: 4, - SPONSORED: 5, - CTA: 6 - }, - DATA_ASSET_TYPE: { - SPONSORED: 1, - DESC: 2, - CTA_TEXT: 12, - }, - } -}; - export const spec = { code: BIDDER_CODE, supportedMediaTypes: SUPPORTED_MEDIA_TYPES, @@ -114,11 +92,12 @@ export const spec = { let computedEndpointUrl = ENDPOINT_URL; - if (bidderRequest.fledgeEnabled) { - const fledgeConfig = config.getConfig('fledgeConfig') || { + if (bidderRequest.paapi?.enabled) { + const fromConfig = config.getConfig('paapiConfig') || config.getConfig('fledgeConfig') || { sellerTimeout: 500 }; + const fledgeConfig = { seller: FLEDGE_SELLER_URL, decisionLogicUrl: FLEDGE_DECISION_LOGIC_URL, - sellerTimeout: 500 + ...fromConfig }; mergeDeep(request, { ext: { fledge_config: fledgeConfig } }); computedEndpointUrl = FLEDGE_ENDPOINT_URL; @@ -165,7 +144,6 @@ export const spec = { interpretResponse: function (serverResponse, originalRequest) { let bids; - const fledgeInterestGroupBuyers = config.getConfig('fledgeConfig.interestGroupBuyers') || []; const responseBody = serverResponse.body; let fledgeAuctionConfigs = null; @@ -173,24 +151,35 @@ export const spec = { // we have fledge response // mimic the original response ([{},...]) bids = this.interpretOrtbResponse({ body: responseBody.seatbid[0]?.bid }, originalRequest); - - const seller = responseBody.ext.seller; - const decisionLogicUrl = responseBody.ext.decisionLogicUrl; - const sellerTimeout = 'sellerTimeout' in responseBody.ext ? { sellerTimeout: responseBody.ext.sellerTimeout } : {}; + const paapiAdapterConfig = config.getConfig('paapiConfig') || config.getConfig('fledgeConfig') || {}; + const fledgeInterestGroupBuyers = paapiAdapterConfig.interestGroupBuyers || []; + // values from the response.ext are the most important + const { + decisionLogicUrl = paapiAdapterConfig.decisionLogicUrl || paapiAdapterConfig.decisionLogicURL || + FLEDGE_DECISION_LOGIC_URL, + seller = paapiAdapterConfig.seller || FLEDGE_SELLER_URL, + sellerTimeout = 500 + } = responseBody.ext; + + const fledgeConfig = { + seller, + decisionLogicUrl, + decisionLogicURL: decisionLogicUrl, + sellerTimeout + }; + // fledgeConfig settings are more important; other paapiAdapterConfig settings are facultative + mergeDeep(fledgeConfig, paapiAdapterConfig, fledgeConfig); responseBody.ext.igbid.forEach((igbid) => { - const perBuyerSignals = {}; + const perBuyerSignals = {...fledgeConfig.perBuyerSignals}; // may come from paapiAdapterConfig igbid.igbuyer.forEach(buyerItem => { perBuyerSignals[buyerItem.igdomain] = buyerItem.buyersignal }); fledgeAuctionConfigs = fledgeAuctionConfigs || {}; - fledgeAuctionConfigs[igbid.impid] = mergeDeep( + fledgeAuctionConfigs[igbid.impid] = mergeDeep({}, fledgeConfig, { - seller, - decisionLogicUrl, - interestGroupBuyers: [...fledgeInterestGroupBuyers, ...Object.keys(perBuyerSignals)], + interestGroupBuyers: [...new Set([...fledgeInterestGroupBuyers, ...Object.keys(perBuyerSignals)])], perBuyerSignals, - }, - sellerTimeout + } ); }); } else { @@ -209,7 +198,7 @@ export const spec = { logInfo('Response with FLEDGE:', { bids, fledgeAuctionConfigs }); return { bids, - fledgeAuctionConfigs, + paapi: fledgeAuctionConfigs, } } return bids; @@ -226,7 +215,7 @@ function applyFloor(slot) { if (typeof slot.getFloor === 'function') { Object.keys(slot.mediaTypes).forEach(type => { if (includes(SUPPORTED_MEDIA_TYPES, type)) { - floors.push(slot.getFloor({ currency: DEFAULT_CURRENCY_ARR[0], mediaType: type, size: slot.sizes || '*' }).floor); + floors.push(slot.getFloor({ currency: DEFAULT_CURRENCY_ARR[0], mediaType: type, size: slot.sizes || '*' })?.floor); } }); } @@ -250,7 +239,7 @@ function mapImpression(slot, bidderRequest) { imp.bidfloor = bidfloor; } - if (bidderRequest.fledgeEnabled) { + if (bidderRequest.paapi?.enabled) { imp.ext = imp.ext || {}; imp.ext.ae = slot?.ortb2Imp?.ext?.ae } else { @@ -478,71 +467,6 @@ function interpretBannerBid(serverBid) { } } -/** - * @param {object} serverBid Bid by OpenRTB 2.5 §4.2.3 - * @returns {object} Prebid native bidObject - */ -function interpretNativeBid(serverBid) { - return { - requestId: serverBid.impid, - mediaType: NATIVE, - cpm: serverBid.price, - creativeId: serverBid.adid, - width: 1, - height: 1, - ttl: TTL, - meta: { - advertiserDomains: serverBid.adomain - }, - netRevenue: true, - currency: 'USD', - native: interpretNativeAd(serverBid.adm), - } -} - -/** - * @param {string} adm JSON-encoded Request by OpenRTB Native Ads 1.1 §4.1 - * @returns {object} Prebid bidObject.native - */ -function interpretNativeAd(adm) { - const native = JSON.parse(adm).native; - const result = { - clickUrl: encodeURI(native.link.url), - impressionTrackers: native.imptrackers - }; - native.assets.forEach(asset => { - switch (asset.id) { - case OPENRTB.NATIVE.ASSET_ID.TITLE: - result.title = asset.title.text; - break; - case OPENRTB.NATIVE.ASSET_ID.IMAGE: - result.image = { - url: encodeURI(asset.img.url), - width: asset.img.w, - height: asset.img.h - }; - break; - case OPENRTB.NATIVE.ASSET_ID.ICON: - result.icon = { - url: encodeURI(asset.img.url), - width: asset.img.w, - height: asset.img.h - }; - break; - case OPENRTB.NATIVE.ASSET_ID.BODY: - result.body = asset.data.value; - break; - case OPENRTB.NATIVE.ASSET_ID.SPONSORED: - result.sponsoredBy = asset.data.value; - break; - case OPENRTB.NATIVE.ASSET_ID.CTA: - result.cta = asset.data.value; - break; - } - }); - return result; -} - /** * https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md * diff --git a/modules/rtbhouseBidAdapter.md b/modules/rtbhouseBidAdapter.md index 338ba6b4df4..7fcae1299b2 100644 --- a/modules/rtbhouseBidAdapter.md +++ b/modules/rtbhouseBidAdapter.md @@ -69,7 +69,7 @@ Please reach out to pmp@rtbhouse.com to receive your own # Protected Audience API (FLEDGE) support There’s an option to receive demand for Protected Audience API (FLEDGE/PAAPI) ads using RTB House bid adapter. -Prebid’s [fledgeForGpt](https://docs.prebid.org/dev-docs/modules/fledgeForGpt.html) +Prebid’s [paapiForGpt](https://docs.prebid.org/dev-docs/modules/paapiForGpt.html) module and Google Ad Manager is currently required. The following steps should be taken to setup Protected Audience for RTB House: @@ -77,15 +77,15 @@ The following steps should be taken to setup Protected Audience for RTB House: 1. Reach out to your RTB House representative for setup coordination. 2. Build and enable FLEDGE module as described in -[fledgeForGpt](https://docs.prebid.org/dev-docs/modules/fledgeForGpt.html) +[paapiForGpt](https://docs.prebid.org/dev-docs/modules/paapiForGpt.html) module documentation. a. Make sure to enable RTB House bidder to participate in FLEDGE. If there are any other bidders to be allowed for that, add them to the **bidders** array: ```javascript - pbjs.setBidderConfig({ - bidders: ["rtbhouse"], - config: { - fledgeEnabled: true + pbjs.setConfig({ + paapi: { + bidders: ["rtbhouse"], + enabled: true } }); ``` @@ -93,15 +93,15 @@ module documentation. b. If you as a publisher have your own [decisionLogicUrl](https://github.com/WICG/turtledove/blob/main/FLEDGE.md#21-initiating-an-on-device-auction) you may utilize it by setting up a dedicated `fledgeConfig` object: ```javascript - pbjs.setBidderConfig({ - bidders: ["rtbhouse"], - config: { - fledgeEnabled: true, - fledgeConfig: { - seller: 'https://seller.domain', - decisionLogicUrl: 'https://seller.domain/decisionLogicFile.js', - sellerTimeout: 100 - } + pbjs.setConfig({ + paapi: { + bidders: ["rtbhouse"], + enabled: true + }, + fledgeConfig: { + seller: 'https://seller.domain', + decisionLogicUrl: 'https://seller.domain/decisionLogicFile.js', + sellerTimeout: 100 } }); ``` diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 0c654fc28b0..e40ae5e0edd 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -188,10 +188,12 @@ let _dataProviders = []; let _userConsent; /** - * Register a RTD submodule. + * Register a Real-Time Data (RTD) submodule. * - * @param {RtdSubmodule} submodule - * @returns {function()} a de-registration function that will unregister the module when called. + * @param {Object} submodule The RTD submodule to register. + * @param {string} submodule.name The name of the RTD submodule. + * @param {number} [submodule.gvlid] The Global Vendor List ID (GVLID) of the RTD submodule. + * @returns {function(): void} A de-registration function that will unregister the module when called. */ export function attachRealTimeDataProvider(submodule) { registeredSubModules.push(submodule); @@ -311,15 +313,14 @@ export const setBidRequestsData = timedAuctionHook('rtd', function setBidRequest return exitHook(); } - waitTimeout = setTimeout(exitHook, shouldDelayAuction ? _moduleConfig.auctionDelay : 0); + const timeout = shouldDelayAuction ? _moduleConfig.auctionDelay : 0; + waitTimeout = setTimeout(exitHook, timeout); relevantSubModules.forEach(sm => { const fpdGuard = guardOrtb2Fragments(reqBidsConfigObj.ortb2Fragments || {}, activityParams(MODULE_TYPE_RTD, sm.name)); verifiers.push(fpdGuard.verify); - sm.getBidRequestData({ - ...reqBidsConfigObj, - ortb2Fragments: fpdGuard.obj - }, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent) + reqBidsConfigObj.ortb2Fragments = fpdGuard.obj; + sm.getBidRequestData(reqBidsConfigObj, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent, timeout); }); function onGetBidRequestDataCallback() { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index c03065cd5a5..22f40ca69e6 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -3,7 +3,6 @@ import { pbsExtensions } from '../libraries/pbsExtensions/pbsExtensions.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { find } from '../src/polyfill.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { Renderer } from '../src/Renderer.js'; import { @@ -19,9 +18,11 @@ import { mergeDeep, parseSizesInput, pick, - _each + _each, + isPlainObject } from '../src/utils.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import {getUserSyncParams} from '../libraries/userSyncUtils/userSyncUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -41,6 +42,8 @@ config.getConfig('rubicon', config => { const GVLID = 52; +let impIdMap = {}; + var sizeMap = { 1: '468x60', 2: '728x90', @@ -147,7 +150,15 @@ var sizeMap = { 580: '505x656', 622: '192x160', 632: '1200x450', - 634: '340x450' + 634: '340x450', + 680: '970x570', + 682: '300x240', + 684: '970x550', + 686: '300x210', + 688: '300x220', + 690: '970x170', + 710: '600x250', + 712: '340x430' }; _each(sizeMap, (item, key) => sizeMap[item] = key); @@ -211,6 +222,9 @@ export const converter = ortbConverter({ setBidFloors(bidRequest, imp); + // ensure unique imp IDs for twin adunits + imp.id = impIdMap[imp.id] ? imp.id + impIdMap[imp.id]++ : (impIdMap[imp.id] = 2, imp.id); + return imp; }, bidResponse(buildBidResponse, bid, context) { @@ -219,9 +233,9 @@ export const converter = ortbConverter({ const {bidRequest} = context; let [parseSizeWidth, parseSizeHeight] = bidRequest.mediaTypes.video?.context === 'outstream' ? parseSizes(bidRequest, VIDEO) : [undefined, undefined]; - - bidResponse.width = bid.w || parseSizeWidth || bidResponse.playerWidth; - bidResponse.height = bid.h || parseSizeHeight || bidResponse.playerHeight; + // 0 by default to avoid undefined size + bidResponse.width = bid.w || parseSizeWidth || bidResponse.playerWidth || 0; + bidResponse.height = bid.h || parseSizeHeight || bidResponse.playerHeight || 0; if (bidResponse.mediaType === VIDEO && bidRequest.mediaTypes.video.context === 'outstream') { bidResponse.renderer = outstreamRenderer(bidResponse); @@ -301,6 +315,7 @@ export const spec = { if (filteredRequests && filteredRequests.length) { const data = converter.toORTB({bidRequests: filteredRequests, bidderRequest}); + resetImpIdMap(); filteredHttpRequest.push({ method: 'POST', @@ -486,6 +501,8 @@ export const spec = { 'x_imp.ext.tid': bidRequest.ortb2Imp?.ext?.tid, 'l_pb_bid_id': bidRequest.bidId, 'o_cdep': bidRequest.ortb2?.device?.ext?.cdep, + 'ip': bidRequest.ortb2?.device?.ip, + 'ipv6': bidRequest.ortb2?.device?.ipv6, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, 'p_geo.latitude': isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), @@ -506,7 +523,7 @@ export const spec = { } catch (e) { logError('Rubicon: getFloor threw an error: ', e); } - data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; + data['rp_hard_floor'] = isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } // Send multiformat data if requested @@ -529,42 +546,47 @@ export const spec = { 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 + } addDesiredSegtaxes(bidderRequest, data); // loop through userIds and add to request - if (bidRequest.userIdAsEids) { - bidRequest.userIdAsEids.forEach(eid => { + if (bidRequest?.ortb2?.user?.ext?.eids) { + bidRequest.ortb2.user.ext.eids.forEach(({ source, uids = [], inserter, matcher, mm, ext = {} }) => { try { - // special cases - if (eid.source === 'adserver.org') { - data['tpid_tdid'] = eid.uids[0].id; - data['eid_adserver.org'] = eid.uids[0].id; - } else if (eid.source === 'liveintent.com') { - data['tpid_liveintent.com'] = eid.uids[0].id; - data['eid_liveintent.com'] = eid.uids[0].id; - if (eid.ext && Array.isArray(eid.ext.segments) && eid.ext.segments.length) { - data['tg_v.LIseg'] = eid.ext.segments.join(','); - } - } else if (eid.source === 'liveramp.com') { - data['x_liverampidl'] = eid.uids[0].id; - } else if (eid.source === 'id5-sync.com') { - data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.linkType) || ''}`; - } else { - // add anything else with this generic format - // if rubicon drop ^ - const id = eid.source === 'rubiconproject.com' ? eid.uids[0].id : `${eid.uids[0].id}^${eid.uids[0].atype || ''}` - data[`eid_${eid.source}`] = id; - } - // send AE "ppuid" signal if exists, and hasn't already been sent + // Ensure there is at least one valid UID in the 'uids' array + const uidData = uids[0]; + if (!uidData) return; // Skip processing if no valid UID exists + + // Function to build the EID value in the required format + const buildEidValue = (uidData) => [ + uidData.id, // uid: The user ID + uidData.atype || '', + '', // third: Always empty, as specified in the requirement + inserter || '', + matcher || '', + mm || '', + uidData?.ext?.rtiPartner || uidData?.ext?.rtipartner || '' + ].join('^'); // Return a single string formatted with '^' delimiter + + const eidValue = buildEidValue(uidData); // Build the EID value string + + // Store the constructed EID value for the given source + data[`eid_${source}`] = eidValue; + + // Handle the "ppuid" signal, ensuring it is set only once if (!data['ppuid']) { - // get the first eid.uids[*].ext.stype === 'ppuid', if one exists - const ppId = find(eid.uids, uid => uid.ext && uid.ext.stype === 'ppuid'); - if (ppId && ppId.id) { - data['ppuid'] = ppId.id; + // Search for a UID with the 'stype' field equal to 'ppuid' in its extension + const ppId = uids.find(uid => uid.ext?.stype === 'ppuid'); + if (ppId?.id) { + data['ppuid'] = ppId.id; // Store the ppuid if found } } } catch (e) { - logWarn('Rubicon: error reading eid:', eid, e); + // Log any errors encountered during processing + logWarn('Rubicon: error reading eid:', { source, uids }, e); } }); } @@ -704,6 +726,10 @@ export const spec = { bid.meta.advertiserDomains = Array.isArray(ad.adomain) ? ad.adomain : [ad.adomain]; } + if (ad.emulated_format) { + bid.meta.mediaType = ad.emulated_format; + } + if (ad.creative_type === VIDEO) { bid.width = associatedBidRequest.params.video.playerWidth; bid.height = associatedBidRequest.params.video.playerHeight; @@ -736,7 +762,7 @@ export const spec = { }); if (fledgeAuctionConfigs) { - return { bids, fledgeAuctionConfigs }; + return { bids, paapi: fledgeAuctionConfigs }; } else { return bids; } @@ -744,26 +770,7 @@ export const spec = { getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { if (!hasSynced && syncOptions.iframeEnabled) { // data is only assigned if params are available to pass to syncEndpoint - let params = {}; - - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - params['gdpr'] = Number(gdprConsent.gdprApplies); - } - if (typeof gdprConsent.consentString === 'string') { - params['gdpr_consent'] = gdprConsent.consentString; - } - } - - if (uspConsent) { - params['us_privacy'] = encodeURIComponent(uspConsent); - } - - if (gppConsent?.gppString) { - params['gpp'] = gppConsent.gppString; - params['gpp_sid'] = gppConsent.applicableSections?.toString(); - } - + let params = getUserSyncParams(gdprConsent, uspConsent, gppConsent); params = Object.keys(params).length ? `?${formatQS(params)}` : ''; hasSynced = true; @@ -842,7 +849,7 @@ function renderBid(bid) { height: bid.height, vastUrl: bid.vastUrl, placement: { - attachTo: adUnitElement, + attachTo: `#${bid.adUnitCode}`, align: config.align, position: config.position }, @@ -917,7 +924,7 @@ function applyFPD(bidRequest, mediaType, data) { const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); const dsa = deepAccess(fpd, 'regs.ext.dsa'); - const SEGTAX = {user: [4], site: [1, 2, 5, 6]}; + const SEGTAX = {user: [4], site: [1, 2, 5, 6, 7]}; const MAP = {user: 'tg_v.', site: 'tg_i.', adserver: 'tg_i.dfp_ad_unit_code', pbadslot: 'tg_i.pbadslot', keywords: 'kw'}; const validate = function(prop, key, parentName) { if (key === 'data' && Array.isArray(prop)) { @@ -982,11 +989,25 @@ function applyFPD(bidRequest, mediaType, data) { 'transparency', (transparency) => { if (Array.isArray(transparency) && transparency.length) { data['dsatransparency'] = transparency.reduce((param, transp) => { + // make sure domain is there, otherwise skip entry + const domain = transp.domain || ''; + if (!domain) { + return param; + } + + // make sure dsaParam array is there (try both 'dsaparams' and 'params', but prefer dsaparams) + const dsaParamArray = transp.dsaparams || transp.params; + if (!Array.isArray(dsaParamArray) || dsaParamArray.length === 0) { + return param; + } + + // finally we will add this one, if param has been added already, add our seperator if (param) { param += '~~' } - return param += `${transp.domain}~${transp.dsaparams.join('_')}` - }, '') + + return param += `${domain}~${dsaParamArray.join('_')}`; + }, ''); } } ]) @@ -1009,7 +1030,10 @@ function applyFPD(bidRequest, mediaType, data) { // reduce down into ua and full version list attributes const [ua, fullVer] = browsers.reduce((accum, browserData) => { accum[0].push(`"${browserData?.brand}"|v="${browserData?.version?.[0]}"`); - accum[1].push(`"${browserData?.brand}"|v="${browserData?.version?.join?.('.')}"`); + // only set fullVer if long enough + if (browserData.version.length > 1) { + accum[1].push(`"${browserData?.brand}"|v="${browserData?.version?.join?.('.')}"`); + } return accum; }, [[], []]); data.m_ch_ua = ua?.join?.(','); @@ -1156,6 +1180,7 @@ function bidType(bid, log = false) { } export const resetRubiConf = () => rubiConf = {}; +export const resetImpIdMap = () => impIdMap = {}; export function masSizeOrdering(sizes) { const MAS_SIZE_PRIORITY = [15, 2, 9]; @@ -1191,7 +1216,7 @@ export function determineRubiconVideoSizeId(bid) { } /** - * @param {PrebidConfig} config + * @param {Object} config * @returns {{ranges: {ranges: Object[]}}} */ export function getPriceGranularity(config) { @@ -1240,7 +1265,6 @@ export function hasValidVideoParams(bid) { /** * Make sure the required params are present * @param {Object} schain - * @param {boolean} */ export function hasValidSupplyChainParams(schain) { let isValid = false; diff --git a/modules/s2sTesting.js b/modules/s2sTesting.js index 8e9628c8810..4c78b62d710 100644 --- a/modules/s2sTesting.js +++ b/modules/s2sTesting.js @@ -44,7 +44,7 @@ s2sTesting.getSourceBidderMap = function(adUnits = [], allS2SBidders = []) { /** * @function calculateBidSources determines the source for each s2s bidder based on bidderControl weightings. these can be overridden at the adUnit level - * @param s2sConfigs server-to-server configuration + * @param s2sConfig server-to-server configuration */ s2sTesting.calculateBidSources = function(s2sConfig = {}) { // calculate bid source (server/client) for each s2s bidder diff --git a/modules/saambaaBidAdapter.js b/modules/saambaaBidAdapter.js index da6e7028abe..3e33496b7d9 100644 --- a/modules/saambaaBidAdapter.js +++ b/modules/saambaaBidAdapter.js @@ -1,419 +1,3 @@ -// TODO: this adapter appears to have no tests - -import {deepAccess, generateUUID, isEmpty, isFn, parseSizesInput, parseUrl} from '../src/utils.js'; -import {config} from '../src/config.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {find, includes} from '../src/polyfill.js'; - -const ADAPTER_VERSION = '1.0'; -const BIDDER_CODE = 'saambaa'; - -export const VIDEO_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; -export const BANNER_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; -export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; -export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'skip', 'playerSize', 'context']; -export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; - -let pubid = ''; - -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO], - - isBidRequestValid(bidRequest) { - if (typeof bidRequest != 'undefined') { - if (bidRequest.bidder !== BIDDER_CODE && typeof bidRequest.params === 'undefined') { return false; } - if (bidRequest === '' || bidRequest.params.placement === '' || bidRequest.params.pubid === '') { return false; } - return true; - } else { return false; } - }, - - buildRequests(bids, bidderRequest) { - let requests = []; - let videoBids = bids.filter(bid => isVideoBidValid(bid)); - let bannerBids = bids.filter(bid => isBannerBidValid(bid)); - videoBids.forEach(bid => { - pubid = getVideoBidParam(bid, 'pubid'); - requests.push({ - method: 'POST', - url: VIDEO_ENDPOINT + pubid, - data: createVideoRequestData(bid, bidderRequest), - bidRequest: bid - }); - }); - - bannerBids.forEach(bid => { - pubid = getBannerBidParam(bid, 'pubid'); - - requests.push({ - method: 'POST', - url: BANNER_ENDPOINT + pubid, - data: createBannerRequestData(bid, bidderRequest), - bidRequest: bid - }); - }); - return requests; - }, - - interpretResponse(serverResponse, {bidRequest}) { - let response = serverResponse.body; - if (response !== null && isEmpty(response) == false) { - if (isVideoBid(bidRequest)) { - let bidResponse = { - requestId: response.id, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - currency: response.cur, - meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, - mediaType: VIDEO, - netRevenue: true - } - - if (response.seatbid[0].bid[0].adm) { - bidResponse.vastXml = response.seatbid[0].bid[0].adm; - bidResponse.adResponse = { - content: response.seatbid[0].bid[0].adm - }; - } else { - bidResponse.vastUrl = response.seatbid[0].bid[0].nurl; - } - - return bidResponse; - } else { - return { - requestId: response.id, - bidderCode: BIDDER_CODE, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ad: response.seatbid[0].bid[0].adm, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - currency: response.cur, - meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, - mediaType: BANNER, - netRevenue: true - } - } - } - } -}; - -function isBannerBid(bid) { - return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); -} - -function isVideoBid(bid) { - return deepAccess(bid, 'mediaTypes.video'); -} - -function getBannerBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }) : {}; - return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); -} - -function getVideoBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'video', size: '*' }) : {}; - return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); -} - -function isVideoBidValid(bid) { - return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); -} - -function isBannerBidValid(bid) { - return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); -} - -function getVideoBidParam(bid, key) { - return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); -} - -function getBannerBidParam(bid, key) { - return deepAccess(bid, 'params.banner.' + key) || deepAccess(bid, 'params.' + key); -} - -function isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); -} - -function isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); -} - -function getDoNotTrack() { - return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; -} - -function findAndFillParam(o, key, value) { - try { - if (typeof value === 'function') { - o[key] = value(); - } else { - o[key] = value; - } - } catch (ex) {} -} - -function getOsVersion() { - let clientStrings = [ - { s: 'Android', r: /Android/ }, - { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, - { s: 'Mac OS X', r: /Mac OS X/ }, - { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, - { s: 'Linux', r: /(Linux|X11)/ }, - { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, - { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, - { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, - { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, - { s: 'Windows Vista', r: /Windows NT 6.0/ }, - { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, - { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, - { s: 'UNIX', r: /UNIX/ }, - { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } - ]; - let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); - return cs ? cs.s : 'unknown'; -} - -function getFirstSize(sizes) { - return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; -} - -function parseSizes(sizes) { - return parseSizesInput(sizes).map(size => { - let [ width, height ] = size.split('x'); - return { - w: parseInt(width, 10) || undefined, - h: parseInt(height, 10) || undefined - }; - }); -} - -function getVideoSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); -} - -function getBannerSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); -} - -function getTopWindowReferrer() { - try { - return window.top.document.referrer; - } catch (e) { - return ''; - } -} - -function getVideoTargetingParams(bid) { - const result = {}; - const excludeProps = ['playerSize', 'context', 'w', 'h']; - Object.keys(Object(bid.mediaTypes.video)) - .filter(key => !includes(excludeProps, key)) - .forEach(key => { - result[ key ] = bid.mediaTypes.video[ key ]; - }); - Object.keys(Object(bid.params.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.params.video[ key ]; - }); - return result; -} - -function createVideoRequestData(bid, bidderRequest) { - let topLocation = getTopWindowLocation(bidderRequest); - let topReferrer = getTopWindowReferrer(); - - // if size is explicitly given via adapter params - let paramSize = getVideoBidParam(bid, 'size'); - let sizes = []; - let coppa = config.getConfig('coppa'); - - if (typeof paramSize !== 'undefined' && paramSize != '') { - sizes = parseSizes(paramSize); - } else { - sizes = getVideoSizes(bid); - } - const firstSize = getFirstSize(sizes); - let floor = (getVideoBidFloor(bid) == null || typeof getVideoBidFloor(bid) == 'undefined') ? 0.5 : getVideoBidFloor(bid); - let video = getVideoTargetingParams(bid); - const o = { - 'device': { - 'langauge': (global.navigator.language).split('-')[0], - 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), - 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, - 'js': 1, - 'os': getOsVersion() - }, - 'at': 2, - 'site': {}, - 'tmax': 3000, - 'cur': ['USD'], - 'id': bid.bidId, - 'imp': [], - 'regs': { - 'ext': { - } - }, - 'user': { - 'ext': { - } - } - }; - - o.site['page'] = topLocation.href; - o.site['domain'] = topLocation.hostname; - o.site['search'] = topLocation.search; - o.site['domain'] = topLocation.hostname; - o.site['ref'] = topReferrer; - o.site['mobile'] = isMobile() ? 1 : 0; - const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; - - o.device['dnt'] = getDoNotTrack() ? 1 : 0; - - findAndFillParam(o.site, 'name', function() { - return global.top.document.title; - }); - - findAndFillParam(o.device, 'h', function() { - return global.screen.height; - }); - findAndFillParam(o.device, 'w', function() { - return global.screen.width; - }); - - let placement = getVideoBidParam(bid, 'placement'); - - for (let j = 0; j < sizes.length; j++) { - o.imp.push({ - 'id': '' + j, - 'displaymanager': '' + BIDDER_CODE, - 'displaymanagerver': '' + ADAPTER_VERSION, - 'tagId': placement, - 'bidfloor': floor, - 'bidfloorcur': 'USD', - 'secure': secure, - 'video': Object.assign({ - 'id': generateUUID(), - 'pos': 0, - 'w': firstSize.w, - 'h': firstSize.h, - 'mimes': DEFAULT_MIMES - }, video) - - }); - } - if (coppa) { - o.regs.ext = {'coppa': 1}; - } - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; - o.user.ext = {'consent': consentString}; - } - - return o; -} - -function getTopWindowLocation(bidderRequest) { - return parseUrl(bidderRequest?.refererInfo?.page || '', { decodeSearchAsString: true }); -} - -function createBannerRequestData(bid, bidderRequest) { - let topLocation = getTopWindowLocation(bidderRequest); - let topReferrer = getTopWindowReferrer(); - - // if size is explicitly given via adapter params - - let paramSize = getBannerBidParam(bid, 'size'); - let sizes = []; - let coppa = config.getConfig('coppa'); - if (typeof paramSize !== 'undefined' && paramSize != '') { - sizes = parseSizes(paramSize); - } else { - sizes = getBannerSizes(bid); - } - - let floor = (getBannerBidFloor(bid) == null || typeof getBannerBidFloor(bid) == 'undefined') ? 0.1 : getBannerBidFloor(bid); - const o = { - 'device': { - 'langauge': (global.navigator.language).split('-')[0], - 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), - 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, - 'js': 1 - }, - 'at': 2, - 'site': {}, - 'tmax': 3000, - 'cur': ['USD'], - 'id': bid.bidId, - 'imp': [], - 'regs': { - 'ext': { - } - }, - 'user': { - 'ext': { - } - } - }; - - o.site['page'] = topLocation.href; - o.site['domain'] = topLocation.hostname; - o.site['search'] = topLocation.search; - o.site['domain'] = topLocation.hostname; - o.site['ref'] = topReferrer; - o.site['mobile'] = isMobile() ? 1 : 0; - const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; - - o.device['dnt'] = getDoNotTrack() ? 1 : 0; - - findAndFillParam(o.site, 'name', function() { - return global.top.document.title; - }); - - findAndFillParam(o.device, 'h', function() { - return global.screen.height; - }); - findAndFillParam(o.device, 'w', function() { - return global.screen.width; - }); - - let placement = getBannerBidParam(bid, 'placement'); - for (let j = 0; j < sizes.length; j++) { - let size = sizes[j]; - - o.imp.push({ - 'id': '' + j, - 'displaymanager': '' + BIDDER_CODE, - 'displaymanagerver': '' + ADAPTER_VERSION, - 'tagId': placement, - 'bidfloor': floor, - 'bidfloorcur': 'USD', - 'secure': secure, - 'banner': { - 'id': generateUUID(), - 'pos': 0, - 'w': size['w'], - 'h': size['h'] - } - }); - } - if (coppa) { - o.regs.ext = {'coppa': 1}; - } - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; - o.user.ext = {'consent': consentString}; - } - - return o; -} +import { spec } from './advangelistsBidAdapter.js'; // eslint-disable-line prebid/validate-imports +import { registerBidder } from '../src/adapters/bidderFactory.js'; registerBidder(spec); diff --git a/modules/seedingAllianceBidAdapter.js b/modules/seedingAllianceBidAdapter.js index e287ea7ff78..10bd6183488 100755 --- a/modules/seedingAllianceBidAdapter.js +++ b/modules/seedingAllianceBidAdapter.js @@ -3,25 +3,44 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; -import {_map, deepSetValue, isArray, isEmpty, replaceAuctionPrice} from '../src/utils.js'; +import {generateUUID, deepSetValue, isEmpty, replaceAuctionPrice} from '../src/utils.js'; import {config} from '../src/config.js'; -import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; const GVL_ID = 371; const BIDDER_CODE = 'seedingAlliance'; const DEFAULT_CUR = 'EUR'; const ENDPOINT_URL = 'https://b.nativendo.de/cds/rtb/bid?format=openrtb2.5&ssp=pb'; +const NATIVENDO_KEY = 'nativendo_id'; -const NATIVE_ASSET_IDS = { 0: 'title', 1: 'body', 2: 'sponsoredBy', 3: 'image', 4: 'cta', 5: 'icon' }; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); -const NATIVE_PARAMS = { - title: { id: 0, name: 'title' }, - body: { id: 1, name: 'data', type: 2 }, - sponsoredBy: { id: 2, name: 'data', type: 1 }, - image: { id: 3, type: 3, name: 'img' }, - cta: { id: 4, type: 12, name: 'data' }, - icon: { id: 5, type: 1, name: 'img' } -}; +const converter = ortbConverter({ + context: { + ttl: 360, + netRevenue: true + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + // set basic page, this might be updated later by adunit param + deepSetValue(request, 'site.page', bidderRequest.refererInfo.page); + deepSetValue(request, 'regs.ext.pb_ver', '$prebid.version$'); + deepSetValue(request, 'cur', [config.getConfig('currency.adServerCurrency') || DEFAULT_CUR]); + + // As this is client side, we get needed info from headers + delete request.device; + + return request; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + // add tagid from params + imp.tagid = bidRequest.params.adUnitId; + + return imp; + } +}); export const spec = { code: BIDDER_CODE, @@ -33,109 +52,30 @@ export const spec = { }, buildRequests: (validBidRequests = [], bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let url = bidderRequest.refererInfo.page; - - const imps = validBidRequests.map((bidRequest, id) => { - const imp = { - id: String(id + 1), - tagid: bidRequest.params.adUnitId - }; - - /** - * Native Ad - */ - if (bidRequest.nativeParams) { - const assets = _map(bidRequest.nativeParams, (nativeAsset, key) => { - const props = NATIVE_PARAMS[key]; - - if (props) { - let wmin, hmin, w, h; - let aRatios = nativeAsset.aspect_ratios; - - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - wmin = aRatios.min_width || 0; - hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; - } - - if (nativeAsset.sizes) { - const sizes = flatten(nativeAsset.sizes); - w = parseInt(sizes[0], 10); - h = parseInt(sizes[1], 10); - } - - const asset = { - id: props.id, - required: nativeAsset.required & 1 - }; - - asset[props.name] = { - len: nativeAsset.len, - type: props.type, - wmin, - hmin, - w, - h - }; - - return asset; - } else { - // TODO Filter impressions with required assets we don't support - } - }).filter(Boolean); - - imp.native = { - request: { - assets - } - }; - } else { - let sizes = transformSizes(bidRequest.sizes); - - imp.banner = { - format: sizes, - w: sizes[0] ? sizes[0].w : 0, - h: sizes[0] ? sizes[0].h : 0 - } - } + const oRtbRequest = converter.toORTB({bidRequests: validBidRequests, bidderRequest}); + let eids = getEids(validBidRequests[0]); + // check for url in params and set in site object + validBidRequests.forEach(bidRequest => { if (bidRequest.params.url) { - url = bidRequest.params.url; + deepSetValue(oRtbRequest, 'site.page', bidRequest.params.url); } - - return imp; }); - const request = { - id: bidderRequest.bidderRequestId, - site: { - page: url - }, - cur: [config.getConfig('currency.adServerCurrency') || DEFAULT_CUR], - imp: imps, - tmax: bidderRequest.timeout, - regs: { - ext: { - gdpr: 0, - pb_ver: '$prebid.version$' - } - } - }; - if (bidderRequest.gdprConsent) { - request.user = {}; + oRtbRequest.user = {}; - deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(request, 'regs.ext.gdpr', (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean' && bidderRequest.gdprConsent.gdprApplies) ? 1 : 0); + deepSetValue(oRtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(oRtbRequest, 'regs.ext.gdpr', (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean' && bidderRequest.gdprConsent.gdprApplies) ? 1 : 0); + deepSetValue(oRtbRequest, 'user.ext.eids', eids); } + let endpoint = config.getConfig('seedingAlliance.endpoint') || ENDPOINT_URL; + return { method: 'POST', - url: config.getConfig('seedingAlliance.endpoint') || ENDPOINT_URL, - data: JSON.stringify(request), + url: endpoint, + data: JSON.stringify(oRtbRequest), bidRequests: validBidRequests }; }, @@ -148,14 +88,13 @@ export const spec = { const { seatbid, cur } = serverResponse.body; const bidResponses = (typeof seatbid != 'undefined') ? flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { - result[bid.impid - 1] = bid; + result[bid.impid] = bid; return result; }, []) : []; - return bidRequests - .map((bidRequest, id) => { - const bidResponse = bidResponses[id]; - + .map((bidRequest) => { + const bidId = bidRequest.bidId; + const bidResponse = bidResponses[bidId]; const type = bidRequest.nativeParams ? NATIVE : BANNER; if (bidResponse) { @@ -173,7 +112,7 @@ export const spec = { }; if (type === NATIVE) { - bidObject.native = parseNative(bidResponse); + bidObject.native = parseNative(bidResponse, bidRequest.nativeParams); bidObject.mediaType = NATIVE; } @@ -191,32 +130,62 @@ export const spec = { } }; -function transformSizes(requestSizes) { - if (!isArray(requestSizes)) { - return []; +const getNativendoID = () => { + let nativendoID = storage.localStorageIsEnabled() && + storage.getDataFromLocalStorage(NATIVENDO_KEY); + + if (!nativendoID) { + if (storage.localStorageIsEnabled()) { + nativendoID = generateUUID(); + storage.setDataInLocalStorage(NATIVENDO_KEY, nativendoID); + } + } + + return nativendoID; +} + +const getEids = (bidRequest) => { + const eids = []; + const nativendoID = getNativendoID(); + + if (nativendoID) { + const nativendoUserEid = { + source: 'nativendo.de', + uids: [ + { + id: nativendoID, + atype: 1 + } + ] + }; + + eids.push(nativendoUserEid); } - if (requestSizes.length === 2 && !isArray(requestSizes[0])) { - return [{ - w: parseInt(requestSizes[0], 10), - h: parseInt(requestSizes[1], 10) - }]; - } else if (isArray(requestSizes[0])) { - return requestSizes.map(item => ({ - w: parseInt(item[0], 10), - h: parseInt(item[1], 10) - })); + if (bidRequest.userIdAsEids) { + eids.push(bidRequest.userIdAsEids); } - return []; + return eids; } function flatten(arr) { return [].concat(...arr); } -function parseNative(bid) { - const { assets, link, imptrackers } = bid.adm.native; +function parseNative(bid, nativeParams) { + let native; + if (typeof bid.adm === 'string') { + try { + native = JSON.parse(bid.adm).native; + } catch (e) { + return; + } + } else { + native = bid.adm.native; + } + + const { assets, link, imptrackers } = native; let clickUrl = link.url.replace(/\$\{AUCTION_PRICE\}/g, bid.price); @@ -239,13 +208,34 @@ function parseNative(bid) { impressionTrackers: imptrackers || undefined }; - assets.forEach(asset => { - const kind = NATIVE_ASSET_IDS[asset.id]; - const content = kind && asset[NATIVE_PARAMS[kind].name]; + let nativeParamKeys = Object.keys(nativeParams); + let id = 0; + + nativeParamKeys.forEach(nativeParam => { + assets.forEach(asset => { + if (asset.id == id) { + switch (nativeParam) { + case 'title': + result.title = asset.title.text; + break; + case 'body': + case 'cta': + case 'sponsoredBy': + result[nativeParam] = asset.data.value; + break; + case 'image': + case 'icon': + result[nativeParam] = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h + }; + break; + } + } + }); - if (content) { - result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; - } + id++; }); return result; diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index e17ff1301a3..e000c6e4ac8 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -1,7 +1,8 @@ +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { _map, isArray, triggerPixel } from '../src/utils.js'; +import { _map, getWinDimensions, isArray, triggerPixel } from '../src/utils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -10,18 +11,14 @@ import { _map, isArray, triggerPixel } from '../src/utils.js'; * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').bidderRequest} bidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid */ const BIDDER_CODE = 'seedtag'; const SEEDTAG_ALIAS = 'st'; const SEEDTAG_SSP_ENDPOINT = 'https://s.seedtag.com/c/hb/bid'; const SEEDTAG_SSP_ONTIMEOUT_ENDPOINT = 'https://s.seedtag.com/se/hb/timeout'; -const ALLOWED_DISPLAY_PLACEMENTS = [ - 'inScreen', - 'inImage', - 'inArticle', - 'inBanner', -]; // Global Vendor List Id // https://iabeurope.eu/vendor-list-tcf-v2-0/ @@ -38,6 +35,22 @@ const deviceConnection = { UNKNOWN: 'unknown', }; +export const BIDFLOOR_CURRENCY = 'USD' + +function getBidFloor(bidRequest) { + let floorInfo = {}; + + if (typeof bidRequest.getFloor === 'function') { + floorInfo = bidRequest.getFloor({ + currency: BIDFLOOR_CURRENCY, + mediaType: '*', + size: '*' + }); + } + + return floorInfo?.floor; +} + const getConnectionType = () => { const connection = navigator.connection || @@ -77,8 +90,7 @@ function hasMandatoryDisplayParams(bid) { const p = bid.params; return ( !!p.publisherId && - !!p.adUnitId && - ALLOWED_DISPLAY_PLACEMENTS.indexOf(p.placement) > -1 + !!p.adUnitId ); } @@ -93,19 +105,7 @@ function hasMandatoryVideoParams(bid) { isArray(videoParams.playerSize) && videoParams.playerSize.length > 0; - switch (bid.params.placement) { - // instream accept only video format - case 'inStream': - return isValid && videoParams.context === 'instream'; - // outstream accept banner/native/video format - default: - return ( - isValid && - videoParams.context === 'outstream' && - hasBannerMediaType(bid) && - hasMandatoryDisplayParams(bid) - ); - } + return isValid } function buildBidRequest(validBidRequest) { @@ -119,6 +119,7 @@ function buildBidRequest(validBidRequest) { const bidRequest = { id: validBidRequest.bidId, transactionId: validBidRequest.ortb2Imp?.ext?.tid, + gpid: validBidRequest.ortb2Imp?.ext?.gpid, sizes: validBidRequest.sizes, supplyTypes: mediaTypes, adUnitId: params.adUnitId, @@ -132,6 +133,11 @@ function buildBidRequest(validBidRequest) { bidRequest.videoParams = getVideoParams(validBidRequest); } + const bidFloor = getBidFloor(validBidRequest) + if (bidFloor) { + bidRequest.bidFloor = bidFloor; + } + return bidRequest; } @@ -148,6 +154,10 @@ function getVideoParams(validBidRequest) { return videoParams; } +function isVideoOutstream(validBidRequest) { + return getVideoParams(validBidRequest).context === 'outstream'; +} + function buildBidResponse(seedtagBid) { const mediaType = mapMediaType(seedtagBid.mediaType); const bid = { @@ -211,10 +221,10 @@ function geom(adunitCode) { const slot = document.getElementById(adunitCode); if (slot) { const scrollY = window.scrollY; - const { top, left, width, height } = slot.getBoundingClientRect(); + const { top, left, width, height } = getBoundingClientRect(slot); const viewport = { - width: window.innerWidth, - height: window.innerHeight, + width: getWinDimensions().innerWidth, + height: getWinDimensions().innerHeight, }; return { @@ -262,15 +272,26 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid(bid) { - return hasVideoMediaType(bid) - ? hasMandatoryVideoParams(bid) - : hasMandatoryDisplayParams(bid); + const hasVideo = hasVideoMediaType(bid); + const hasBanner = hasBannerMediaType(bid); + + // when accept both mediatype but it must be outstream + if (hasVideo && hasBanner) { + return hasMandatoryVideoParams(bid) && isVideoOutstream(bid) && hasMandatoryDisplayParams(bid); + } else if (hasVideo) { + return hasMandatoryVideoParams(bid); + } else if (hasBanner) { + return hasMandatoryDisplayParams(bid); + } else { + return false; + } }, /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests[]} validBidRequests an array of bids + * @param {bidderRequest} bidderRequest an array of bids * @return ServerRequest Info describing the request to the server. */ buildRequests(validBidRequests, bidderRequest) { @@ -284,7 +305,8 @@ export const spec = { auctionStart: bidderRequest.auctionStart || Date.now(), ttfb: ttfb(), bidRequests: _map(validBidRequests, buildBidRequest), - user: { topics: [], eids: [] } + user: { topics: [], eids: [] }, + site: {} }; if (payload.cmp) { @@ -324,6 +346,30 @@ export const spec = { payload.user.eids = validBidRequests[0].userIdAsEids } + if (bidderRequest.ortb2?.bcat) { + payload.bcat = bidderRequest.ortb2?.bcat + } + + if (bidderRequest.ortb2?.badv) { + payload.badv = bidderRequest.ortb2?.badv + } + + if (bidderRequest.ortb2?.device?.sua) { + payload.sua = bidderRequest.ortb2.device.sua + } + + if (bidderRequest.ortb2?.site?.cat) { + payload.site.cat = bidderRequest.ortb2.site.cat + } + + if (bidderRequest.ortb2?.site?.cattax) { + payload.site.cattax = bidderRequest.ortb2.site.cattax + } + + if (bidderRequest.ortb2?.site?.pagecat) { + payload.site.pagecat = bidderRequest.ortb2.site.pagecat + } + const payloadString = JSON.stringify(payload); return { @@ -369,7 +415,7 @@ export const spec = { /** * Register bidder specific code, which will execute if bidder timed out after an auction - * @param {data} Containing timeout specific data + * @param {TimedOutBid} data Containing timeout specific data */ onTimeout(data) { const url = getTimeoutUrl(data); @@ -378,7 +424,7 @@ export const spec = { /** * Function to call when the adapter wins the auction - * @param {bid} Bid information received from the server + * @param {Bid} bid The bid information received from the server */ onBidWon: function (bid) { if (bid && bid.nurl) { diff --git a/modules/semantiqRtdProvider.js b/modules/semantiqRtdProvider.js new file mode 100644 index 00000000000..5a46b019cc0 --- /dev/null +++ b/modules/semantiqRtdProvider.js @@ -0,0 +1,256 @@ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { ajax, fetch } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { generateUUID, getWindowLocation, logError, logInfo, logWarn, mergeDeep } from '../src/utils.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'semantiq'; +const LOG_PREFIX = '[SemantIQ RTD Module]: '; +const KEYWORDS_URL = 'https://api.adnz.co/api/ws-semantiq/page-keywords'; +const EVENT_COLLECTOR_URL = 'https://api.adnz.co/api/ws-clickstream-collector/submit'; +const STORAGE_KEY = `adnz_${SUBMODULE_NAME}`; +const AUDIENZZ_COMPANY_ID = 1; +const FALLBACK_TENANT_IDS = [AUDIENZZ_COMPANY_ID]; +const AUDIENZZ_GLOBAL_VENDOR_ID = 783; +const DEFAULT_TIMEOUT = 1000; + +export const storage = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: SUBMODULE_NAME, +}); + +/** + * Gets SemantIQ keywords from local storage. + * @param {string} pageUrl + * @returns {Object.} + */ +const getStorageKeywords = (pageUrl) => { + try { + const storageValue = JSON.parse(storage.getDataFromLocalStorage(STORAGE_KEY)); + + if (storageValue?.url === pageUrl) { + return storageValue.keywords; + } + + return null; + } catch (error) { + logError('Unable to get SemantiQ keywords from local storage', error); + + return null; + } +}; + +/** + * Gets URL of the current page. + * @returns {string} + */ +const getPageUrl = () => getWindowLocation().href; + +/** + * Gets tenant IDs based on the module params + * @param {Object} params + * @returns {number[]} + */ +const getTenantIds = (params = {}) => { + const { companyId } = params; + const companyIdArray = Array.isArray(companyId) ? companyId : [companyId]; + + return companyIdArray.filter(Boolean).length ? companyIdArray : FALLBACK_TENANT_IDS; +}; + +/** + * Gets keywords from cache or SemantIQ service. + * @param {Object} params + * @returns {Promise>} + */ +const getKeywords = (params) => new Promise((resolve, reject) => { + const pageUrl = getPageUrl(); + const storageKeywords = getStorageKeywords(pageUrl); + + if (storageKeywords) { + return resolve(storageKeywords); + } + + const tenantIds = getTenantIds(params); + const searchParams = new URLSearchParams(); + + searchParams.append('url', pageUrl); + searchParams.append('tenantIds', tenantIds.join(',')); + + const requestUrl = `${KEYWORDS_URL}?${searchParams.toString()}`; + + const callbacks = { + success(responseText, response) { + try { + if (response.status !== 200) { + throw new Error('Invalid response status'); + } + + const data = JSON.parse(responseText); + + if (!data) { + throw new Error('Failed to parse the response'); + } + + storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify({ url: pageUrl, keywords: data })); + resolve(data); + } catch (error) { + reject(error); + } + }, + error(error) { + reject(error); + } + } + + ajax(requestUrl, callbacks); +}); + +/** + * Converts a single key-value pair to an ORTB keyword string. + * @param {string} key + * @param {string | string[]} value + * @returns {string} + */ +export const convertSemantiqKeywordToOrtb = (key, value) => { + if (!value || !value.length) { + return ''; + } + + if (Array.isArray(value)) { + return value.map((valueItem) => `${key}=${valueItem}`).join(','); + } + + return `${key}=${value}`; +}; + +/** + * Converts SemantIQ keywords to ORTB format. + * @param {Object.} keywords + * @returns {string} + */ +export const getOrtbKeywords = (keywords) => Object.entries(keywords).reduce((acc, entry) => { + const [key, values] = entry; + const ortbKeywordString = convertSemantiqKeywordToOrtb(key, values); + + return ortbKeywordString ? [...acc, ortbKeywordString] : acc; +}, []).join(','); + +/** + * Dispatches a page impression event to the SemantIQ service. + * + * @param {number} companyId + * @returns {Promise} + */ +const dispatchPageImpressionEvent = (companyId) => { + window.audienzz = window.audienzz || {}; + window.audienzz.collectorPageImpressionId = window.audienzz.collectorPageImpressionId || generateUUID(); + const pageImpressionId = window.audienzz.collectorPageImpressionId; + const pageUrl = getPageUrl(); + + const payload = { + company_id: companyId, + event_id: generateUUID(), + event_timestamp: new Date().toISOString(), + event_type: 'pageImpression', + page_impression_id: pageImpressionId, + source: 'semantiqPrebidModule', + url: pageUrl, + }; + + return fetch(EVENT_COLLECTOR_URL, { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + }, + keepalive: true, + }); +}; + +/** + * Module init + * @param {Object} config + * @param {Object} userConsent + * @return {boolean} + */ +const init = (config, userConsent) => { + const { params = {} } = config; + const [mainCompanyId] = getTenantIds(params); + + dispatchPageImpressionEvent(mainCompanyId); + + return true; +}; + +/** + * Receives real-time data from SemantIQ service. + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} moduleConfig + */ +const getBidRequestData = ( + reqBidsConfigObj, + onDone, + moduleConfig, +) => { + let isDone = false; + + const { params = {} } = moduleConfig || {}; + const { timeout = DEFAULT_TIMEOUT } = params; + + try { + logInfo(LOG_PREFIX, { reqBidsConfigObj }); + + const { adUnits = [] } = reqBidsConfigObj; + + if (!adUnits.length) { + logWarn(LOG_PREFIX, 'No ad units found in the request'); + isDone = true; + onDone(); + } + + getKeywords(params) + .then((keywords) => { + const ortbKeywords = getOrtbKeywords(keywords); + const siteKeywords = reqBidsConfigObj.ortb2Fragments?.global?.site?.keywords; + const updatedGlobalOrtb = { site: { keywords: [siteKeywords, ortbKeywords].filter(Boolean).join(',') } }; + + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, updatedGlobalOrtb); + }) + .catch((error) => { + logError(LOG_PREFIX, error); + }) + .finally(() => { + isDone = true; + onDone(); + }); + } catch (error) { + logError(LOG_PREFIX, error); + isDone = true; + onDone(); + } + + setTimeout(() => { + if (!isDone) { + logWarn(LOG_PREFIX, 'Timeout exceeded'); + isDone = true; + onDone(); + } + }, timeout); +} + +/** @type {RtdSubmodule} */ +export const semantiqRtdSubmodule = { + name: SUBMODULE_NAME, + getBidRequestData, + init, + gvlid: AUDIENZZ_GLOBAL_VENDOR_ID, +}; + +submodule(MODULE_NAME, semantiqRtdSubmodule); diff --git a/modules/semantiqRtdProvider.md b/modules/semantiqRtdProvider.md new file mode 100644 index 00000000000..8949ad00ece --- /dev/null +++ b/modules/semantiqRtdProvider.md @@ -0,0 +1,46 @@ +# Overview + +**Module Name:** Semantiq Rtd Provider +**Module Type:** Rtd Provider +**Maintainer:** [Audienzz](https://audienzz.com) + +## Description + +This module retrieves real-time data from the SemantIQ service and populates ORTB data. + +You need to obtain a company ID from [Audienzz](https://audienzz.com) for the module to function properly. Contact [service@audienzz.ch](mailto:service@audienzz.ch) for details. + +## Integration + +1. Include the module into your `Prebid.js` build. + + ```sh + gulp build --modules='rtdModule,semantiqRtdProvider,...' + ``` + +1. Configure the module via `pbjs.setConfig`. + + ```js + pbjs.setConfig({ + ... + realTimeData: { + dataProviders: [ + { + name: 'semantiq', + waitForIt: true, + params: { + companyId: 12345, + timeout: 1000, + }, + }, + ], + }, + }); + ``` + +## Parameters + +| Name | Required | Description | Type | Default value | Example | +| ---------- | -------- | ----------------------------------------------------------------- | ------------------ | ------------- | --------------------- | +| companyId | No | Company ID or IDs obtained from [Audienzz](https://audienzz.com). | number \| number[] | - | 12345 | +| timeout | No | The maximum time to wait for a response in milliseconds. | number | 1000 | 3000 | diff --git a/modules/setupadBidAdapter.js b/modules/setupadBidAdapter.js index 55677d51c56..7687b46aab5 100644 --- a/modules/setupadBidAdapter.js +++ b/modules/setupadBidAdapter.js @@ -1,26 +1,54 @@ import { _each, - createTrackPixelHtml, - deepAccess, isStr, getBidIdParameter, triggerPixel, logWarn, + deepSetValue, } 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 BIDDER_CODE = 'setupad'; const ENDPOINT = 'https://prebid.setupad.io/openrtb2/auction'; const SYNC_ENDPOINT = 'https://cookie.stpd.cloud/sync?'; -const REPORT_ENDPOINT = 'https://adapter-analytics.setupad.io/api/adapter-analytics'; +const REPORT_ENDPOINT = 'https://adapter-analytics.setupad.io/api/adapter-analytics?'; const GVLID = 1241; const TIME_TO_LIVE = 360; -const biddersCreativeIds = {}; - -function getEids(bidRequest) { - if (deepAccess(bidRequest, 'userIdAsEids')) return bidRequest.userIdAsEids; -} +export const biddersCreativeIds = {}; // export only for tests +const NET_REVENUE = true; +const TEST_REQUEST = 0; // used only for testing + +const converter = ortbConverter({ + context: { + netRevenue: NET_REVENUE, + ttl: TIME_TO_LIVE, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue( + imp, + 'ext.prebid.storedrequest.id', + getBidIdParameter('placement_id', bidRequest.params) + ); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'test', TEST_REQUEST); + deepSetValue( + request, + 'ext.prebid.storedrequest.id', + getBidIdParameter( + 'account_id', + bidderRequest.bids.find((bid) => bid.hasOwnProperty('params')).params + ) + ); + deepSetValue(request, 'setupad', 'adapter'); + return request; + }, +}); export const spec = { code: BIDDER_CODE, @@ -28,101 +56,26 @@ export const spec = { gvlid: GVLID, isBidRequestValid: function (bid) { - return !!(bid.params.placement_id && isStr(bid.params.placement_id)); + return !!( + bid.params.placement_id && + isStr(bid.params.placement_id) && + bid.params.account_id && + isStr(bid.params.account_id) + ); }, buildRequests: function (validBidRequests, bidderRequest) { - const requests = []; - - _each(validBidRequests, function (bid) { - const id = getBidIdParameter('placement_id', bid.params); - const accountId = getBidIdParameter('account_id', bid.params); - const auctionId = bid.auctionId; - const bidId = bid.bidId; - const eids = getEids(bid) || undefined; - let sizes = bid.sizes; - if (sizes && !Array.isArray(sizes[0])) sizes = [sizes]; - - const site = { - page: bidderRequest?.refererInfo?.page, - ref: bidderRequest?.refererInfo?.ref, - domain: bidderRequest?.refererInfo?.domain, - }; - const device = { - w: bidderRequest?.ortb2?.device?.w, - h: bidderRequest?.ortb2?.device?.h, - }; - - const payload = { - id: bid?.bidderRequestId, - ext: { - prebid: { - storedrequest: { - id: accountId || 'default', - }, - }, - }, - user: { ext: { eids } }, - device, - site, - imp: [], - }; - - const imp = { - id: bid.adUnitCode, - ext: { - prebid: { - storedrequest: { id }, - }, - }, - }; - - if (deepAccess(bid, 'mediaTypes.banner')) { - imp.banner = { - format: (sizes || []).map((s) => { - return { w: s[0], h: s[1] }; - }), - }; - } - - payload.imp.push(imp); - - const gdprConsent = bidderRequest && bidderRequest.gdprConsent; - const uspConsent = bidderRequest && bidderRequest.uspConsent; - - if (gdprConsent || uspConsent) { - payload.regs = { ext: {} }; - - if (uspConsent) payload.regs.ext.us_privacy = uspConsent; - - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies !== 'undefined') { - payload.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; - } - - if (typeof gdprConsent.consentString !== 'undefined') { - payload.user.ext.consent = gdprConsent.consentString; - } - } - } - const params = bid.params; - - requests.push({ - method: 'POST', - url: ENDPOINT, - data: JSON.stringify(payload), - options: { - contentType: 'text/plain', - withCredentials: true, - }, - - bidId, - params, - auctionId, - }); - }); - - return requests; + const data = converter.toORTB({ validBidRequests, bidderRequest }); + + return { + method: 'POST', + url: ENDPOINT, + data, + options: { + contentType: 'text/plain', + withCredentials: true, + }, + }; }, interpretResponse: function (serverResponse, bidRequest) { @@ -136,40 +89,22 @@ export const spec = { return []; } - const serverBody = serverResponse.body; - const bidResponses = []; - - _each(serverBody.seatbid, (res) => { + // set a seat for creativeId for triggerPixel url + _each(serverResponse.body.seatbid, (res) => { _each(res.bid, (bid) => { - const requestId = bidRequest.bidId; - const params = bidRequest.params; - const { ad, adUrl } = getAd(bid); - - const bidResponse = { - requestId, - params, - cpm: bid.price, - width: bid.w, - height: bid.h, - creativeId: bid.id, - currency: serverBody.cur, - netRevenue: true, - ttl: TIME_TO_LIVE, - meta: { - advertiserDomains: bid.adomain || [], - }, - }; - - // set a seat for creativeId for triggerPixel url - biddersCreativeIds[bidResponse.creativeId] = res.seat; - - bidResponse.ad = ad; - bidResponse.adUrl = adUrl; - bidResponses.push(bidResponse); + biddersCreativeIds[bid.crid] = res.seat; }); }); - return bidResponses; + // used for a test case "should update biddersCreativeIds correctly" to return early and not throw ORTB error + if (serverResponse.testCase === 1) return; + + const bids = converter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }).bids; + + return bids; }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { @@ -182,8 +117,8 @@ export const spec = { const queryParams = []; queryParams.push(`bidders=${bidders}`); - queryParams.push('gdpr=' + +gdprConsent.gdprApplies); - queryParams.push('gdpr_consent=' + gdprConsent.consentString); + queryParams.push('gdpr=' + +gdprConsent?.gdprApplies); + queryParams.push('gdpr_consent=' + gdprConsent?.consentString); queryParams.push('usp_consent=' + (uspConsent || '')); const strQueryParams = queryParams.join('&'); @@ -225,17 +160,23 @@ export const spec = { if (!placementIds) return; - let extraBidParams = ''; - // find the winning bidder by using creativeId as identification if (biddersCreativeIds.hasOwnProperty(bid.creativeId) && biddersCreativeIds[bid.creativeId]) { bidder = biddersCreativeIds[bid.creativeId]; } - // Add extra parameters - extraBidParams = `&cpm=${bid.originalCpm}¤cy=${bid.originalCurrency}`; + const queryParams = []; + queryParams.push(`event=bidWon`); + queryParams.push('bidder=' + bidder); + queryParams.push('placementIds=' + placementIds); + queryParams.push('auctionId=' + auctionId); + queryParams.push('cpm=' + bid.originalCpm); + queryParams.push('currency=' + bid.originalCurrency); + queryParams.push('timestamp=' + Date.now()); - const url = `${REPORT_ENDPOINT}?event=bidWon&bidder=${bidder}&placementIds=${placementIds}&auctionId=${auctionId}${extraBidParams}×tamp=${Date.now()}`; + const strQueryParams = queryParams.join('&'); + + const url = REPORT_ENDPOINT + strQueryParams; triggerPixel(url); }, }; @@ -250,22 +191,4 @@ function getBidders(serverResponse) { } } -function getAd(bid) { - let ad, adUrl; - - switch (deepAccess(bid, 'ext.prebid.type')) { - default: - if (bid.adm && bid.nurl) { - ad = bid.adm; - ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); - } else if (bid.adm) { - ad = bid.adm; - } else if (bid.nurl) { - adUrl = bid.nurl; - } - } - - return { ad, adUrl }; -} - registerBidder(spec); diff --git a/modules/setupadBidAdapter.md b/modules/setupadBidAdapter.md index 0d4f0ef392e..c11e8eb4bae 100644 --- a/modules/setupadBidAdapter.md +++ b/modules/setupadBidAdapter.md @@ -26,7 +26,7 @@ const adUnits = [ bidder: 'setupad', params: { placement_id: '123', //required - account_id: '123', //optional + account_id: '123', //required }, }, ], diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index fa8b5e3bfdb..d045587262f 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -7,7 +7,6 @@ import {parseUrl, buildUrl, triggerPixel, logInfo, hasDeviceAccess, generateUUID} from '../src/utils.js'; import {submodule} from '../src/hook.js'; -import {coppaDataHandler} from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {VENDORLESS_GVLID} from '../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; @@ -118,9 +117,7 @@ export const sharedIdSystemSubmodule = { logInfo('PubCommonId: Has opted-out'); return; } - const coppa = coppaDataHandler.getCoppa(); - - if (coppa) { + if (consentData?.coppa) { logInfo('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); return; } @@ -164,8 +161,7 @@ export const sharedIdSystemSubmodule = { logInfo('PubCommonId: Has opted-out'); return {id: undefined}; } - const coppa = coppaDataHandler.getCoppa(); - if (coppa) { + if (consentData?.coppa) { logInfo('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); return; } @@ -183,9 +179,15 @@ export const sharedIdSystemSubmodule = { domainOverride: domainOverrideToRootDomain(storage, 'sharedId'), eids: { - 'pubcid': { - source: 'pubcid.org', - atype: 1 + 'pubcid'(values, config) { + const eid = { + source: 'pubcid.org', + uids: values.map(id => ({id, atype: 1})) + } + if (config?.params?.inserter != null) { + eid.inserter = config.params.inserter; + } + return eid; }, } }; diff --git a/modules/sharethroughAnalyticsAdapter.md b/modules/sharethroughAnalyticsAdapter.md new file mode 100644 index 00000000000..4acf8eacfd0 --- /dev/null +++ b/modules/sharethroughAnalyticsAdapter.md @@ -0,0 +1,41 @@ +# Overview + +```txt +Module Name: Sharethrough Analytics Adapter +Module Type: Analytics Adapter +Maintainer: pubgrowth.engineering@sharethrough.com +``` + +#### About + +This analytics adapter collects data about win/loss events (beacon firings) from each auction run on your site. This data is communicated to Sharethrough via API calls the analytics adapter makes to an endpoint dedicated to the collection of beacon information. Sharethrough uses this information to improve its services as a SSP. + +This analytics adapter is free to use. + +#### Configuration + +In order to guarantee consistent reporting events, we recommend +including the GPT Pre-Auction Module, `gptPreAuction`. This module is included +by default when Prebid is downloaded. + +If you are compiling from source, this might look something like: + +```sh +gulp bundle --modules=gptPreAuction,sharethroughBidAdapter,sharethroughAnalyticsAdapter +``` + +Please note that the above snippet is a "bare bones" example - you will likely want to include other modules as well. A more realistic example might look something like the example below (with other bid adapters also included in the list as needed): + +```sh +gulp bundle --modules=gptPreAuction,consentManagement,consentManagementGpp,consentManagementUsp,enrichmentFpdModule,gdprEnforcement,sharethroughBidAdapter,sharethroughAnalyticsAdapter +``` + +Enable the Sharethrough Analytics Adapter in Prebid.js using the analytics provider `sharethrough` as seen in the example below. + +#### Example Configuration + +```js +pbjs.enableAnalytics({ + provider: 'sharethrough', +}); +``` diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 2264bc37ebb..7144370dc9c 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,13 +1,14 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { deepAccess, generateUUID, inIframe, mergeDeep } from '../src/utils.js'; +import { deepAccess, generateUUID, inIframe, isPlainObject, logWarn, mergeDeep } from '../src/utils.js'; const VERSION = '4.3.0'; const BIDDER_CODE = 'sharethrough'; const SUPPLY_ID = 'WYu2BXv1'; const STR_ENDPOINT = `https://btlr.sharethrough.com/universal/v1?supply_id=${SUPPLY_ID}`; +const IDENTIFIER_PREFIX = 'Sharethrough:'; // this allows stubbing of utility function that is used internally by the sharethrough adapter export const sharethroughInternal = { @@ -18,7 +19,7 @@ export const sharethroughAdapterSpec = { code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], gvlid: 80, - isBidRequestValid: (bid) => !!bid.params.pkey && bid.bidder === BIDDER_CODE, + isBidRequestValid: (bid) => !!bid.params.pkey, buildRequests: (bidRequests, bidderRequest) => { const timeout = bidderRequest.timeout; @@ -68,6 +69,11 @@ export const sharethroughAdapterSpec = { req.device.ext['cdep'] = bidderRequest.ortb2.device.ext.cdep; } + // if present, merge device object from ortb2 into `req.device` + if (bidderRequest?.ortb2?.device) { + mergeDeep(req.device, bidderRequest.ortb2.device); + } + req.user = nullish(firstPartyData.user, {}); if (!req.user.ext) req.user.ext = {}; req.user.ext.eids = bidRequests[0].userIdAsEids || []; @@ -103,12 +109,12 @@ export const sharethroughAdapterSpec = { // mergeDeep(impression, bidReq.ortb2Imp); // leaving this out for now as we may want to leave stuff out on purpose const tid = deepAccess(bidReq, 'ortb2Imp.ext.tid'); if (tid) impression.ext.tid = tid; - const gpid = deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot')); + const gpid = deepAccess(bidReq, 'ortb2Imp.ext.gpid') || deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot'); if (gpid) impression.ext.gpid = gpid; const videoRequest = deepAccess(bidReq, 'mediaTypes.video'); - if (bidderRequest.fledgeEnabled && bidReq.mediaTypes.banner) { + if (bidderRequest.paapi?.enabled && bidReq.mediaTypes.banner) { mergeDeep(impression, { ext: { ae: 1 } }); // ae = auction environment; if this is 1, ad server knows we have a fledge auction } @@ -124,43 +130,48 @@ export const sharethroughAdapterSpec = { [w, h] = videoRequest.playerSize[0]; } - const getVideoPlacementValue = (vidReq) => { - if (vidReq.plcmt) { - return vidReq.placement; - } else { - return vidReq.context === 'instream' ? 1 : +deepAccess(vidReq, 'placement', 4); + /** + * Applies a specified property to an impression object if it is present in the video request + * @param {string} prop A property to apply to the impression object + * @param {object} vidReq A video request object from which to extract the property + * @param {object} imp A video impression object to which to apply the property + */ + const applyVideoProperty = (prop, vidReq, imp) => { + const propIsTypeArray = ['api', 'battr', 'mimes', 'playbackmethod', 'protocols'].includes(prop); + 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.`); + return; + } + } + if (vidReq[prop]) { + imp.video[prop] = vidReq[prop]; } }; impression.video = { pos: nullish(videoRequest.pos, 0), topframe: inIframe() ? 0 : 1, - skip: nullish(videoRequest.skip, 0), - linearity: nullish(videoRequest.linearity, 1), - minduration: nullish(videoRequest.minduration, 5), - maxduration: nullish(videoRequest.maxduration, 60), - playbackmethod: videoRequest.playbackmethod || [2], - api: getVideoApi(videoRequest), - mimes: videoRequest.mimes || ['video/mp4'], - protocols: getVideoProtocols(videoRequest), w, h, - startdelay: nullish(videoRequest.startdelay, 0), - skipmin: nullish(videoRequest.skipmin, 0), - skipafter: nullish(videoRequest.skipafter, 0), - placement: getVideoPlacementValue(videoRequest), - plcmt: videoRequest.plcmt ? videoRequest.plcmt : null, }; - if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; - if (videoRequest.companiontype) impression.video.companiontype = videoRequest.companiontype; - if (videoRequest.companionad) impression.video.companionad = videoRequest.companionad; + const propertiesToConsider = [ + 'api', 'battr', 'companionad', 'companiontype', 'delivery', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', 'playbackmethod', 'plcmt', 'protocols', 'skip', 'skipafter', 'skipmin', 'startdelay' + ] + + propertiesToConsider.forEach(propertyToConsider => { + applyVideoProperty(propertyToConsider, videoRequest, impression); + }); } else { impression.banner = { pos: deepAccess(bidReq, 'mediaTypes.banner.pos', 0), 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') + if (battr) impression.banner.battr = battr } return { @@ -242,7 +253,7 @@ export const sharethroughAdapterSpec = { if (fledgeAuctionEnabled) { return { bids: bidsFromExchange, - fledgeAuctionConfigs: body.ext?.auctionConfigs || {}, + paapi: body.ext?.auctionConfigs || {}, }; } else { return bidsFromExchange; @@ -266,24 +277,6 @@ export const sharethroughAdapterSpec = { onSetTargeting: (bid) => {}, }; -function getVideoApi({ api }) { - let defaultValue = [2]; - if (api && Array.isArray(api) && api.length > 0) { - return api; - } else { - return defaultValue; - } -} - -function getVideoProtocols({ protocols }) { - let defaultValue = [2, 3, 5, 6, 7, 8]; - if (protocols && Array.isArray(protocols) && protocols.length > 0) { - return protocols; - } else { - return defaultValue; - } -} - function getBidRequestFloor(bid) { let floor = null; if (typeof bid.getFloor === 'function') { @@ -292,7 +285,7 @@ function getBidRequestFloor(bid) { mediaType: bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner', size: bid.sizes.map((size) => ({ w: size[0], h: size[1] })), }); - if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + if (isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = parseFloat(floorInfo.floor); } } diff --git a/modules/shinezBidAdapter.js b/modules/shinezBidAdapter.js index 47fca317de2..f88360c4c38 100644 --- a/modules/shinezBidAdapter.js +++ b/modules/shinezBidAdapter.js @@ -1,39 +1,17 @@ -import { - logWarn, - logInfo, - isArray, - isFn, - deepAccess, - isEmpty, - contains, - timestamp, - triggerPixel, - isInteger, - getBidIdParameter -} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; +import {makeBaseSpec} from '../libraries/riseUtils/index.js'; -const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'shinez'; -const ADAPTER_VERSION = '1.0.0'; -const TTL = 360; -const CURRENCY = 'USD'; -const SELLER_ENDPOINT = 'https://hb.sweetgum.io/'; +const BASE_URL = 'https://hb.sweetgum.io/'; const MODES = { PRODUCTION: 'hb-sz-multi', TEST: 'hb-multi-sz-test' -} -const SUPPORTED_SYNC_METHODS = { - IFRAME: 'iframe', - PIXEL: 'pixel' -} +}; export const spec = { + ...makeBaseSpec(BASE_URL, MODES), code: BIDDER_CODE, - version: ADAPTER_VERSION, - supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { if (!bidRequest.params) { logWarn('no params have been set to Shinez adapter'); @@ -46,402 +24,7 @@ export const spec = { } return true; - }, - buildRequests: function (validBidRequests, bidderRequest) { - const combinedRequestsObject = {}; - - // use data from the first bid, to create the general params for all bids - const generalObject = validBidRequests[0]; - const testMode = generalObject.params.testMode; - - combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); - combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); - - return { - method: 'POST', - url: getEndpoint(testMode), - data: combinedRequestsObject - } - }, - interpretResponse: function ({body}) { - const bidResponses = []; - - if (body.bids) { - body.bids.forEach(adUnit => { - const bidResponse = { - requestId: adUnit.requestId, - cpm: adUnit.cpm, - currency: adUnit.currency || CURRENCY, - width: adUnit.width, - height: adUnit.height, - ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, - netRevenue: adUnit.netRevenue || true, - nurl: adUnit.nurl, - mediaType: adUnit.mediaType, - meta: { - mediaType: adUnit.mediaType - } - }; - - if (adUnit.mediaType === VIDEO) { - bidResponse.vastXml = adUnit.vastXml; - } else if (adUnit.mediaType === BANNER) { - bidResponse.ad = adUnit.ad; - } - - if (adUnit.adomain && adUnit.adomain.length) { - bidResponse.meta.advertiserDomains = adUnit.adomain; - } - - bidResponses.push(bidResponse); - }); - } - - return bidResponses; - }, - getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; - for (const response of serverResponses) { - if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { - syncs.push({ - type: 'iframe', - url: response.body.params.userSyncURL - }); - } - if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { - const pixels = response.body.params.userSyncPixels.map(pixel => { - return { - type: 'image', - url: pixel - } - }) - syncs.push(...pixels) - } - } - return syncs; - }, - onBidWon: function (bid) { - if (bid == null) { - return; - } - - logInfo('onBidWon:', bid); - if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { - triggerPixel(bid.nurl); - } } }; registerBidder(spec); - -/** - * Get floor price - * @param bid {bid} - * @returns {Number} - */ -function getFloor(bid, mediaType) { - if (!isFn(bid.getFloor)) { - return 0; - } - let floorResult = bid.getFloor({ - currency: CURRENCY, - mediaType: mediaType, - size: '*' - }); - return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; -} - -/** - * Get the the ad sizes array from the bid - * @param bid {bid} - * @returns {Array} - */ -function getSizesArray(bid, mediaType) { - let sizesArray = [] - - if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { - sizesArray = bid.mediaTypes[mediaType].sizes; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - sizesArray = bid.sizes; - } - - return sizesArray; -} - -/** - * Get schain string value - * @param schainObject {Object} - * @returns {string} - */ -function getSupplyChain(schainObject) { - if (isEmpty(schainObject)) { - return ''; - } - let scStr = `${schainObject.ver},${schainObject.complete}`; - schainObject.nodes.forEach((node) => { - scStr += '!'; - scStr += `${getEncodedValIfNotEmpty(node.asi)},`; - scStr += `${getEncodedValIfNotEmpty(node.sid)},`; - scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; - scStr += `${getEncodedValIfNotEmpty(node.rid)},`; - scStr += `${getEncodedValIfNotEmpty(node.name)},`; - scStr += `${getEncodedValIfNotEmpty(node.domain)}`; - }); - return scStr; -} - -/** - * Get encoded node value - * @param val {string} - * @returns {string} - */ -function getEncodedValIfNotEmpty(val) { - return !isEmpty(val) ? encodeURIComponent(val) : ''; -} - -/** - * Get preferred user-sync method based on publisher configuration - * @param bidderCode {string} - * @returns {string} - */ -function getAllowedSyncMethod(filterSettings, bidderCode) { - const iframeConfigsToCheck = ['all', 'iframe']; - const pixelConfigToCheck = 'image'; - if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { - return SUPPORTED_SYNC_METHODS.IFRAME; - } - if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { - return SUPPORTED_SYNC_METHODS.PIXEL; - } -} - -/** - * Check if sync rule is supported - * @param syncRule {Object} - * @param bidderCode {string} - * @returns {boolean} - */ -function isSyncMethodAllowed(syncRule, bidderCode) { - if (!syncRule) { - return false; - } - const isInclude = syncRule.filter === 'include'; - const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; - return isInclude && contains(bidders, bidderCode); -} - -/** - * Get the seller endpoint - * @param testMode {boolean} - * @returns {string} - */ -function getEndpoint(testMode) { - return testMode - ? SELLER_ENDPOINT + MODES.TEST - : SELLER_ENDPOINT + MODES.PRODUCTION; -} - -/** - * get device type - * @param uad {ua} - * @returns {string} - */ -function getDeviceType(ua) { - if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i - .test(ua.toLowerCase())) { - return '5'; - } - if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i - .test(ua.toLowerCase())) { - return '4'; - } - if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i - .test(ua.toLowerCase())) { - return '3'; - } - return '1'; -} - -function generateBidsParams(validBidRequests, bidderRequest) { - const bidsArray = []; - - if (validBidRequests.length) { - validBidRequests.forEach(bid => { - bidsArray.push(generateBidParameters(bid, bidderRequest)); - }); - } - - return bidsArray; -} - -/** - * Generate bid specific parameters - * @param {bid} bid - * @param {bidderRequest} bidderRequest - * @returns {Object} bid specific params object - */ -function generateBidParameters(bid, bidderRequest) { - const {params} = bid; - const mediaType = isBanner(bid) ? BANNER : VIDEO; - const sizesArray = getSizesArray(bid, mediaType); - - // fix floor price in case of NAN - if (isNaN(params.floorPrice)) { - params.floorPrice = 0; - } - - const bidObject = { - mediaType, - adUnitCode: getBidIdParameter('adUnitCode', bid), - sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), - bidId: getBidIdParameter('bidId', bid), - bidderRequestId: getBidIdParameter('bidderRequestId', bid), - transactionId: bid.ortb2Imp?.ext?.tid || '', - }; - - const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); - if (pos) { - bidObject.pos = pos; - } - - const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); - if (gpid) { - bidObject.gpid = gpid; - } - - const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); - if (placementId) { - bidObject.placementId = placementId; - } - - if (mediaType === VIDEO) { - const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); - let playbackMethodValue; - - // verify playbackMethod is of type integer array, or integer only. - if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { - // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation - playbackMethodValue = playbackMethod[0]; - } else if (isInteger(playbackMethod)) { - playbackMethodValue = playbackMethod; - } - - if (playbackMethodValue) { - bidObject.playbackMethod = playbackMethodValue; - } - - const placement = deepAccess(bid, `mediaTypes.video.placement`); - if (placement) { - bidObject.placement = placement; - } - - const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); - if (minDuration) { - bidObject.minDuration = minDuration; - } - - const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); - if (maxDuration) { - bidObject.maxDuration = maxDuration; - } - - const skip = deepAccess(bid, `mediaTypes.video.skip`); - if (skip) { - bidObject.skip = skip; - } - - const linearity = deepAccess(bid, `mediaTypes.video.linearity`); - if (linearity) { - bidObject.linearity = linearity; - } - } - - return bidObject; -} - -function isBanner(bid) { - return bid.mediaTypes && bid.mediaTypes.banner; -} - -/** - * Generate params that are common between all bids - * @param {single bid object} generalObject - * @param {bidderRequest} bidderRequest - * @returns {object} the common params object - */ -function generateGeneralParams(generalObject, bidderRequest) { - const domain = window.location.hostname; - const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; - const {bidderCode} = bidderRequest; - const generalBidParams = generalObject.params; - const timeout = bidderRequest.timeout; - - // these params are snake_case instead of camelCase to allow backwards compatability on the server. - // in the future, these will be converted to camelCase to match our convention. - const generalParams = { - wrapper_type: 'prebidjs', - wrapper_vendor: '$$PREBID_GLOBAL$$', - wrapper_version: '$prebid.version$', - adapter_version: ADAPTER_VERSION, - auction_start: timestamp(), - publisher_id: generalBidParams.org, - publisher_name: domain, - site_domain: domain, - dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, - device_type: getDeviceType(navigator.userAgent), - ua: navigator.userAgent, - session_id: getBidIdParameter('auctionId', generalObject), - tmax: timeout - }; - - const userIdsParam = getBidIdParameter('userId', generalObject); - if (userIdsParam) { - generalParams.userIds = JSON.stringify(userIdsParam); - } - - const ortb2Metadata = bidderRequest.ortb2 || {}; - if (ortb2Metadata.site) { - generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); - } - if (ortb2Metadata.user) { - generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); - } - - if (syncEnabled) { - const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); - if (allowedSyncMethod) { - generalParams.cs_method = allowedSyncMethod; - } - } - - if (bidderRequest.uspConsent) { - generalParams.us_privacy = bidderRequest.uspConsent; - } - - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; - generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - - if (generalBidParams.ifa) { - generalParams.ifa = generalBidParams.ifa; - } - - if (generalObject.schain) { - generalParams.schain = getSupplyChain(generalObject.schain); - } - - if (bidderRequest.ortb2 && bidderRequest.ortb2.site) { - generalParams.referrer = bidderRequest.ortb2.site.ref; - generalParams.page_url = bidderRequest.ortb2.site.page; - } - - if (bidderRequest && bidderRequest.refererInfo) { - generalParams.referrer = generalParams.referrer || deepAccess(bidderRequest, 'refererInfo.referer'); - generalParams.page_url = generalParams.page_url || config.getConfig('pageUrl') || deepAccess(window, 'location.href'); - } - - return generalParams; -} diff --git a/modules/shinezBidAdapter.md b/modules/shinezBidAdapter.md index f0ef7a6c218..203b6ece4ff 100644 --- a/modules/shinezBidAdapter.md +++ b/modules/shinezBidAdapter.md @@ -13,7 +13,7 @@ Module that connects to Shinez's demand sources. The Shinez adapter requires setup and approval from the Shinez. Please reach out to tech-team@shinez.io to create an Shinez account. -The adapter supports Video(instream) & Banner. +The adapter supports Video(instream), Banner, Native and multi-format bid requests. # Bid Parameters ## Video @@ -73,4 +73,4 @@ var adUnits = [{ }] } ]; -``` \ No newline at end of file +``` diff --git a/modules/shinezRtbBidAdapter.js b/modules/shinezRtbBidAdapter.js index d1d9f36a569..f2034bd1992 100644 --- a/modules/shinezRtbBidAdapter.js +++ b/modules/shinezRtbBidAdapter.js @@ -1,327 +1,29 @@ -import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; -import {config} from '../src/config.js'; +import { + isBidRequestValid, + createBuildRequestsFn, + createInterpretResponseFn, createUserSyncGetter +} from '../libraries/vidazooUtils/bidderUtils.js'; const DEFAULT_SUB_DOMAIN = 'exchange'; const BIDDER_CODE = 'shinezRtb'; const BIDDER_VERSION = '1.0.0'; -const CURRENCY = 'USD'; -const TTL_SECONDS = 60 * 5; -const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); - -function getTopWindowQueryParams() { - try { - const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); - return parsedUrl.search; - } catch (e) { - return ''; - } -} +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.sweetgum.io`; } -export function extractCID(params) { - return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; -} - -export function extractPID(params) { - return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; -} - -export function extractSubDomain(params) { - return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; -} - -function isBidRequestValid(bid) { - const params = bid.params || {}; - return !!(extractCID(params) && extractPID(params)); -} - -function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { - const { - params, - bidId, - userId, - adUnitCode, - schain, - mediaTypes, - ortb2Imp, - bidderRequestId, - bidRequestsCount, - bidderRequestsCount, - bidderWinsCount - } = bid; - let {bidFloor, ext} = params; - const hashUrl = hashCode(topWindowUrl); - const uniqueDealId = getUniqueDealId(hashUrl); - const cId = extractCID(params); - const pId = extractPID(params); - const subDomain = extractSubDomain(params); - - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); - - if (isFn(bid.getFloor)) { - const floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - - if (floorInfo.currency === 'USD') { - bidFloor = floorInfo.floor; - } - } - - let data = { - url: encodeURIComponent(topWindowUrl), - uqs: getTopWindowQueryParams(), - cb: Date.now(), - bidFloor: bidFloor, - bidId: bidId, - referrer: bidderRequest.refererInfo.ref, - adUnitCode: adUnitCode, - publisherId: pId, - sizes: sizes, - uniqueDealId: uniqueDealId, - bidderVersion: BIDDER_VERSION, - prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}`, - schain: schain, - mediaTypes: mediaTypes, - gpid: gpid, - transactionId: ortb2Imp?.ext?.tid, - bidderRequestId: bidderRequestId, - bidRequestsCount: bidRequestsCount, - bidderRequestsCount: bidderRequestsCount, - bidderWinsCount: bidderWinsCount, - bidderTimeout: bidderTimeout - }; - - appendUserIdsToRequestPayload(data, userId); - - const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); - - if (sua) { - data.sua = sua; - } - - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.consentString) { - data.gdprConsent = bidderRequest.gdprConsent.consentString; - } - if (bidderRequest.gdprConsent.gdprApplies !== undefined) { - data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - } - if (bidderRequest.uspConsent) { - data.usPrivacy = bidderRequest.uspConsent; - } - - if (bidderRequest.gppConsent) { - data.gppString = bidderRequest.gppConsent.gppString; - data.gppSid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - data.gppString = bidderRequest.ortb2.regs.gpp; - data.gppSid = bidderRequest.ortb2.regs.gpp_sid; - } - - const dto = { - method: 'POST', - url: `${createDomain(subDomain)}/prebid/multi/${cId}`, - data: data - }; - - _each(ext, (value, key) => { - dto.data['ext.' + key] = value; - }); - - return dto; -} - -function appendUserIdsToRequestPayload(payloadRef, userIds) { - let key; - _each(userIds, (userId, idSystemProviderName) => { - key = `uid.${idSystemProviderName}`; +const buildRequests = createBuildRequestsFn(createDomain, null, storage, BIDDER_CODE, BIDDER_VERSION, false); - switch (idSystemProviderName) { - case 'digitrustid': - payloadRef[key] = deepAccess(userId, 'data.id'); - break; - case 'lipb': - payloadRef[key] = userId.lipbid; - break; - case 'parrableId': - payloadRef[key] = userId.eid; - break; - case 'id5id': - payloadRef[key] = userId.uid; - break; - default: - payloadRef[key] = userId; - } - }); -} - -function buildRequests(validBidRequests, bidderRequest) { - const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - const bidderTimeout = config.getConfig('bidderTimeout'); - const requests = []; - validBidRequests.forEach(validBidRequest => { - const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); - requests.push(request); - }); - return requests; -} - -function interpretResponse(serverResponse, request) { - if (!serverResponse || !serverResponse.body) { - return []; - } - const {bidId} = request.data; - const {results} = serverResponse.body; - - let output = []; - - try { - results.forEach(result => { - const { - creativeId, - ad, - price, - exp, - width, - height, - currency, - metaData, - advertiserDomains, - mediaType = BANNER - } = result; - if (!ad || !price) { - return; - } - - const response = { - requestId: bidId, - cpm: price, - width: width, - height: height, - creativeId: creativeId, - currency: currency || CURRENCY, - netRevenue: true, - ttl: exp || TTL_SECONDS, - }; - - if (metaData) { - Object.assign(response, { - meta: metaData - }) - } else { - Object.assign(response, { - meta: { - advertiserDomains: advertiserDomains || [] - } - }) - } - - if (mediaType === BANNER) { - Object.assign(response, { - ad: ad, - }); - } else { - Object.assign(response, { - vastXml: ad, - mediaType: VIDEO - }); - } - output.push(response); - }); - return output; - } catch (e) { - return []; - } -} - -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { - let syncs = []; - const {iframeEnabled, pixelEnabled} = syncOptions; - const {gdprApplies, consentString = ''} = gdprConsent; - - const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` - if (iframeEnabled) { - syncs.push({ - type: 'iframe', - url: `https://sync.sweetgum.io/api/sync/iframe/${params}` - }); - } - if (pixelEnabled) { - syncs.push({ - type: 'image', - url: `https://sync.sweetgum.io/api/sync/image/${params}` - }); - } - return syncs; -} +const interpretResponse = createInterpretResponseFn(BIDDER_CODE, false); -export function hashCode(s, prefix = '_') { - const l = s.length; - let h = 0 - let i = 0; - if (l > 0) { - while (i < l) { - h = (h << 5) - h + s.charCodeAt(i++) | 0; - } - } - return prefix + h; -} - -export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { - const storageKey = `u_${key}`; - const now = Date.now(); - const data = getStorageItem(storageKey); - let uniqueId; - - if (!data || !data.value || now - data.created > expiry) { - uniqueId = `${key}_${now.toString()}`; - setStorageItem(storageKey, uniqueId); - } else { - uniqueId = data.value; - } - - return uniqueId; -} - -export function getStorageItem(key) { - try { - return tryParseJSON(storage.getDataFromLocalStorage(key)); - } catch (e) { - } - - return null; -} - -export function setStorageItem(key, value, timestamp) { - try { - const created = timestamp || Date.now(); - const data = JSON.stringify({value, created}); - storage.setDataInLocalStorage(key, data); - } catch (e) { - } -} - -export function tryParseJSON(value) { - try { - return JSON.parse(value); - } catch (e) { - return value; - } -} +const getUserSyncs = createUserSyncGetter({ + iframeSyncUrl: 'https://sync.sweetgum.io/api/sync/iframe', + imageSyncUrl: 'https://sync.sweetgum.io/api/sync/image', +}); export const spec = { code: BIDDER_CODE, diff --git a/modules/showheroes-bsBidAdapter.js b/modules/showheroes-bsBidAdapter.js index bd2706a21d5..afac0a88567 100644 --- a/modules/showheroes-bsBidAdapter.js +++ b/modules/showheroes-bsBidAdapter.js @@ -1,193 +1,114 @@ import { deepAccess, - getWindowTop, + deepSetValue, triggerPixel, - logInfo, - logError, getBidIdParameter + isFn, + logInfo } from '../src/utils.js'; -import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import { loadExternalScript } from '../src/adloader.js'; +import { VIDEO } from '../src/mediaTypes.js'; -const PROD_ENDPOINT = 'https://bs.showheroes.com/api/v1/bid'; -const STAGE_ENDPOINT = 'https://bid-service.stage.showheroes.com/api/v1/bid'; -const VIRALIZE_ENDPOINT = 'https://ads.viralize.tv/prebid-sh/'; -const PROD_PUBLISHER_TAG = 'https://static.showheroes.com/publishertag.js'; -const STAGE_PUBLISHER_TAG = 'https://pubtag.stage.showheroes.com/publishertag.js'; -const PROD_VL = 'https://video-library.showheroes.com'; -const STAGE_VL = 'https://video-library.stage.showheroes.com'; +const ENDPOINT = 'https://ads.viralize.tv/openrtb2/auction/'; const BIDDER_CODE = 'showheroes-bs'; const TTL = 300; -function getEnvURLs(isStage) { - return { - pubTag: isStage ? STAGE_PUBLISHER_TAG : PROD_PUBLISHER_TAG, - vlHost: isStage ? STAGE_VL : PROD_VL - } -} - -const GVLID = 111; - -export const spec = { - code: BIDDER_CODE, - gvlid: GVLID, - aliases: ['showheroesBs'], - supportedMediaTypes: [VIDEO, BANNER], - isBidRequestValid: function(bid) { - return !!bid.params.playerId || !!bid.params.unitId; +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: TTL, + currency: 'EUR', + mediaType: VIDEO, }, - buildRequests: function(validBidRequests, bidderRequest) { - let adUnits = []; - const pageURL = validBidRequests[0].params.contentPageUrl || - bidderRequest.refererInfo.canonicalUrl || - deepAccess(window, 'location.href'); - const isStage = !!validBidRequests[0].params.stage; - const isViralize = !!validBidRequests[0].params.unitId; - const isOutstream = deepAccess(validBidRequests[0], 'mediaTypes.video.context') === 'outstream'; - const isCustomRender = deepAccess(validBidRequests[0], 'params.outstreamOptions.customRender'); - const isNodeRender = deepAccess(validBidRequests[0], 'params.outstreamOptions.slot') || deepAccess(validBidRequests[0], 'params.outstreamOptions.iframe'); - const isNativeRender = deepAccess(validBidRequests[0], 'renderer'); - const outstreamOptions = deepAccess(validBidRequests[0], 'params.outstreamOptions'); - const isBanner = !!validBidRequests[0].mediaTypes.banner || (isOutstream && !(isCustomRender || isNativeRender || isNodeRender)); - const defaultSchain = validBidRequests[0].schain || {}; - - const consentData = bidderRequest.gdprConsent || {}; - const uspConsent = bidderRequest.uspConsent || ''; - const gdprConsent = { - apiVersion: consentData.apiVersion || 2, - gdprApplies: consentData.gdprApplies || 0, - consentString: consentData.consentString || '', + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); + deepSetValue(imp, 'video.ext.context', videoContext); + imp.ext = imp.ext || {}; + imp.ext.params = bidRequest.params; + imp.ext.adUnitCode = bidRequest.adUnitCode; + + if (!imp.displaymanager) { + imp.displaymanager = 'Prebid.js'; + imp.displaymanagerver = '$prebid.version$'; // prebid version } - validBidRequests.forEach((bid) => { - const videoSizes = getVideoSizes(bid); - const bannerSizes = getBannerSizes(bid); - const vpaidMode = getBidIdParameter('vpaidMode', bid.params); - - const makeBids = (type, size, isViralize) => { - let context = ''; - let streamType = 2; - - if (type === BANNER) { - streamType = 5; - } else { - context = deepAccess(bid, 'mediaTypes.video.context'); - if (vpaidMode && context === 'instream') { - streamType = 1; - } - if (context === 'outstream') { - streamType = 5; - } - } + if (!isFn(bidRequest.getFloor)) { + return imp + } - let rBid = { - type: streamType, - adUnitCode: bid.adUnitCode, - bidId: bid.bidId, - context: context, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bidderRequest.auctionId, - start: +new Date(), - timeout: 3000, - params: bid.params, - schain: bid.schain || defaultSchain - }; + let floor = bidRequest.getFloor({ + currency: 'EUR', + mediaType: '*', + size: '*', + }); + if (!isNaN(floor?.floor) && floor?.currency === 'EUR') { + imp.bidfloor = floor.floor; + imp.bidfloorcur = 'EUR'; + } + return imp; + }, - if (isViralize) { - rBid.unitId = getBidIdParameter('unitId', bid.params); - rBid.sizes = size; - rBid.mediaTypes = { - [type]: {'context': context} - }; - } else { - rBid.playerId = getBidIdParameter('playerId', bid.params); - rBid.mediaType = type; - rBid.size = { - width: size[0], - height: size[1] - }; - rBid.gdprConsent = gdprConsent; - rBid.uspConsent = uspConsent; - } + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); - return rBid; + if (context.imp?.video?.ext?.context === 'outstream') { + const renderConfig = { + rendererUrl: bid.ext?.rendererConfig?.rendererUrl, + renderFunc: bid.ext?.rendererConfig?.renderFunc, + renderOptions: bid.ext?.rendererConfig?.renderOptions, }; - - if (isViralize) { - if (videoSizes && videoSizes[0]) { - adUnits.push(makeBids(VIDEO, videoSizes, isViralize)); - } - if (bannerSizes && bannerSizes[0]) { - adUnits.push(makeBids(BANNER, bannerSizes, isViralize)); - } - } else { - videoSizes.forEach((size) => { - adUnits.push(makeBids(VIDEO, size)); - }); - - bannerSizes.forEach((size) => { - adUnits.push(makeBids(BANNER, size)); - }); + if (renderConfig.renderFunc && renderConfig.rendererUrl) { + bidResponse.renderer = createRenderer(bidResponse, renderConfig); } - }); - - let endpointUrl; - let data; + } + bidResponse.callbacks = bid.ext?.callbacks; + bidResponse.extra = bid.ext?.extra; + return bidResponse; + }, +}) - const QA = validBidRequests[0].params.qa || {}; +const GVLID = 111; - if (isViralize) { - endpointUrl = VIRALIZE_ENDPOINT; - data = { - 'bidRequests': adUnits, - 'context': { - 'gdprConsent': gdprConsent, - 'uspConsent': uspConsent, - 'schain': defaultSchain, - 'pageURL': QA.pageURL || encodeURIComponent(pageURL) - } - } - } else { - endpointUrl = isStage ? STAGE_ENDPOINT : PROD_ENDPOINT; +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: ['showheroesBs'], + supportedMediaTypes: [VIDEO], + isBidRequestValid: (bid) => { + return !!bid.params.unitId; + }, + buildRequests: (bidRequests, bidderRequest) => { + const QA = bidRequests[0].params.qa; - data = { - 'user': [], - 'meta': { - 'adapterVersion': 2, - 'pageURL': QA.pageURL || encodeURIComponent(pageURL), - 'vastCacheEnabled': (!!config.getConfig('cache') && !isBanner && !outstreamOptions) || false, - 'isDesktop': getWindowTop().document.documentElement.clientWidth > 700, - 'xmlAndTag': !!(isOutstream && isCustomRender) || false, - 'stage': isStage || undefined - }, - 'requests': adUnits, - 'debug': validBidRequests[0].params.debug || false, - } - } + const ortbData = converter.toORTB({ bidRequests, bidderRequest }) return { - url: QA.endpoint || endpointUrl, + url: QA?.endpoint || ENDPOINT, method: 'POST', - options: {contentType: 'application/json', accept: 'application/json'}, - data: data + data: ortbData, }; }, - interpretResponse: function(response, request) { - return createBids(response.body, request.data); + interpretResponse: (response, request) => { + if (!response.body) { + return []; + } + + const bids = converter.fromORTB({response: response.body, request: request.data}).bids; + return bids; }, - getUserSyncs: function(syncOptions, serverResponses) { + getUserSyncs: (syncOptions, serverResponses) => { const syncs = []; - if (!serverResponses.length || !serverResponses[0].body.userSync) { + if (!serverResponses.length || !serverResponses[0].body?.ext?.userSync) { return syncs; } - const userSync = serverResponses[0].body.userSync; + const userSync = serverResponses[0].body.ext.userSync; - if (syncOptions.iframeEnabled) { - (userSync.iframes || []).forEach(url => { + if (syncOptions.iframeEnabled && userSync?.iframes?.length) { + userSync.iframes.forEach(url => { syncs.push({ type: 'iframe', url @@ -203,6 +124,7 @@ export const spec = { }); }); } + return syncs; }, @@ -216,177 +138,33 @@ export const spec = { }, }; -function createBids(bidRes, reqData) { - if (!bidRes) { - return []; - } - const responseBids = bidRes.bids || bidRes.bidResponses; - if (!Array.isArray(responseBids) || responseBids.length < 1) { - return []; - } - - const bids = []; - const bidMap = {}; - (reqData.requests || reqData.bidRequests || []).forEach((bid) => { - bidMap[bid.bidId] = bid; - }); - - responseBids.forEach(function (bid) { - const requestId = bid.bidId || bid.requestId; - const reqBid = bidMap[requestId]; - const currentBidParams = reqBid.params; - const isViralize = !!reqBid.params.unitId; - const size = { - width: bid.width || bid.size.width, - height: bid.height || bid.size.height - }; - - let bidUnit = {}; - bidUnit.cpm = bid.cpm; - bidUnit.requestId = requestId; - bidUnit.adUnitCode = reqBid.adUnitCode; - bidUnit.currency = bid.currency; - bidUnit.mediaType = bid.mediaType || VIDEO; - bidUnit.ttl = TTL; - bidUnit.creativeId = 'c_' + requestId; - bidUnit.netRevenue = true; - bidUnit.width = size.width; - bidUnit.height = size.height; - bidUnit.meta = { - advertiserDomains: bid.adomain || [] - }; - if (bid.vastXml) { - bidUnit.vastXml = bid.vastXml; - bidUnit.adResponse = { - content: bid.vastXml, - }; - } - if (bid.vastTag || bid.vastUrl) { - bidUnit.vastUrl = bid.vastTag || bid.vastUrl; +function outstreamRender(response, renderConfig) { + response.renderer.push(() => { + const func = deepAccess(window, renderConfig.renderFunc); + if (!isFn(func)) { + return; } - if (bid.mediaType === BANNER) { - bidUnit.ad = getBannerHtml(bid, reqBid, reqData); - } else if (bid.context === 'outstream') { - const renderer = Renderer.install({ - id: requestId, - url: 'https://static.showheroes.com/renderer.js', - adUnitCode: reqBid.adUnitCode, - config: { - playerId: reqBid.playerId, - width: size.width, - height: size.height, - vastUrl: bid.vastTag, - vastXml: bid.vastXml, - ad: bid.ad, - debug: reqData.debug, - isStage: reqData.meta && !!reqData.meta.stage, - isViralize: isViralize, - customRender: getBidIdParameter('customRender', currentBidParams.outstreamOptions), - slot: getBidIdParameter('slot', currentBidParams.outstreamOptions), - iframe: getBidIdParameter('iframe', currentBidParams.outstreamOptions), - } - }); - renderer.setRender(outstreamRender); - bidUnit.renderer = renderer; + const renderPayload = { ...renderConfig.renderOptions }; + if (response.vastXml) { + renderPayload.adResponse = { + content: response.vastXml, + }; } - bids.push(bidUnit); + func(renderPayload); }); - - return bids; } -function outstreamRender(bid) { - let embedCode; - if (bid.renderer.config.isViralize) { - embedCode = createOutstreamEmbedCodeV2(bid); - } else { - embedCode = createOutstreamEmbedCode(bid); - } - if (typeof bid.renderer.config.customRender === 'function') { - bid.renderer.config.customRender(bid, embedCode); - } else { - try { - const inIframe = getBidIdParameter('iframe', bid.renderer.config); - if (inIframe && window.document.getElementById(inIframe).nodeName === 'IFRAME') { - const iframe = window.document.getElementById(inIframe); - let framedoc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); - framedoc.body.appendChild(embedCode); - return; - } - - const slot = getBidIdParameter('slot', bid.renderer.config) || bid.adUnitCode; - if (slot && window.document.getElementById(slot)) { - window.document.getElementById(slot).appendChild(embedCode); - } else if (slot) { - logError('[ShowHeroes][renderer] Error: spot not found'); - } - } catch (err) { - logError('[ShowHeroes][renderer] Error:' + err.message); - } - } -} - -function createOutstreamEmbedCode(bid) { - const isStage = getBidIdParameter('isStage', bid.renderer.config); - const urls = getEnvURLs(isStage); - - const fragment = window.document.createDocumentFragment(); - - let script = loadExternalScript(urls.pubTag, 'outstream', function () { - window.ShowheroesTag = this; +function createRenderer(bid, renderConfig) { + const renderer = Renderer.install({ + id: bid.id, + url: renderConfig.rendererUrl, + loaded: false, + adUnitCode: bid.adUnitCode, }); - script.setAttribute('data-player-host', urls.vlHost); - - const spot = window.document.createElement('div'); - spot.setAttribute('class', 'showheroes-spot'); - spot.setAttribute('data-player', getBidIdParameter('playerId', bid.renderer.config)); - spot.setAttribute('data-debug', getBidIdParameter('debug', bid.renderer.config)); - spot.setAttribute('data-ad-vast-tag', getBidIdParameter('vastUrl', bid.renderer.config)); - spot.setAttribute('data-stream-type', 'outstream'); - - fragment.appendChild(spot); - fragment.appendChild(script); - return fragment; -} - -function createOutstreamEmbedCodeV2(bid) { - const range = document.createRange(); - range.selectNode(document.getElementsByTagName('body')[0]); - return range.createContextualFragment(getBidIdParameter('ad', bid.renderer.config)); -} - -function getBannerHtml (bid, reqBid, reqData) { - const isStage = !!reqData.meta.stage; - const urls = getEnvURLs(isStage); - return ` - - - -
- - `; -} - -function getVideoSizes(bidRequest) { - return formatSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize') || []); -} - -function getBannerSizes(bidRequest) { - return formatSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes') || []); -} - -function formatSizes(sizes) { - if (!sizes || !sizes.length) { - return [] - } - return Array.isArray(sizes[0]) ? sizes : [sizes]; + renderer.setRender((render) => { + return outstreamRender(render, renderConfig); + }); + return renderer; } registerBidder(spec); diff --git a/modules/showheroes-bsBidAdapter.md b/modules/showheroes-bsBidAdapter.md index a32a77a2525..f306593a783 100644 --- a/modules/showheroes-bsBidAdapter.md +++ b/modules/showheroes-bsBidAdapter.md @@ -1,16 +1,14 @@ # Overview +``` Module Name: ShowHeroes Bidder Adapter - Module Type: Bidder Adapter - Alias: showheroesBs - Maintainer: tech@showheroes.com - +``` # Description -Module that connects to ShowHeroes demand source to fetch bids. +A module that connects to ShowHeroes demand source to fetch bids. # Test Parameters ``` @@ -27,122 +25,7 @@ Module that connects to ShowHeroes demand source to fetch bids. { bidder: "showheroes-bs", params: { - playerId: '0151f985-fb1a-4f37-bb26-cfc62e43ec05', - vpaidMode: true // by default is 'false' - } - } - ] - }, - { - // if you have adSlot renderer or oustream should be returned as banner - code: 'video', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - } - }, - bids: [ - { - bidder: "showheroes-bs", - params: { - playerId: '0151f985-fb1a-4f37-bb26-cfc62e43ec05', - } - } - ] - }, - { - code: 'video', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - } - }, - bids: [ - { - bidder: "showheroes-bs", - params: { - playerId: '0151f985-fb1a-4f37-bb26-cfc62e43ec05', - - outstreamOptions: { - // Required for the outstream renderer to exact node, one of - iframe: 'iframe_id', - // or - slot: 'slot_id' - } - } - } - ] - }, - { - code: 'video', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - } - }, - bids: [ - { - bidder: "showheroes-bs", - params: { - playerId: '0151f985-fb1a-4f37-bb26-cfc62e43ec05', - - outstreamOptions: { - // Custom outstream rendering function - customRender: function(bid, embedCode) { - // Example with embedCode - someContainer.appendChild(embedCode); - - // bid config data - var vastUrl = bid.renderer.config.vastUrl; - var vastXML = bid.renderer.config.vastXML; - var videoWidth = bid.renderer.config.width; - var videoHeight = bid.renderer.config.height; - var playerId = bid.renderer.config.playerId; - }, - } - } - } - ] - }, - { - code: 'banner', - mediaTypes: { - banner: { - sizes: [[640, 480]], - } - }, - bids: [ - { - bidder: "showheroes-bs", - params: { - playerId: '0151f985-fb1a-4f37-bb26-cfc62e43ec05', - } - } - ] - } - ]; -``` - -# Test Parameters (V2) -``` - var adUnits = [ - { - code: 'video', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'instream', - } - }, - bids: [ - { - bidder: "showheroes-bs", - params: { - unitId: 'AACBWAcof-611K4U', - vpaidMode: true // by default is 'false' + unitId: '1234abcd-5678efgh', } } ] @@ -159,18 +42,10 @@ Module that connects to ShowHeroes demand source to fetch bids. { bidder: "showheroes-bs", params: { - unitId: 'AACBTwsZVANd9NlB', - - outstreamOptions: { - // Required for the outstream renderer to exact node, one of - iframe: 'iframe_id', - // or - slot: 'slot_id' - } + unitId: '1234abcd-5678efgh', } } ] } ]; ``` - diff --git a/modules/sigmoidAnalyticsAdapter.js b/modules/sigmoidAnalyticsAdapter.js deleted file mode 100644 index a9d92b67e24..00000000000 --- a/modules/sigmoidAnalyticsAdapter.js +++ /dev/null @@ -1,293 +0,0 @@ -/* Sigmoid Analytics Adapter for prebid.js v1.1.0-pre -Updated : 2018-03-28 */ -import {includes} from '../src/polyfill.js'; -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; -import { EVENTS } from '../src/constants.js'; -import adapterManager from '../src/adapterManager.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {generateUUID, logError, logInfo} from '../src/utils.js'; -import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; - -const MODULE_CODE = 'sigmoid'; -const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); - -const url = 'https://kinesis.us-east-1.amazonaws.com/'; -const analyticsType = 'endpoint'; - -const auctionInitConst = EVENTS.AUCTION_INIT; -const auctionEndConst = EVENTS.AUCTION_END; -const bidWonConst = EVENTS.BID_WON; -const bidRequestConst = EVENTS.BID_REQUESTED; -const bidAdjustmentConst = EVENTS.BID_ADJUSTMENT; -const bidResponseConst = EVENTS.BID_RESPONSE; - -let initOptions = { publisherIds: [], utmTagData: [], adUnits: [] }; -let bidWon = {options: {}, events: []}; -let eventStack = {options: {}, events: []}; - -let auctionStatus = 'not_started'; - -let localStoragePrefix = 'sigmoid_analytics_'; -let utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; -let utmTimeoutKey = 'utm_timeout'; -let utmTimeout = 60 * 60 * 1000; -let sessionTimeout = 60 * 60 * 1000; -let sessionIdStorageKey = 'session_id'; -let sessionTimeoutKey = 'session_timeout'; - -function getParameterByName(param) { - let vars = {}; - window.location.href.replace(location.hash, '').replace( - /[?&]+([^=&]+)=?([^&]*)?/gi, - function(m, key, value) { - vars[key] = value !== undefined ? value : ''; - } - ); - - return vars[param] ? vars[param] : ''; -} - -function buildSessionIdLocalStorageKey() { - return localStoragePrefix.concat(sessionIdStorageKey); -} - -function buildSessionIdTimeoutLocalStorageKey() { - return localStoragePrefix.concat(sessionTimeoutKey); -} - -function updateSessionId() { - if (isSessionIdTimeoutExpired()) { - let newSessionId = generateUUID(); - storage.setDataInLocalStorage(buildSessionIdLocalStorageKey(), newSessionId); - } - initOptions.sessionId = getSessionId(); - updateSessionIdTimeout(); -} - -function updateSessionIdTimeout() { - storage.setDataInLocalStorage(buildSessionIdTimeoutLocalStorageKey(), Date.now()); -} - -function isSessionIdTimeoutExpired() { - let cpmSessionTimestamp = storage.getDataFromLocalStorage(buildSessionIdTimeoutLocalStorageKey()); - return Date.now() - cpmSessionTimestamp > sessionTimeout; -} - -function getSessionId() { - return storage.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) ? storage.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) : ''; -} - -function updateUtmTimeout() { - storage.setDataInLocalStorage(buildUtmLocalStorageTimeoutKey(), Date.now()); -} - -function isUtmTimeoutExpired() { - let utmTimestamp = storage.getDataFromLocalStorage(buildUtmLocalStorageTimeoutKey()); - return (Date.now() - utmTimestamp) > utmTimeout; -} - -function buildUtmLocalStorageTimeoutKey() { - return localStoragePrefix.concat(utmTimeoutKey); -} - -function buildUtmLocalStorageKey(utmMarkKey) { - return localStoragePrefix.concat(utmMarkKey); -} - -function checkOptions() { - if (typeof initOptions.publisherIds === 'undefined') { - return false; - } - - return initOptions.publisherIds.length > 0; -} - -function checkAdUnitConfig() { - if (typeof initOptions.adUnits === 'undefined') { - return false; - } - - return initOptions.adUnits.length > 0; -} - -function buildBidWon(eventType, args) { - bidWon.options = initOptions; - if (checkAdUnitConfig()) { - if (includes(initOptions.adUnits, args.adUnitCode)) { - bidWon.events = [{ args: args, eventType: eventType }]; - } - } else { - bidWon.events = [{ args: args, eventType: eventType }]; - } -} - -function buildEventStack() { - eventStack.options = initOptions; -} - -function filterBidsByAdUnit(bids) { - var filteredBids = []; - bids.forEach(function (bid) { - if (includes(initOptions.adUnits, bid.placementCode)) { - filteredBids.push(bid); - } - }); - return filteredBids; -} - -function isValidEvent(eventType, adUnitCode) { - if (checkAdUnitConfig()) { - let validationEvents = [bidAdjustmentConst, bidResponseConst, bidWonConst]; - if (!includes(initOptions.adUnits, adUnitCode) && includes(validationEvents, eventType)) { - return false; - } - } - return true; -} - -function isValidEventStack() { - if (eventStack.events.length > 0) { - return eventStack.events.some(function(event) { - return bidRequestConst === event.eventType || bidWonConst === event.eventType; - }); - } - return false; -} - -function isValidBidWon() { - return bidWon.events.length > 0; -} - -function flushEventStack() { - eventStack.events = []; -} - -let sigmoidAdapter = Object.assign(adapter({url, analyticsType}), - { - track({eventType, args}) { - if (!checkOptions()) { - return; - } - - let info = Object.assign({}, args); - - if (info && info.ad) { - info.ad = ''; - } - - if (eventType === auctionInitConst) { - auctionStatus = 'started'; - } - - if (eventType === bidWonConst && auctionStatus === 'not_started') { - updateSessionId(); - buildBidWon(eventType, info); - if (isValidBidWon()) { - send(eventType, bidWon, 'bidWon'); - } - return; - } - - if (eventType === auctionEndConst) { - updateSessionId(); - buildEventStack(); - if (isValidEventStack()) { - send(eventType, eventStack, 'eventStack'); - } - auctionStatus = 'not_started'; - } else { - pushEvent(eventType, info); - } - }, - - }); - -sigmoidAdapter.originEnableAnalytics = sigmoidAdapter.enableAnalytics; - -sigmoidAdapter.enableAnalytics = function (config) { - initOptions = config.options; - initOptions.utmTagData = this.buildUtmTagData(); - logInfo('Sigmoid Analytics enabled with config', initOptions); - sigmoidAdapter.originEnableAnalytics(config); -}; - -sigmoidAdapter.buildUtmTagData = function () { - let utmTagData = {}; - let utmTagsDetected = false; - utmTags.forEach(function(utmTagKey) { - let utmTagValue = getParameterByName(utmTagKey); - if (utmTagValue !== '') { - utmTagsDetected = true; - } - utmTagData[utmTagKey] = utmTagValue; - }); - utmTags.forEach(function(utmTagKey) { - if (utmTagsDetected) { - storage.setDataInLocalStorage(buildUtmLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); - updateUtmTimeout(); - } else { - if (!isUtmTimeoutExpired()) { - utmTagData[utmTagKey] = storage.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) ? storage.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) : ''; - updateUtmTimeout(); - } - } - }); - return utmTagData; -}; - -function send(eventType, data, sendDataType) { - // eslint-disable-next-line no-undef - AWS.config.credentials = new AWS.Credentials({ - accessKeyId: 'accesskey', secretAccessKey: 'secretkey' - }); - - // eslint-disable-next-line no-undef - AWS.config.region = 'us-east-1'; - // eslint-disable-next-line no-undef - AWS.config.credentials.get(function(err) { - // attach event listener - if (err) { - logError(err); - return; - } - // create kinesis service object - // eslint-disable-next-line no-undef - var kinesis = new AWS.Kinesis({ - apiVersion: '2013-12-02' - }); - var dataList = []; - var jsonData = {}; - jsonData['Data'] = JSON.stringify(data) + '\n'; - jsonData['PartitionKey'] = 'partition-' + Math.random().toString(36).substring(7); - dataList.push(jsonData); - kinesis.putRecords({ - Records: dataList, - StreamName: 'sample-stream' - }); - if (sendDataType === 'eventStack') { - flushEventStack(); - } - }); -}; - -function pushEvent(eventType, args) { - if (eventType === bidRequestConst) { - if (checkAdUnitConfig()) { - args.bids = filterBidsByAdUnit(args.bids); - } - if (args.bids.length > 0) { - eventStack.events.push({ eventType: eventType, args: args }); - } - } else { - if (isValidEvent(eventType, args.adUnitCode)) { - eventStack.events.push({ eventType: eventType, args: args }); - } - } -} - -adapterManager.registerAnalyticsAdapter({ - adapter: sigmoidAdapter, - code: MODULE_CODE, -}); - -export default sigmoidAdapter; diff --git a/modules/silverpushBidAdapter.js b/modules/silverpushBidAdapter.js index 5403f3bd88c..70c0e475cc4 100644 --- a/modules/silverpushBidAdapter.js +++ b/modules/silverpushBidAdapter.js @@ -1,4 +1,3 @@ -import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { mergeDeep } from '../src/utils.js'; @@ -6,6 +5,7 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { Renderer } from '../src/Renderer.js'; import { ajax } from '../src/ajax.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; const BIDDER_CODE = 'silverpush'; const bidderConfig = 'sp_pb_ortb'; @@ -65,7 +65,7 @@ export const CONVERTER = ortbConverter({ imp = buildBannerImp(bidRequest, imp); } - const bidFloor = getBidFloor(bidRequest); + const bidFloor = getBidFloor(bidRequest, bidRequest.bidderRequest); utils.deepSetValue(imp, 'bidfloor', bidFloor); @@ -128,7 +128,7 @@ export const CONVERTER = ortbConverter({ }); return { bids: response.bids, - fledgeAuctionConfigs, + paapi: fledgeAuctionConfigs, } } else { return response.bids @@ -216,7 +216,8 @@ function createRequest(bidRequests, bidderRequest, mediaType) { return { method: 'POST', url: REQUEST_URL, - data: CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } }) + data: CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } }), + bidderRequest } } @@ -247,8 +248,8 @@ function buildVideoOutstreamResponse(bidResponse, context) { return {...bidResponse}; } -function getBidFloor(bid) { - const currency = config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; +function getBidFloor(bid, bidderRequest) { + const currency = getCurrencyFromBidderRequest(bidderRequest) || DEFAULT_CURRENCY; if (typeof bid.getFloor !== 'function') { return utils.deepAccess(bid, 'params.bidFloor', 0.05); @@ -259,7 +260,7 @@ function getBidFloor(bid) { mediaType: '*', size: '*', }); - return bidFloor.floor; + return bidFloor?.floor; } function _renderer(bid) { diff --git a/modules/sirdataRtdProvider.js b/modules/sirdataRtdProvider.js index 29d10a3a071..129d708cf8f 100644 --- a/modules/sirdataRtdProvider.js +++ b/modules/sirdataRtdProvider.js @@ -7,21 +7,35 @@ * @module modules/sirdataRtdProvider * @requires module:modules/realTimeData */ -import {deepAccess, deepSetValue, isEmpty, logError, logInfo, mergeDeep} from '../src/utils.js'; -import {submodule} from '../src/hook.js'; -import {ajax} from '../src/ajax.js'; -import {findIndex} from '../src/polyfill.js'; -import {getRefererInfo} from '../src/refererDetection.js'; -import {config} from '../src/config.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import adapterManager from '../src/adapterManager.js'; +import { ajax, sendBeacon } from '../src/ajax.js'; +import { + deepAccess, checkCookieSupport, deepSetValue, hasDeviceAccess, inIframe, isEmpty, + logError, logInfo, mergeDeep +} from '../src/utils.js'; +import { findIndex } from '../src/polyfill.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { submodule } from '../src/hook.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'SirdataRTDModule'; const ORTB2_NAME = 'sirdata.com'; const LOG_PREFIX = 'Sirdata RTD: '; +const EUIDS_STORAGE_NAME = 'SDDAN'; +// Get the cookie domain from the referer info or fallback to window location hostname +const cookieDomain = getRefererInfo().domain || window.location.hostname; -const partnerIds = { +/** @type {number} */ +const GVLID = 53; + +/** @type {object} */ +const STORAGE = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); +const bidderAliasRegistry = adapterManager.aliasRegistry || {}; +const biddersId = { // Partner IDs mapping for different SSPs and DSPs 'criteo': 27443, 'openx': 30342, 'pubmatic': 30345, @@ -30,25 +44,9 @@ const partnerIds = { 'yahoossp': 30339, 'rubicon': 27452, 'appnexus': 27446, - 'appnexusAst': 27446, - 'brealtime': 27446, - 'emetriq': 27446, - 'emxdigital': 27446, - 'pagescience': 27446, 'gourmetads': 33394, - 'matomy': 27446, - 'featureforward': 27446, - 'oftmedia': 27446, - 'districtm': 27446, - 'adasta': 27446, - 'beintoo': 27446, - 'gravity': 27446, - 'msq_classic': 27878, - 'msq_max': 27878, - '366_apx': 27878, 'mediasquare': 27878, 'smartadserver': 27440, - 'smart': 27440, 'proxistore': 27484, 'ix': 27248, 'sdRtdForGpt': 27449, @@ -65,57 +63,367 @@ const partnerIds = { 'zeta_global_ssp': 33385, }; -export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, userConsent) { - logInfo(LOG_PREFIX, 'init'); - moduleConfig.params = moduleConfig.params || {}; - - let tcString = (userConsent && userConsent.gdpr && userConsent.gdpr.consentString ? userConsent.gdpr.consentString : ''); - let gdprApplies = (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies ? userConsent.gdpr.gdprApplies : ''); - - moduleConfig.params.partnerId = moduleConfig.params.partnerId ? moduleConfig.params.partnerId : 1; - moduleConfig.params.key = moduleConfig.params.key ? moduleConfig.params.key : 1; - moduleConfig.params.actualUrl = moduleConfig.params.actualUrl || null; - - let sirdataDomain; - let sendWithCredentials; - - if (userConsent.coppa || (userConsent.usp && (userConsent.usp[0] === '1' && (userConsent.usp[1] === 'N' || userConsent.usp[2] === 'Y')))) { - // if children or "Do not Sell" management in California, no segments, page categories only whatever TCF signal - sirdataDomain = 'cookieless-data.com'; - sendWithCredentials = false; - gdprApplies = null; - tcString = ''; - } else if (config.getConfig('consentManagement.gdpr')) { - // Default endpoint for Contextual results only is cookieless if gdpr management is set. Needed because the cookie-based endpoint will fail and return error if user is located in Europe and no consent has been given - sirdataDomain = 'cookieless-data.com'; - sendWithCredentials = false; +const eidsProvidersMap = { + 'id5': 'id5-sync.com', + 'id5id': 'id5-sync.com', + 'id5_id': 'id5-sync.com', + 'pubprovided_id': 'pubProvidedId', + 'ppid': 'pubProvidedId', + 'first-id.fr': 'pubProvidedId', + 'sharedid': 'pubcid.org', + 'publishercommonid': 'pubcid.org', + 'pubcid.org': 'pubcid.org', +} + +// params +let params = { + partnerId: 1, + key: 1, + actualUrl: getRefererInfo().stack.pop() || getRefererInfo().page, + cookieAccessGranted: false, + setGptKeyValues: true, + contextualMinRelevancyScore: 30, + preprod: false, + authorizedEids: ['pubProvidedId', 'id5-sync.com', 'pubcid.org'], + avoidPostContent: false, + sirdataDomain: 'cookieless-data.com', + bidders: [] +}; + +/** + * Sets a cookie on the top-level domain + * @param {string} key - The cookie name + * @param {string} value - The cookie value + * @param {string} hostname - The hostname for setting the cookie + * @param {boolean} deleteCookie - The cookie must be deleted + * @returns {boolean} - True if the cookie was successfully set, otherwise false + */ +export function setCookieOnTopDomain(key, value, hostname, deleteCookie) { + const subDomains = hostname.split('.'); + let expTime = new Date(); + expTime.setTime(expTime.getTime() + (deleteCookie ? -1 : 365 * 24 * 60 * 60 * 1000)); // Set expiration time + for (let i = 0; i < subDomains.length; ++i) { + // Try to write the cookie on this subdomain (we want it to be stored only on the TLD+1) + const domain = subDomains.slice(subDomains.length - i - 1).join('.'); + try { + STORAGE.setCookie(key, value, expTime.toUTCString(), 'Lax', '.' + domain); + // Try to read the cookie to check if we wrote it + if (STORAGE.getCookie(key, null) === value) return true; // Check if the cookie was set, and if so top domain was found. If deletion with expire date -1 will parse until complete host + } catch (e) { + logError(LOG_PREFIX, e); + } + } + return false; +} + +/** + * Retrieves the UID from storage (cookies or local storage) + * @returns {Array|null} - Array of UID objects or null if no UID found + */ +export function getUidFromStorage() { + let cUid = STORAGE.getCookie(EUIDS_STORAGE_NAME, null); + let lsUid = STORAGE.getDataFromLocalStorage(EUIDS_STORAGE_NAME, null); + if (cUid && (!lsUid || cUid !== lsUid)) { + STORAGE.setDataInLocalStorage(EUIDS_STORAGE_NAME, cUid, null); + } else if (lsUid && !cUid) { + setCookieOnTopDomain(EUIDS_STORAGE_NAME, lsUid, cookieDomain, false); + cUid = lsUid; + } + return cUid ? [{ source: 'sddan.com', uids: [{ id: cUid, atype: 1 }] }] : null; +} + +/** + * Sets the UID in storage (cookies and local storage) + * @param {string} sddanId - The UID to be set + * @returns {boolean} - True if the UID was successfully set, otherwise false + */ +export function setUidInStorage(sddanId) { + if (!sddanId) return false; + sddanId = encodeURI(sddanId.toString()); + setCookieOnTopDomain(EUIDS_STORAGE_NAME, sddanId, cookieDomain, false); + STORAGE.setDataInLocalStorage(EUIDS_STORAGE_NAME, sddanId, null); + return true; +} + +/** + * Merges and cleans objects from two eids arrays based on the 'source' and unique 'id' within the 'uids' array. + * Processes each array to add unique items or merge uids if the source already exists. + * @param {Array} euids1 - The first array to process and merge. + * @param {Array} euids2 - The second array to process and merge. + * @returns {Array} The merged array with unique sources and uid ids. + */ +export function mergeEuidsArrays(euids1, euids2) { + if (isEmpty(euids1)) return euids2; + if (isEmpty(euids2)) return euids1; + const mergedArray = []; + // Helper function to process each array + const processArray = (array) => { + array.forEach(item => { + if (item.uids) { + const foundIndex = findIndex(mergedArray, function (x) { + return x.source === item.source; + }); + if (foundIndex !== -1) { + // Merge uids if the source is found + item.uids.forEach(uid => { + if (!mergedArray[foundIndex].uids.some(u => u.id === uid.id)) { + mergedArray[foundIndex].uids.push(uid); + } + }); + } else { + // Add the entire item if the source does not exist + mergedArray.push({ ...item, uids: [...item.uids] }); + } + } + }); + }; + // Process both euids1 and euids2 + processArray(euids1); + processArray(euids2); + return mergedArray; +} + +/** + * Handles data deletion request by removing stored EU IDs + * @param {Object} moduleConfig - The module configuration + * @returns {boolean} - True if data was deleted successfully + */ +export function onDataDeletionRequest(moduleConfig) { + if (moduleConfig && moduleConfig.params) { + setCookieOnTopDomain(EUIDS_STORAGE_NAME, '', window.location.hostname, true); + STORAGE.removeDataFromLocalStorage(EUIDS_STORAGE_NAME, null); + } + return !getUidFromStorage(); +} + +/** + * Sends the page content for semantic analysis to Sirdata's server. + * @param {string} postContentToken - The token required to post content. + * @param {string} actualUrl - The actual URL of the current page. + * @returns {boolean} - True if the content was sent successfully + */ +export function postContentForSemanticAnalysis(postContentToken, actualUrl) { + if (!postContentToken || !actualUrl) return false; + + try { + let content = document.implementation.createHTMLDocument(''); + // Clone the current document content to avoid altering the original page content + content.documentElement.innerHTML = document.documentElement.innerHTML; + // Sanitize the cloned content to remove unnecessary elements and PII + content = sanitizeContent(content); + // Serialize the sanitized content to a string + const payload = new XMLSerializer().serializeToString(content.documentElement); + + if (payload && payload.length > 300 && payload.length < 300000) { + const url = `https://contextual.sirdata.io/api/v1/push/contextual?post_content_token=${postContentToken}&url=${encodeURIComponent(actualUrl)}`; + + if (!sendBeacon(url, payload)) { + // Fallback to using AJAX if Beacon API is not supported + ajax(url, {}, payload, { + contentType: 'text/plain', + method: 'POST', + withCredentials: false, // No user-specific data is tied to the request + referrerPolicy: 'unsafe-url', + crossOrigin: true + }); + } + } + } catch (e) { + logError(LOG_PREFIX, e); + return false; } + return true; +} + +/** + * Executes a callback function when the document is fully loaded. + * @param {function} callback - The function to execute when the document is ready. + */ +export function onDocumentReady(callback) { + if (typeof callback !== 'function') return false; + try { + if (document.readyState && document.readyState !== 'loading') { + callback(); + } else if (typeof document.addEventListener === 'function') { + document.addEventListener('DOMContentLoaded', callback); + } + } catch (e) { + callback(); + } + return true; +} - // default global endpoint is cookie-based if no rules falls into cookieless or consent has been given or GDPR doesn't apply +/** + * Removes Personally Identifiable Information (PII) from the content + * @param {string} content - The content to be sanitized + * @returns {string} - The sanitized content + */ +export function removePII(content) { + const patterns = [ + /\b(?:\d{4}[ -]?){3}\d{4}\b/g, // Credit card numbers + /\b\d{10,12}\b/g, // US bank account numbers + /\b\d{5}\d{5}\d{11}\d{2}\b/g, // EU bank account numbers + /\b(\d{3}-\d{2}-\d{4}|\d{9}|\d{13}|\d{2} \d{2} \d{2} \d{3} \d{3} \d{3})\b/g, // SSN + /\b[A-Z]{1,2}\d{6,9}\b/g, // Passport numbers + /\b(\d{8,10}|\d{3}-\d{3}-\d{3}-\d{3}|\d{2} \d{2} \d{2} \d{3} \d{3})\b/g, // ID card numbers + /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Email addresses + /(\+?\d{1,3}[-.\s]?)?(\(?\d{2,3}\)?[-.\s]?)(\d{2}[-.\s]?){3,4}\d{2}/g // Phone numbers + ]; + patterns.forEach(pattern => { + content = content.replace(pattern, ''); + }); + return content; +} - if (!sirdataDomain || !gdprApplies || (deepAccess(userConsent, 'gdpr.vendorData.vendor.consents') && userConsent.gdpr.vendorData.vendor.consents[53] && userConsent.gdpr.vendorData.purpose.consents[1] && userConsent.gdpr.vendorData.purpose.consents[4])) { - sirdataDomain = 'sddan.com'; - sendWithCredentials = true; +/** + * Sanitizes the content by removing unnecessary elements and PII + * @param {Object} content - The content to be sanitized + * @returns {Object} - The sanitized content + */ +export function sanitizeContent(content) { + if (content && content.documentElement.textContent && content.documentElement.textContent.length > 500) { + // Reduce size by removing useless content + // Allowed tags + const allowedTags = [ + 'div', 'span', 'a', 'article', 'section', 'p', 'h1', 'h2', 'body', 'b', 'u', 'i', 'big', 'mark', 'ol', 'small', 'strong', 'blockquote', + 'nav', 'menu', 'li', 'ul', 'ins', 'head', 'title', 'main', 'var', 'table', 'caption', 'colgroup', 'col', 'tr', 'td', 'th', + 'summary', 'details', 'dl', 'dt', 'dd' + ]; + + const processElement = (element) => { + Array.from(element.childNodes).reverse().forEach(child => { + if (child.nodeType === Node.ELEMENT_NODE) { + processElement(child); + Array.from(child.attributes).forEach(attr => { + // keeps only id attributes and class attribute if useful for contextualisation + if (attr.name === 'class' && !/^(main|article|product)/.test(attr.value)) { + child.removeAttribute(attr.name); + } else if (attr.name !== 'id') { + child.removeAttribute(attr.name); + } + }); + if (!child.innerHTML.trim() || !allowedTags.includes(child.tagName.toLowerCase())) { // Keeps only allowed Tags (allowedTags) + child.remove(); + } + } + }); + }; + + const removeEmpty = (element) => { // remove empty tags + Array.from(element.childNodes).reverse().forEach(child => { + if (child.nodeType === Node.ELEMENT_NODE) { + removeEmpty(child); + if (!child.innerHTML.trim()) { + child.remove(); + } + } else if (child.nodeType === Node.TEXT_NODE && !child.textContent.trim()) { + child.remove(); + } + }); + }; + + processElement(content.documentElement); + removeEmpty(content.documentElement); + + // Clean any potential PII + content.documentElement.innerHTML = removePII(content.documentElement.innerHTML); + + let htmlContent = content.documentElement.innerHTML; + // Remove HTML comments + // This regex removes HTML comments, including those that might not be properly closed + htmlContent = htmlContent.replace(/|$)/g, ''); + // Remove multiple spaces + htmlContent = htmlContent.replace(/\s+/g, ' '); + // Remove spaces between tags + htmlContent = htmlContent.replace(/>\s+<'); + // Assign the cleaned content + content.documentElement.innerHTML = htmlContent; + } + return content; +} + +/** + * Fetches segments and categories from Sirdata server and processes the response + * @param {Object} reqBidsConfigObj - The bids configuration object + * @param {function} onDone - The callback function to be called upon completion + * @param {Object} moduleConfig - The module Config + * @param {Object} userConsent - The user consent information + */ +export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, userConsent) { + logInfo(LOG_PREFIX, 'get Segments And Categories'); + const adUnits = (reqBidsConfigObj && reqBidsConfigObj.adUnits) || getGlobal().adUnits; + if (!adUnits) { + logInfo(LOG_PREFIX, 'no ad unit, RTD processing is useless'); + onDone(); + return; } - let actualUrl = moduleConfig.params.actualUrl || getRefererInfo().stack.pop() || getRefererInfo().page; + const gdprApplies = deepAccess(userConsent, 'gdpr.gdprApplies') ? userConsent.gdpr.gdprApplies : false; + const sirdataSubDomain = params.preprod ? 'kvt-preprod' : 'kvt'; + + let euids; // empty if no right to access device (publisher or user reject) + let privacySignals = ''; + + // Default global endpoint is cookie-based only if no rules falls into cookieless or consent has been given or GDPR doesn't apply + if (hasDeviceAccess() && !userConsent.coppa && (isEmpty(userConsent.usp) || userConsent.usp === -1 || (userConsent.usp[0] === '1' && (userConsent.usp[1] !== 'N' && userConsent.usp[2] !== 'Y'))) && (!gdprApplies || (deepAccess(userConsent, 'gdpr.vendorData.vendor.consents') && userConsent.gdpr.vendorData.vendor.consents[GVLID] && deepAccess(userConsent, 'gdpr.vendorData.purpose.consents') && userConsent.gdpr.vendorData.purpose.consents[1] && (userConsent.gdpr.vendorData.purpose.consents[2] || userConsent.gdpr.vendorData.purpose.consents[3]) && userConsent.gdpr.vendorData.purpose.consents[4])) && (isEmpty(userConsent.gpp) || userConsent.gpp.gppString) && checkCookieSupport()) { + params.sirdataDomain = 'sddan.com'; // cookie based domain + params.cookieAccessGranted = true; // cookies sent in request + + if (gdprApplies && deepAccess(userConsent, 'gdpr.consentString')) { + privacySignals = `&gdpr=${gdprApplies}&gdpr_consent=${userConsent.gdpr.consentString}`; + } else if (!isEmpty(userConsent.usp)) { + privacySignals = `&ccpa_consent=${userConsent.usp.toString()}`; + } else if (deepAccess(userConsent, 'gpp.gppString')) { + const sid = deepAccess(userConsent, 'gpp.applicableSections') ? `&gpp_sid=${userConsent.gpp.applicableSections.join(',')}` : ''; + privacySignals = `&gpp=${userConsent.gpp.gppString}${sid}`; + } + + // Authorized EUIDS from storage and sync global for graph + euids = getUidFromStorage(); // Sirdata Id + + if (!isEmpty(params.authorizedEids) && typeof getGlobal().getUserIds === 'function') { + let filteredEids = {}; + const authorizedEids = params.authorizedEids; + const globalUserIds = getGlobal().getUserIds(); + const globalUserIdsAsEids = getGlobal().getUserIdsAsEids(); + + const hasPubProvidedId = authorizedEids.indexOf('pubProvidedId') !== -1; + + if (hasPubProvidedId && !isEmpty(globalUserIds.pubProvidedId)) { // Publisher allows pubProvidedId + filteredEids = mergeEuidsArrays(filteredEids, globalUserIds.pubProvidedId); + } + + if (!hasPubProvidedId || authorizedEids.length > 1) { // Publisher allows other Id providers + const filteredGlobalEids = globalUserIdsAsEids.filter(entry => authorizedEids.includes(entry.source)); + if (!isEmpty(filteredGlobalEids)) { + filteredEids = mergeEuidsArrays(filteredEids, filteredGlobalEids); + } + } + + if (!isEmpty(filteredEids)) { + euids = mergeEuidsArrays(euids, filteredEids); // merge ids for graph id + } + } + } - const url = 'https://kvt.' + sirdataDomain + '/api/v1/public/p/' + moduleConfig.params.partnerId + '/d/' + moduleConfig.params.key + '/s?callback=&gdpr=' + gdprApplies + '&gdpr_consent=' + tcString + (actualUrl ? '&url=' + encodeURIComponent(actualUrl) : ''); + const url = `https://${sirdataSubDomain}.${params.sirdataDomain}/api/v1/public/p/${params.partnerId.toString()}/d/${params.key.toString()}/s?callback=&allowed_post_content=${!params.avoidPostContent}${privacySignals}${params.actualUrl ? `&url=${encodeURIComponent(params.actualUrl)}` : ''}`; + const method = isEmpty(euids) ? 'GET' : 'POST'; + const payload = isEmpty(euids) ? null : JSON.stringify({ external_ids: euids }); - ajax(url, - { + try { + ajax(url, { success: function (response, req) { if (req.status === 200) { try { const data = JSON.parse(response); if (data && data.segments) { - addSegmentData(reqBidsConfigObj, data, moduleConfig, onDone); + addSegmentData(reqBidsConfigObj, data, adUnits, onDone); } else { onDone(); } } catch (e) { onDone(); - logError('unable to parse Sirdata data' + e); + logError(LOG_PREFIX, 'unable to parse Sirdata data' + e); } } else if (req.status === 204) { onDone(); @@ -123,104 +431,136 @@ export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, }, error: function () { onDone(); - logError('unable to get Sirdata data'); + logError(LOG_PREFIX, 'unable to get Sirdata data'); } - }, - null, - { + }, payload, { contentType: 'text/plain', - method: 'GET', - withCredentials: sendWithCredentials, + method: method, + withCredentials: params.cookieAccessGranted, referrerPolicy: 'unsafe-url', crossOrigin: true }); + } catch (e) { + logError(LOG_PREFIX, e); + } } +/** + * Pushes data to OpenRTB 2.5 fragments for the specified bidder + * @param {Object} ortb2Fragments - The OpenRTB 2.5 fragments + * @param {string} bidder - The bidder name + * @param {Object} data - The data to be pushed + * @param {number} segtaxid - The segment taxonomy ID + * @param {number} cattaxid - The category taxonomy ID + * @returns {boolean} - True if data was pushed successfully + */ export function pushToOrtb2(ortb2Fragments, bidder, data, segtaxid, cattaxid) { try { if (!isEmpty(data.segments)) { if (segtaxid) { setOrtb2Sda(ortb2Fragments, bidder, 'user', data.segments, segtaxid); } else { - setOrtb2(ortb2Fragments, bidder, 'user.ext.data', {sd_rtd: {segments: data.segments}}); + setOrtb2(ortb2Fragments, bidder, 'user.ext.data', { sd_rtd: { segments: data.segments } }); } } if (!isEmpty(data.categories)) { if (cattaxid) { setOrtb2Sda(ortb2Fragments, bidder, 'site', data.categories, cattaxid); } else { - setOrtb2(ortb2Fragments, bidder, 'site.ext.data', {sd_rtd: {categories: data.categories}}); + setOrtb2(ortb2Fragments, bidder, 'site.ext.data', { sd_rtd: { categories: data.categories } }); } } if (!isEmpty(data.categories_score) && !cattaxid) { - setOrtb2(ortb2Fragments, bidder, 'site.ext.data', {sd_rtd: {categories_score: data.categories_score}}); + setOrtb2(ortb2Fragments, bidder, 'site.ext.data', { sd_rtd: { categories_score: data.categories_score } }); } } catch (e) { - logError(e) + logError(LOG_PREFIX, e); } return true; } +/** + * Sets OpenRTB 2.5 Seller Defined Audiences (SDA) data + * @param {Object} ortb2Fragments - The OpenRTB 2.5 fragments + * @param {string} bidder - The bidder name + * @param {string} type - The type of data ('user' or 'site') + * @param {Array} segments - The segments to be set + * @param {number} segtaxValue - The segment taxonomy value + * @returns {boolean} - True if data was set successfully + */ export function setOrtb2Sda(ortb2Fragments, bidder, type, segments, segtaxValue) { try { - let ortb2Data = [{ - name: ORTB2_NAME, - segment: segments.map((segmentId) => ({ id: segmentId })), - }]; - if (segtaxValue) { - ortb2Data[0].ext = { segtax: segtaxValue }; - } - let ortb2Conf = (type === 'site' ? {site: {content: {data: ortb2Data}}} : {user: {data: ortb2Data}}); - if (bidder) { - ortb2Conf = {[bidder]: ortb2Conf}; - } + let ortb2Data = [{ name: ORTB2_NAME, segment: segments.map(segmentId => ({ id: segmentId })) }]; + if (segtaxValue) ortb2Data[0].ext = { segtax: segtaxValue }; + let ortb2Conf = (type === 'site') ? { site: { content: { data: ortb2Data } } } : { user: { data: ortb2Data } }; + if (bidder) ortb2Conf = { [bidder]: ortb2Conf }; mergeDeep(ortb2Fragments, ortb2Conf); } catch (e) { - logError(e) + logError(LOG_PREFIX, e); } return true; } +/** + * Sets OpenRTB 2.5 data at the specified path + * @param {Object} ortb2Fragments - The OpenRTB 2.5 fragments + * @param {string} bidder - The bidder name + * @param {string} path - The path to set the data at + * @param {Object} segments - The segments to be set + * @returns {boolean} - True if data was set successfully + */ export function setOrtb2(ortb2Fragments, bidder, path, segments) { try { - if (isEmpty(segments)) { return false; } + if (isEmpty(segments)) return false; let ortb2Conf = {}; - deepSetValue(ortb2Conf, path, segments || {}); - if (bidder) { - ortb2Conf = {[bidder]: ortb2Conf}; - } + deepSetValue(ortb2Conf, path, segments); + if (bidder) ortb2Conf = { [bidder]: ortb2Conf }; mergeDeep(ortb2Fragments, ortb2Conf); } catch (e) { - logError(e) + logError(LOG_PREFIX, e); } - return true; } +/** + * Loads a custom function for processing ad unit data + * @param {function} todo - The custom function to be executed + * @param {Object} adUnit - The ad unit object + * @param {Object} list - The list of data + * @param {Object} data - The data object + * @param {Object} bid - The bid object + * @returns {boolean} - True if the function was executed successfully + */ export function loadCustomFunction(todo, adUnit, list, data, bid) { try { - if (typeof todo == 'function') { - todo(adUnit, list, data, bid); - } + if (typeof todo === 'function') todo(adUnit, list, data, bid); } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } return true; } +/** + * Gets segments and categories array from the data object + * @param {Object} data - The data object + * @param {number} minScore - The minimum score threshold for contextual relevancy + * @param {string} pid - The partner ID (attributed by Sirdata to bidder) + * @returns {Object} - The segments and categories data + */ export function getSegAndCatsArray(data, minScore, pid) { - let sirdataData = {'segments': [], 'categories': [], 'categories_score': {}}; - minScore = minScore && typeof minScore == 'number' ? minScore : 30; - let cattaxid = data.cattaxid || null; - let segtaxid = data.segtaxid || null; + let sirdataData = { segments: [], categories: [], categories_score: {} }; + minScore = typeof minScore === 'number' ? minScore : 30; + const { cattaxid, segtaxid, segments } = data; + const contextualCategories = data.contextual_categories || {}; + // parses contextual categories try { - if (data && data.contextual_categories) { - for (let catId in data.contextual_categories) { - if (data.contextual_categories.hasOwnProperty(catId) && data.contextual_categories[catId]) { - let value = data.contextual_categories[catId]; - if (value >= minScore && sirdataData.categories.indexOf(catId) === -1) { - if (pid && pid === '27440' && cattaxid) { // Equativ only - sirdataData.categories.push(pid.toString() + 'cc' + catId.toString()); + if (contextualCategories) { + for (let catId in contextualCategories) { + if (contextualCategories.hasOwnProperty(catId) && contextualCategories[catId]) { + let value = contextualCategories[catId]; + if (value >= minScore && !sirdataData.categories.includes(catId)) { + if (pid === '27440' && cattaxid) { // Equativ only + sirdataData.categories.push(`${pid}cc${catId}`); } else { sirdataData.categories.push(catId.toString()); sirdataData.categories_score[catId] = value; @@ -230,15 +570,16 @@ export function getSegAndCatsArray(data, minScore, pid) { } } } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } + // parses user-centric segments (empty if no right to access device/process PII) try { - if (data && data.segments) { - for (let segId in data.segments) { - if (data.segments.hasOwnProperty(segId) && data.segments[segId]) { - let id = data.segments[segId].toString(); - if (pid && pid === '27440' && segtaxid) { // Equativ only - sirdataData.segments.push(pid.toString() + 'us' + id); + if (segments) { + for (let segId in segments) { + if (segments.hasOwnProperty(segId) && segments[segId]) { + let id = segments[segId].toString(); + if (pid === '27440' && segtaxid) { // Equativ only + sirdataData.segments.push(`${pid}us${id}`); } else { sirdataData.segments.push(id); } @@ -246,150 +587,177 @@ export function getSegAndCatsArray(data, minScore, pid) { } } } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } return sirdataData; } -export function applySdaGetSpecificData(data, sirdataData, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit) { - // only share SDA data if whitelisted - if (!biddersParamsExist || indexFound) { +/** + * Applies Seller Defined Audience (SDA) data and specific data for the bidder + * @param {Object} data - The data object + * @param {Object} sirdataData - The Sirdata data object + * @param {boolean} biddersParamsExist - Flag indicating if bidder parameters exist + * @param {Object} reqBids - The request bids object + * @param {Object} bid - The bid object + * @param {number} bidderIndex - The bidder index + * @param {Object} adUnit - The ad unit object + * @param {string} aliasActualBidder - The bidder Alias + * @returns {Object} - The modified Sirdata data + */ +export function applySdaGetSpecificData(data, sirdataData, biddersParamsExist, reqBids, bid, bidderIndex, adUnit, aliasActualBidder) { + // Apply custom function or return Bidder Specific Data if publisher is ok + if (bidderIndex && params.bidders[bidderIndex]?.customFunction && typeof (params.bidders[bidderIndex]?.customFunction) === 'function') { + return loadCustomFunction(params.bidders[bidderIndex].customFunction, adUnit, sirdataData, data, bid); + } + + // Only share Publisher SDA data if whitelisted + if (!biddersParamsExist || bidderIndex) { // SDA Publisher - let sirdataDataForSDA = getSegAndCatsArray(data, minScore, moduleConfig.params.partnerId.toString()); + let sirdataDataForSDA = getSegAndCatsArray(data, params.contextualMinRelevancyScore, params.partnerId.toString()); pushToOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, sirdataDataForSDA, data.segtaxid, data.cattaxid); } - // always share SDA for curation - let curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : (partnerIds[bid.bidder] ? partnerIds[bid.bidder] : null)); - if (curationId) { - // seller defined audience & bidder specific data - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - // Get Bidder Specific Data - let curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore, curationId.toString()); - pushToOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, curationData, data.shared_taxonomy[curationId].segtaxid, data.shared_taxonomy[curationId].cattaxid); + // Always share SDA for curation + if (!isEmpty(data.shared_taxonomy)) { + let curationId = (bidderIndex && params.bidders[bidderIndex]?.curationId) || biddersId[aliasActualBidder]; + if (curationId && data.shared_taxonomy[curationId]) { + // Seller defined audience & bidder specific data + let curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], params.contextualMinRelevancyScore, curationId.toString()); + if (!isEmpty(curationData)) { + pushToOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, curationData, data.shared_taxonomy[curationId].segtaxid, data.shared_taxonomy[curationId].cattaxid); + mergeDeep(sirdataData, curationData); + } } } - // Apply custom function or return Bidder Specific Data if publisher is ok - if (!biddersParamsExist || indexFound) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - return loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataData, data, bid); - } else { - return sirdataData; - } - } + return sirdataData; } -export function addSegmentData(reqBids, data, moduleConfig, onDone) { - const adUnits = (reqBids && reqBids.adUnits) || getGlobal().adUnits; - if (!adUnits) { - onDone(); - return; - } - - moduleConfig = moduleConfig || {}; - moduleConfig.params = moduleConfig.params || {}; - const globalMinScore = moduleConfig.params.hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.contextualMinRelevancyScore : 30; - let sirdataData = getSegAndCatsArray(data, globalMinScore, null); +/** + * Adds segment data to the request bids object and processes the data + * @param {Object} reqBids - The request bids object + * @param {Object} data - The data object + * @param {Array} adUnits - The ad units array + * @param {function} onDone - The callback function to be called upon completion + * @returns {Array} - The ad units array + */ +export function addSegmentData(reqBids, data, adUnits, onDone) { + logInfo(LOG_PREFIX, 'Dispatch Segments And Categories'); + const minScore = params.contextualMinRelevancyScore || 30; + let sirdataData = getSegAndCatsArray(data, minScore, ''); - const biddersParamsExist = (!!(moduleConfig.params && moduleConfig.params.bidders && moduleConfig.params.bidders.length > 0)); + const biddersParamsExist = params.bidders.length > 0; // Global ortb2 SDA - if (data.global_taxonomy && !isEmpty(data.global_taxonomy)) { - let globalData = {'segments': [], 'categories': [], 'categories_score': []}; + if (!isEmpty(data.global_taxonomy)) { for (let i in data.global_taxonomy) { + let globalData; if (!isEmpty(data.global_taxonomy[i])) { - globalData = getSegAndCatsArray(data.global_taxonomy[i], globalMinScore, null); - pushToOrtb2(reqBids.ortb2Fragments?.global, null, globalData, data.global_taxonomy[i].segtaxid, data.global_taxonomy[i].cattaxid); + globalData = getSegAndCatsArray(data.global_taxonomy[i], params.contextualMinRelevancyScore, ''); + if (!isEmpty(globalData)) { + pushToOrtb2(reqBids.ortb2Fragments?.global, '', globalData, data.global_taxonomy[i].segtaxid, data.global_taxonomy[i].cattaxid); + } } } } // Google targeting - if (typeof window.googletag !== 'undefined' && (moduleConfig.params.setGptKeyValues || !moduleConfig.params.hasOwnProperty('setGptKeyValues'))) { + if (typeof window.googletag !== 'undefined' && params.setGptKeyValues) { try { - let gptCurationId = (moduleConfig.params.gptCurationId ? moduleConfig.params.gptCurationId : (partnerIds['sdRtdForGpt'] ? partnerIds['sdRtdForGpt'] : null)); - let sirdataMergedList = []; - if (gptCurationId && data.shared_taxonomy && data.shared_taxonomy[gptCurationId]) { - let gamCurationData = getSegAndCatsArray(data.shared_taxonomy[gptCurationId], globalMinScore, null); - sirdataMergedList = sirdataMergedList.concat(gamCurationData.segments).concat(gamCurationData.categories); - } else { - sirdataMergedList = sirdataData.segments.concat(sirdataData.categories); + const gptCurationId = params.gptCurationId || biddersId.sdRtdForGpt; + let sirdataMergedList = [...sirdataData.segments, ...sirdataData.categories]; + + if (gptCurationId && data.shared_taxonomy?.[gptCurationId]) { + const gamCurationData = getSegAndCatsArray(data.shared_taxonomy[gptCurationId], params.contextualMinRelevancyScore, ''); + sirdataMergedList = [...sirdataMergedList, ...gamCurationData.segments, ...gamCurationData.categories]; } - window.googletag.cmd.push(function() { - window.googletag.pubads().getSlots().forEach(function (n) { - if (typeof n.setTargeting !== 'undefined' && sirdataMergedList && sirdataMergedList.length > 0) { - n.setTargeting('sd_rtd', sirdataMergedList); + + window.googletag.cmd.push(() => { + window.googletag.pubads().getSlots().forEach(slot => { + if (typeof slot.setTargeting !== 'undefined' && sirdataMergedList.length > 0) { + slot.setTargeting('sd_rtd', sirdataMergedList); } }); }); } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } } - // Bid targeting level for FPD non-generic biders - let bidderIndex = ''; - let indexFound = false; - adUnits.forEach(adUnit => { - adUnit.hasOwnProperty('bids') && adUnit.bids.forEach(bid => { - bidderIndex = (moduleConfig.params.hasOwnProperty('bidders') ? findIndex(moduleConfig.params.bidders, function (i) { - return i.bidder === bid.bidder; - }) : false); - indexFound = (!!(typeof bidderIndex == 'number' && bidderIndex >= 0)); + return adUnit.bids?.forEach(bid => { + const bidderIndex = findIndex(params.bidders, function (i) { return i.bidder === bid.bidder; }); try { - let minScore = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.bidders[bidderIndex].contextualMinRelevancyScore : globalMinScore); - switch (bid.bidder) { - case 'appnexus': - case 'appnexusAst': - case 'brealtime': - case 'emetriq': - case 'emxdigital': - case 'pagescience': - case 'gourmetads': - case 'matomy': - case 'featureforward': - case 'oftmedia': - case 'districtm': - case 'adasta': - case 'beintoo': - case 'gravity': - case 'msq_classic': - case 'msq_max': - case '366_apx': - sirdataData = applySdaGetSpecificData(data, sirdataData, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); - if (sirdataData.segments && sirdataData.segments.length > 0) { - setOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'user.keywords', 'sd_rtd=' + sirdataData.segments.join(',sd_rtd=')); - } - if (sirdataData.categories && sirdataData.categories.length > 0) { - setOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'site.content.keywords', 'sd_rtd=' + sirdataData.categories.join(',sd_rtd=')); - } - break; - - default: - if (!biddersParamsExist || (indexFound && (!moduleConfig.params.bidders[bidderIndex].hasOwnProperty('adUnitCodes') || moduleConfig.params.bidders[bidderIndex].adUnitCodes.indexOf(adUnit.code) !== -1))) { - applySdaGetSpecificData(data, sirdataData, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); - } + const aliasActualBidder = bidderAliasRegistry[bid.bidder] || bid.bidder; + if (aliasActualBidder === 'appnexus') { + let xandrData = applySdaGetSpecificData(data, sirdataData, biddersParamsExist, reqBids, bid, bidderIndex, adUnit, aliasActualBidder); + // Surprisingly, to date Xandr doesn't support SDA, we need to set specific 'keywords' entries + if (xandrData.segments.length > 0) { + setOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'user.keywords', `sd_rtd=${xandrData.segments.join(',sd_rtd=')}`); + } + if (xandrData.categories.length > 0) { + setOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'site.content.keywords', `sd_rtd=${xandrData.categories.join(',sd_rtd=')}`); + } + } else { + applySdaGetSpecificData(data, sirdataData, biddersParamsExist, reqBids, bid, bidderIndex, adUnit, aliasActualBidder); } } catch (e) { - logError(e); + logError(LOG_PREFIX, e); } - }) + }); }); + + // Trigger onDone onDone(); + + // Postprocessing: should we send async content to categorize content and/or store Sirdata ID (stored in 3PC) within local scope ? + if (params.sirdataDomain === 'sddan.com') { // Means consent has been given for device and content access + if (!isEmpty(data.sddan_id) && params.cookieAccessGranted) { // Device access allowed by publisher and cookies are supported + setUidInStorage(data.sddan_id); // Save Sirdata user ID + } + if (!params.avoidPostContent && params.actualUrl && !inIframe() && !isEmpty(data.post_content_token)) { + onDocumentReady(() => postContentForSemanticAnalysis(data.post_content_token, params.actualUrl)); + } + } return adUnits; } -export function init(config) { - logInfo(LOG_PREFIX, config); +/** + * Initializes the module with the given configuration + * @param {Object} moduleConfig - The module configuration + * @returns {boolean} - True if the initialization was successful + */ +export function init(moduleConfig) { + logInfo(LOG_PREFIX, moduleConfig); + if (typeof (moduleConfig.params) !== 'object' || !moduleConfig.params.key) return false; + if (typeof (moduleConfig.params.authorizedEids) !== 'object' || !Array.isArray(moduleConfig.params.authorizedEids)) { + delete (moduleConfig.params.authorizedEids); // must be array of strings + } else { + // we need it if the publishers uses user Id module name instead of key or source + const resultSet = new Set( + moduleConfig.params.authorizedEids.map(item => { + const formattedItem = item.toLowerCase().replace(/\s+/g, '_'); // Normalize + return eidsProvidersMap[formattedItem] || formattedItem; + }) + ); + moduleConfig.params.authorizedEids = Array.from(resultSet); + } + if (typeof (moduleConfig.params.bidders) !== 'object' || !Array.isArray(moduleConfig.params.bidders)) delete (moduleConfig.params.bidders); // must be array of objects + delete (moduleConfig.params.sirdataDomain); // delete cookieless domain if specified => shouldn't be overridden by publisher + params = Object.assign({}, params, moduleConfig.params); return true; } +/** + * The Sirdata submodule definition + * @type {Object} + */ export const sirdataSubmodule = { name: SUBMODULE_NAME, + gvlid: GVLID, init: init, getBidRequestData: getSegmentsAndCategories }; +// Register the Sirdata submodule with the real time data module submodule(MODULE_NAME, sirdataSubmodule); diff --git a/modules/sirdataRtdProvider.md b/modules/sirdataRtdProvider.md index f67e34db43a..9e712e5fbd6 100644 --- a/modules/sirdataRtdProvider.md +++ b/modules/sirdataRtdProvider.md @@ -1,30 +1,28 @@ -# Sirdata Real-Time Data Submodule +# Sirdata RTD/SDA Module -Module Name: Sirdata Rtd Provider +Module Name: Sirdata Real-time SDA Module Module Type: Rtd Provider Maintainer: bob@sirdata.com # Description -Sirdata provides a disruptive API that allows its partners to leverage its -cutting-edge contextualization technology and its audience segments based on -cookies and consent or without cookies nor consent! +Sirdata provides a disruptive API that allows publishers and SDA compliant SSPs/DSPs to leverage its cutting-edge contextualization technology and its audience segments! -User-based segments and page-level automatic contextual categories will be -attached to bid request objects sent to different SSPs in order to optimize -targeting. +User-based segments and page-level automatic contextual categories will be attached to bid request objects sent to different bidders in order to optimize targeting. -Automatic integration with Google Ad Manager and major bidders like Xandr/Appnexus, -Smartadserver, Index Exchange, Proxistore, Magnite/Rubicon or Triplelift ! +Automatic or custom integration with Google Ad Manager and major bidders like Xandr's, Equativ's, Index Exchange's, Proxistore's, Magnite's or Triplelift's ! -User's country and choice management are included in the module, so it's 100% -compliant with local and regional laws like GDPR and CCPA/CPRA. +User's country and choice management are included in the module, so it's 100% compliant with local and regional laws like GDPR and CCPA/CPRA. ORTB2 compliant and FPD support for Prebid versions < 4.29 -Contact bob@sirdata.com for information. +Fully supports Seller Defined Audience ! Please find the full SDA taxonomy ids list here. -### Publisher Usage +Please contact for more information. + +## Publisher Usage + +### Configure Prebid.js Compile the Sirdata RTD module into your Prebid build: @@ -32,15 +30,39 @@ Compile the Sirdata RTD module into your Prebid build: Add the Sirdata RTD provider to your Prebid config. -Segments ids (user-centric) and category ids (page-centric) will be provided -salted and hashed : you can use them with a dedicated and private matching table. -Should you want to allow a SSP or a partner to curate your media and operate -cross-publishers campaigns with our data, please ask Sirdata (bob@sirdata.com) to -open it for you account. +`actualUrl` MUST be set with actual location of parent page if prebid.js is loaded in an iframe (e.g. hosted). It can be left blank ('') or removed otherwise. + +`partnerId` and `key` should be provided by your partnering SSP or get one and your dedicated taxonomy from Sirdata (). Segments ids (user-centric) and category ids (page-centric) will be provided salted and hashed : you can use them with a dedicated and private matching table. + +Should you want to allow any SSP or a partner to curate your media and operate cross-publishers campaigns with our data, please ask Sirdata () to whitelist him it in your account. + +#### Typical configuration +```javascript +pbjs.setConfig({ + // ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "SirdataRTDModule", + waitForIt: true, + params: { + partnerId: 1, + key: 1, + } + } + ] + } + // ... +}); ``` -pbjs.setConfig( - ... + +#### Advanced configuration + +```javascript +pbjs.setConfig({ + // ... realTimeData: { auctionDelay: 1000, dataProviders: [ @@ -48,85 +70,114 @@ pbjs.setConfig( name: "SirdataRTDModule", waitForIt: true, params: { - partnerId: 1, + partnerId: 1, key: 1, - setGptKeyValues: true, - contextualMinRelevancyScore: 50, //Min score to filter contextual category globally (0-100 scale) - actualUrl: actual_url, //top location url, for contextual categories - bidders: [{ - bidder: 'appnexus', - adUnitCodes: ['adUnit-1','adUnit-2'], - customFunction: overrideAppnexus, - curationId: '111', - },{ - bidder: 'ix', - sizeLimit: 1200 //specific to Index Exchange, - contextualMinRelevancyScore: 50, //Min score to filter contextual category for curation in the bidder (0-100 scale) - }] + setGptKeyValues: true, + contextualMinRelevancyScore: 50, //Min score to filter contextual category globally (0-100 scale) + actualUrl: '' //top location url, for contextual categories } } ] } - ... -} + // ... +}); ``` ### Parameter Descriptions for the Sirdata Configuration Section -| Name |Type | Description | Notes | -| :------------ | :------------ | :------------ |:------------ | -| name | String | Real time data module name | Mandatory. Always 'SirdataRTDModule' | -| waitForIt | Boolean | Mandatory. Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false but recommended to true | -| params | Object | | Optional | -| params.partnerId | Integer | Partner ID, required to get results and provided by Sirdata. Use 1 for tests and get one running at bob@sirdata.com | Mandatory. Defaults 1. | -| params.key | Integer | Key linked to Partner ID, required to get results and provided by Sirdata. Use 1 for tests and get one running at bob@sirdata.com | Mandatory. Defaults 1. | -| params.setGptKeyValues | Boolean | This parameter Sirdata to set Targeting for GPT/GAM | Optional. Defaults to true. | -| params.contextualMinRelevancyScore | Integer | Min score to keep filter category in the bidders (0-100 scale). Optional. Defaults to 30. | -| params.bidders | Object | Dictionary of bidders you would like to supply Sirdata data for. | Optional. In case no bidder is specified Sirdata will atend to ad data custom and ortb2 to all bidders, adUnits & Globalconfig | +| Name | Type | Description | Notes | +|:-----------------------------------|:--------------------------|:----------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| +| name | String | Real time data module name | Mandatory. Always 'SirdataRTDModule' | +| waitForIt | Boolean | Required to ensure that the auction is delayed until prefetch is complete | Optional. Default to false but recommended to true | +| params | Object | Settings | Optional | +| params.partnerId | Integer | Partner ID, required to get results and provided by Sirdata. Use 1 for tests and request your Id at | Mandatory. Default 1 | +| params.key | Integer | Key linked to Partner ID, required to get results and provided by Sirdata. Use 1 for tests and request your key at | Mandatory. Default 1 | +| params.setGptKeyValues | Boolean | Sets Targeting for GPT/GAM | Optional. Default to true | +| params.authorizedEids | Array of String | List of authorised Eids for graph. Set [] to prevent all Eids usage | Optional. Default to : ID5, pubProvidedId and sharedId | +| params.avoidPostContent | Boolean | Block contextual data POST from user's device (a crawler is use instead) | Optional. Default to false, and setting it to true results in your content downloaded by Sirdata crawler | +| params.contextualMinRelevancyScore | Integer | Min relevancy score to filter categories sent to the bidders (0-100 scale). | Optional. Defaults to 30. | +| params.bidders | Array of Object | Bidders you want to supply your own data to (works only with your private data bought to Sirdata) | Optional | Bidders can receive common setting : -| Name |Type | Description | Notes | -| :------------ | :------------ | :------------ |:------------ | -| bidder | String | Bidder name | Mandatory if params.bidders are specified | -| adUnitCodes | Array of String | Use if you want to limit data injection to specified adUnits for the bidder | Optional. Default is false and data shared with the bidder isn't filtered | -| customFunction | Function | Use it to override the way data is shared with a bidder | Optional. Default is false | -| curationId | String | Specify the curation ID of the bidder. Provided by Sirdata, request it at bob@sirdata.com | Optional. Default curation ids are specified for main bidders | -| contextualMinRelevancyScore | Integer | Min score to filter contextual categories for curation in the bidder (0-100 scale). Optional. Defaults to 30 or global params.contextualMinRelevancyScore if exits. | -| sizeLimit | Integer | used only for bidder 'ix' to limit the size of the get parameter in Index Exchange ad call | Optional. Default is 1000 | +| Name | Type | Description | Notes | +|:---------------|:----------------|:-----------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------| +| bidder | String | Bidder name | Mandatory if params.bidders are specified | +| adUnitCodes | Array of String | Use if you want to limit data injection to specified adUnits for the bidder | Optional. Default is false and data shared with the bidder isn't filtered | +| customFunction | Function | Use it to override the way data is shared with a bidder | Optional. Default is false | +| curationId | String | Specify the curation ID of the bidder. Provided by Sirdata, request it at | Optional. Default curation ids are specified for main bidders | ### Overriding data sharing function -As indicated above, it is possible to provide your own bid augmentation -functions. This is useful if you know a bid adapter's API supports segment -fields which aren't specifically being added to request objects in the Prebid -bid adapter. -Please see the following example, which provides a function to modify bids for -a bid adapter called ix and overrides the appnexus. +As indicated above, it is possible to provide your own bid augmentation functions. This is useful if you know a bid adapter's API supports segment fields which aren't specifically being added to request objects in the Prebid bid adapter. +Please see the following example, which provides a function to modify bids for a bid adapter called ix and overrides the appnexus. data Object format for usage in this kind of function : + +```json { - "segments":[111111,222222], - "contextual_categories":{"333333":100}, - "shared_taxonomy":{ - "27446":{ //CurationId - "segments":[444444,555555], - "contextual_categories":{"666666":100} - } - } + "segments": [ + 111111, + 222222 + ], + "segtaxid": null, + "cattaxid": null, + "contextual_categories": { + "333333": 100 + }, + "shared_taxonomy": { + "27440": { + "segments": [ + 444444, + 555555 + ], + "segtaxid": 552, + "cattaxid": 553, + "contextual_categories": { + "666666": 100 + } + } + }, + "global_taxonomy": { + "9998": { + "segments": [ + 123, + 234 + ], + "segtaxid": 4, + "cattaxid": 7, + "contextual_categories": { + "345": 100, + "456": 100 + } + }, + "9999": { + "segments": [ + 12345, + 23456 + ], + "segtaxid": 550, + "cattaxid": 551, + "contextual_categories": { + "34567": 100, + "45678": 100 + } + } + } } - ``` + +```javascript function overrideAppnexus (adUnit, segmentsArray, dataObject, bid) { - for (var i = 0; i < segmentsArray.length; i++) { + for (var i = 0; i < segmentsArray.length; i++) { if (segmentsArray[i]) { bid.params.user.segments.push(segmentsArray[i]); } } } -pbjs.setConfig( - ... +pbjs.setConfig({ + // ... realTimeData: { auctionDelay: 1000, dataProviders: [ @@ -134,18 +185,17 @@ pbjs.setConfig( name: "SirdataRTDModule", waitForIt: true, params: { - partnerId: 1, + partnerId: 1, key: 1, - setGptKeyValues: true, - contextualMinRelevancyScore: 50, //Min score to keep contextual category in the bidders (0-100 scale) - actualUrl: actual_url, //top location url, for contextual categories + setGptKeyValues: true, + contextualMinRelevancyScore: 50, //Min score to keep contextual category in the bidders (0-100 scale) + actualUrl: actual_url, //top location url, for contextual categories bidders: [{ bidder: 'appnexus', customFunction: overrideAppnexus, curationId: '111' },{ bidder: 'ix', - sizeLimit: 1200, //specific to Index Exchange customFunction: function(adUnit, segmentsArray, dataObject, bid) { bid.params.contextual.push(dataObject.contextual_categories); }, @@ -153,17 +203,19 @@ pbjs.setConfig( } } ] - } + }, ... -} +}); ``` ### Testing To view an example of available segments returned by Sirdata's backends: -`gulp serve --modules=rtdModule,sirdataRtdProvider,appnexusBidAdapter` +```bash +gulp serve --modules=rtdModule,sirdataRtdProvider,appnexusBidAdapter +``` and then point your browser at: -`http://localhost:9999/integrationExamples/gpt/sirdataRtdProvider_example.html` \ No newline at end of file +[http://localhost:9999/integrationExamples/gpt/sirdataRtdProvider_example.html] diff --git a/modules/sizeMapping.js b/modules/sizeMapping.js index fcd0b0963f2..9b2a37d0235 100644 --- a/modules/sizeMapping.js +++ b/modules/sizeMapping.js @@ -4,7 +4,6 @@ import {includes} from '../src/polyfill.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {setupAdUnitMediaTypes} from '../src/adapterManager.js'; -let installed = false; let sizeConfig = []; /** @@ -24,18 +23,16 @@ let sizeConfig = []; */ export function setSizeConfig(config) { sizeConfig = config; - if (!installed) { - setupAdUnitMediaTypes.before((next, adUnit, labels) => next(processAdUnitsForLabels(adUnit, labels), labels)); - installed = true; - } } + +setupAdUnitMediaTypes.before((next, adUnit, labels) => next(processAdUnitsForLabels(adUnit, labels), labels)); config.getConfig('sizeConfig', config => setSizeConfig(config.sizeConfig)); /** * Returns object describing the status of labels on the adUnit or bidder along with labels passed into requestBids * @param bidOrAdUnit the bidder or adUnit to get label info on * @param activeLabels the labels passed to requestBids - * @returns {LabelDescriptor} + * @returns {object} */ export function getLabels(bidOrAdUnit, activeLabels) { if (bidOrAdUnit.labelAll) { @@ -66,14 +63,18 @@ if (FEATURES.VIDEO) { } /** - * Resolves the unique set of the union of all sizes and labels that are active from a SizeConfig.mediaQuery match - * @param {Array} labels Labels specified on adUnit or bidder - * @param {boolean} labelAll if true, all labels must match to be enabled - * @param {Array} activeLabels Labels passed in through requestBids - * @param {object} mediaTypes A mediaTypes object describing the various media types (banner, video, native) - * @param {Array>} sizes Sizes specified on adUnit (deprecated) - * @param {Array} configs - * @returns {{labels: Array, sizes: Array>}} + * Resolves the unique set of the union of all sizes and labels that are active from a SizeConfig.mediaQuery match. + * + * @param {Object} options - The options object. + * @param {Array} [options.labels=[]] - Labels specified on adUnit or bidder. + * @param {boolean} [options.labelAll=false] - If true, all labels must match to be enabled. + * @param {Array} [options.activeLabels=[]] - Labels passed in through requestBids. + * @param {Object} mediaTypes - A mediaTypes object describing the various media types (banner, video, native). + * @param {Array} configs - An array of SizeConfig objects. + * @returns {Object} - An object containing the active status, media types, and filter results. + * @returns {boolean} return.active - Whether the media types are active. + * @returns {Object} return.mediaTypes - The media types object. + * @returns {Object} [return.filterResults] - The filter results before and after applying size filtering. */ export function resolveStatus({labels = [], labelAll = false, activeLabels = []} = {}, mediaTypes, configs = sizeConfig) { let maps = evaluateSizeConfig(configs); diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index 5ddb2e410cb..ec97cc6a57d 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -6,7 +6,7 @@ import { deepClone, - getWindowTop, + getWinDimensions, isArray, isArrayOfNums, isValidMediaTypes, @@ -331,14 +331,9 @@ export function getFilteredMediaTypes(mediaTypes) { native: undefined } - try { - activeViewportWidth = getWindowTop().innerWidth; - activeViewportHeight = getWindowTop().innerHeight; - } catch (e) { - logWarn(`SizeMappingv2:: Unfriendly iframe blocks viewport size to be evaluated correctly`); - activeViewportWidth = window.innerWidth; - activeViewportHeight = window.innerHeight; - } + activeViewportWidth = getWinDimensions().innerWidth; + activeViewportHeight = getWinDimensions().innerHeight; + const activeViewport = [activeViewportWidth, activeViewportHeight]; Object.keys(mediaTypes).map(mediaType => { const sizeConfig = mediaTypes[mediaType].sizeConfig; diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 64d8765dc04..fc7ad9d7d94 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -3,10 +3,11 @@ import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import { NATIVE_IMAGE_TYPES } from '../src/constants.js'; +import {NATIVE_IMAGE_TYPES} from '../src/constants.js'; import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; import {fill} from '../libraries/appnexusUtils/anUtils.js'; import {chunk} from '../libraries/chunk/chunk.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -18,134 +19,16 @@ import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'smaato'; const SMAATO_ENDPOINT = 'https://prebid.ad.smaato.net/oapi/prebid'; -const SMAATO_CLIENT = 'prebid_js_$prebid.version$_1.8' +const SMAATO_CLIENT = 'prebid_js_$prebid.version$_3.3' +const TTL = 300; const CURRENCY = 'USD'; - -const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { - const requestTemplate = { - id: bidderRequest.bidderRequestId, - at: 1, - cur: [CURRENCY], - tmax: bidderRequest.timeout, - site: { - id: window.location.hostname, - // TODO: do the fallbacks make sense here? - domain: bidderRequest.refererInfo.domain || window.location.hostname, - page: bidderRequest.refererInfo.page || window.location.href, - ref: bidderRequest.refererInfo.ref - }, - device: { - language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', - ua: navigator.userAgent, - dnt: getDNT() ? 1 : 0, - h: screen.height, - w: screen.width - }, - regs: { - coppa: config.getConfig('coppa') === true ? 1 : 0, - ext: {} - }, - user: { - ext: {} - }, - source: { - ext: { - schain: bidRequest.schain - } - }, - ext: { - client: SMAATO_CLIENT - } - }; - - let ortb2 = bidderRequest.ortb2 || {}; - Object.assign(requestTemplate.user, ortb2.user); - Object.assign(requestTemplate.site, ortb2.site); - - deepSetValue(requestTemplate, 'site.publisher.id', deepAccess(bidRequest, 'params.publisherId')); - - if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies === true) { - deepSetValue(requestTemplate, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); - deepSetValue(requestTemplate, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - } - - if (bidderRequest.uspConsent !== undefined) { - deepSetValue(requestTemplate, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (ortb2.regs?.gpp !== undefined) { - deepSetValue(requestTemplate, 'regs.ext.gpp', ortb2.regs.gpp); - deepSetValue(requestTemplate, 'regs.ext.gpp_sid', ortb2.regs.gpp_sid); - } - - if (ortb2.device?.ifa !== undefined) { - deepSetValue(requestTemplate, 'device.ifa', ortb2.device.ifa); - } - - if (ortb2.device?.geo !== undefined) { - deepSetValue(requestTemplate, 'device.geo', ortb2.device.geo); - } - - if (deepAccess(bidRequest, 'params.app')) { - if (!deepAccess(requestTemplate, 'device.geo')) { - const geo = deepAccess(bidRequest, 'params.app.geo'); - deepSetValue(requestTemplate, 'device.geo', geo); - } - if (!deepAccess(requestTemplate, 'device.ifa')) { - const ifa = deepAccess(bidRequest, 'params.app.ifa'); - deepSetValue(requestTemplate, 'device.ifa', ifa); - } - } - - const eids = deepAccess(bidRequest, 'userIdAsEids'); - if (eids && eids.length) { - deepSetValue(requestTemplate, 'user.ext.eids', eids); - } - - let requests = []; - - if (deepAccess(bidRequest, 'mediaTypes.banner')) { - const bannerRequest = Object.assign({}, requestTemplate, createBannerImp(bidRequest)); - requests.push(bannerRequest); - } - - const videoMediaType = deepAccess(bidRequest, 'mediaTypes.video'); - if (videoMediaType) { - if (videoMediaType.context === ADPOD) { - const adPodRequest = Object.assign({}, requestTemplate, createAdPodImp(bidRequest, videoMediaType)); - addOptionalAdpodParameters(adPodRequest, videoMediaType); - requests.push(adPodRequest); - } else { - const videoRequest = Object.assign({}, requestTemplate, createVideoImp(bidRequest, videoMediaType)); - requests.push(videoRequest); - } - } - - const nativeOrtbRequest = bidRequest.nativeOrtbRequest; - if (nativeOrtbRequest) { - const nativeRequest = Object.assign({}, requestTemplate, createNativeImp(bidRequest, nativeOrtbRequest)); - requests.push(nativeRequest); - } - - return requests; -} - -const buildServerRequest = (validBidRequest, data) => { - logInfo('[SMAATO] OpenRTB Request:', data); - return { - method: 'POST', - url: validBidRequest.params.endpoint || SMAATO_ENDPOINT, - data: JSON.stringify(data), - options: { - withCredentials: true, - crossOrigin: true, - } - }; -} +const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO, NATIVE]; +const IMAGE_SYNC_URL = 'https://s.ad.smaato.net/c/?adExInit=p' +const IFRAME_SYNC_URL = 'https://s.ad.smaato.net/i/?adExInit=p' export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, gvlid: 82, /** @@ -195,13 +78,30 @@ export const spec = { return true; }, - buildRequests: (validBidRequests, bidderRequest) => { + buildRequests: (bidRequests, bidderRequest) => { logInfo('[SMAATO] Client version:', SMAATO_CLIENT); - return validBidRequests.map((validBidRequest) => { - const openRtbBidRequests = buildOpenRtbBidRequest(validBidRequest, bidderRequest); - return openRtbBidRequests.map((openRtbBidRequest) => buildServerRequest(validBidRequest, openRtbBidRequest)); - }).reduce((acc, item) => item != null && acc.concat(item), []); + let requests = []; + bidRequests.forEach(bid => { + // separate requests per mediaType + SUPPORTED_MEDIA_TYPES.forEach(mediaType => { + if ((bid.mediaTypes && bid.mediaTypes[mediaType]) || (mediaType === NATIVE && bid.nativeOrtbRequest)) { + const data = converter.toORTB({bidderRequest, bidRequests: [bid], context: {mediaType}}); + requests.push({ + method: 'POST', + url: bid.params.endpoint || SMAATO_ENDPOINT, + data: JSON.stringify(data), + options: { + withCredentials: true, + crossOrigin: true, + }, + bidderRequest + }) + } + }); + }); + + return requests; }, /** * Unpack the response from the server into a list of bids. @@ -239,11 +139,12 @@ export const spec = { creativeId: bid.crid, dealId: bid.dealid || null, netRevenue: deepAccess(bid, 'ext.net', true), - currency: response.cur, + currency: CURRENCY, meta: { advertiserDomains: bid.adomain, networkName: bid.bidderName, - agencyId: seatbid.seat + agencyId: seatbid.seat, + ...(bid.ext?.dsa && {dsa: bid.ext.dsa}) } }; @@ -262,12 +163,8 @@ export const spec = { } else { switch (smtAdType) { case 'Img': - resultingBid.ad = createImgAd(bid.adm); - resultingBid.mediaType = BANNER; - bids.push(resultingBid); - break; case 'Richmedia': - resultingBid.ad = createRichmediaAd(bid.adm); + resultingBid.ad = createBannerAd(bid); resultingBid.mediaType = BANNER; bids.push(resultingBid); break; @@ -301,42 +198,218 @@ export const spec = { * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + if (syncOptions) { + let gdprParams = ''; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `&gdpr_consent=${gdprConsent.consentString}`; + } + } + + if (syncOptions.iframeEnabled) { + let maxUrlsParam = ''; + if (config.getConfig('userSync') && config.getConfig('userSync').syncsPerBidder) { + maxUrlsParam = `&maxUrls=${config.getConfig('userSync').syncsPerBidder}`; + } + + return [{ + type: 'iframe', + url: IFRAME_SYNC_URL + gdprParams + maxUrlsParam + }]; + } else if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: IMAGE_SYNC_URL + gdprParams + }]; + } + } + return []; } } registerBidder(spec); -const createImgAd = (adm) => { - const image = JSON.parse(adm).image; +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: TTL, + currency: CURRENCY + }, + request(buildRequest, imps, bidderRequest, context) { + function isGdprApplicable() { + return bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies; + } - let clickEvent = ''; - image.clicktrackers.forEach(src => { - clickEvent += `fetch(decodeURIComponent('${encodeURIComponent(src)}'), {cache: 'no-cache'});`; - }) + function setPublisherId(node) { + deepSetValue(node, 'publisher.id', bidRequest.params.publisherId); + } - let markup = `
`; + const request = buildRequest(imps, bidderRequest, context); + const bidRequest = context.bidRequests[0]; + let content; + const mediaType = context.mediaType; + if (mediaType === VIDEO) { + const videoParams = bidRequest.mediaTypes[VIDEO]; + if (videoParams.context === ADPOD) { + request.imp = createAdPodImp(request.imp[0], videoParams); + content = addOptionalAdpodParameters(videoParams); + } + } - image.impressiontrackers.forEach(src => { - markup += ``; - }); + request.at = 1; - return markup + '
'; -}; + if (request.user) { + if (isGdprApplicable()) { + deepSetValue(request.user, 'ext.consent', bidderRequest.gdprConsent.consentString); + } + } else { + const eids = deepAccess(bidRequest, 'userIdAsEids'); + request.user = { + ext: { + consent: isGdprApplicable() ? bidderRequest.gdprConsent.consentString : null, + eids: (eids && eids.length) ? eids : null + } + } + } -const createRichmediaAd = (adm) => { - const rich = JSON.parse(adm).richmedia; - let clickEvent = ''; - rich.clicktrackers.forEach(src => { - clickEvent += `fetch(decodeURIComponent('${encodeURIComponent(src)}'), {cache: 'no-cache'});`; - }) + if (request.site) { + request.site.id = window.location.hostname + if (content) { + request.site.content = content; + } + setPublisherId(request.site); + } else if (request.dooh) { + request.dooh.id = window.location.hostname + if (content) { + request.dooh.content = content; + } + setPublisherId(request.dooh); + } else { + request.site = { + id: window.location.hostname, + domain: bidderRequest.refererInfo.domain || window.location.hostname, + page: bidderRequest.refererInfo.page || window.location.href, + ref: bidderRequest.refererInfo.ref, + content: content || null + } + setPublisherId(request.site); + } + + if (request.regs) { + if (isGdprApplicable()) { + deepSetValue(request.regs, 'ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); + } + if (bidderRequest.uspConsent !== undefined) { + deepSetValue(request.regs, 'ext.us_privacy', bidderRequest.uspConsent); + } + if (request.regs?.gpp) { + deepSetValue(request.regs, 'ext.gpp', request.regs.gpp); + deepSetValue(request.regs, 'ext.gpp_sid', request.regs.gpp_sid); + } + } else { + request.regs = { + coppa: config.getConfig('coppa') === true ? 1 : 0, + ext: { + gdpr: isGdprApplicable() ? bidderRequest.gdprConsent.gdprApplies ? 1 : 0 : null, + us_privacy: bidderRequest.uspConsent + } + } + } + + if (!request.device) { + request.device = { + language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + ua: navigator.userAgent, + dnt: getDNT() ? 1 : 0, + h: screen.height, + w: screen.width + } + } + if (bidRequest.params.app) { + if (!deepAccess(request.device, 'geo')) { + const geo = deepAccess(bidRequest, 'params.app.geo'); + deepSetValue(request.device, 'geo', geo); + } + if (!deepAccess(request.device, 'ifa')) { + const ifa = deepAccess(bidRequest, 'params.app.ifa'); + deepSetValue(request.device, 'ifa', ifa); + } + } + + request.source = { + ext: { + schain: bidRequest.schain + } + }; + request.ext = { + client: SMAATO_CLIENT + } + return request; + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'tagid', bidRequest.params.adbreakId || bidRequest.params.adspaceId); + if (imp.bidfloorcur && imp.bidfloorcur !== CURRENCY) { + delete imp.bidfloor; + delete imp.bidfloorcur; + } + return imp; + }, + + overrides: { + imp: { + banner(orig, imp, bidRequest, context) { + const mediaType = context.mediaType; + + if (mediaType === BANNER) { + imp.bidfloor = getBidFloor(bidRequest, BANNER, getAdUnitSizes(bidRequest)); + } + + orig(imp, bidRequest, context); + }, + + video(orig, imp, bidRequest, context) { + const mediaType = context.mediaType; + if (mediaType === VIDEO) { + const videoParams = bidRequest.mediaTypes[VIDEO]; + imp.bidfloor = getBidFloor(bidRequest, VIDEO, videoParams.playerSize); + if (videoParams.context !== ADPOD) { + deepSetValue(imp, 'video.ext', { + rewarded: videoParams.ext && videoParams.ext.rewarded ? videoParams.ext.rewarded : 0 + }) + } + } + + orig(imp, bidRequest, context); + }, - let markup = `
${rich.mediadata.content}`; + native(orig, imp, bidRequest, context) { + const mediaType = context.mediaType; + + if (mediaType === NATIVE) { + imp.bidfloor = getBidFloor(bidRequest, NATIVE, getNativeMainImageSize(bidRequest.nativeOrtbRequest)); + } + + orig(imp, bidRequest, context); + } + }, + } +}); - rich.impressiontrackers.forEach(src => { - markup += ``; - }); +const createBannerAd = (bid) => { + let clickEvent = ''; + if (bid.ext && bid.ext.curls) { + let clicks = '' + bid.ext.curls.forEach(src => { + clicks += `fetch(decodeURIComponent('${encodeURIComponent(src)}'), {cache: 'no-cache'});`; + }) + clickEvent = `onclick="${clicks}"` + } - return markup + '
'; + return `
${bid.adm}
`; }; const createNativeAd = (adm) => { @@ -346,65 +419,6 @@ const createNativeAd = (adm) => { } }; -function createBannerImp(bidRequest) { - const adUnitSizes = getAdUnitSizes(bidRequest); - const sizes = adUnitSizes.map((size) => ({w: size[0], h: size[1]})); - return { - imp: [{ - id: bidRequest.bidId, - tagid: deepAccess(bidRequest, 'params.adspaceId'), - bidfloor: getBidFloor(bidRequest, BANNER, adUnitSizes), - instl: deepAccess(bidRequest.ortb2Imp, 'instl'), - banner: { - w: sizes[0].w, - h: sizes[0].h, - format: sizes - } - }] - }; -} - -function createVideoImp(bidRequest, videoMediaType) { - return { - imp: [{ - id: bidRequest.bidId, - tagid: deepAccess(bidRequest, 'params.adspaceId'), - bidfloor: getBidFloor(bidRequest, VIDEO, videoMediaType.playerSize), - instl: deepAccess(bidRequest.ortb2Imp, 'instl'), - video: { - mimes: videoMediaType.mimes, - minduration: videoMediaType.minduration, - startdelay: videoMediaType.startdelay, - linearity: videoMediaType.linearity, - w: videoMediaType.playerSize[0][0], - h: videoMediaType.playerSize[0][1], - maxduration: videoMediaType.maxduration, - skip: videoMediaType.skip, - protocols: videoMediaType.protocols, - ext: { - rewarded: videoMediaType.ext && videoMediaType.ext.rewarded ? videoMediaType.ext.rewarded : 0 - }, - skipmin: videoMediaType.skipmin, - api: videoMediaType.api - } - }] - }; -} - -function createNativeImp(bidRequest, nativeRequest) { - return { - imp: [{ - id: bidRequest.bidId, - tagid: deepAccess(bidRequest, 'params.adspaceId'), - bidfloor: getBidFloor(bidRequest, NATIVE, getNativeMainImageSize(nativeRequest)), - native: { - request: JSON.stringify(nativeRequest), - ver: '1.2' - } - }] - }; -} - function getNativeMainImageSize(nativeRequest) { const mainImage = find(nativeRequest.assets, asset => asset.hasOwnProperty('img') && asset.img.type === NATIVE_IMAGE_TYPES.MAIN) if (mainImage) { @@ -418,30 +432,12 @@ function getNativeMainImageSize(nativeRequest) { return [] } -function createAdPodImp(bidRequest, videoMediaType) { - const tagid = deepAccess(bidRequest, 'params.adbreakId') +function createAdPodImp(imp, videoMediaType) { const bce = config.getConfig('adpod.brandCategoryExclusion') - let imp = { - id: bidRequest.bidId, - tagid: tagid, - bidfloor: getBidFloor(bidRequest, VIDEO, videoMediaType.playerSize), - instl: deepAccess(bidRequest.ortb2Imp, 'instl'), - video: { - w: videoMediaType.playerSize[0][0], - h: videoMediaType.playerSize[0][1], - mimes: videoMediaType.mimes, - startdelay: videoMediaType.startdelay, - linearity: videoMediaType.linearity, - skip: videoMediaType.skip, - protocols: videoMediaType.protocols, - skipmin: videoMediaType.skipmin, - api: videoMediaType.api, - ext: { - context: ADPOD, - brandcategoryexclusion: bce !== undefined && bce - } - } - } + imp.video.ext = { + context: ADPOD, + brandcategoryexclusion: bce !== undefined && bce + }; const numberOfPlacements = getAdPodNumberOfPlacements(videoMediaType) let imps = fill(imp, numberOfPlacements) @@ -471,9 +467,7 @@ function createAdPodImp(bidRequest, videoMediaType) { }); } - return { - imp: imps - } + return imps } function getAdPodNumberOfPlacements(videoMediaType) { @@ -486,7 +480,7 @@ function getAdPodNumberOfPlacements(videoMediaType) { : numberOfPlacements } -const addOptionalAdpodParameters = (request, videoMediaType) => { +const addOptionalAdpodParameters = (videoMediaType) => { const content = {} if (videoMediaType.tvSeriesName) { @@ -509,7 +503,7 @@ const addOptionalAdpodParameters = (request, videoMediaType) => { } if (!isEmpty(content)) { - request.site.content = content + return content } } diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 7edaaa36957..e3cdb3dc5bf 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -1,17 +1,27 @@ -import { deepAccess, deepClone, isArrayOfNums, isFn, isInteger, isPlainObject, logError } from '../src/utils.js'; +import { + deepAccess, + deepClone, + isArray, + isArrayOfNums, + isEmpty, + isInteger, + logError +} from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; +import { getBidFloor } from '../libraries/equativUtils/equativUtils.js' import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.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').UserSync} UserSync */ const BIDDER_CODE = 'smartadserver'; const GVL_ID = 45; -const DEFAULT_FLOOR = 0.0; export const spec = { code: BIDDER_CODE, @@ -156,6 +166,9 @@ export const spec = { method: 'POST', url: (domain !== undefined ? domain : 'https://prg.smartadserver.com') + '/prebid/v1', data: JSON.stringify(payload), + options: { + browsingTopics: false + } }; }, @@ -163,12 +176,12 @@ export const spec = { * Makes server requests from the list of BidRequests. * * @param {BidRequest[]} validBidRequests an array of bids - * @param {BidderRequest} bidderRequest bidder request object + * @param {BidRequest} bidderRequest bidder request object * @return {ServerRequest[]} Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { // use bidderRequest.bids[] to get bidder-dependent request info - const adServerCurrency = config.getConfig('currency.adServerCurrency'); + const adServerCurrency = getCurrencyFromBidderRequest(bidderRequest); const sellerDefinedAudience = deepAccess(bidderRequest, 'ortb2.user.data', config.getAnyConfig('ortb2.user.data')); const sellerDefinedContext = deepAccess(bidderRequest, 'ortb2.site.content.data', config.getAnyConfig('ortb2.site.content.data')); @@ -196,11 +209,16 @@ export const spec = { sdc: sellerDefinedContext }; - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { payload.gpid = gpid; } + const dsa = deepAccess(bid, 'ortb2.regs.ext.dsa'); + if (dsa) { + payload.dsa = dsa; + } + if (bidderRequest) { if (bidderRequest.gdprConsent) { payload.addtl_consent = bidderRequest.gdprConsent.addtlConsent; @@ -239,7 +257,7 @@ export const spec = { if (isSupportedVideoContext) { let videoPayload = deepClone(payload); spec.fillPayloadForVideoBidRequest(videoPayload, videoMediaType, bid.params.video); - videoPayload.bidfloor = bid.params.bidfloor || spec.getBidFloor(bid, adServerCurrency, VIDEO); + videoPayload.bidfloor = bid.params.bidfloor || getBidFloor(bid, adServerCurrency, VIDEO); bidRequests.push(spec.createServerRequest(videoPayload, bid.params.domain)); } } else { @@ -247,7 +265,7 @@ export const spec = { spec.fillPayloadForVideoBidRequest(payload, videoMediaType, bid.params.video); } - payload.bidfloor = bid.params.bidfloor || spec.getBidFloor(bid, adServerCurrency, type); + payload.bidfloor = bid.params.bidfloor || getBidFloor(bid, adServerCurrency, type); bidRequests.push(spec.createServerRequest(payload, bid.params.domain)); } else { bidRequests.push({}); @@ -282,7 +300,10 @@ export const spec = { netRevenue: response.isNetCpm, ttl: response.ttl, dspPixels: response.dspPixels, - meta: { advertiserDomains: response.adomain ? response.adomain : [] } + meta: { + ...isArray(response.adomain) && !isEmpty(response.adomain) ? { advertiserDomains: response.adomain } : {}, + ...!isEmpty(response.dsa) ? { dsa: response.dsa } : {} + } }; if (bidRequest.mediaType === VIDEO) { @@ -303,34 +324,12 @@ export const spec = { return bidResponses; }, - /** - * Get floors from Prebid Price Floors module - * - * @param {object} bid Bid request object - * @param {string} currency Ad server currency - * @param {string} mediaType Bid media type - * @return {number} Floor price - */ - getBidFloor: function (bid, currency, mediaType) { - if (!isFn(bid.getFloor)) { - return DEFAULT_FLOOR; - } - - const floor = bid.getFloor({ - currency: currency || 'USD', - mediaType, - size: '*' - }); - - return isPlainObject(floor) && !isNaN(floor.floor) ? floor.floor : DEFAULT_FLOOR; - }, - /** * User syncs. * * @param {*} syncOptions Publisher prebid configuration. * @param {*} serverResponses A successful response from the server. - * @return {syncs[]} An array of syncs that should be executed. + * @return {UserSync[]} An array of syncs that should be executed. */ getUserSyncs: function (syncOptions, serverResponses) { const syncs = []; diff --git a/modules/smarthubBidAdapter.js b/modules/smarthubBidAdapter.js index 2889bd5358b..3d3b15dae13 100644 --- a/modules/smarthubBidAdapter.js +++ b/modules/smarthubBidAdapter.js @@ -1,190 +1,95 @@ -import {deepAccess, isFn, logError, logMessage} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { + buildPlacementProcessingFunction, + buildRequestsBase, + interpretResponseBuilder, + isBidRequestValid, + getUserSyncs +} from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'smarthub'; +const SYNC_URL = 'https://us.shb-sync.com' +const ALIASES = [ + {code: 'attekmi'}, + {code: 'markapp'}, + {code: 'jdpmedia'}, + {code: 'tredio'}, + {code: 'felixads'}, + {code: 'vimayx'}, + {code: 'artechnology'}, + {code: 'adinify'}, + {code: 'addigi'}, + {code: 'jambojar'}, +]; +const BASE_URLS = { + attekmi: 'https://prebid.attekmi.com/pbjs', + smarthub: 'https://prebid.attekmi.com/pbjs', + markapp: 'https://markapp-prebid.attekmi.com/pbjs', + jdpmedia: 'https://jdpmedia-prebid.attekmi.com/pbjs', + tredio: 'https://tredio-prebid.attekmi.com/pbjs', + felixads: 'https://felixads-prebid.attekmi.com/pbjs', + vimayx: 'https://vimayx-prebid.attekmi.com/pbjs', + artechnology: 'https://artechnology-prebid.attekmi.com/pbjs', + adinify: 'https://adinify-prebid.attekmi.com/pbjs', + addigi: 'https://addigi-prebid.attekmi.com/pbjs', + jambojar: 'https://jambojar-prebid.attekmi.com/pbjs', +}; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency || !bid.hasOwnProperty('netRevenue')) { - return false; - } - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.width && bid.height && (bid.vastUrl || bid.vastXml)); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { partnerName, seat, token, iabCat, minBidfloor, pos } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - partnerName: partnerName.toLowerCase(), - seat, - token, - iabCat, - minBidfloor, - pos, - bidId, - schain, - bidfloor - }; - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; +const _getUrl = (partnerName) => { + const aliases = ALIASES.map(el => el.code); + if (aliases.includes(partnerName)) { + return BASE_URLS[partnerName]; } - return placement; + return `${BASE_URLS[BIDDER_CODE]}?partnerName=${partnerName}`; } -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', +const getPartnerName = (bid) => String(bid.params?.partnerName || bid.bidder).toLowerCase(); + +const getPlacementReqData = buildPlacementProcessingFunction({ + addPlacementType() {}, + addCustomFieldsToPlacement(bid, bidderRequest, placement) { + const { seat, token, iabCat, minBidfloor, pos } = bid.params; + Object.assign(placement, { + partnerName: getPartnerName(bid), + seat, + token, + iabCat, + minBidfloor, + pos, }); - return bidFloor.floor; - } catch (e) { - logError(e); - return 0; } -} - -function buildRequestParams(bidderRequest = {}, placements = []) { - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - return { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; +}) + +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + const bidsByPartner = validBidRequests.reduce((bidsByPartner, bid) => { + const partner = getPartnerName(bid); + (bidsByPartner[partner] = bidsByPartner[partner] || []).push(bid); + return bidsByPartner; + }, {}); + return Object.entries(bidsByPartner).map(([partner, validBidRequests]) => { + return buildRequestsBase({ + adUrl: _getUrl(partner), + bidderRequest, + validBidRequests, + placementProcessingFunction: getPlacementReqData + }) + }) } export const spec = { code: BIDDER_CODE, + aliases: ALIASES, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && params.partnerName && params.seat && params.token); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const tempObj = {}; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - const data = getPlacementReqData(bid); - tempObj[data.partnerName] = tempObj[data.partnerName] || []; - tempObj[data.partnerName].push(data); + isBidRequestValid: isBidRequestValid(['seat', 'token'], 'every'), + buildRequests, + interpretResponse: interpretResponseBuilder({ + addtlBidValidation(bid) { + return bid.hasOwnProperty('netRevenue') } - - return Object.keys(tempObj).map(key => { - const request = buildRequestParams(bidderRequest, tempObj[key]); - return { - method: 'POST', - url: `https://${key}-prebid.smart-hub.io/pbjs`, - data: request, - } - }); - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - } + }), + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/smarthubBidAdapter.md b/modules/smarthubBidAdapter.md index c09855303e2..dbdae927097 100644 --- a/modules/smarthubBidAdapter.md +++ b/modules/smarthubBidAdapter.md @@ -1,16 +1,16 @@ # Overview ``` -Module Name: SmartHub Bidder Adapter -Module Type: SmartHub Bidder Adapter -Maintainer: support@smart-hub.io +Module Name: Attekmi Bidder Adapter +Module Type: Attekmi Bidder Adapter +Maintainer: prebid@attekmi.com ``` # Description -Connects to SmartHub exchange for bids. +Connects to Attekmi exchange for bids. -SmartHub bid adapter supports Banner, Video (instream and outstream) and Native. +Attekmi bid adapter supports Banner, Video (instream and outstream) and Native. # Test Parameters ``` diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index 8394814365c..1442199cd6d 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -119,12 +119,6 @@ export const spec = { const pos = getBidIdParameter('pos', bid.params) || 1; const api = getBidIdParameter('api', bid.params) || [2]; const protocols = getBidIdParameter('protocols', bid.params) || [2, 3, 5, 6]; - var contextcustom = deepAccess(bid, 'mediaTypes.video.context'); - var placement = 1; - - if (contextcustom === 'outstream') { - placement = 3; - } let smartxReq = [{ id: bid.bidId, @@ -144,7 +138,6 @@ export const spec = { maxbitrate: maxbitrate, delivery: delivery, pos: pos, - placement: placement, api: api, ext: ext }, @@ -429,6 +422,12 @@ function createOutstreamConfig(bid) { var playerListener = function callback(event) { switch (event) { + case 'AdError': + try { + window.sc_smartIntxtError(); + } catch (f) {} + break; + case 'AdSlotStarted': try { window.sc_smartIntxtStart(); diff --git a/modules/smartyadsAnalyticsAdapter.js b/modules/smartyadsAnalyticsAdapter.js new file mode 100644 index 00000000000..7784e0bc831 --- /dev/null +++ b/modules/smartyadsAnalyticsAdapter.js @@ -0,0 +1,133 @@ +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import { EVENTS } from '../src/constants.js'; +import { ajax } from '../src/ajax.js'; + +const { + AUCTION_INIT, + AUCTION_END, + BID_WON, + BID_TIMEOUT, + BIDDER_ERROR, + BID_REJECTED, + BID_REQUESTED, + AD_RENDER_FAILED, + AD_RENDER_SUCCEEDED, + AUCTION_TIMEOUT +} = EVENTS; + +const URL = 'https://ps.itdsmr.com'; +const ANALYTICS_TYPE = 'endpoint'; +const BIDDER_CODE = 'smartyads'; +const GVLID = 534; + +let smartyParams = {}; + +let smartyadsAdapter = Object.assign({}, + adapter({ + url: URL, + analyticsType: ANALYTICS_TYPE, + }), + { + track({ eventType, args }) { + switch (eventType) { + case AUCTION_INIT: + case AUCTION_TIMEOUT: + case AUCTION_END: + auctionHandler(eventType, args); + break; + case BID_REQUESTED: + if (args.bidderCode === BIDDER_CODE) { + for (const bid of args.bids) { + const bidParams = bid.params?.length ? bid.params[0] : bid.params; + smartyParams[bid.bidId] = bidParams; + } + }; + break; + case BID_WON: + case BID_TIMEOUT: + case BID_REJECTED: + bidHandler(eventType, args); + break; + case BIDDER_ERROR: + onBidderError(args); + break; + case AD_RENDER_FAILED: + case AD_RENDER_SUCCEEDED: + onAdRender(eventType, args); + break; + default: + break; + } + } + } +); + +const sendDataToServer = (data) => { + ajax(URL, () => {}, JSON.stringify(data)); +} + +const auctionHandler = (eventType, data) => { + const auctionData = { + auctionId: data.auctionId, + status: eventType, + timeout: data.timeout, + metrics: data.metrics, + bidderRequests: data.bidderRequests?.map(bidderRequest => { + delete bidderRequest.gdprConsent; + delete bidderRequest.refererInfo; + return bidderRequest; + }).filter(request => request.bidderCode === BIDDER_CODE), + } + + sendDataToServer({ eventType, auctionData }); +} + +const bidHandler = (eventType, bid) => { + let bids = bid.length ? bid : [ bid ]; + + for (const bidObj of bids) { + let bidToSend; + + if (bidObj.bidderCode != BIDDER_CODE) { + if (eventType === BID_WON) { + bidToSend = { + cpm: bidObj.cpm, + auctionId: bidObj.auctionId + }; + } else continue; + } + + bidToSend = bidObj; + + if (eventType === BID_REJECTED) { + bidToSend.params = smartyParams[bid.requestId]; + } + + sendDataToServer({ eventType, bid: bidToSend }); + } +} + +const onBidderError = (data) => { + sendDataToServer({ + eventType: BIDDER_ERROR, + error: data.error, + bidderRequests: data?.bidderRequests?.length + ? data.bidderRequests.filter(request => request.bidderCode === BIDDER_CODE) + : [ data.bidderRequest ] + }); +} + +const onAdRender = (eventType, data) => { + if (data?.bid?.bidderCode === BIDDER_CODE) { + sendDataToServer({ eventType, renderData: data }); + } +} + +adapterManager.registerAnalyticsAdapter({ + adapter: smartyadsAdapter, + code: BIDDER_CODE, + gvlid: GVLID +}) + +export default smartyadsAdapter; diff --git a/modules/smartyadsAnalyticsAdapter.md b/modules/smartyadsAnalyticsAdapter.md new file mode 100644 index 00000000000..e08b2a02cb7 --- /dev/null +++ b/modules/smartyadsAnalyticsAdapter.md @@ -0,0 +1,11 @@ +#### Description + +Module that enables SmartyAds analytics + +### Configuration + +```javascript + pbjs.enableAnalytics({ + provider: 'smartyads' + }); +``` \ No newline at end of file diff --git a/modules/smartyadsBidAdapter.js b/modules/smartyadsBidAdapter.js index 6920983e50d..de7c61b7163 100644 --- a/modules/smartyadsBidAdapter.js +++ b/modules/smartyadsBidAdapter.js @@ -1,64 +1,15 @@ -import { logMessage } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { ajax } from '../src/ajax.js'; +import { getAdUrlByRegion } from '../libraries/smartyadsUtils/getAdUrlByRegion.js'; +import { interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'smartyads'; const GVLID = 534; -const adUrls = { - US_EAST: 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', - EU: 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', - SGP: 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' -} const URL_SYNC = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { - return false; - } - switch (bid['mediaType']) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl) || Boolean(bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); - default: - return false; - } -} - -function getAdUrlByRegion(bid) { - let adUrl; - - if (bid.params.region && adUrls[bid.params.region]) { - adUrl = adUrls[bid.params.region]; - } else { - try { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const region = timezone.split('/')[0]; - - switch (region) { - case 'Europe': - adUrl = adUrls['EU']; - break; - case 'Asia': - adUrl = adUrls['SGP']; - break; - default: adUrl = adUrls['US_EAST']; - } - } catch (err) { - adUrl = adUrls['US_EAST']; - } - } - - return adUrl; -} - export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -74,33 +25,20 @@ export const spec = { let winTop = window; let location; - // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - + location = bidderRequest?.refererInfo ?? null; let placements = []; let request = { 'deviceWidth': winTop.screen.width, 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language : '', - 'secure': 1, - 'host': location.host, - 'page': location.pathname, + 'host': location?.domain ?? '', + 'page': location?.page ?? '', 'coppa': config.getConfig('coppa') === true ? 1 : 0, 'placements': placements, 'eeid': validBidRequests[0]?.userIdAsEids, 'ifa': bidderRequest?.ortb2?.device?.ifa, }; - request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) + if (bidderRequest) { - if (bidderRequest.uspConsent) { - request.ccpa = bidderRequest.uspConsent; - } if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent } @@ -137,59 +75,8 @@ export const spec = { } }, - interpretResponse: (serverResponse) => { - let response = []; - serverResponse = serverResponse.body; - for (let i = 0; i < serverResponse.length; i++) { - let resItem = serverResponse[i]; - if (isBidResponseValid(resItem)) { - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { - let syncs = []; - let { gdprApplies, consentString = '' } = gdprConsent; - - if (syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=iframe&us_privacy=${uspConsent}&gpp=${gppConsent}` - }); - } else { - syncs.push({ - type: 'image', - url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=image&us_privacy=${uspConsent}&gpp=${gppConsent}` - }); - } - - return syncs - }, - - onBidWon: function(bid) { - if (bid.winUrl) { - ajax(bid.winUrl, () => {}, JSON.stringify(bid)); - } else { - if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { - ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&winTest=1', () => {}, JSON.stringify(bid)); - } - } - }, - - onTimeout: function(bid) { - if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { - ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidTimeout=1', () => {}, JSON.stringify(bid)); - } - }, - - onBidderError: function(bid) { - if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { - ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidderError=1', () => {}, JSON.stringify(bid)); - } - }, - + interpretResponse, + getUserSyncs: getUserSyncs(URL_SYNC), }; registerBidder(spec); diff --git a/modules/smartytechBidAdapter.js b/modules/smartytechBidAdapter.js index af1442bd301..c081b49c2e6 100644 --- a/modules/smartytechBidAdapter.js +++ b/modules/smartytechBidAdapter.js @@ -127,14 +127,19 @@ export const spec = { creativeId: response.creativeId, netRevenue: true, currency: response.currency, - mediaType: BANNER - } + mediaType: BANNER, + meta: {} + }; if (response.mediaType === VIDEO) { bidObject.vastXml = response.ad; bidObject.mediaType = VIDEO; } + if (response.meta) { + bidObject.meta = response.meta; + } + return bidObject; }, diff --git a/modules/smartytechBidAdapter.md b/modules/smartytechBidAdapter.md index 9df57ddbde7..53b246e4cab 100644 --- a/modules/smartytechBidAdapter.md +++ b/modules/smartytechBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: SmartyTech Bid Adapter Module Type: Bidder Adapter -Maintainer: info@adpartner.pro +Maintainer: info@fusify.io ``` # Description diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index 7d4a4bca615..a78c60f8308 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -1,10 +1,11 @@ import {deepAccess, deepClone, isArray, isFn, isPlainObject, logError, logWarn} from '../src/utils.js'; import {Renderer} from '../src/Renderer.js'; -import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {serializeSupplyChain} from '../libraries/schainSerializer/schainSerializer.js' import {convertOrtbRequestToProprietaryNative, toOrtbNativeRequest, toLegacyResponse} from '../src/native.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; const BIDDER_CODE = 'smilewanted'; @@ -65,7 +66,7 @@ export const spec = { return validBidRequests.map(bid => { const payload = { zoneId: bid.params.zoneId, - currencyCode: config.getConfig('currency.adServerCurrency') || 'EUR', + currencyCode: getCurrencyFromBidderRequest(bidderRequest) || 'EUR', tagId: bid.adUnitCode, sizes: bid.sizes.map(size => ({ w: size[0], @@ -82,7 +83,8 @@ export const spec = { or from mediaTypes.banner.pos */ positionType: bid.params.positionType || '', - prebidVersion: '$prebid.version$' + prebidVersion: '$prebid.version$', + schain: serializeSupplyChain(bid.schain, ['asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext']), }; const floor = getBidFloor(bid); @@ -154,16 +156,16 @@ export const spec = { if (response) { const dealId = response.dealId || ''; const bidResponse = { - requestId: bidRequestData.bidId, + ad: response.ad, cpm: response.cpm, - width: response.width, - height: response.height, creativeId: response.creativeId, - dealId: response.dealId, currency: response.currency, + dealId: response.dealId, + height: response.height, netRevenue: response.isNetCpm, + requestId: bidRequestData.bidId, ttl: response.ttl, - ad: response.ad, + width: response.width, }; if (response.formatTypeSw === 'video_instream' || response.formatTypeSw === 'video_outstream') { @@ -209,28 +211,30 @@ export const spec = { * @param {Object} uspConsent The USP consent parameters * @return {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent) { - let params = ''; - - if (gdprConsent && typeof gdprConsent.consentString === 'string') { - // add 'gdpr' only if 'gdprApplies' is defined - if (typeof gdprConsent.gdprApplies === 'boolean') { - params += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - params += `?gdpr_consent=${gdprConsent.consentString}`; + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + const syncs = []; + + if (syncOptions.iframeEnabled) { + let params = []; + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + // add 'gdpr' only if 'gdprApplies' is defined + if (typeof gdprConsent.gdprApplies === 'boolean') { + params.push(`gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); + } else { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } } - } - if (uspConsent) { - params += `${params ? '&' : '?'}us_privacy=${encodeURIComponent(uspConsent)}`; - } + if (uspConsent) { + params.push(`us_privacy=${encodeURIComponent(uspConsent)}`); + } - const syncs = [] + const paramsStr = params.length > 0 ? '?' + params.join('&') : ''; - if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: 'https://csync.smilewanted.com' + params + url: 'https://csync.smilewanted.com' + paramsStr }); } diff --git a/modules/smootBidAdapter.js b/modules/smootBidAdapter.js new file mode 100644 index 00000000000..cd55dc5f253 --- /dev/null +++ b/modules/smootBidAdapter.js @@ -0,0 +1,19 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'smoot'; +const AD_URL = 'https://endpoint1.smoot.ai/pbjs'; +const SYNC_URL = 'https://usync.smxconv.com'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(['placementId']), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/smootBidAdapter.md b/modules/smootBidAdapter.md new file mode 100644 index 00000000000..322584f19ea --- /dev/null +++ b/modules/smootBidAdapter.md @@ -0,0 +1,80 @@ +# Overview + +``` +Module Name: Smoot Bidder Adapter +Module Type: Smoot Bidder Adapter +Maintainer: it.ops@smoot.ai +``` + +# Description + +Connects to Smoot exchange for bids. +Smoot 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: 'smoot', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'smoot', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'smoot', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index 5a327b05cd0..0f6d8d29e4c 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -2,9 +2,8 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {deepAccess, isArray, isFn, isPlainObject, inIframe, getDNT, generateUUID} from '../src/utils.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import {getGlobal} from '../src/prebidGlobal.js'; import {getStorageManager} from '../src/storageManager.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js'; const BIDDER_CODE = 'snigel'; const GVLID = 1076; @@ -17,6 +16,7 @@ const SESSION_ID_KEY = '_sn_session_pba'; 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; @@ -31,6 +31,7 @@ export const spec = { }, buildRequests: function (bidRequests, bidderRequest) { + const { width: w, height: h } = getViewportSize(); const gdprApplies = deepAccess(bidderRequest, 'gdprConsent.gdprApplies'); return { method: 'POST', @@ -46,7 +47,8 @@ export const spec = { gdprConsent: gdprApplies === true ? hasFullGdprConsent(deepAccess(bidderRequest, 'gdprConsent')) : false, cur: getCurrencies(), test: getTestFlag(), - version: getGlobal().version, + version: 'v' + '$prebid.version$', + adapterVersion: '2.0', gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || deepAccess(bidderRequest, 'ortb2.regs.gpp'), gpp_sid: deepAccess(bidderRequest, 'gppConsent.applicableSections') || deepAccess(bidderRequest, 'ortb2.regs.gpp_sid'), @@ -60,8 +62,8 @@ export const spec = { page: getPage(bidderRequest), topframe: inIframe() === true ? 0 : 1, device: { - w: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, - h: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, + w, + h, dnt: getDNT() ? 1 : 0, language: getLanguage(), }, @@ -72,6 +74,7 @@ export const spec = { gpid: deepAccess(r, 'ortb2Imp.ext.gpid'), pbadslot: deepAccess(r, 'ortb2Imp.ext.data.pbadslot') || deepAccess(r, 'ortb2Imp.ext.gpid'), name: r.params.placement, + counter: getPlacementCounter(r.params.placement), sizes: r.sizes, floor: getPriceFloor(r, BANNER, FLOOR_MATCH_ALL_SIZES), refresh: getRefreshInformation(r.adUnitCode), @@ -106,8 +109,8 @@ export const spec = { getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncUrl = getSyncUrl(responses || []); - if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent, gppConsent)) { - return [{type: 'iframe', url: getSyncEndpoint(syncUrl, gdprConsent)}]; + if (syncUrl && syncOptions.iframeEnabled) { + return [{type: 'iframe', url: getSyncEndpoint(syncUrl, gdprConsent, uspConsent, gppConsent)}]; } }, }; @@ -183,23 +186,19 @@ function getRefreshInformation(adUnitCode) { }; } -function mapIdToRequestId(id, bidRequest) { - return bidRequest.bidderRequest.bids.filter((bid) => bid.adUnitCode === id)[0].bidId; -} - -function hasUspConsent(uspConsent) { - return typeof uspConsent !== 'string' || !(uspConsent[0] === '1' && uspConsent[2] === 'Y'); -} +function getPlacementCounter(placement) { + const counter = placementCounters[placement]; + if (counter === undefined) { + placementCounters[placement] = 0; + return 0; + } -function hasGppConsent(gppConsent) { - return ( - !(gppConsent && Array.isArray(gppConsent.applicableSections)) || - gppConsent.applicableSections.every((section) => typeof section === 'number' && section <= 5) - ); + placementCounters[placement]++; + return placementCounters[placement]; } -function hasSyncConsent(gdprConsent, uspConsent, gppConsent) { - return hasPurpose1Consent(gdprConsent) && hasUspConsent(uspConsent) && hasGppConsent(gppConsent); +function mapIdToRequestId(id, bidRequest) { + return bidRequest.bidderRequest.bids.filter((bid) => bid.adUnitCode === id)[0].bidId; } function hasFullGdprConsent(gdprConsent) { @@ -219,10 +218,12 @@ function getSyncUrl(responses) { return getConfig(`${BIDDER_CODE}.syncUrl`) || deepAccess(responses[0], 'body.syncUrl'); } -function getSyncEndpoint(url, gdprConsent) { +function getSyncEndpoint(url, gdprConsent, uspConsent, gppConsent) { return `${url}?gdpr=${gdprConsent?.gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent( gdprConsent?.consentString || '' - )}`; + )}&gpp_sid=${gppConsent?.applicableSections?.join(',') || ''}&gpp=${encodeURIComponent( + gppConsent?.gppString || '' + )}&us_privacy=${uspConsent || ''}`; } function getSessionId() { diff --git a/modules/sonobiAnalyticsAdapter.js b/modules/sonobiAnalyticsAdapter.js deleted file mode 100644 index 8242df7e0c5..00000000000 --- a/modules/sonobiAnalyticsAdapter.js +++ /dev/null @@ -1,275 +0,0 @@ -import { deepClone, logInfo, logError } from '../src/utils.js'; -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; -import { EVENTS } from '../src/constants.js'; -import adapterManager from '../src/adapterManager.js'; -import {ajaxBuilder} from '../src/ajax.js'; - -let ajax = ajaxBuilder(0); - -export const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; -const analyticsType = 'endpoint'; -const QUEUE_TIMEOUT_DEFAULT = 200; -const { - AUCTION_INIT, - AUCTION_END, - BID_REQUESTED, - BID_ADJUSTMENT, - BIDDER_DONE, - BID_WON, - BID_RESPONSE, - BID_TIMEOUT -} = EVENTS; - -let initOptions = {}; -let auctionCache = {}; -let auctionTtl = 60 * 60 * 1000; - -function deleteOldAuctions() { - for (let auctionId in auctionCache) { - let auction = auctionCache[auctionId]; - if (Date.now() - auction.start > auctionTtl) { - delete auctionCache[auctionId]; - } - } -} - -function buildAuctionEntity(args) { - return { - 'id': args.auctionId, - 'start': args.timestamp, - 'timeout': args.timeout, - 'adUnits': {}, - 'stats': {}, - 'queue': [], - 'qTimeout': false - }; -} -function buildAdUnit(data) { - return `/${initOptions.pubId}/${initOptions.siteId}/${data.adUnitCode.toLowerCase()}`; -} -function getLatency(data) { - if (!data.responseTimestamp) { - return -1; - } else { - return data.responseTimestamp - data.requestTimestamp; - } -} -function getBid(data) { - if (data.cpm) { - return Math.round(data.cpm * 100); - } else { - return 0; - } -} -function buildItem(data, response, phase = 1) { - let size = data.width ? {width: data.width, height: data.height} : {width: data.sizes[0][0], height: data.sizes[0][1]}; - return { - 'bidid': data.bidId || data.requestId, - 'p': phase, - 'buyerid': data.bidder.toLowerCase(), - 'bid': getBid(data), - 'adunit_code': buildAdUnit(data), - 's': `${size.width}x${size.height}`, - 'latency': getLatency(data), - 'response': response, - 'jsLatency': getLatency(data), - 'buyername': data.bidder.toLowerCase() - }; -} -function sendQueue(auctionId) { - let auction = auctionCache[auctionId]; - let data = auction.queue; - auction.queue = []; - auction.qTimeout = false; - sonobiAdapter.sendData(auction, data); -} -function addToAuctionQueue(auctionId, id) { - let auction = auctionCache[auctionId]; - auction.queue = auction.queue.filter((item) => { - if (item.bidid !== id) { return true; } - return auction.stats[id].data.p !== item.p; - }); - auction.queue.push(deepClone(auction.stats[id].data)); - if (!auction.qTimeout) { - auction.qTimeout = setTimeout(() => { - sendQueue(auctionId); - }, initOptions.delay) - } -} -function updateBidStats(auctionId, id, data) { - let auction = auctionCache[auctionId]; - auction.stats[id].data = {...auction.stats[id].data, ...data}; - addToAuctionQueue(auctionId, id); - _logInfo('Updated Bid Stats: ', auction.stats[id]); - return auction.stats[id]; -} - -function handleOtherEvents(eventType, args) { - _logInfo('Other Event: ' + eventType, args); -} - -function handlerAuctionInit(args) { - auctionCache[args.auctionId] = buildAuctionEntity(args); - deleteOldAuctions(); - _logInfo('Auction Init', args); -} -function handlerBidRequested(args) { - let auction = auctionCache[args.auctionId]; - let data = []; - let phase = 1; - let response = 1; - args.bids.forEach(function (bidRequest) { - auction = auctionCache[bidRequest.auctionId] - let built = buildItem(bidRequest, response, phase); - auction.stats[built.bidid] = {id: built.bidid, adUnitCode: bidRequest.adUnitCode, data: built}; - addToAuctionQueue(args.auctionId, built.bidid); - }) - - _logInfo('Bids Requested ', data); -} - -function handlerBidAdjustment(args) { - _logInfo('Bid Adjustment', args); -} -function handlerBidderDone(args) { - _logInfo('Bidder Done', args); -} - -function handlerAuctionEnd(args) { - let winners = {}; - args.bidsReceived.forEach((bid) => { - if (!winners[bid.adUnitCode]) { - winners[bid.adUnitCode] = {bidId: bid.requestId, cpm: bid.cpm}; - } else if (winners[bid.adUnitCode].cpm < bid.cpm) { - winners[bid.adUnitCode] = {bidId: bid.requestId, cpm: bid.cpm}; - } - }) - args.adUnitCodes.forEach((adUnitCode) => { - if (winners[adUnitCode]) { - let bidId = winners[adUnitCode].bidId; - updateBidStats(args.auctionId, bidId, {response: 4}); - } - }) - _logInfo('Auction End', args); - _logInfo('Auction Cache', auctionCache[args.auctionId].stats); -} -function handlerBidWon(args) { - let {auctionId, requestId} = args; - let res = updateBidStats(auctionId, requestId, {p: 3, response: 6}); - _logInfo('Bid Won ', args); - _logInfo('Bid Update Result: ', res); -} -function handlerBidResponse(args) { - let {auctionId, requestId, cpm, size, timeToRespond} = args; - updateBidStats(auctionId, requestId, {bid: cpm, s: size, jsLatency: timeToRespond, latency: timeToRespond, p: 2, response: 9}); - - _logInfo('Bid Response ', args); -} -function handlerBidTimeout(args) { - let {auctionId, bidId} = args; - _logInfo('Bid Timeout ', args); - updateBidStats(auctionId, bidId, {p: 2, response: 0, latency: args.timeout, jsLatency: args.timeout}); -} -let sonobiAdapter = Object.assign(adapter({url: DEFAULT_EVENT_URL, analyticsType}), { - track({eventType, args}) { - switch (eventType) { - case AUCTION_INIT: - handlerAuctionInit(args); - break; - case BID_REQUESTED: - handlerBidRequested(args); - break; - case BID_ADJUSTMENT: - handlerBidAdjustment(args); - break; - case BIDDER_DONE: - handlerBidderDone(args); - break; - case AUCTION_END: - handlerAuctionEnd(args); - break; - case BID_WON: - handlerBidWon(args); - break; - case BID_RESPONSE: - handlerBidResponse(args); - break; - case BID_TIMEOUT: - handlerBidTimeout(args); - break; - default: - handleOtherEvents(eventType, args); - break; - } - }, - -}); - -sonobiAdapter.originEnableAnalytics = sonobiAdapter.enableAnalytics; - -sonobiAdapter.enableAnalytics = function (config) { - if (this.initConfig(config)) { - _logInfo('Analytics adapter enabled', initOptions); - sonobiAdapter.originEnableAnalytics(config); - } -}; - -sonobiAdapter.initConfig = function (config) { - let isCorrectConfig = true; - initOptions = {}; - initOptions.options = deepClone(config.options); - - initOptions.pubId = initOptions.options.pubId || null; - initOptions.siteId = initOptions.options.siteId || null; - initOptions.delay = initOptions.options.delay || QUEUE_TIMEOUT_DEFAULT; - if (!initOptions.pubId) { - _logError('"options.pubId" is empty'); - isCorrectConfig = false; - } - if (!initOptions.siteId) { - _logError('"options.siteId" is empty'); - isCorrectConfig = false; - } - - initOptions.server = DEFAULT_EVENT_URL; - initOptions.host = initOptions.options.host || window.location.hostname; - this.initOptions = initOptions; - return isCorrectConfig; -}; - -sonobiAdapter.getOptions = function () { - return initOptions; -}; - -sonobiAdapter.sendData = function (auction, data) { - let url = 'https://' + initOptions.server + '?pageviewid=' + auction.id + '&corscred=1&pubId=' + initOptions.pubId + '&siteId=' + initOptions.siteId; - ajax( - url, - function () { _logInfo('Auction [' + auction.id + '] sent ', data); }, - JSON.stringify(data), - { - method: 'POST', - // withCredentials: true, - contentType: 'text/plain' - } - ); -}; - -function _logInfo(message, meta) { - logInfo(buildLogMessage(message), meta); -} - -function _logError(message) { - logError(buildLogMessage(message)); -} - -function buildLogMessage(message) { - return 'Sonobi Prebid Analytics: ' + message; -} - -adapterManager.registerAnalyticsAdapter({ - adapter: sonobiAdapter, - code: 'sonobi' -}); - -export default sonobiAdapter; diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index e1b51affd09..c21d0ae4e95 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -1,5 +1,5 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, isFn, isPlainObject } from '../src/utils.js'; +import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, isFn, isPlainObject, parseQueryStringParameters, getWinDimensions } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; @@ -170,10 +170,13 @@ export const spec = { } return { - method: 'GET', + method: 'POST', url: url, + options: { + contentType: 'application/x-www-form-urlencoded' + }, withCredentials: true, - data: payload, + data: parseQueryStringParameters(payload), bidderRequests: validBidRequests }; }, @@ -329,7 +332,7 @@ function _validateFloor(bid) { } function _validateGPID(bid) { - const gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot') || deepAccess(getGptSlotInfoForAdUnitCode(bid.adUnitCode), 'gptSlot') || bid.params.ad_unit; + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot') || deepAccess(getGptSlotInfoForAdUnitCode(bid.adUnitCode), 'gptSlot') || bid.params.ad_unit; if (gpid) { return `gpid=${gpid},` @@ -359,6 +362,51 @@ function _validateMediaType(bidRequest) { let plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt'); mediaTypeValidation = `${mediaTypeValidation}pl=${plcmt},`; } + if (deepAccess(bidRequest, 'mediaTypes.video.protocols')) { + mediaTypeValidation = `${mediaTypeValidation}protocols=${deepAccess(bidRequest, 'mediaTypes.video.protocols').join(':')},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.mimes')) { + mediaTypeValidation = `${mediaTypeValidation}mimes=${deepAccess(bidRequest, 'mediaTypes.video.mimes').join(':')},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.battr')) { + mediaTypeValidation = `${mediaTypeValidation}battr=${deepAccess(bidRequest, 'mediaTypes.video.battr').join(':')},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.api')) { + mediaTypeValidation = `${mediaTypeValidation}api=${deepAccess(bidRequest, 'mediaTypes.video.api').join(':')},`; + } + + if (deepAccess(bidRequest, 'mediaTypes.video.minduration')) { + let minduration = deepAccess(bidRequest, 'mediaTypes.video.minduration'); + mediaTypeValidation = `${mediaTypeValidation}minduration=${minduration},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.maxduration')) { + let maxduration = deepAccess(bidRequest, 'mediaTypes.video.maxduration'); + mediaTypeValidation = `${mediaTypeValidation}maxduration=${maxduration},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.skip')) { + let skip = deepAccess(bidRequest, 'mediaTypes.video.skip'); + mediaTypeValidation = `${mediaTypeValidation}skip=${skip},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.skipafter')) { + let skipafter = deepAccess(bidRequest, 'mediaTypes.video.skipafter'); + mediaTypeValidation = `${mediaTypeValidation}skipafter=${skipafter},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.startdelay')) { + let startdelay = deepAccess(bidRequest, 'mediaTypes.video.startdelay'); + mediaTypeValidation = `${mediaTypeValidation}startdelay=${startdelay},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.linearity')) { + let linearity = deepAccess(bidRequest, 'mediaTypes.video.linearity'); + mediaTypeValidation = `${mediaTypeValidation}linearity=${linearity},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.minbitrate')) { + let minbitrate = deepAccess(bidRequest, 'mediaTypes.video.minbitrate'); + mediaTypeValidation = `${mediaTypeValidation}minbitrate=${minbitrate},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.maxbitrate')) { + let maxbitrate = deepAccess(bidRequest, 'mediaTypes.video.maxbitrate'); + mediaTypeValidation = `${mediaTypeValidation}maxbitrate=${maxbitrate},`; + } } else if (mediaType === 'display') { mediaTypeValidation = 'c=d,'; } @@ -385,7 +433,7 @@ function _getBidIdFromTrinityKey(key) { /** * @param context - the window to determine the innerWidth from. This is purely for test purposes as it should always be the current window */ -export const _isInbounds = (context = window) => (lowerBound = 0, upperBound = Number.MAX_SAFE_INTEGER) => context.innerWidth >= lowerBound && context.innerWidth < upperBound; +export const _isInbounds = (context = getWinDimensions()) => (lowerBound = 0, upperBound = Number.MAX_SAFE_INTEGER) => deepAccess(context, 'innerWidth') >= lowerBound && deepAccess(context, 'innerWidth') < upperBound; /** * @param context - the window to determine the innerWidth from. This is purely for test purposes as it should always be the current window @@ -414,8 +462,6 @@ export function _getPlatform(context = window) { * @return {object} firstPartyData - Data object containing first party information */ function loadOrCreateFirstPartyData() { - var localStorageEnabled; - var FIRST_PARTY_KEY = '_iiq_fdata'; var tryParse = function (data) { try { @@ -426,19 +472,14 @@ function loadOrCreateFirstPartyData() { }; var readData = function (key) { if (hasLocalStorage()) { + // TODO FIX RULES VIOLATION + // eslint-disable-next-line no-restricted-properties return window.localStorage.getItem(key); } return null; }; + // TODO FIX RULES VIOLATION - USE STORAGE MANAGER var hasLocalStorage = function () { - if (typeof localStorageEnabled != 'undefined') { return localStorageEnabled; } else { - try { - localStorageEnabled = !!window.localStorage; - return localStorageEnabled; - } catch (e) { - localStorageEnabled = false; - } - } return false; }; var generateGUID = function () { @@ -452,6 +493,8 @@ function loadOrCreateFirstPartyData() { var storeData = function (key, value) { try { if (hasLocalStorage()) { + // TODO FIX RULES VIOLATION + // eslint-disable-next-line no-restricted-properties window.localStorage.setItem(key, value); } } catch (error) { diff --git a/modules/sovrnAnalyticsAdapter.js b/modules/sovrnAnalyticsAdapter.js deleted file mode 100644 index a89b365e074..00000000000 --- a/modules/sovrnAnalyticsAdapter.js +++ /dev/null @@ -1,287 +0,0 @@ -import {logError, timestamp} from '../src/utils.js'; -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; -import adaptermanager from '../src/adapterManager.js'; -import { EVENTS } from '../src/constants.js'; -import {ajaxBuilder} from '../src/ajax.js'; -import {config} from '../src/config.js'; -import {find, includes} from '../src/polyfill.js'; -import {getRefererInfo} from '../src/refererDetection.js'; - -const ajax = ajaxBuilder(0) - -const { - AUCTION_END, - BID_REQUESTED, - BID_ADJUSTMENT, - BID_RESPONSE, - BID_WON -} = EVENTS; - -let pbaUrl = 'https://pba.aws.lijit.com/analytics' -let currentAuctions = {}; -const analyticsType = 'endpoint' - -const rootURL = (() => { - const ref = getRefererInfo(); - // TODO: does the fallback make sense here? - return ref.page || ref.topmostLocation; -})(); - -let sovrnAnalyticsAdapter = Object.assign(adapter({url: pbaUrl, analyticsType}), { - track({ eventType, args }) { - try { - if (eventType === BID_WON) { - new BidWinner(this.sovrnId, args).send(); - return - } - if (args && args.auctionId && currentAuctions[args.auctionId] && currentAuctions[args.auctionId].status === 'complete') { - throw new Error('Event Received after Auction Close Auction Id ' + args.auctionId) - } - if (args && args.auctionId && currentAuctions[args.auctionId] === undefined) { - currentAuctions[args.auctionId] = new AuctionData(this.sovrnId, args.auctionId) - } - switch (eventType) { - case BID_REQUESTED: - currentAuctions[args.auctionId].bidRequested(args) - break - case BID_ADJUSTMENT: - currentAuctions[args.auctionId].originalBid(args) - break - case BID_RESPONSE: - currentAuctions[args.auctionId].adjustedBid(args) - break - case AUCTION_END: - currentAuctions[args.auctionId].send(); - break - } - } catch (e) { - new LogError(e, this.sovrnId, {eventType, args}).send() - } - }, -}) - -sovrnAnalyticsAdapter.getAuctions = function () { - return currentAuctions; -}; - -sovrnAnalyticsAdapter.originEnableAnalytics = sovrnAnalyticsAdapter.enableAnalytics; - -// override enableAnalytics so we can get access to the config passed in from the page -sovrnAnalyticsAdapter.enableAnalytics = function (config) { - let sovrnId = '' - if (config && config.options && (config.options.sovrnId || config.options.affiliateId)) { - sovrnId = config.options.sovrnId || config.options.affiliateId; - } else { - logError('Need Sovrn Id to log auction results. Please contact a Sovrn representative if you do not know your Sovrn Id.') - return - } - sovrnAnalyticsAdapter.sovrnId = sovrnId; - if (config.options.pbaUrl) { - pbaUrl = config.options.pbaUrl; - } - sovrnAnalyticsAdapter.originEnableAnalytics(config) // call the base class function -}; - -adaptermanager.registerAnalyticsAdapter({ - adapter: sovrnAnalyticsAdapter, - code: 'sovrn' -}); - -/** Class Representing a Winning Bid */ -class BidWinner { - /** - * Creates a new bid winner - * @param {string} sovrnId - the affiliate id from the analytics config - * @param {*} event - the args object from the auction event - */ - constructor(sovrnId, event) { - this.body = {} - // eslint-disable-next-line no-undef - this.body.prebidVersion = $$REPO_AND_VERSION$$ - this.body.sovrnId = sovrnId - this.body.winningBid = JSON.parse(JSON.stringify(event)) - this.body.url = rootURL - this.body.payload = 'winner' - delete this.body.winningBid.ad - } - - /** - * Sends the auction to the the ingest server - */ - send() { - this.body.ts = timestamp() - ajax( - pbaUrl, - null, - JSON.stringify(this.body), - { - contentType: 'application/json', - method: 'POST', - } - ) - } -} - -/** Class representing an Auction */ -class AuctionData { - /** - * Create a new auction data collector - * @param {string} sovrnId - the affiliate id from the analytics config - * @param {string} auctionId - the auction id from the auction event - */ - constructor(sovrnId, auctionId) { - this.auction = {} - // eslint-disable-next-line no-undef - this.auction.prebidVersion = $$REPO_AND_VERSION$$ - this.auction.sovrnId = sovrnId - this.auction.auctionId = auctionId - this.auction.payload = 'auction' - this.auction.timeouts = { - buffer: config.getConfig('timeoutBuffer'), - bidder: config.getConfig('bidderTimeout'), - } - this.auction.priceGranularity = config.getConfig('priceGranularity') - this.auction.url = rootURL - this.auction.requests = [] - this.auction.unsynced = [] - this.dropBidFields = ['auctionId', 'ad', 'requestId', 'bidderCode'] - - setTimeout(function(id) { - delete currentAuctions[id] - }, 300000, this.auction.auctionId) - } - - /** - * Record a bid request event - * @param {*} event - the args object from the auction event - */ - bidRequested(event) { - const eventCopy = JSON.parse(JSON.stringify(event)) - delete eventCopy.doneCbCallCount - delete eventCopy.auctionId - this.auction.requests.push(eventCopy) - } - - /** - * Finds the bid from the auction that the event is associated with - * @param {*} event - the args object from the auction event - * @return {*} - the bid - */ - findBid(event) { - const bidder = find(this.auction.requests, r => (r.bidderCode === event.bidderCode)) - if (!bidder) { - this.auction.unsynced.push(JSON.parse(JSON.stringify(event))) - } - let bid = find(bidder.bids, b => (b.bidId === event.requestId)) - - if (!bid) { - event.unmatched = true - bidder.bids.push(JSON.parse(JSON.stringify(event))) - } - return bid - } - - /** - * Records the original bid before any adjustments have been made - * @param {*} event - the args object from the auction event - * NOTE: the bid adjustment occurs before the bid response - * the bid adjustment seems to be the bid ready to be adjusted - */ - originalBid(event) { - let bid = this.findBid(event) - if (bid) { - Object.assign(bid, JSON.parse(JSON.stringify(event))) - this.dropBidFields.forEach((f) => delete bid[f]) - } - } - - /** - * Replaces original values with adjusted values and records the original values for changed values - * in bid.originalValues - * @param {*} event - the args object from the auction event - */ - adjustedBid(event) { - let bid = this.findBid(event) - if (bid) { - bid.originalValues = Object.keys(event).reduce((o, k) => { - if (JSON.stringify(bid[k]) !== JSON.stringify(event[k]) && !includes(this.dropBidFields, k)) { - o[k] = bid[k] - bid[k] = event[k] - } - return o - }, {}) - } - } - - /** - * Sends the auction to the the ingest server - */ - send() { - let maxBids = {} - this.auction.requests.forEach(request => { - request.bids.forEach(bid => { - maxBids[bid.adUnitCode] = maxBids[bid.adUnitCode] || {cpm: 0} - if (bid.cpm > maxBids[bid.adUnitCode].cpm) { - maxBids[bid.adUnitCode] = bid - } - }) - }) - Object.keys(maxBids).forEach(unit => { - maxBids[unit].isAuctionWinner = true - }) - this.auction.ts = timestamp() - ajax( - pbaUrl, - () => { - currentAuctions[this.auction.auctionId] = {status: 'complete', auctionId: this.auction.auctionId} - }, - JSON.stringify(this.auction), - { - contentType: 'application/json', - method: 'POST', - } - ) - } -} -class LogError { - constructor(e, sovrnId, data) { - this.error = {} - this.error.payload = 'error' - this.error.message = e.message - this.error.stack = e.stack - this.error.data = data - // eslint-disable-next-line no-undef - this.error.prebidVersion = $$REPO_AND_VERSION$$ - this.error.sovrnId = sovrnId - this.error.url = rootURL - this.error.userAgent = navigator.userAgent - } - send() { - if (this.error.data && this.error.data.requests) { - this.error.data.requests.forEach(request => { - if (request.bids) { - request.bids.forEach(bid => { - if (bid.ad) { - delete bid.ad - } - }) - } - }) - } - if (ErrorEvent.data && this.error.data.ad) { - delete this.error.data.ad - } - this.error.ts = timestamp() - ajax( - pbaUrl, - null, - JSON.stringify(this.error), - { - contentType: 'application/json', - method: 'POST', - } - ) - } -} - -export default sovrnAnalyticsAdapter; diff --git a/modules/sovrnAnalyticsAdapter.md b/modules/sovrnAnalyticsAdapter.md deleted file mode 100644 index b4fe7c971a2..00000000000 --- a/modules/sovrnAnalyticsAdapter.md +++ /dev/null @@ -1,23 +0,0 @@ -# Overview - -``` -Module Name: Sovrn Analytics Adapter -Module Type: Analytics Adapter -Maintainer: exchange@sovrn.com -``` - -# Description - -Sovrn's analytics adaptor allows you to view detailed auction information in Meridian. - -For more information, visit Sovrn.com. - -# Test Parameters -``` -{ - provider: 'sovrn', - options: { - sovrnId: 'xxxxx', // Sovrn ID (required) you can get this by contacting Sovrn support. - } -} -``` diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 3df025b1619..d4e53c00472 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -6,7 +6,10 @@ import { logError, deepAccess, isInteger, - logWarn, getBidIdParameter + logWarn, + getBidIdParameter, + isEmptyStr, + mergeDeep } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import { @@ -14,35 +17,18 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js' +import { COMMON_ORTB_VIDEO_PARAMS } from '../libraries/deepintentUtils/index.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid */ const ORTB_VIDEO_PARAMS = { - 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), - 'minduration': (value) => isInteger(value), - 'maxduration': (value) => isInteger(value), - 'protocols': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 10), - 'w': (value) => isInteger(value), - 'h': (value) => isInteger(value), - 'startdelay': (value) => isInteger(value), + ...COMMON_ORTB_VIDEO_PARAMS, 'placement': (value) => isInteger(value) && value >= 1 && value <= 5, - 'linearity': (value) => [1, 2].indexOf(value) !== -1, - 'skip': (value) => [0, 1].indexOf(value) !== -1, - 'skipmin': (value) => isInteger(value), - 'skipafter': (value) => isInteger(value), - 'sequence': (value) => isInteger(value), - 'battr': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 17), - 'maxextended': (value) => isInteger(value), - 'minbitrate': (value) => isInteger(value), - 'maxbitrate': (value) => isInteger(value), - 'boxingallowed': (value) => [0, 1].indexOf(value) !== -1, - 'playbackmethod': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6), - 'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1, + 'plcmt': (value) => isInteger(value) && value >= 1 && value <= 4, 'delivery': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 3), 'pos': (value) => isInteger(value) && value >= 1 && value <= 7, - 'api': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6) } const REQUIRED_VIDEO_PARAMS = { @@ -139,7 +125,7 @@ export const spec = { } const auctionEnvironment = bid?.ortb2Imp?.ext?.ae - if (bidderRequest.fledgeEnabled && isInteger(auctionEnvironment)) { + if (bidderRequest.paapi?.enabled && isInteger(auctionEnvironment)) { imp.ext = imp.ext || {} imp.ext.ae = auctionEnvironment } else { @@ -197,6 +183,12 @@ export const spec = { deepSetValue(sovrnBidReq, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); } + // if present, merge device object from ortb2 into `sovrnBidReq.device` + if (bidderRequest?.ortb2?.device) { + sovrnBidReq.device = sovrnBidReq.device || {}; + mergeDeep(sovrnBidReq.device, bidderRequest.ortb2.device); + } + if (eids) { deepSetValue(sovrnBidReq, 'user.ext.eids', eids) if (criteoId) { @@ -254,19 +246,41 @@ export const spec = { })) .flat() - let fledgeAuctionConfigs = deepAccess(ext, 'fledge_auction_configs'); + let fledgeAuctionConfigs = null; + if (isArray(ext?.igbid)) { + const seller = ext.seller + const decisionLogicUrl = ext.decisionLogicUrl + const sellerTimeout = ext.sellerTimeout + ext.igbid.filter(item => isValidIgBid(item)).forEach((igbid) => { + const perBuyerSignals = {} + igbid.igbuyer.filter(item => isValidIgBuyer(item)).forEach(buyerItem => { + perBuyerSignals[buyerItem.igdomain] = buyerItem.buyerdata + }) + const interestGroupBuyers = [...Object.keys(perBuyerSignals)] + if (interestGroupBuyers.length) { + fledgeAuctionConfigs = fledgeAuctionConfigs || {} + fledgeAuctionConfigs[igbid.impid] = { + seller, + decisionLogicUrl, + sellerTimeout, + interestGroupBuyers: interestGroupBuyers, + perBuyerSignals, + } + } + }) + } if (fledgeAuctionConfigs) { fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { return { bidId, config: Object.assign({ - auctionSignals: {}, + auctionSignals: {} }, cfg) } - }); + }) return { bids, - fledgeAuctionConfigs, + paapi: fledgeAuctionConfigs, } } return bids @@ -359,7 +373,7 @@ function _getBidFloors(bid) { mediaType: bid.mediaTypes && bid.mediaTypes.banner ? 'banner' : 'video', size: '*' }) : {} - const floorModuleValue = parseFloat(floorInfo.floor) + const floorModuleValue = parseFloat(floorInfo?.floor) if (!isNaN(floorModuleValue)) { return floorModuleValue } @@ -367,4 +381,12 @@ function _getBidFloors(bid) { return !isNaN(paramValue) ? paramValue : undefined } +function isValidIgBid(igBid) { + return !isEmptyStr(igBid.impid) && isArray(igBid.igbuyer) && igBid.igbuyer.length +} + +function isValidIgBuyer(igBuyer) { + return !isEmptyStr(igBuyer.igdomain) +} + registerBidder(spec) diff --git a/modules/sparteoBidAdapter.js b/modules/sparteoBidAdapter.js index 0bccc1ec140..2bb08707c85 100644 --- a/modules/sparteoBidAdapter.js +++ b/modules/sparteoBidAdapter.js @@ -24,12 +24,14 @@ 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) { - deepSetValue(request, 'site.publisher.ext.params.networkId', bidderRequest.bids[0].params.networkId); + request.site.publisher.ext.params.networkId = bidderRequest.bids[0].params.networkId; } if (bidderRequest.bids[0].params.publisherId) { - deepSetValue(request, 'site.publisher.ext.params.publisherId', bidderRequest.bids[0].params.publisherId); + request.site.publisher.ext.params.publisherId = bidderRequest.bids[0].params.publisherId; } return request; @@ -38,6 +40,7 @@ const converter = ortbConverter({ const imp = buildImp(bidRequest, context); deepSetValue(imp, 'ext.sparteo.params', bidRequest.params); + imp.ext.sparteo.params.adUnitCode = bidRequest.adUnitCode; return imp; }, @@ -76,7 +79,8 @@ export const spec = { } if (!bid.params.networkId && !bid.params.publisherId) { - logError('The networkId or publisherId is required'); + // publisherId is deprecated but is still accepted for now for retrocompatibility purpose. + logError('The networkId is required'); return false; } diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js deleted file mode 100644 index c1f1c5159fc..00000000000 --- a/modules/spotxBidAdapter.js +++ /dev/null @@ -1,528 +0,0 @@ -import { - logError, - deepAccess, - isArray, - getDNT, - deepSetValue, - isEmpty, - _each, - logMessage, - logWarn, - isBoolean, - isNumber, - isPlainObject, - isFn, - setScriptAttributes, - getBidIdParameter -} from '../src/utils.js'; -import { config } from '../src/config.js'; -import { Renderer } from '../src/Renderer.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO } from '../src/mediaTypes.js'; -import { loadExternalScript } from '../src/adloader.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 - */ - -const BIDDER_CODE = 'spotx'; -const URL = 'https://search.spotxchange.com/openrtb/2.3/dados/'; -const ORTB_VERSION = '2.3'; -export const GOOGLE_CONSENT = { consented_providers: ['3', '7', '11', '12', '15', '20', '22', '35', '43', '46', '48', '55', '57', '61', '62', '66', '70', '80', '83', '85', '86', '89', '93', '108', '122', '124', '125', '126', '131', '134', '135', '136', '143', '144', '147', '149', '153', '154', '159', '161', '162', '165', '167', '171', '178', '184', '188', '192', '195', '196', '202', '209', '211', '218', '221', '228', '229', '230', '236', '239', '241', '253', '255', '259', '266', '271', '272', '274', '286', '291', '294', '303', '308', '310', '311', '313', '314', '316', '317', '322', '323', '327', '336', '338', '340', '348', '350', '358', '359', '363', '367', '370', '371', '384', '385', '389', '393', '394', '397', '398', '407', '414', '415', '424', '429', '430', '432', '436', '438', '440', '442', '443', '445', '448', '449', '453', '459', '479', '482', '486', '491', '492', '494', '495', '503', '505', '510', '522', '523', '528', '537', '540', '550', '559', '560', '568', '571', '574', '575', '576', '584', '585', '587', '588', '590', '591', '592', '595', '609', '621', '624', '723', '725', '733', '737', '776', '780', '782', '787', '797', '798', '802', '803', '814', '817', '820', '821', '827', '829', '839', '853', '864', '867', '874', '899', '904', '922', '926', '931', '932', '933', '938', '955', '973', '976', '979', '981', '985', '987', '991', '1003', '1024', '1025', '1027', '1028', '1029', '1033', '1034', '1040', '1047', '1048', '1051', '1052', '1053', '1054', '1062', '1063', '1067', '1072', '1085', '1092', '1095', '1097', '1099', '1100', '1107', '1126', '1127', '1143', '1149', '1152', '1162', '1166', '1167', '1170', '1171', '1172', '1188', '1192', '1199', '1201', '1204', '1205', '1211', '1212', '1215', '1220', '1225', '1226', '1227', '1230', '1232', '1236', '1241', '1248', '1250', '1252', '1268', '1275', '1276', '1284', '1286', '1298', '1301', '1307', '1312', '1313', '1317', '1329', '1336', '1344', '1345', '1356', '1362', '1365', '1375', '1403', '1409', '1411', '1415', '1416', '1419', '1423', '1440', '1442', '1449', '1451', '1455', '1456', '1468', '1496', '1503', '1509', '1512', '1514', '1517', '1520', '1525', '1540', '1547', '1548', '1555', '1558', '1570', '1575', '1577', '1579', '1583', '1584', '1591', '1598', '1603', '1608', '1613', '1616', '1626', '1631', '1633', '1638', '1642', '1648', '1651', '1652', '1653', '1660', '1665', '1667', '1669', '1671', '1674', '1677', '1678', '1682', '1684', '1697', '1703', '1705', '1716', '1720', '1721', '1722', '1725', '1732', '1733', '1735', '1739', '1741', '1745', '1750', '1753', '1760', '1765', '1769', '1776', '1780', '1782', '1786', '1791', '1794', '1799', '1800', '1801', '1810', '1827', '1831', '1832', '1834', '1837', '1840', '1843', '1844', '1845', '1858', '1859', '1863', '1866', '1870', '1872', '1875', '1878', '1880', '1882', '1883', '1889', '1892', '1896', '1898', '1899', '1902', '1905', '1911', '1922', '1928', '1929', '1934', '1942', '1943', '1944', '1945', '1958', '1960', '1962', '1963', '1964', '1967', '1968', '1978', '1985', '1986', '1987', '1998', '2003', '2007', '2012', '2013', '2027', '2035', '2038', '2039', '2044', '2047', '2052', '2056', '2059', '2062', '2064', '2068', '2070', '2072', '2078', '2079', '2084', '2088', '2090', '2095', '2100', '2103', '2107', '2109', '2113', '2115', '2121', '2127', '2130', '2133', '2137', '2140', '2141', '2145', '2147', '2150', '2156', '2166', '2170', '2171', '2176', '2177', '2179', '2183', '2186', '2192', '2198', '2202', '2205', '2214', '2216', '2219', '2220', '2222', '2223', '2224', '2225', '2227', '2228', '2234', '2238', '2247', '2251', '2253', '2262', '2264', '2271', '2276', '2278', '2279', '2282', '2290', '2292', '2295', '2299', '2305', '2306', '2310', '2311', '2312', '2315', '2320', '2325', '2328', '2331', '2334', '2335', '2336', '2337', '2343', '2346', '2354', '2357', '2358', '2359', '2366', '2370', '2373', '2376', '2377', '2380', '2382', '2387', '2389', '2392', '2394', '2400', '2403', '2405', '2406', '2407', '2410', '2411', '2413', '2414', '2415', '2416', '2418', '2422', '2425', '2427', '2435', '2437', '2440', '2441', '2447', '2453', '2459', '2461', '2462', '2464', '2467', '2468', '2472', '2477', '2481', '2484', '2486', '2492', '2493', '2496', '2497', '2498', '2499', '2504', '2506', '2510', '2511', '2512', '2517', '2526', '2527', '2531', '2532', '2534', '2542', '2544', '2552', '2555', '2559', '2563', '2564', '2567', '2568', '2569', '2571', '2572', '2573', '2575', '2577', '2579', '2583', '2584', '2586', '2589', '2595', '2596', '2597', '2601', '2604', '2605', '2609', '2610', '2612', '2614', '2621', '2622', '2624', '2628', '2629', '2632', '2634', '2636', '2639', '2643', '2645', '2646', '2647', '2649', '2650', '2651', '2652', '2656', '2657', '2658', '2660', '2661', '2662', '2663', '2664', '2669', '2670', '2673', '2676', '2677', '2678', '2681', '2682', '2684', '2685', '2686', '2689', '2690', '2691', '2695', '2698', '2699', '2702', '2704', '2705', '2706', '2707', '2709', '2710', '2713', '2714', '2727', '2729', '2739', '2758', '2765', '2766', '2767', '2768', '2770', '2771', '2772', '2776', '2777', '2778', '2779', '2780', '2783', '2784', '2786', '2787', '2791', '2792', '2793', '2797', '2798', '2801', '2802', '2803', '2805', '2808', '2809', '2810', '2811', '2812', '2813', '2814', '2817', '2818', '2824', '2826', '2827', '2829', '2830', '2831', '2832', '2834', '2836', '2838', '2840', '2842', '2843', '2844', '2850', '2851', '2852', '2854', '2858', '2860', '2862', '2864', '2865', '2866', '2867', '2868', '2869', '2871'] }; - -export const spec = { - code: BIDDER_CODE, - gvlid: 165, - supportedMediaTypes: [VIDEO], - - /** - * Determines whether or not the given bid request is valid. - * From Prebid.js: isBidRequestValid - Verify the the AdUnits.bids, respond with true (valid) or false (invalid). - * - * @param {object} bid The bid to validate. - * @return {boolean} True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function(bid) { - if (bid && typeof bid.params !== 'object') { - logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.'); - return false; - } - - if (!deepAccess(bid, 'mediaTypes.video')) { - logError(BIDDER_CODE + ': mediaTypes.video is not present in the bidder settings.'); - return false; - } - - const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); - if (!playerSize || !isArray(playerSize)) { - logError(BIDDER_CODE + ': mediaTypes.video.playerSize is not defined in the bidder settings.'); - return false; - } - - if (!getBidIdParameter('channel_id', bid.params)) { - logError(BIDDER_CODE + ': channel_id is not present in bidder params'); - return false; - } - - if (deepAccess(bid, 'mediaTypes.video.context') == 'outstream' || deepAccess(bid, 'params.ad_unit') == 'outstream') { - if (!getBidIdParameter('outstream_function', bid.params)) { - if (!getBidIdParameter('outstream_options', bid.params)) { - logError(BIDDER_CODE + ': please define outstream_options parameter or override the default SpotX outstream rendering by defining your own Outstream function using field outstream_function.'); - return false; - } - if (!getBidIdParameter('slot', bid.params.outstream_options)) { - logError(BIDDER_CODE + ': please define parameter slot in outstream_options object in the configuration.'); - return false; - } - } - } - - return true; - }, - - /** - * Make a server request from the list of BidRequests. - * from Prebid.js: buildRequests - Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test. - * - * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. - * @param {object} bidderRequest - The master bidRequest object. - * @return {ServerRequest} Info describing the request to the server. - */ - buildRequests: function(bidRequests, bidderRequest) { - // TODO: does the fallback make sense here? - const referer = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - const isPageSecure = !!referer.match(/^https:/); - - const siteId = ''; - const spotxRequests = bidRequests.map(function(bid) { - let page; - if (getBidIdParameter('page', bid.params)) { - page = getBidIdParameter('page', bid.params); - } else { - page = referer; - } - - const channelId = getBidIdParameter('channel_id', bid.params); - let pubcid = null; - - const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); - const contentWidth = playerSize[0][0]; - const contentHeight = playerSize[0][1]; - - const secure = isPageSecure || (getBidIdParameter('secure', bid.params) ? 1 : 0); - - const ext = { - sdk_name: 'Prebid 1+', - versionOrtb: ORTB_VERSION - }; - - if (getBidIdParameter('hide_skin', bid.params) != '') { - ext.hide_skin = +!!getBidIdParameter('hide_skin', bid.params); - } - - if (getBidIdParameter('ad_volume', bid.params) != '') { - ext.ad_volume = getBidIdParameter('ad_volume', bid.params); - } - - if (getBidIdParameter('ad_unit', bid.params) != '') { - ext.ad_unit = getBidIdParameter('ad_unit', bid.params); - } - - if (getBidIdParameter('outstream_options', bid.params) != '') { - ext.outstream_options = getBidIdParameter('outstream_options', bid.params); - } - - if (getBidIdParameter('outstream_function', bid.params) != '') { - ext.outstream_function = getBidIdParameter('outstream_function', bid.params); - } - - if (getBidIdParameter('custom', bid.params) != '') { - ext.custom = getBidIdParameter('custom', bid.params); - } - - if (getBidIdParameter('pre_market_bids', bid.params) != '' && isArray(getBidIdParameter('pre_market_bids', bid.params))) { - const preMarketBids = getBidIdParameter('pre_market_bids', bid.params); - ext.pre_market_bids = []; - for (let i in preMarketBids) { - const preMarketBid = preMarketBids[i]; - let vastStr = ''; - if (preMarketBid['vast_url']) { - vastStr = '' + preMarketBid['vast_url'] + ''; - } else if (preMarketBid['vast_string']) { - vastStr = preMarketBid['vast_string']; - } - ext.pre_market_bids.push({ - id: preMarketBid['deal_id'], - seatbid: [{ - bid: [{ - impid: Date.now(), - dealid: preMarketBid['deal_id'], - price: preMarketBid['price'], - adm: vastStr - }] - }], - cur: preMarketBid['currency'], - ext: { - event_log: [{}] - } - }); - } - } - - const mimes = getBidIdParameter('mimes', bid.params) || deepAccess(bid, 'mediaTypes.video.mimes') || ['application/javascript', 'video/mp4', 'video/webm']; - - const spotxReq = { - id: bid.bidId, - secure: secure, - video: { - w: contentWidth, - h: contentHeight, - ext: ext, - mimes: mimes - } - }; - - if (isFn(bid.getFloor)) { - let floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: 'video', - size: '*' - }); - - if (floorInfo.currency === 'USD') { - spotxReq.bidfloor = floorInfo.floor; - } - } else if (getBidIdParameter('price_floor', bid.params) != '') { - spotxReq.bidfloor = getBidIdParameter('price_floor', bid.params); - } - - const startdelay = getBidIdParameter('start_delay', bid.params) || deepAccess(bid, 'mediaTypes.video.startdelay'); - if (startdelay) { - spotxReq.video.startdelay = 0 + Boolean(startdelay); - } - - const minduration = getBidIdParameter('min_duration', bid.params) || deepAccess(bid, 'mediaTypes.video.minduration'); - if (minduration) { - spotxReq.video.minduration = minduration; - } - - const maxduration = getBidIdParameter('max_duration', bid.params) || deepAccess(bid, 'mediaTypes.video.maxduration'); - if (maxduration) { - spotxReq.video.maxduration = maxduration; - } - - const placement = getBidIdParameter('placement_type', bid.params) || deepAccess(bid, 'mediaTypes.video.placement'); - if (placement) { - spotxReq.video.ext.placement = placement; - } - - const position = getBidIdParameter('position', bid.params) || deepAccess(bid, 'mediaTypes.video.pos'); - if (position) { - spotxReq.video.ext.pos = position; - } - - if (bid.crumbs && bid.crumbs.pubcid) { - pubcid = bid.crumbs.pubcid; - } - - const language = navigator.language ? 'language' : 'userLanguage'; - const device = { - h: screen.height, - w: screen.width, - dnt: getDNT() ? 1 : 0, - language: navigator[language].split('-')[0], - make: navigator.vendor ? navigator.vendor : '', - ua: navigator.userAgent - }; - - const requestPayload = { - id: channelId, - imp: spotxReq, - site: { - id: siteId, - page: page, - content: 'content', - }, - device: device, - ext: { - wrap_response: 1 - } - }; - - // If the publisher asks to ignore the bidder cache key we need to return the full vast xml - // so that it can be cached on the publishes specified server. - if (!!config.getConfig('cache') && !!config.getConfig('cache.url') && (config.getConfig('cache.ignoreBidderCacheKey') === true)) { - requestPayload['ext']['wrap_response'] = 0; - } - - if (getBidIdParameter('number_of_ads', bid.params)) { - requestPayload['ext']['number_of_ads'] = getBidIdParameter('number_of_ads', bid.params); - } - - const userExt = {}; - - if (getBidIdParameter('spotx_all_google_consent', bid.params) == 1) { - userExt['consented_providers_settings'] = GOOGLE_CONSENT; - } - - // Add GDPR flag and consent string - if (bidderRequest && bidderRequest.gdprConsent) { - userExt.consent = bidderRequest.gdprConsent.consentString; - - if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') { - deepSetValue(requestPayload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); - } - } - - if (bidderRequest && bidderRequest.uspConsent) { - deepSetValue(requestPayload, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (bid.userIdAsEids) { - userExt.eids = bid.userIdAsEids; - - userExt.eids.forEach(eid => { - if (eid.source === 'uidapi.com') { - eid.uids.forEach(uid => { - uid.ext = uid.ext || {}; - uid.ext.rtiPartner = 'UID2' - }); - } - }); - } - - // Add common id if available - if (pubcid) { - userExt.fpc = pubcid; - } - - // Add schain object if it is present - if (bid && bid.schain) { - requestPayload['source'] = { - ext: { - schain: bid.schain - } - }; - } - - // Only add the user object if it's not empty - if (!isEmpty(userExt)) { - requestPayload.user = { ext: userExt }; - } - const urlQueryParams = 'src_sys=prebid'; - return { - method: 'POST', - url: URL + channelId + '?' + urlQueryParams, - data: requestPayload, - bidRequest: bidderRequest - }; - }); - - return spotxRequests; - }, - - /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: function(serverResponse, bidderRequest) { - const bidResponses = []; - const serverResponseBody = serverResponse.body; - - if (serverResponseBody && isArray(serverResponseBody.seatbid)) { - _each(serverResponseBody.seatbid, function(bids) { - _each(bids.bid, function(spotxBid) { - let currentBidRequest = {}; - for (let i in bidderRequest.bidRequest.bids) { - if (spotxBid.impid == bidderRequest.bidRequest.bids[i].bidId) { - currentBidRequest = bidderRequest.bidRequest.bids[i]; - } - } - - /** - * Make sure currency and price are the right ones - * TODO: what about the pre_market_bid partners sizes? - */ - _each(currentBidRequest.params.pre_market_bids, function(pmb) { - if (pmb.deal_id == spotxBid.id) { - spotxBid.price = pmb.price; - serverResponseBody.cur = pmb.currency; - } - }); - - const bid = { - requestId: currentBidRequest.bidId, - currency: serverResponseBody.cur || 'USD', - cpm: spotxBid.price, - creativeId: spotxBid.crid || '', - dealId: spotxBid.dealid || '', - ttl: 360, - netRevenue: true, - channel_id: serverResponseBody.id, - mediaType: VIDEO, - width: spotxBid.w, - height: spotxBid.h - }; - - if (!!config.getConfig('cache') && !!config.getConfig('cache.url') && (config.getConfig('cache.ignoreBidderCacheKey') === true)) { - bid.vastXml = spotxBid.adm; - } else { - bid.cache_key = spotxBid.ext.cache_key; - bid.vastUrl = 'https://search.spotxchange.com/ad/vast.html?key=' + spotxBid.ext.cache_key; - bid.videoCacheKey = spotxBid.ext.cache_key; - } - - bid.meta = bid.meta || {}; - if (spotxBid && spotxBid.adomain && spotxBid.adomain.length > 0) { - bid.meta.advertiserDomains = spotxBid.adomain; - } - - const context1 = deepAccess(currentBidRequest, 'mediaTypes.video.context'); - const context2 = deepAccess(currentBidRequest, 'params.ad_unit'); - if (context1 == 'outstream' || context2 == 'outstream') { - const playersize = deepAccess(currentBidRequest, 'mediaTypes.video.playerSize'); - const renderer = Renderer.install({ - id: 0, - renderNow: true, - url: '/', - config: { - adText: 'SpotX Outstream Video Ad via Prebid.js', - player_width: playersize[0][0], - player_height: playersize[0][1], - content_page_url: deepAccess(bidderRequest, 'data.site.page'), - ad_mute: +!!deepAccess(currentBidRequest, 'params.ad_mute'), - hide_skin: +!!deepAccess(currentBidRequest, 'params.hide_skin'), - outstream_options: deepAccess(currentBidRequest, 'params.outstream_options'), - outstream_function: deepAccess(currentBidRequest, 'params.outstream_function') - } - }); - - try { - renderer.setRender(outstreamRender); - renderer.setEventHandlers({ - impression: function impression() { - return logMessage('SpotX outstream video impression event'); - }, - loaded: function loaded() { - return logMessage('SpotX outstream video loaded event'); - }, - ended: function ended() { - logMessage('SpotX outstream renderer video event'); - } - }); - } catch (err) { - logWarn('Prebid Error calling setRender or setEventHandlers on renderer', err); - } - bid.renderer = renderer; - } - - bidResponses.push(bid); - }) - }); - } - - return bidResponses; - } -} - -function createOutstreamScript(bid) { - const script = window.document.createElement('script'); - let dataSpotXParams = createScriptAttributeMap(bid); - - script.type = 'text/javascript'; - script.src = 'https://js.spotx.tv/easi/v1/' + bid.channel_id + '.js'; - - setScriptAttributes(script, dataSpotXParams); - - return script; -} - -function outstreamRender(bid) { - if (bid.renderer.config.outstream_function != null && typeof bid.renderer.config.outstream_function === 'function') { - const script = createOutstreamScript(bid); - bid.renderer.config.outstream_function(bid, script); - } else { - try { - const inIframe = getBidIdParameter('in_iframe', bid.renderer.config.outstream_options); - const easiUrl = 'https://js.spotx.tv/easi/v1/' + bid.channel_id + '.js'; - let attributes = createScriptAttributeMap(bid); - if (inIframe && window.document.getElementById(inIframe).nodeName == 'IFRAME') { - const rawframe = window.document.getElementById(inIframe); - let framedoc = rawframe.contentDocument; - if (!framedoc && rawframe.contentWindow) { - framedoc = rawframe.contentWindow.document; - } - loadExternalScript(easiUrl, BIDDER_CODE, undefined, framedoc, attributes); - } else { - loadExternalScript(easiUrl, BIDDER_CODE, undefined, undefined, attributes); - } - } catch (err) { - logError('[SPOTX][renderer] Error:' + err.message); - } - } -} - -function createScriptAttributeMap(bid) { - const slot = getBidIdParameter('slot', bid.renderer.config.outstream_options); - logMessage('[SPOTX][renderer] Handle SpotX outstream renderer'); - let dataSpotXParams = {}; - dataSpotXParams['data-spotx_channel_id'] = '' + bid.channel_id; - dataSpotXParams['data-spotx_vast_url'] = '' + bid.vastUrl; - dataSpotXParams['data-spotx_content_page_url'] = bid.renderer.config.content_page_url; - dataSpotXParams['data-spotx_ad_unit'] = 'incontent'; - - logMessage('[SPOTX][renderer] Default behavior'); - if (getBidIdParameter('ad_mute', bid.renderer.config.outstream_options)) { - dataSpotXParams['data-spotx_ad_mute'] = '1'; - } - dataSpotXParams['data-spotx_collapse'] = '0'; - dataSpotXParams['data-spotx_autoplay'] = '1'; - dataSpotXParams['data-spotx_blocked_autoplay_override_mode'] = '1'; - dataSpotXParams['data-spotx_video_slot_can_autoplay'] = '1'; - dataSpotXParams['data-spotx_content_container_id'] = slot; - - const playersizeAutoAdapt = getBidIdParameter('playersize_auto_adapt', bid.renderer.config.outstream_options); - if (playersizeAutoAdapt && isBoolean(playersizeAutoAdapt) && playersizeAutoAdapt === true) { - const ratio = bid.width && isNumber(bid.width) && bid.height && isNumber(bid.height) ? bid.width / bid.height : 4 / 3; - const slotClientWidth = window.document.getElementById(slot).clientWidth; - let playerWidth = bid.renderer.config.player_width; - let playerHeight = bid.renderer.config.player_height; - let contentWidth = 0; - let contentHeight = 0; - if (slotClientWidth < playerWidth) { - playerWidth = slotClientWidth; - playerHeight = playerWidth / ratio; - } - if (ratio <= 1) { - contentWidth = Math.round(playerHeight * ratio); - contentHeight = playerHeight; - } else { - contentWidth = playerWidth; - contentHeight = Math.round(playerWidth / ratio); - } - - dataSpotXParams['data-spotx_content_width'] = '' + contentWidth; - dataSpotXParams['data-spotx_content_height'] = '' + contentHeight; - } - - const customOverride = getBidIdParameter('custom_override', bid.renderer.config.outstream_options); - if (customOverride && isPlainObject(customOverride)) { - logMessage('[SPOTX][renderer] Custom behavior.'); - for (let name in customOverride) { - if (customOverride.hasOwnProperty(name)) { - if (name === 'channel_id' || name === 'vast_url' || name === 'content_page_url' || name === 'ad_unit') { - logWarn('[SPOTX][renderer] Custom behavior: following option cannot be overridden: ' + name); - } else { - dataSpotXParams['data-spotx_' + name] = customOverride[name]; - } - } - } - } - return dataSpotXParams; -} - -registerBidder(spec); diff --git a/modules/spotxBidAdapter.md b/modules/spotxBidAdapter.md deleted file mode 100644 index 0bd1cf71aa1..00000000000 --- a/modules/spotxBidAdapter.md +++ /dev/null @@ -1,136 +0,0 @@ -# Overview - -``` -Module Name: SpotX Bidder Adapter -Module Type: Bidder Adapter -Maintainer: teameighties@spotx.tv -``` - -# Description - -Connect to SpotX for bids. - -This adapter requires setup and approval from the SpotX team. - -# Test Parameters - Use case #1 - outstream with default rendering options -``` - var adUnits = [{ - code: 'something', - mediaTypes: { - video: { - context: 'outstream', // 'instream' or 'outstream' - playerSize: [640, 480] - } - }, - bids: [{ - bidder: 'spotx', - params: { - channel_id: 85394, - ad_unit: 'outstream', - outstream_options: { // Needed for the default outstream renderer - fields video_slot/content_width/content_height are mandatory - slot: 'adSlot1', - content_width: 300, - content_height: 250 - } - } - }] - }]; -``` - -# Test Parameters - Use case #2 - outstream with default rendering options + some other options -``` - var adUnits = [{ - code: 'something', - mediaTypes: { - video: { - context: 'outstream', // 'instream' or 'outstream' - playerSize: [640, 480] - } - }, - bids: [{ - bidder: 'spotx', - params: { - channel_id: 85394, - ad_unit: 'outstream', - outstream_options: { - slot: 'adSlot1', - custom_override: { // This option is not mandatory though used to override default renderer parameters using EASI player options in here: https://developer.spotxchange.com/content/local/docs/sdkDocs/EASI/README.md - content_width: 300, - content_height: 250, - collapse: '1', - hide_fullscreen: '1', - unmute_on_mouse: '1', - continue_out_of_view: '1', - ad_volume: '100', - content_container_id: 'video1', - hide_skin: '1', - spotx_all_google_consent: '1' - } - } - } - }] - }]; -``` - -# Test Parameters - Use case #3 - outstream with your own outstream redering function -``` - var adUnits = [{ - code: 'something', - mediaTypes: { - video: { - context: 'outstream', // 'instream' or 'outstream' - playerSize: [640, 480] - } - }, - bids: [{ - bidder: 'spotx', - params: { - channel_id: 79391, - ad_unit: 'outstream', - outstream_function: myOutstreamFunction // Override the default outstream renderer by this referenced function - } - }] - }]; -``` - -# Sample of a custom outstream rendering function -``` -function myOutstreamFunction(bid) { - const videoDiv = 'video1'; - const playerWidth = 300; - const playerHeight = 250; - - window.console.log('[SPOTX][renderer] Handle SpotX custom outstream renderer'); - let script = window.document.createElement('script'); - script.type = 'text/javascript'; - script.src = '//js.spotx.tv/easi/v1/' + bid.channel_id + '.js'; - script.setAttribute('data-spotx_channel_id', '' + bid.channel_id); - script.setAttribute('data-spotx_vast_url', '' + bid.vastUrl); - script.setAttribute('data-spotx_content_width', playerWidth); - script.setAttribute('data-spotx_content_height', playerHeight); - script.setAttribute('data-spotx_content_page_url', bid.renderer.config.content_page_url); - if (bid.renderer.config.ad_mute) { - script.setAttribute('data-spotx_ad_mute', '0'); - } - script.setAttribute('data-spotx_ad_unit', 'incontent'); - script.setAttribute('data-spotx_collapse', '0'); - script.setAttribute('data-spotx_hide_fullscreen', '1'); - script.setAttribute('data-spotx_autoplay', '1'); - script.setAttribute('data-spotx_blocked_autoplay_override_mode', '1'); - script.setAttribute('data-spotx_video_slot_can_autoplay', '1'); - script.setAttribute('data-spotx_unmute_on_mouse', '1'); - script.setAttribute('data-spotx_click_to_replay', '1'); - script.setAttribute('data-spotx_continue_out_of_view', '1'); - script.setAttribute('data-spotx_ad_volume', '100'); - if (bid.renderer.config.inIframe && window.document.getElementById(bid.renderer.config.inIframe).nodeName == 'IFRAME') { - let rawframe = window.document.getElementById(bid.renderer.config.inIframe); - let framedoc = rawframe.contentDocument; - if (!framedoc && rawframe.contentWindow) { - framedoc = rawframe.contentWindow.document; - } - framedoc.body.appendChild(script); - } else { - window.document.getElementById(videoDiv).appendChild(script); - } -}; -``` diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index 08b25abee01..2dd9ee68733 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -1,18 +1,19 @@ -import { deepAccess, getWindowTop, isArray, logWarn } from '../src/utils.js'; +import { deepAccess, getWinDimensions, getWindowTop, isArray, logInfo, logWarn } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { includes as strIncludes } from '../src/polyfill.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; -const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; +const SYNC_URL_IFRAME = 'https://ssp.wp.pl/bidder/usersync'; +const SYNC_URL_IMAGE = 'https://ssp.wp.pl/v1/sync/pixel'; const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; const GVLID = 676; const TMAX = 450; -const BIDDER_VERSION = '5.93'; +const BIDDER_VERSION = '6.10'; const DEFAULT_CURRENCY = 'PLN'; const W = window; const { navigator } = W; @@ -21,7 +22,6 @@ const adUnitsCalled = {}; const adSizesCalled = {}; const bidderRequestsMap = {}; const pageView = {}; -var consentApiVersion; /** * Native asset mapping - we use constant id per type @@ -71,7 +71,20 @@ const getContentLanguage = () => { const topWindow = getWindowTop(); return topWindow.document.body.parentNode.lang; } catch (err) { - logWarn('Could not read language form top-level html', err); + logWarn('Could not read language from top-level html', err); + } +}; + +/** + * Get host name of the top level html object + * @returns {string} host name + */ +const getTopHost = () => { + try { + const topWindow = getWindowTop(); + return topWindow.location.host; + } catch (err) { + logWarn('Could not read host from top-level window', err); } }; @@ -99,7 +112,8 @@ const getNotificationPayload = bidData => { tagid: [], } bids.forEach(bid => { - const { adUnitCode, cpm, creativeId, meta, mediaType, params: bidParams, bidderRequestId, requestId, timeout } = bid; + const { adUnitCode, cpm, creativeId, meta = {}, mediaType, params: bidParams, bidderRequestId, requestId, timeout } = bid; + const { platform = 'wpartner' } = meta; const params = unpackParams(bidParams); // basic notification data @@ -107,6 +121,7 @@ const getNotificationPayload = bidData => { requestId: bidderRequestId || bidderRequestsMap[requestId], timeout: timeout || result.timeout, pvid: pageView.id, + platform } result = { ...result, ...bidBasicData } @@ -146,7 +161,7 @@ const getNotificationPayload = bidData => { const applyClientHints = ortbRequest => { const { location } = document; const { connection = {}, deviceMemory, userAgentData = {} } = navigator; - const viewport = W.visualViewport || false; + const viewport = getWinDimensions().visualViewport || false; const segments = []; const hints = { 'CH-Ect': connection.effectiveType, @@ -233,10 +248,9 @@ const applyUserIds = (validBidRequest, ortbRequest) => { const applyGdpr = (bidderRequest, ortbRequest) => { const { gdprConsent } = bidderRequest; if (gdprConsent) { - const { apiVersion, gdprApplies, consentString } = gdprConsent; - consentApiVersion = apiVersion; - ortbRequest.regs = Object.assign(ortbRequest.regs, { 'gdpr': gdprApplies ? 1 : 0 }); - ortbRequest.user = Object.assign(ortbRequest.user, { 'consent': consentString }); + const { gdprApplies, consentString } = gdprConsent; + ortbRequest.regs = Object.assign(ortbRequest.regs || {}, { 'gdpr': gdprApplies ? 1 : 0 }); + ortbRequest.user = Object.assign(ortbRequest.user || {}, { 'consent': consentString }); } } @@ -261,18 +275,18 @@ const getHighestFloor = (slot) => { mediaType: 'banner', size: next, currency - }); + }) || {}; return prev > currentFloor ? prev : currentFloor; }, 0); } const { floor: nativeFloor = 0 } = slot.getFloor({ mediaType: 'native', currency - }); + }) || {}; const { floor: videoFloor = 0 } = slot.getFloor({ mediaType: 'video', currency - }); + }) || {}; result.floor = Math.max(bannerFloor, nativeFloor, videoFloor); } @@ -284,7 +298,7 @@ const getHighestFloor = (slot) => { * Get currency (either default or adserver) * @returns {string} currency name */ -const getCurrency = () => config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; +const getCurrency = (bidderRequest) => getCurrencyFromBidderRequest(bidderRequest) || DEFAULT_CURRENCY; /** * Get value for first occurence of key within the collection @@ -340,7 +354,7 @@ var mapAsset = function mapAsset(paramName, paramValue) { id: id, required: required, title: { - len: len + len: len || 140 } }); break; @@ -469,7 +483,7 @@ var mapVideo = (slot, videoFromBid) => { const mapImpression = slot => { const { adUnitCode, bidderRequestId, bidId, params = {}, ortb2Imp = {} } = slot; const { id, siteId, video } = params; - const { ext = {} } = ortb2Imp; + const { instl, ext = {} } = ortb2Imp; /* store bidId <-> bidderRequestId mapping for bidWon notification @@ -497,6 +511,7 @@ const mapImpression = slot => { video: mapVideo(slot, video), tagid: adUnitCode, ext, + instl, }; // Check floorprices for this imp @@ -519,6 +534,11 @@ const isNativeAd = bid => { return bid.admNative || (bid.adm && bid.adm.match(xmlTester)); } +const isHTML = bid => { + const xmlTester = new RegExp(/^ { const { link = {}, imptrackers: impressionTrackers, jstracker } = nativeData; const { url: clickUrl, clicktrackers: clickTrackers = [] } = link; @@ -572,82 +592,6 @@ const parseNative = (nativeData, adUnitCode) => { return result; } -const renderCreative = (site, auctionId, bid, seat, request) => { - const { adLabel, id, slot, sn, page, publisherId, ref } = site; - let gam; - - const mcad = { - id: auctionId, - seat, - seatbid: [{ - bid: [bid], - }], - }; - - const mcbase = btoa(encodeURI(JSON.stringify(mcad))); - - if (bid.adm) { - // parse adm for gam config - try { - gam = JSON.parse(bid.adm).gam; - - if (!gam || !Object.keys(gam).length) { - gam = undefined; - } else { - gam.namedSizes = ['fluid']; - gam.div = 'div-gpt-ad-x01'; - gam.targeting = Object.assign(gam.targeting || {}, { - OAS_retarg: '0', - PREBID_ON: '1', - emptygaf: '0', - }); - } - - if (gam && !gam.targeting) { - gam.targeting = {}; - } - } catch (err) { - logWarn('Could not parse adm data', bid.adm); - } - } - - let adcode = ` - - - - - - - -
- - - - `; - - return adcode; -} - const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -674,7 +618,9 @@ const spec = { const pbver = '$prebid.version$'; const testMode = setOnAny(validBidRequests, 'params.test') ? 1 : undefined; const ref = bidderRequest.refererInfo.ref; - const { regs = {} } = ortb2 || {}; + const { source = {}, regs = {} } = ortb2 || {}; + + source.schain = setOnAny(validBidRequests, 'schain'); const payload = { id: bidderRequest.bidderRequestId, @@ -687,10 +633,11 @@ const spec = { content: { language: getContentLanguage() }, }, imp: validBidRequests.map(slot => mapImpression(slot)), - cur: [getCurrency()], + cur: [getCurrency(bidderRequest)], tmax, user: {}, regs, + source, device: { language: getBrowserLanguage(), w: screen.width, @@ -714,21 +661,22 @@ const spec = { interpretResponse(serverResponse, request) { const { bidderRequest } = request; - const response = serverResponse.body; + const { body: response = {} } = serverResponse; + const { seatbid: responseSeat, ext: responseExt = {} } = response; + const { paapi: fledgeAuctionConfigs = [] } = responseExt; const bids = []; let site = JSON.parse(request.data).site; // get page and referer data from request site.sn = response.sn || 'mc_adapter'; // WPM site name (wp_sn) pageView.sn = site.sn; // store site_name (for syncing and notifications) - let seat; - if (response.seatbid !== undefined) { + if (responseSeat !== undefined) { /* Match response to request, by comparing bid id's 'bidid-' prefix indicates oneCode (parameterless) request and response */ - response.seatbid.forEach(seatbid => { - seat = seatbid.seat; - seatbid.bid.forEach(serverBid => { + responseSeat.forEach(seatbid => { + const { seat, bid } = seatbid; + bid.forEach(serverBid => { // get data from bid response const { adomain, crid = `mcad_${bidderRequest.auctionId}_${site.slot}`, impid, exp = 300, ext = {}, price, w, h } = serverBid; @@ -744,7 +692,7 @@ const spec = { const { bidId } = bidRequest || {}; // get ext data from bid - const { siteid = site.id, slotid = site.slot, pubid, adlabel, cache: creativeCache, vurls = [], dsa } = ext; + const { siteid = site.id, slotid = site.slot, pubid, adlabel, cache: creativeCache, vurls = [], dsa, platform = 'wpartner', pricepl } = ext; // update site data site = { @@ -775,8 +723,9 @@ const spec = { meta: { advertiserDomains: adomain, networkName: seat, - pricepl: ext && ext.pricepl, + pricepl, dsa, + platform, }, netRevenue: true, vurls, @@ -790,6 +739,8 @@ const spec = { bid.vastXml = serverBid.adm; bid.vastContent = serverBid.adm; bid.vastUrl = creativeCache; + + logInfo(`Bid ${bid.creativeId} is a video ad`); } else if (isNativeAd(serverBid)) { // native bid.mediaType = 'native'; @@ -803,10 +754,18 @@ const spec = { logWarn('Could not parse native data', serverBid.adm); bid.cpm = 0; } - } else { - // banner ad (default) + logInfo(`Bid ${bid.creativeId} as a native ad`); + } else if (isHTML(serverBid)) { + // banner ad (preformatted) bid.mediaType = 'banner'; - bid.ad = renderCreative(site, response.id, serverBid, seat, bidderRequest); + logInfo(`Bid ${bid.creativeId} as a preformatted banner`); + bid.ad = serverBid.adm; + } else { + // unsupported bid format - send notification and set CPM to zero + const payload = getNotificationPayload(bid); + payload.event = 'parseError'; + sendNotification(payload); + bid.cpm = 0; } if (bid.cpm > 0) { @@ -820,17 +779,24 @@ const spec = { }); } - return bids; + return fledgeAuctionConfigs.length ? { bids, fledgeAuctionConfigs } : bids; }, - getUserSyncs(syncOptions, serverResponses, gdprConsent) { + + getUserSyncs(syncOptions, _, gdprConsent = {}) { + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; let mySyncs = []; - // TODO: the check on CMP api version does not seem to make sense here. It means "always run the usersync unless an old (v1) CMP was detected". No attention is paid to the consent choices. - if (syncOptions.iframeEnabled && consentApiVersion != 1) { + if (iframeEnabled) { mySyncs.push({ type: 'iframe', - url: `${SYNC_URL}?tcf=${consentApiVersion}&pvid=${pageView.id}&sn=${pageView.sn}`, + url: `${SYNC_URL_IFRAME}?tcf=2&pvid=${pageView.id}&sn=${pageView.sn}`, }); - }; + } else if (pixelEnabled) { + mySyncs.push({ + type: 'image', + url: `${SYNC_URL_IMAGE}?inver=0&platform=wpartner&host=${getTopHost() || ''}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}`, + }); + } return mySyncs; }, @@ -843,6 +809,15 @@ const spec = { } }, + onBidderError(errorData) { + const payload = getNotificationPayload(errorData); + if (payload) { + payload.event = 'parseError'; + sendNotification(payload); + return payload; + } + }, + onBidViewable(bid) { const payload = getNotificationPayload(bid); if (payload) { @@ -852,6 +827,15 @@ const spec = { } }, + onBidBillable(bid) { + const payload = getNotificationPayload(bid); + if (payload) { + payload.event = 'bidBillable'; + sendNotification(payload); + return payload; + } + }, + onBidWon(bid) { const payload = getNotificationPayload(bid); if (payload) { @@ -860,6 +844,7 @@ const spec = { return payload; } }, + }; registerBidder(spec); diff --git a/modules/ssp_genieeBidAdapter.js b/modules/ssp_genieeBidAdapter.js new file mode 100644 index 00000000000..49afcaec033 --- /dev/null +++ b/modules/ssp_genieeBidAdapter.js @@ -0,0 +1,433 @@ + +import * as utils from '../src/utils.js'; +import { isPlainObject } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js'; +import { highEntropySUAAccessor } from '../src/fpd/sua.js'; +import { config } from '../src/config.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + +const BIDDER_CODE = 'ssp_geniee'; +export const BANNER_ENDPOINT = 'https://aladdin.genieesspv.jp/yie/ld/api/ad_call/v2'; +// export const ENDPOINT_USERSYNC = ''; +const SUPPORTED_MEDIA_TYPES = [ BANNER ]; +const DEFAULT_CURRENCY = 'JPY'; +const ALLOWED_CURRENCIES = ['USD', 'JPY']; +const NET_REVENUE = true; +const MODULE_NAME = `ssp_geniee`; +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}) + +/** + * List of keys for geparams (parameters we use) + * key: full name of the parameter + * value: shortened name used in geparams + */ +const GEPARAMS_KEY = { + /** + * location.href whose protocol is not http + */ + LOCATION: 'loc', + /** + * document.referrer whose protocol is not http + */ + REFERRER: 'ref', + /** + * URL parameter to be linked to clicks + */ + GENIEE_CT0: 'ct0', + /** + * zipcode + */ + ZIP: 'zip', + /** + * country + */ + COUNTRY: 'country', + /** + * city + */ + CITY: 'city', + /** + * longitude + */ + LONGITUDE: 'long', + /** + * lattitude + */ + LATITUDE: 'lati', + /** + * for customised parameters + */ + CUSTOM: 'custom', + /** + * advertising identifier for iOS + */ + IDENTIFIER_FOR_ADVERTISERS: 'idfa', + /** + * tracked Ad restrictions for iOS + */ + LIMIT_AD_TRACKING: 'lat', + /** + * bundle ID of iOS applications? + */ + BUNDLE: 'bundle', +}; + +/** + * List of keys for gecuparams (parameters we use) + * key: full name of the parameter + * value: shortened name used in geparams + */ +const GECUPARAMS_KEY = { + /** + * version no of gecuparams + */ + VERSION: 'ver', + /** + * minor version no of gecuparams + */ + MINOR_VERSION: 'minor', + /** + * encrypted value of LTSV format + */ + VALUE: 'value', +}; + +/** + * executing encodeURIComponent including single quotation + * @param {string} str + * @returns + */ +function encodeURIComponentIncludeSingleQuotation(str) { + return encodeURIComponent(str).replace(/'/g, '%27'); +} + +/** + * Checking "params" has a value for the key "key" and it is not undefined, null, or an empty string + * To support IE in the same way, we cannot use the ?? operator + * @param {Object} params + * @param {string} key + * @returns {boolean} + */ +function hasParamsNotBlankString(params, key) { + return ( + key in params && + typeof params[key] !== 'undefined' && + params[key] != null && + params[key] != '' + ); +} + +export const buildExtuidQuery = ({id5, imuId}) => { + const params = [ + ...(id5 ? [`id5:${id5}`] : []), + ...(imuId ? [`im:${imuId}`] : []), + ]; + + const queryString = params.join('\t'); + if (!queryString) return null; + return queryString; +} + +/** + * making request data be used commonly banner and native + * @see https://docs.prebid.org/dev-docs/bidder-adaptor.html#location-and-referrers + */ +function makeCommonRequestData(bid, geparameter, refererInfo) { + const gpid = utils.deepAccess(bid, 'ortb2Imp.ext.gpid') || utils.deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + + const data = { + zoneid: bid.params.zoneId, + cb: Math.floor(Math.random() * 99999999999), + charset: document.charset || document.characterSet || '', + loc: refererInfo?.page || refererInfo?.location || refererInfo?.topmostLocation || refererInfo?.legacy.referer || encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.LOCATION]) || '', + ct0: geparameter[GEPARAMS_KEY.GENIEE_CT0] !== 'undefined' + ? encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.GENIEE_CT0]) + : '', + referer: refererInfo?.ref || encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.REFERRER]) || '', + topframe: window.parent == window.self ? 1 : 0, + cur: bid.params.hasOwnProperty('currency') ? bid.params.currency : DEFAULT_CURRENCY, + requestid: bid.bidId, + ua: navigator.userAgent, + tpaf: 1, + cks: 1, + ib: 0, + ...(gpid ? { gpid } : {}), + }; + + try { + if (window.self.toString() !== '[object Window]' || window.parent.toString() !== '[object Window]') { + data.err = '1'; + } + } catch (e) {} + + if (GEPARAMS_KEY.IDENTIFIER_FOR_ADVERTISERS in geparameter) { + data.idfa = encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.IDENTIFIER_FOR_ADVERTISERS]); + } + if (GEPARAMS_KEY.LIMIT_AD_TRACKING in geparameter) { + data.adtk = geparameter[GEPARAMS_KEY.LIMIT_AD_TRACKING] ? '0' : '1'; + } + // makeScreenSizeForQueryParameter + if (typeof screen !== 'undefined') { + const screenWidth = screen.width; + const screenHeight = screen.height; + if (screenWidth > screenHeight) { + data.sw = screenHeight; + data.sh = screenWidth; + } else { + data.sw = screenWidth; + data.sh = screenHeight; + } + } + // makeBannerJskQuery + if (hasParamsNotBlankString(geparameter, GEPARAMS_KEY.ZIP)) { + data.zip = encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.ZIP]); + } + if (hasParamsNotBlankString(geparameter, GEPARAMS_KEY.COUNTRY)) { + data.country = encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.COUNTRY]); + } + if (hasParamsNotBlankString(geparameter, GEPARAMS_KEY.CITY)) { + data.city = encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.CITY]); + } + if (hasParamsNotBlankString(geparameter, GEPARAMS_KEY.LONGITUDE)) { + data.long = encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.LONGITUDE]); + } + if (hasParamsNotBlankString(geparameter, GEPARAMS_KEY.LATITUDE)) { + data.lati = encodeURIComponentIncludeSingleQuotation( + geparameter[GEPARAMS_KEY.LATITUDE] + ); + } + if (GEPARAMS_KEY.CUSTOM in geparameter && isPlainObject(geparameter[GEPARAMS_KEY.CUSTOM])) { + for (const c in geparameter[GEPARAMS_KEY.CUSTOM]) { + if (hasParamsNotBlankString(geparameter[GEPARAMS_KEY.CUSTOM], c)) { + data[encodeURIComponentIncludeSingleQuotation('custom_' + c)] = + encodeURIComponentIncludeSingleQuotation( + geparameter[GEPARAMS_KEY.CUSTOM][c] + ); + } + } + } + const gecuparameter = window.gecuparams || {}; + if (isPlainObject(gecuparameter)) { + if (hasParamsNotBlankString(gecuparameter, GECUPARAMS_KEY.VERSION)) { + data.gc_ver = encodeURIComponentIncludeSingleQuotation(gecuparameter[GECUPARAMS_KEY.VERSION]); + } + if (hasParamsNotBlankString(gecuparameter, GECUPARAMS_KEY.MINOR_VERSION)) { + data.gc_minor = encodeURIComponentIncludeSingleQuotation(gecuparameter[GECUPARAMS_KEY.MINOR_VERSION]); + } + if (hasParamsNotBlankString(gecuparameter, GECUPARAMS_KEY.VALUE)) { + data.gc_value = encodeURIComponentIncludeSingleQuotation(gecuparameter[GECUPARAMS_KEY.VALUE]); + } + } + + // imuid, id5 + const id5 = utils.deepAccess(bid, 'userId.id5id.uid'); + const imuId = utils.deepAccess(bid, 'userId.imuid'); + const extuidQuery = buildExtuidQuery({id5, imuId}); + if (extuidQuery) data.extuid = extuidQuery; + + // makeUAQuery + // To avoid double encoding, not using encodeURIComponent here + const ua = JSON.parse(getUserAgent()); + if (ua && ua.fullVersionList) { + const fullVersionList = ua.fullVersionList.reduce((acc, cur) => { + let str = acc; + if (str) str += ','; + str += '"' + cur.brand + '";v="' + cur.version + '"'; + return str; + }, ''); + data.ucfvl = fullVersionList; + } + if (ua && ua.platform) data.ucp = '"' + ua.platform + '"'; + if (ua && ua.architecture) data.ucarch = '"' + ua.architecture + '"'; + if (ua && ua.platformVersion) data.ucpv = '"' + ua.platformVersion + '"'; + if (ua && ua.bitness) data.ucbit = '"' + ua.bitness + '"'; + data.ucmbl = '?' + (ua && ua.mobile ? '1' : '0'); + if (ua && ua.model) data.ucmdl = '"' + ua.model + '"'; + + return data; +} + +/** + * making request data for banner + */ +function makeBannerRequestData(bid, geparameter, refererInfo) { + const data = makeCommonRequestData(bid, geparameter, refererInfo); + + // this query is not used in nad endpoint but used in ad_call endpoint + if (hasParamsNotBlankString(geparameter, GEPARAMS_KEY.BUNDLE)) { + data.apid = encodeURIComponentIncludeSingleQuotation(geparameter[GEPARAMS_KEY.BUNDLE]); + } + + return data; +} + +/** + * making bid response be used commonly banner and native + */ +function makeCommonBidResponse(bid, width, height) { + return { + requestId: bid.requestid, + cpm: bid.price, + creativeId: bid.creativeId, + currency: bid.cur, + netRevenue: NET_REVENUE, + ttl: 700, + width: width, // width of the ad iframe + height: height, // height of the ad iframe + }; +} + +/** + * making bid response for banner + */ +function makeBannerBidResponse(bid, request) { + const bidResponse = makeCommonBidResponse(bid, bid.width, bid.height); + const loc = encodeURIComponentIncludeSingleQuotation( + window.top === window.self ? location.href : window.top.document.referrer + ); + const beacon = !bid.ib + ? '' + : ` +
+ +
`; + bidResponse.ad = makeBidResponseAd( + beacon + '
' + makeChangeHeightEventMarkup(request) + decodeURIComponent(bid.adm) + '
' + ); + bidResponse.mediaType = BANNER; + + return bidResponse; +} + +/** + * making change height event markup for af iframe. About passback ad, it is possible that ad image is cut off. To handle this, we add this event to change height after ad is loaded. + */ +function makeChangeHeightEventMarkup(request) { + return ( + '' + ); +} + +/** + * making bid response ad. This is also the value to be used by document.write in renderAd function. + * @param {string} innerHTML + * @returns + */ +function makeBidResponseAd(innerHTML) { + return '' + innerHTML + ''; +} + +function getUserAgent() { + return storage.getDataFromLocalStorage('key') || null; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + /** + * Determines whether or not the given bid request is valid. + * @param {BidRequest} bidRequest The bid request params to validate. + * @return boolean True if this is a valid bid request, and false otherwise. + */ + 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; + } + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {Array} validBidRequests an array of bid requests + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const serverRequests = []; + + const HIGH_ENTROPY_HINTS = [ + 'architecture', + 'model', + 'mobile', + 'platform', + 'bitness', + 'platformVersion', + 'fullVersionList', + ]; + + const uaData = window.navigator?.userAgentData; + if (uaData && uaData.getHighEntropyValues) { + const getHighEntropySUA = highEntropySUAAccessor(uaData); + getHighEntropySUA(HIGH_ENTROPY_HINTS).then((ua) => { + if (ua) { + storage.setDataInLocalStorage('ua', JSON.stringify(ua)); + } + }); + } + + validBidRequests.forEach((bid) => { + // const isNative = bid.mediaTypes?.native; + const geparameter = window.geparams || {}; + + serverRequests.push({ + method: 'GET', + url: BANNER_ENDPOINT, + data: makeBannerRequestData(bid, geparameter, bidderRequest?.refererInfo), + bid: bid, + }); + }); + + return serverRequests; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidderRequest A matched bid request for this response. + * @return Array An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidderRequest) { + const bidResponses = []; + + if (!serverResponse || !serverResponse.body) { + return bidResponses; + } + + const zoneId = bidderRequest.bid.params.zoneId; + let successBid; + successBid = serverResponse.body || {}; + + if (successBid.hasOwnProperty(zoneId)) { + const bid = successBid[zoneId]; + bidResponses.push(makeBannerBidResponse(bid, bidderRequest)); + } + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + + // if we need user sync, we add this part after preparing the endpoint + /* if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: ENDPOINT_USERSYNC + }); + } */ + + return syncs; + }, + onTimeout: function (timeoutData) {}, + onBidWon: function (bid) {}, + onSetTargeting: function (bid) {}, +}; + +registerBidder(spec); diff --git a/modules/ssp_genieeBidAdapter.md b/modules/ssp_genieeBidAdapter.md new file mode 100644 index 00000000000..fe77a6f858b --- /dev/null +++ b/modules/ssp_genieeBidAdapter.md @@ -0,0 +1,43 @@ +# Overview + +``` +Module Name: Geniee Bid Adapter +Module Type: Bidder Adapter +Maintainer: supply-carpet@geniee.co.jp +``` + +# Description +This is [Geniee](https://geniee.co.jp) Bidder Adapter for Prebid.js. +(This is Geniee *SSP* Bidder Adapter. The another adapter named "Geniee Bid Adapter" is Geniee *DSP* Bidder Adapter.) + +Please contact us before using the adapter. + +We will provide ads when satisfy the following conditions: + +- There are a certain number bid requests by zone +- The request is a Banner ad +- Payment is possible in Japanese yen or US dollars +- The request is not for GDPR or COPPA users + +Thus, even if the following test, it will be no bids if the request does not reach a certain requests. + +# Test Parameters + +```js +var adUnits = [ + { + code: 'banner-ad', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'ssp_geniee', + params: { + zoneId: 1573195 + } + }] + }, +]; +``` diff --git a/modules/stackadaptBidAdapter.js b/modules/stackadaptBidAdapter.js new file mode 100644 index 00000000000..f6989b24fb3 --- /dev/null +++ b/modules/stackadaptBidAdapter.js @@ -0,0 +1,200 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { deepSetValue, logWarn, parseSizesInput, isNumber, isInteger, replaceAuctionPrice, formatQS, isFn, isPlainObject } from '../src/utils.js'; +import {getUserSyncParams} from '../libraries/userSyncUtils/userSyncUtils.js'; + +const BIDDER_CODE = 'stackadapt'; +const ENDPOINT_URL = 'https://pjs.srv.stackadapt.com/br'; +const USER_SYNC_ENDPOINT = 'https://sync.srv.stackadapt.com/sync?nid=pjs'; +const CURRENCY = 'USD'; + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: CURRENCY, + }, + + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + request.id = bidderRequest.bidderRequestId + + deepSetValue(request, 'site.publisher.id', bid.params.publisherId); + deepSetValue(request, 'test', bid.params.testMode); + + return request; + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (bidRequest.params.placementId) { + deepSetValue(imp, 'tagid', bidRequest.params.placementId); + } + if (bidRequest.params.banner?.expdir) { + deepSetValue(imp, 'banner.expdir', bidRequest.params.banner.expdir); + } + + const bidfloor = getBidFloor(bidRequest); + if (bidfloor) { + imp.bidfloor = parseFloat(bidfloor); + imp.bidfloorcur = CURRENCY; + } + + if (!isNumber(imp.secure)) { + imp.secure = 1 + } + + return imp; + }, + + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + const requestMediaTypes = Object.keys(bidRequest.mediaTypes); + + if (requestMediaTypes.length === 1) { + context.mediaType = requestMediaTypes[0]; + } else { + if (bid.adm?.search(/^(<\?xml| { - logInfo('Enabling STAQ Adapter'); - staqAdapterRefWin = getRefererInfo(window); - if (!config.options.connId) { - logError('ConnId is not defined. STAQ Analytics won\'t work'); - return; - } - if (!config.options.url) { - logError('URL is not defined. STAQ Analytics won\'t work'); - return; - } - analyticsAdapter.context = { - host: config.options.host || DEFAULT_HOST, - url: config.options.url, - connectionId: config.options.connId, - requestTemplate: buildRequestTemplate(config.options.connId), - queue: new ExpiringQueue(sendAll, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) - }; - analyticsAdapter.originEnableAnalytics(config); -}; - -adapterManager.registerAnalyticsAdapter({ - adapter: analyticsAdapter, - code: MODULE_CODE, -}); - -export default analyticsAdapter; - -function sendAll() { - let events = analyticsAdapter.context.queue.popAll(); - if (events.length !== 0) { - let req = analyticsAdapter.context.requestTemplate; - req.auctionId = analyticsAdapter.context.auctionId; - req.events = events - - analyticsAdapter.ajaxCall(JSON.stringify(req)); - } -} - -analyticsAdapter.ajaxCall = function ajaxCall(data) { - logInfo('SENDING DATA: ' + data); - ajax(`https://${analyticsAdapter.context.url}/prebid/${analyticsAdapter.context.connectionId}`, () => {}, data, { contentType: 'text/plain' }); -}; - -function trackAuctionInit(args) { - analyticsAdapter.context.auctionTimeStart = Date.now(); - analyticsAdapter.context.auctionId = args.auctionId; - const event = createHbEvent(args.auctionId, undefined, STAQ_EVENTS.AUCTION_INIT); - return [event]; -} - -function trackBidRequest(args) { - return args.bids.map(bid => - createHbEvent(args.auctionId, args.bidderCode, STAQ_EVENTS.BID_REQUEST, bid.adUnitCode)); -} - -function trackBidResponse(args) { - const event = createHbEvent(args.auctionId, args.bidderCode, STAQ_EVENTS.BID_RESPONSE, - args.adUnitCode, args.cpm, args.timeToRespond / 1000, false, args); - return [event]; -} - -function trackBidWon(args) { - const event = createHbEvent(args.auctionId, args.bidderCode, STAQ_EVENTS.BID_WON, args.adUnitCode, args.cpm, undefined, true, args); - return [event]; -} - -function trackAuctionEnd(args) { - const event = createHbEvent(args.auctionId, undefined, STAQ_EVENTS.AUCTION_END, undefined, - undefined, (Date.now() - analyticsAdapter.context.auctionTimeStart) / 1000); - return [event]; -} - -function trackBidTimeout(args) { - return args.map(arg => createHbEvent(arg.auctionId, arg.bidderCode, STAQ_EVENTS.TIMEOUT)); -} - -function createHbEvent(auctionId, adapter, event, adUnitCode = undefined, value = 0, time = 0, bidWon = undefined, eventArgs) { - let ev = { event: event }; - if (adapter) { - ev.adapter = adapter; - ev.bidderName = adapter; - } - if (adUnitCode) { - ev.adUnitCode = adUnitCode; - } - if (value) { - ev.cpm = value; - } - if (time) { - ev.timeToRespond = time; - } - if (typeof bidWon !== 'undefined') { - ev.bidWon = bidWon; - } else if (event === 'bidResponse') { - ev.bidWon = false; - } - ev.auctionId = auctionId; - - if (eventArgs) { - if (STAQ_EVENTS.BID_RESPONSE == event || STAQ_EVENTS.BID_WON == event) { - ev.width = eventArgs.width; - ev.height = eventArgs.height; - - ev.adId = eventArgs.adId; - } - } - - return ev; -} - -const UTM_TAGS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', - 'utm_c1', 'utm_c2', 'utm_c3', 'utm_c4', 'utm_c5' -]; -const STAQ_PREBID_KEY = 'staq_analytics'; -const DIRECT = '(direct)'; -const REFERRAL = '(referral)'; -const ORGANIC = '(organic)'; - -export let storage = { - getItem: (name) => { - return storageObj.getDataFromLocalStorage(name); - }, - setItem: (name, value) => { - storageObj.setDataInLocalStorage(name, value); - } -}; - -export function getUmtSource(pageUrl, referrer) { - let prevUtm = getPreviousTrafficSource(); - let currUtm = getCurrentTrafficSource(pageUrl, referrer); - let [updated, actual] = chooseActualUtm(prevUtm, currUtm); - if (updated) { - storeUtm(actual); - } - return actual; - - function getPreviousTrafficSource() { - let val = storage.getItem(STAQ_PREBID_KEY); - if (!val) { - return getDirect(); - } - return JSON.parse(val); - } - - function getCurrentTrafficSource(pageUrl, referrer) { - var source = getUTM(pageUrl); - if (source) { - return source; - } - if (referrer) { - let se = getSearchEngine(referrer); - if (se) { - return asUtm(se, ORGANIC, ORGANIC); - } - let parsedUrl = parseUrl(pageUrl); - let [refHost, refPath] = getReferrer(referrer); - if (refHost && refHost !== parsedUrl.hostname) { - return asUtm(refHost, REFERRAL, REFERRAL, '', refPath); - } - } - return getDirect(); - } - - function getSearchEngine(pageUrl) { - let engines = { - 'google': /^https?\:\/\/(?:www\.)?(?:google\.(?:com?\.)?(?:com|cat|[a-z]{2})|g.cn)\//i, - 'yandex': /^https?\:\/\/(?:www\.)?ya(?:ndex\.(?:com|net)?\.?(?:asia|mobi|org|[a-z]{2})?|\.ru)\//i, - 'bing': /^https?\:\/\/(?:www\.)?bing\.com\//i, - 'duckduckgo': /^https?\:\/\/(?:www\.)?duckduckgo\.com\//i, - 'ask': /^https?\:\/\/(?:www\.)?ask\.com\//i, - 'yahoo': /^https?\:\/\/(?:[-a-z]+\.)?(?:search\.)?yahoo\.com\//i - }; - - for (let engine in engines) { - if (engines.hasOwnProperty(engine) && engines[engine].test(pageUrl)) { - return engine; - } - } - } - - function getReferrer(referrer) { - let ref = parseUrl(referrer); - return [ref.hostname, ref.pathname]; - } - - function getUTM(pageUrl) { - let urlParameters = parseUrl(pageUrl).search; - if (!urlParameters['utm_campaign'] || !urlParameters['utm_source']) { - return; - } - let utmArgs = []; - _each(UTM_TAGS, (utmTagName) => { - let utmValue = urlParameters[utmTagName] || ''; - utmArgs.push(utmValue); - }); - return asUtm.apply(this, utmArgs); - } - - function getDirect() { - return asUtm(DIRECT, DIRECT, DIRECT); - } - - function storeUtm(utm) { - let val = JSON.stringify(utm); - storage.setItem(STAQ_PREBID_KEY, val); - } - - function asUtm(source, medium, campaign, term = '', content = '', c1 = '', c2 = '', c3 = '', c4 = '', c5 = '') { - let result = { - source: source, - medium: medium, - campaign: campaign - }; - if (term) { - result.term = term; - } - if (content) { - result.content = content; - } - if (c1) { - result.c1 = c1; - } - if (c2) { - result.c2 = c2; - } - if (c3) { - result.c3 = c3; - } - if (c4) { - result.c4 = c4; - } - if (c5) { - result.c5 = c5; - } - return result; - } - - function chooseActualUtm(prev, curr) { - if (ord(prev) < ord(curr)) { - return [true, curr]; - } - if (ord(prev) > ord(curr)) { - return [false, prev]; - } else { - if (prev.campaign === REFERRAL && prev.content !== curr.content) { - return [true, curr]; - } else if (prev.campaign === ORGANIC && prev.source !== curr.source) { - return [true, curr]; - } else if (isCampaignTraffic(prev) && (prev.campaign !== curr.campaign || prev.source !== curr.source)) { - return [true, curr]; - } - } - return [false, prev]; - } - - function ord(utm) { - switch (utm.campaign) { - case DIRECT: - return 0; - case ORGANIC: - return 1; - case REFERRAL: - return 2; - default: - return 3; - } - } - - function isCampaignTraffic(utm) { - return [DIRECT, REFERRAL, ORGANIC].indexOf(utm.campaign) === -1; - } -} - -/** - * Expiring queue implementation. Fires callback on elapsed timeout since last last update or creation. - * @param callback - * @param ttl - * @constructor - */ -export function ExpiringQueue(callback, ttl) { - let queue = []; - let timeoutId; - - this.push = (event) => { - if (event instanceof Array) { - queue.push.apply(queue, event); - } else { - queue.push(event); - } - reset(); - }; - - this.updateWithWins = (winEvents) => { - winEvents.forEach(winEvent => { - queue.forEach(prevEvent => { - if (prevEvent.event === 'bidResponse' && - prevEvent.auctionId == winEvent.auctionId && - prevEvent.adUnitCode == winEvent.adUnitCode && - prevEvent.adId == winEvent.adId && - prevEvent.adapter == winEvent.adapter) { - prevEvent.bidWon = true; - } - }); - }); - } - - this.popAll = () => { - let result = queue; - queue = []; - reset(); - return result; - }; - - /** - * For test/debug purposes only - * @return {Array} - */ - this.peekAll = () => { - return queue; - }; - - this.init = reset; - - function reset() { - if (timeoutId) { - clearTimeout(timeoutId); - } - timeoutId = setTimeout(() => { - if (queue.length) { - callback(); - } - }, ttl); - } -} diff --git a/modules/stnBidAdapter.js b/modules/stnBidAdapter.js index 42b69ee7c2b..62d82b8d4b2 100644 --- a/modules/stnBidAdapter.js +++ b/modules/stnBidAdapter.js @@ -1,39 +1,17 @@ -import { - logWarn, - logInfo, - isArray, - isFn, - deepAccess, - isEmpty, - contains, - timestamp, - triggerPixel, - isInteger, - getBidIdParameter -} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; +import {makeBaseSpec} from '../libraries/riseUtils/index.js'; -const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'stn'; -const ADAPTER_VERSION = '6.0.0'; -const TTL = 360; -const DEFAULT_CURRENCY = 'USD'; -const SELLER_ENDPOINT = 'https://hb.stngo.com/'; +const BASE_URL = 'https://hb.stngo.com/'; const MODES = { PRODUCTION: 'hb-multi', TEST: 'hb-multi-test' -} -const SUPPORTED_SYNC_METHODS = { - IFRAME: 'iframe', - PIXEL: 'pixel' -} +}; export const spec = { + ...makeBaseSpec(BASE_URL, MODES), code: BIDDER_CODE, - version: ADAPTER_VERSION, - supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { if (!bidRequest.params) { logWarn('no params have been set to STN adapter'); @@ -46,440 +24,7 @@ export const spec = { } return true; - }, - buildRequests: function (validBidRequests, bidderRequest) { - const combinedRequestsObject = {}; - - // use data from the first bid, to create the general params for all bids - const generalObject = validBidRequests[0]; - const testMode = generalObject.params.testMode; - - combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); - combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); - - return { - method: 'POST', - url: getEndpoint(testMode), - data: combinedRequestsObject - } - }, - interpretResponse: function ({body}) { - const bidResponses = []; - - if (body.bids) { - body.bids.forEach(adUnit => { - const bidResponse = { - requestId: adUnit.requestId, - cpm: adUnit.cpm, - currency: adUnit.currency || DEFAULT_CURRENCY, - width: adUnit.width, - height: adUnit.height, - ttl: adUnit.ttl || TTL, - creativeId: adUnit.requestId, - netRevenue: adUnit.netRevenue || true, - nurl: adUnit.nurl, - mediaType: adUnit.mediaType, - meta: { - mediaType: adUnit.mediaType - } - }; - - if (adUnit.mediaType === VIDEO) { - bidResponse.vastXml = adUnit.vastXml; - } else if (adUnit.mediaType === BANNER) { - bidResponse.ad = adUnit.ad; - } - - if (adUnit.adomain && adUnit.adomain.length) { - bidResponse.meta.advertiserDomains = adUnit.adomain; - } - - bidResponses.push(bidResponse); - }); - } - - return bidResponses; - }, - getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; - for (const response of serverResponses) { - if (syncOptions.iframeEnabled && deepAccess(response, 'body.params.userSyncURL')) { - syncs.push({ - type: 'iframe', - url: deepAccess(response, 'body.params.userSyncURL') - }); - } - if (syncOptions.pixelEnabled && isArray(deepAccess(response, 'body.params.userSyncPixels'))) { - const pixels = response.body.params.userSyncPixels.map(pixel => { - return { - type: 'image', - url: pixel - } - }) - syncs.push(...pixels) - } - } - return syncs; - }, - onBidWon: function (bid) { - if (bid == null) { - return; - } - - logInfo('onBidWon:', bid); - if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { - triggerPixel(bid.nurl); - } } }; registerBidder(spec); - -/** - * Get floor price - * @param bid {bid} - * @param mediaType {String} - * @returns {Number} - */ -function getFloor(bid, mediaType) { - if (!isFn(bid.getFloor)) { - return 0; - } - let floorResult = bid.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: mediaType, - size: '*' - }); - return floorResult.currency === DEFAULT_CURRENCY && floorResult.floor ? floorResult.floor : 0; -} - -/** - * Get the ad sizes array from the bid - * @param bid {bid} - * @param mediaType {String} - * @returns {Array} - */ -function getSizesArray(bid, mediaType) { - let sizesArray = [] - - if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { - sizesArray = bid.mediaTypes[mediaType].sizes; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - sizesArray = bid.sizes; - } - - return sizesArray; -} - -/** - * Get schain string value - * @param schainObject {Object} - * @returns {string} - */ -function getSupplyChain(schainObject) { - if (isEmpty(schainObject)) { - return ''; - } - let scStr = `${schainObject.ver},${schainObject.complete}`; - schainObject.nodes.forEach((node) => { - scStr += '!'; - scStr += `${getEncodedValIfNotEmpty(node.asi)},`; - scStr += `${getEncodedValIfNotEmpty(node.sid)},`; - scStr += `${getEncodedValIfNotEmpty(node.hp)},`; - scStr += `${getEncodedValIfNotEmpty(node.rid)},`; - scStr += `${getEncodedValIfNotEmpty(node.name)},`; - scStr += `${getEncodedValIfNotEmpty(node.domain)}`; - }); - return scStr; -} - -/** - * Get encoded node value - * @param val {string} - * @returns {string} - */ -function getEncodedValIfNotEmpty(val) { - return (val !== '' && val !== undefined) ? encodeURIComponent(val) : ''; -} - -/** - * Get preferred user-sync method based on publisher configuration - * @param filterSettings {Object} - * @param bidderCode {string} - * @returns {string} - */ -function getAllowedSyncMethod(filterSettings, bidderCode) { - const iframeConfigsToCheck = ['all', 'iframe']; - const pixelConfigToCheck = 'image'; - if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { - return SUPPORTED_SYNC_METHODS.IFRAME; - } - if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { - return SUPPORTED_SYNC_METHODS.PIXEL; - } -} - -/** - * Check if sync rule is supported - * @param syncRule {Object} - * @param bidderCode {string} - * @returns {boolean} - */ -function isSyncMethodAllowed(syncRule, bidderCode) { - if (!syncRule) { - return false; - } - const isInclude = syncRule.filter === 'include'; - const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; - return isInclude && contains(bidders, bidderCode); -} - -/** - * Get the seller endpoint - * @param testMode {boolean} - * @returns {string} - */ -function getEndpoint(testMode) { - return testMode - ? SELLER_ENDPOINT + MODES.TEST - : SELLER_ENDPOINT + MODES.PRODUCTION; -} - -/** - * get device type - * @param ua {ua} - * @returns {string} - */ -function getDeviceType(ua) { - if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i - .test(ua.toLowerCase())) { - return '5'; - } - if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i - .test(ua.toLowerCase())) { - return '4'; - } - if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i - .test(ua.toLowerCase())) { - return '3'; - } - return '1'; -} - -function generateBidsParams(validBidRequests, bidderRequest) { - const bidsArray = []; - - if (validBidRequests.length) { - validBidRequests.forEach(bid => { - bidsArray.push(generateBidParameters(bid, bidderRequest)); - }); - } - - return bidsArray; -} - -/** - * Generate bid specific parameters - * @param {bid} bid - * @param {bidderRequest} bidderRequest - * @returns {Object} bid specific params object - */ -function generateBidParameters(bid, bidderRequest) { - const {params} = bid; - const mediaType = isBanner(bid) ? BANNER : VIDEO; - const sizesArray = getSizesArray(bid, mediaType); - - // fix floor price in case of NAN - if (isNaN(params.floorPrice)) { - params.floorPrice = 0; - } - - const bidObject = { - mediaType, - adUnitCode: getBidIdParameter('adUnitCode', bid), - sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), - bidId: getBidIdParameter('bidId', bid), - loop: getBidIdParameter('bidderRequestsCount', bid), - bidderRequestId: getBidIdParameter('bidderRequestId', bid), - transactionId: bid.ortb2Imp?.ext?.tid || '', - coppa: 0, - }; - - const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); - if (pos) { - bidObject.pos = pos; - } - - const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); - if (gpid) { - bidObject.gpid = gpid; - } - - const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); - if (placementId) { - bidObject.placementId = placementId; - } - - const mimes = deepAccess(bid, `mediaTypes.${mediaType}.mimes`); - if (mimes) { - bidObject.mimes = mimes; - } - const api = deepAccess(bid, `mediaTypes.${mediaType}.api`); - if (api) { - bidObject.api = api; - } - - const sua = deepAccess(bid, `ortb2.device.sua`); - if (sua) { - bidObject.sua = sua; - } - - const coppa = deepAccess(bid, `ortb2.regs.coppa`) - if (coppa) { - bidObject.coppa = 1; - } - - if (mediaType === VIDEO) { - const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); - let playbackMethodValue; - - // verify playbackMethod is of type integer array, or integer only. - if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { - // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation - playbackMethodValue = playbackMethod[0]; - } else if (isInteger(playbackMethod)) { - playbackMethodValue = playbackMethod; - } - - if (playbackMethodValue) { - bidObject.playbackMethod = playbackMethodValue; - } - - const placement = deepAccess(bid, `mediaTypes.video.placement`); - if (placement) { - bidObject.placement = placement; - } - - const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); - if (minDuration) { - bidObject.minDuration = minDuration; - } - - const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); - if (maxDuration) { - bidObject.maxDuration = maxDuration; - } - - const skip = deepAccess(bid, `mediaTypes.video.skip`); - if (skip) { - bidObject.skip = skip; - } - - const linearity = deepAccess(bid, `mediaTypes.video.linearity`); - if (linearity) { - bidObject.linearity = linearity; - } - - const protocols = deepAccess(bid, `mediaTypes.video.protocols`); - if (protocols) { - bidObject.protocols = protocols; - } - - const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); - if (plcmt) { - bidObject.plcmt = plcmt; - } - } - - return bidObject; -} - -function isBanner(bid) { - return bid.mediaTypes && bid.mediaTypes.banner; -} - -/** - * Generate params that are common between all bids - * @param {single bid object} generalObject - * @param {bidderRequest} bidderRequest - * @returns {object} the common params object - */ -function generateGeneralParams(generalObject, bidderRequest) { - const domain = window.location.hostname; - const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; - const {bidderCode} = bidderRequest; - const generalBidParams = generalObject.params; - const timeout = bidderRequest.timeout; - - // these params are snake_case instead of camelCase to allow backwards compatability on the server. - // in the future, these will be converted to camelCase to match our convention. - const generalParams = { - wrapper_type: 'prebidjs', - wrapper_vendor: '$$PREBID_GLOBAL$$', - wrapper_version: '$prebid.version$', - adapter_version: ADAPTER_VERSION, - auction_start: timestamp(), - publisher_id: generalBidParams.org, - publisher_name: domain, - site_domain: domain, - dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0, - device_type: getDeviceType(navigator.userAgent), - ua: navigator.userAgent, - is_wrapper: !!generalBidParams.isWrapper, - session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), - tmax: timeout - } - - const userIdsParam = getBidIdParameter('userId', generalObject); - if (userIdsParam) { - generalParams.userIds = JSON.stringify(userIdsParam); - } - - const ortb2Metadata = bidderRequest.ortb2 || {}; - if (ortb2Metadata.site) { - generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); - } - if (ortb2Metadata.user) { - generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); - } - - if (syncEnabled) { - const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); - if (allowedSyncMethod) { - generalParams.cs_method = allowedSyncMethod; - } - } - - if (bidderRequest.uspConsent) { - generalParams.us_privacy = bidderRequest.uspConsent; - } - - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; - generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - - if (bidderRequest.gppConsent) { - generalParams.gpp = bidderRequest.gppConsent.gppString; - generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - generalParams.gpp = bidderRequest.ortb2.regs.gpp; - generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - - if (generalBidParams.ifa) { - generalParams.ifa = generalBidParams.ifa; - } - - if (generalObject.schain) { - generalParams.schain = getSupplyChain(generalObject.schain); - } - - if (bidderRequest && bidderRequest.refererInfo) { - generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); - generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); - } - - return generalParams -} diff --git a/modules/stnBidAdapter.md b/modules/stnBidAdapter.md index 90b0b58e34b..d5f5f4f3e1d 100644 --- a/modules/stnBidAdapter.md +++ b/modules/stnBidAdapter.md @@ -13,7 +13,7 @@ Module that connects to STN's demand sources. The STN adapter requires setup and approval from the STN. Please reach out to hb@stnvideo.com to create an STN account. -The adapter supports Video(instream) & Banner. +The adapter supports Video(instream), Banner, Native and multi-format bid requests. # Bid Parameters ## Video diff --git a/modules/stroeerCoreBidAdapter.js b/modules/stroeerCoreBidAdapter.js index e67941ed3a1..bf818250c63 100644 --- a/modules/stroeerCoreBidAdapter.js +++ b/modules/stroeerCoreBidAdapter.js @@ -1,7 +1,8 @@ -import { buildUrl, deepAccess, deepSetValue, generateUUID, getWindowSelf, getWindowTop, isEmpty, isStr, logWarn } from '../src/utils.js'; +import { buildUrl, deepAccess, deepSetValue, generateUUID, getWinDimensions, getWindowSelf, getWindowTop, isEmpty, isStr, logWarn } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find} from '../src/polyfill.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; const GVL_ID = 136; const BIDDER_CODE = 'stroeerCore'; @@ -52,7 +53,6 @@ export const spec = { const basePayload = { id: generateUUID(), ref: refererInfo.ref, - ssl: isSecureWindow(), mpa: isMainPageAccessible(), timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart), url: refererInfo.page, @@ -76,7 +76,7 @@ export const spec = { }; } - const ORTB2_KEYS = ['regs.ext.dsa', 'device.ext.cdep']; + const ORTB2_KEYS = ['regs.ext.dsa', 'device.ext.cdep', 'site.ext']; ORTB2_KEYS.forEach(key => { const value = deepAccess(bidderRequest.ortb2, key); if (value !== undefined) { @@ -117,7 +117,8 @@ export const spec = { creativeId: '', meta: { advertiserDomains: bidResponse.adomain, - dsa: bidResponse.dsa + dsa: bidResponse.dsa, + campaignType: bidResponse.campaignType, }, mediaType, }; @@ -147,8 +148,6 @@ export const spec = { } }; -const isSecureWindow = () => getWindowSelf().location.protocol === 'https:'; - const isMainPageAccessible = () => { try { return !!getWindowTop().location.href; @@ -165,8 +164,8 @@ const elementInView = (elementId) => { }; const visibleInWindow = (el, win) => { - const rect = el.getBoundingClientRect(); - const inView = (rect.top + rect.height >= 0) && (rect.top <= win.innerHeight); + const rect = getBoundingClientRect(el); + const inView = (rect.top + rect.height >= 0) && (rect.top <= getWinDimensions().innerHeight); if (win !== win.parent) { return inView && visibleInWindow(win.frameElement, win.parent); @@ -219,6 +218,7 @@ const mapToPayloadBaseBid = (bidRequest) => ({ bid: bidRequest.bidId, sid: bidRequest.params.sid, viz: elementInView(bidRequest.adUnitCode), + sfp: bidRequest.params.sfp, }); const mapToPayloadBannerBid = (bidRequest) => { @@ -254,14 +254,14 @@ const createFloorPriceObject = (mediaType, sizes, bidRequest) => { currency: 'EUR', mediaType: mediaType, size: '*' - }); + }) || {}; const sizeFloors = sizes.map(size => { const floor = bidRequest.getFloor({ currency: 'EUR', mediaType: mediaType, size: [size[0], size[1]] - }); + }) || {}; return {...floor, size}; }); diff --git a/modules/stvBidAdapter.js b/modules/stvBidAdapter.js index 5cffc5853b5..ef8b815b5f9 100644 --- a/modules/stvBidAdapter.js +++ b/modules/stvBidAdapter.js @@ -1,7 +1,16 @@ -import {deepAccess} from '../src/utils.js'; +import {deepAccess, logMessage} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {includes} from '../src/polyfill.js'; +import { + handleSyncUrls, + isBannerRequest, + isVideoRequest, + convertMediaInfoForRequest, + getMediaTypesInfo, + getBidFloor, + interpretResponse +} from '../libraries/dspxUtils/bidderUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -121,88 +130,21 @@ export const spec = { return { method: 'GET', url: endpoint, - data: objectToQueryString(payload), + data: stvObjectToQueryString(payload), }; }); }, interpretResponse: function(serverResponse, bidRequest) { - const bidResponses = []; - const response = serverResponse.body; - const crid = response.crid || 0; - const cpm = response.cpm / 1000000 || 0; - if (cpm !== 0 && crid !== 0) { - const dealId = response.dealid || ''; - const currency = response.currency || 'EUR'; - const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue; - const bidResponse = { - requestId: response.bid_id, - cpm: cpm, - width: response.width, - height: response.height, - creativeId: crid, - dealId: dealId, - currency: currency, - netRevenue: netRevenue, - ttl: 60, - meta: { - advertiserDomains: response.adomain || [] - } - }; - if (response.vastXml) { - bidResponse.vastXml = response.vastXml; - bidResponse.mediaType = 'video'; - } else { - bidResponse.ad = response.adTag; - } - - bidResponses.push(bidResponse); - } - return bidResponses; + logMessage('STV: serverResponse', serverResponse); + logMessage('STV: bidRequest', bidRequest); + return interpretResponse(serverResponse, bidRequest, (bidRequest, response) => null); // we don't use any renderer }, getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { - if (!serverResponses || serverResponses.length === 0) { - return []; - } - - const syncs = [] - - let gdprParams = ''; - if (gdprConsent) { - if ('gdprApplies' in gdprConsent && typeof gdprConsent.gdprApplies === 'boolean') { - gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - gdprParams = `gdpr_consent=${gdprConsent.consentString}`; - } - } - - if (serverResponses.length > 0 && serverResponses[0].body !== undefined && - serverResponses[0].body.userSync !== undefined && serverResponses[0].body.userSync.iframeUrl !== undefined && - serverResponses[0].body.userSync.iframeUrl.length > 0) { - if (syncOptions.iframeEnabled) { - serverResponses[0].body.userSync.iframeUrl.forEach((url) => syncs.push({ - type: 'iframe', - url: appendToUrl(url, gdprParams) - })); - } - if (syncOptions.pixelEnabled) { - serverResponses[0].body.userSync.imageUrl.forEach((url) => syncs.push({ - type: 'image', - url: appendToUrl(url, gdprParams) - })); - } - } - return syncs; + return handleSyncUrls(syncOptions, serverResponses, gdprConsent); } } -function appendToUrl(url, what) { - if (!what) { - return url; - } - return url + (url.indexOf('?') !== -1 ? '&' : '?') + what; -} - -function objectToQueryString(obj, prefix) { +function stvObjectToQueryString(obj, prefix) { let str = []; let p; for (p in obj) { @@ -210,7 +152,7 @@ function objectToQueryString(obj, prefix) { let k = prefix ? prefix + '[' + p + ']' : p; let v = obj[p]; str.push((v !== null && typeof v === 'object') - ? objectToQueryString(v, k) + ? stvObjectToQueryString(v, k) : (k == 'schain' || k == 'uids' ? k + '=' + v : encodeURIComponent(k) + '=' + encodeURIComponent(v))); } } @@ -291,129 +233,4 @@ function serializeUids(bidRequest) { return uids.join(','); } -/** - * Check if it's a banner bid request - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {boolean} True if it's a banner bid - */ -function isBannerRequest(bid) { - return bid.mediaType === 'banner' || !!deepAccess(bid, 'mediaTypes.banner') || !isVideoRequest(bid); -} - -/** - * Check if it's a video bid request - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {boolean} True if it's a video bid - */ -function isVideoRequest(bid) { - return bid.mediaType === 'video' || !!deepAccess(bid, 'mediaTypes.video'); -} - -/** - * Get video sizes - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {object} True if it's a video bid - */ -function getVideoSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); -} - -/** - * Get banner sizes - * - * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {object} True if it's a video bid - */ -function getBannerSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); -} - -/** - * Parse size - * @param sizes - * @returns {width: number, h: height} - */ -function parseSize(size) { - let sizeObj = {} - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - return sizeObj; -} - -/** - * Parse sizes - * @param sizes - * @returns {{width: number , height: number }[]} - */ -function parseSizes(sizes) { - if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]]) - return sizes.map(size => parseSize(size)); - } - return [parseSize(sizes)]; // or a single one ? (ie. [728,90]) -} - -/** - * Get MediaInfo object for server request - * - * @param mediaTypesInfo - * @returns {*} - */ -function convertMediaInfoForRequest(mediaTypesInfo) { - let requestData = {}; - Object.keys(mediaTypesInfo).forEach(mediaType => { - requestData[mediaType] = mediaTypesInfo[mediaType].map(size => { - return size.width + 'x' + size.height; - }).join(','); - }); - return requestData; -} - -/** - * Get Bid Floor - * @param bid - * @returns {number|*} - */ -function getBidFloor(bid) { - if (typeof bid.getFloor !== 'function') { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'EUR', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (_) { - return 0 - } -} - -/** - * Get media types info - * - * @param bid - */ -function getMediaTypesInfo(bid) { - let mediaTypesInfo = {}; - - if (bid.mediaTypes) { - Object.keys(bid.mediaTypes).forEach(mediaType => { - if (mediaType === BANNER) { - mediaTypesInfo[mediaType] = getBannerSizes(bid); - } - if (mediaType === VIDEO) { - mediaTypesInfo[mediaType] = getVideoSizes(bid); - } - }); - } else { - mediaTypesInfo[BANNER] = getBannerSizes(bid); - } - return mediaTypesInfo; -} - registerBidder(spec); diff --git a/modules/sublimeBidAdapter.js b/modules/sublimeBidAdapter.js index a29265ce9cd..eaae877ba0e 100644 --- a/modules/sublimeBidAdapter.js +++ b/modules/sublimeBidAdapter.js @@ -1,6 +1,7 @@ import { logInfo, generateUUID, formatQS, triggerPixel, deepAccess } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -152,7 +153,7 @@ function buildRequests(validBidRequests, bidderRequest) { pbav: SUBLIME_VERSION, // Current Prebid params prebidVersion: '$prebid.version$', - currencyCode: config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY, + currencyCode: getCurrencyFromBidderRequest(bidderRequest) || DEFAULT_CURRENCY, timeout: (typeof bidderRequest === 'object' && !!bidderRequest) ? bidderRequest.timeout : config.getConfig('bidderTimeout'), }; diff --git a/modules/symitriAnalyticsAdapter.js b/modules/symitriAnalyticsAdapter.js new file mode 100644 index 00000000000..89dc27886e3 --- /dev/null +++ b/modules/symitriAnalyticsAdapter.js @@ -0,0 +1,61 @@ +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import { EVENTS } from '../src/constants.js'; +import { logMessage } from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; + +const analyticsType = 'endpoint'; +const url = 'https://ProdSymPrebidEventhub1.servicebus.windows.net/prebid-said-1/messages'; + +const { BID_WON } = EVENTS; + +let initOptions; + +let symitriAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ eventType, args }) { + switch (eventType) { + case BID_WON: + logMessage('##### symitriAnalytics :: Event Triggered : ' + eventType); + sendEvent(args); + break; + default: + logMessage('##### symitriAnalytics :: Event Triggered : ' + eventType); + break; + } + } +}); + +function sendEvent(payload) { + try { + if (initOptions.apiAuthToken) { + const body = JSON.stringify(payload); + logMessage('##### symitriAnalytics :: sendEvent ', payload); + let cb = { + success: () => { + logMessage('##### symitriAnalytics :: Bid Reported Successfully'); + }, + error: (request, error) => { + logMessage('##### symitriAnalytics :: Bid Report Failed' + error); + } + }; + + ajax(url, cb, body, { + method: 'POST', + customHeaders: {'Content-Type': 'application/atom+xml;type=entry;charset=utf-8', 'Authorization': initOptions.apiAuthToken} + }); + } + } catch (err) { logMessage('##### symitriAnalytics :: error' + err) } +} + +symitriAnalytics.originEnableAnalytics = symitriAnalytics.enableAnalytics; +symitriAnalytics.enableAnalytics = function (config) { + initOptions = config.options; + symitriAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: symitriAnalytics, + code: 'symitri' +}); + +export default symitriAnalytics; diff --git a/modules/symitriAnalyticsAdapter.md b/modules/symitriAnalyticsAdapter.md new file mode 100644 index 00000000000..d7da72ae166 --- /dev/null +++ b/modules/symitriAnalyticsAdapter.md @@ -0,0 +1,35 @@ +### Overview + + Symitri Analytics Adapter. + +### Integration + + 1) Build the symitriAnalyticsAdapter module into the Prebid.js package with: + + ``` + gulp build --modules=symitriAnalyticsAdapter,... + ``` + + 2) Use `enableAnalytics` to instruct Prebid.js to initilaize the symitriAnalyticsAdapter module, as specified below. + +### Configuration + +``` + pbjs.enableAnalytics({ + provider: 'symitri', + options: { + 'apiAuthToken': '' + } + }); + ``` + +Please reach out to your Symitri account representative(Prebid@symitri.com) to get provisioned on the DAP platform. + + +### Testing +To view an example of available segments returned by dap: +``` +‘gulp serve --modules=rtdModule,symitriDapRtdProvider,symitriAnalyticsAdapter,appnexusBidAdapter,sovrnBidAdapter’ +``` +and then point your browser at: +"http://localhost:9999/integrationExamples/gpt/symitridap_segments_example.html" diff --git a/modules/symitriDapRtdProvider.js b/modules/symitriDapRtdProvider.js new file mode 100644 index 00000000000..51303f772ff --- /dev/null +++ b/modules/symitriDapRtdProvider.js @@ -0,0 +1,864 @@ +/** + * This module adds the Symitri DAP RTD provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch real-time data from DAP + * @module modules/symitriDapRtdProvider + * @requires module:modules/realTimeData + */ +import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {submodule} from '../src/hook.js'; +import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ +export function createRtdProvider(moduleName, moduleCode, headerPrefix) { + const MODULE_NAME = 'realTimeData'; + const SUBMODULE_NAME = moduleName; + const MODULE_CODE = moduleCode; + + const DAP_TOKEN = 'async_dap_token'; + const DAP_MEMBERSHIP = 'async_dap_membership'; + const DAP_ENCRYPTED_MEMBERSHIP = 'encrypted_dap_membership'; + const DAP_SS_ID = 'dap_ss_id'; + const DAP_DEFAULT_TOKEN_TTL = 3600; // in seconds + const DAP_MAX_RETRY_TOKENIZE = 1; + const DAP_CLIENT_ENTROPY = 'dap_client_entropy' + + const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); + let dapRetryTokenize = 0; + + /** + * Lazy merge objects. + * @param {String} target + * @param {String} source + */ + function mergeLazy(target, source) { + if (!isPlainObject(target)) { + target = {}; + } + if (!isPlainObject(source)) { + source = {}; + } + return mergeDeep(target, source); + } + + /** + * Add real-time data & merge segments. + * @param {Object} ortb2 destination object to merge RTD into + * @param {Object} rtd + */ + function addRealTimeData(ortb2, rtd) { + logInfo('DEBUG(addRealTimeData) - ENTER'); + if (isPlainObject(rtd.ortb2)) { + logMessage('DEBUG(addRealTimeData): merging original: ', ortb2); + logMessage('DEBUG(addRealTimeData): merging in: ', rtd.ortb2); + mergeLazy(ortb2, rtd.ortb2); + } + logInfo('DEBUG(addRealTimeData) - EXIT'); + } + + /** + * Real-time data retrieval from Audigent + * @param {Object} bidConfig + * @param {function} onDone + * @param {Object} rtdConfig + * @param {Object} userConsent + */ + function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { + let entropyDict = JSON.parse(storage.getDataFromLocalStorage(DAP_CLIENT_ENTROPY)); + + // Attempt to load entroy script if no entropy object exist and entropy config settings are present. + // Else + if (!entropyDict && rtdConfig && rtdConfig.params && dapUtils.isValidHttpsUrl(rtdConfig.params.dapEntropyUrl)) { + let loadScriptPromise = new Promise((resolve, reject) => { + if (rtdConfig && rtdConfig.params && rtdConfig.params.dapEntropyTimeout && Number.isInteger(rtdConfig.params.dapEntropyTimeout)) { + setTimeout(reject, rtdConfig.params.dapEntropyTimeout, Error('DapEntropy script could not be loaded')); + } + if (entropyDict && entropyDict.expires_at > Math.round(Date.now() / 1000.0)) { + logMessage('Using cached entropy'); + resolve(); + } else { + if (typeof window.dapCalculateEntropy === 'function') { + window.dapCalculateEntropy(resolve, reject); + } else { + if (rtdConfig && rtdConfig.params && dapUtils.isValidHttpsUrl(rtdConfig.params.dapEntropyUrl)) { + loadExternalScript(rtdConfig.params.dapEntropyUrl, MODULE_TYPE_RTD, MODULE_CODE, () => { + dapUtils.dapGetEntropy(resolve, reject) + }); + } else { + reject(Error('Please check if dapEntropyUrl is specified and is valid under config.params')); + } + } + } + }); + + loadScriptPromise + .catch((error) => { + logError('Entropy could not be calculated due to: ', error.message); + }) + .finally(() => { + generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent); + }); + } else { + logMessage('No dapEntropyUrl is specified.'); + generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent); + } + } + + function generateRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { + logInfo('DEBUG(generateRealTimeData) - ENTER'); + logMessage(' - apiHostname: ' + rtdConfig.params.apiHostname); + logMessage(' - apiVersion: ' + rtdConfig.params.apiVersion); + dapRetryTokenize = 0; + var jsonData = null; + if (rtdConfig && isPlainObject(rtdConfig.params)) { + if (rtdConfig.params.segtax == 710) { + let encMembership = dapUtils.dapGetEncryptedMembershipFromLocalStorage(); + if (encMembership) { + jsonData = dapUtils.dapGetEncryptedRtdObj(encMembership, rtdConfig.params.segtax) + } + } else { + let membership = dapUtils.dapGetMembershipFromLocalStorage(); + if (membership) { + jsonData = dapUtils.dapGetRtdObj(membership, rtdConfig.params.segtax) + } + } + } + if (jsonData) { + if (jsonData.rtd) { + addRealTimeData(bidConfig.ortb2Fragments?.global, jsonData.rtd); + onDone(); + logInfo('DEBUG(generateRealTimeData) - 1'); + // Don't return - ensure the data is always fresh. + } + } + // Calling setTimeout to release the main thread so that the bid request could be sent. + setTimeout(dapUtils.callDapAPIs, 0, bidConfig, onDone, rtdConfig, userConsent); + } + + /** + * Module init + * @param {Object} config + * @param {Object} userConsent + * @return {boolean} + */ + function init(config, userConsent) { + if (dapUtils.checkConsent(userConsent) === false) { + return false; + } + return true; + } + + function onBidResponse(bidResponse, config, userConsent) { + if (bidResponse.dealId && typeof (bidResponse.dealId) != typeof (undefined)) { + let membership = dapUtils.dapGetMembershipFromLocalStorage(); // Get Membership details from Local Storage + let deals = membership.deals; // Get list of Deals the user is mapped to + deals.forEach((deal) => { + deal = JSON.parse(deal); + if (bidResponse.dealId == deal.id) { // Check if the bid response deal Id matches to the deals mapped to the user + let token = dapUtils.dapGetTokenFromLocalStorage(); + let url = config.params.pixelUrl + '?token=' + token + '&ad_id=' + bidResponse.adId + '&bidder=' + bidResponse.bidder + '&bidder_code=' + bidResponse.bidderCode + '&cpm=' + bidResponse.cpm + '&creative_id=' + bidResponse.creativeId + '&deal_id=' + bidResponse.dealId + '&media_type=' + bidResponse.mediaType + '&response_timestamp=' + bidResponse.responseTimestamp; + bidResponse.ad = `${bidResponse.ad} - - - `; } registerBidder(spec); diff --git a/modules/targetVideoBidAdapter.md b/modules/targetVideoBidAdapter.md index 557c9f94410..a34ad0aff27 100644 --- a/modules/targetVideoBidAdapter.md +++ b/modules/targetVideoBidAdapter.md @@ -3,17 +3,17 @@ ``` Module Name: Target Video Bid Adapter Module Type: Bidder Adapter -Maintainer: grajzer@gmail.com +Maintainers: grajzer@gmail.com, danijel.ristic@target-video.com ``` # Description Connects to Appnexus exchange for bids. -TargetVideo bid adapter supports Banner. +TargetVideo bid adapter supports Banner and Video. # Test Parameters -``` +```js var adUnits = [ // Banner adUnit { @@ -29,6 +29,23 @@ var adUnits = [ placementId: 13232361 } }] + }, + // Video adUnit + { + mediaTypes: { + video: { + playerSize: [[640, 360]], + context: 'instream', + playbackmethod: [1, 2, 3, 4] + } + }, + bids: [{ + bidder: 'targetVideo', + params: { + placementId: 12345, + reserve: 0, + } + }] } ]; ``` diff --git a/modules/gdprEnforcement.js b/modules/tcfControl.js similarity index 90% rename from modules/gdprEnforcement.js rename to modules/tcfControl.js index caa498c7364..603c91443a3 100644 --- a/modules/gdprEnforcement.js +++ b/modules/tcfControl.js @@ -6,8 +6,8 @@ import {deepAccess, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; import * as events from '../src/events.js'; -import { EVENTS } from '../src/constants.js'; -import {GDPR_GVLIDS, VENDORLESS_GVLID, FIRST_PARTY_GVLID} from '../src/consentHandler.js'; +import {EVENTS} from '../src/constants.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; import { MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, @@ -23,10 +23,14 @@ import { import {registerActivityControl} from '../src/activities/rules.js'; import { ACTIVITY_ACCESS_DEVICE, - ACTIVITY_ENRICH_EIDS, ACTIVITY_ENRICH_UFPD, + ACTIVITY_ENRICH_EIDS, + ACTIVITY_ENRICH_UFPD, ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS, - ACTIVITY_SYNC_USER, ACTIVITY_TRANSMIT_EIDS, ACTIVITY_TRANSMIT_PRECISE_GEO, ACTIVITY_TRANSMIT_UFPD + ACTIVITY_SYNC_USER, + ACTIVITY_TRANSMIT_EIDS, + ACTIVITY_TRANSMIT_PRECISE_GEO, + ACTIVITY_TRANSMIT_UFPD } from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; @@ -37,7 +41,7 @@ export const ACTIVE_RULES = { }; const CONSENT_PATHS = { - purpose: 'purpose.consents', + purpose: false, feature: 'specialFeatureOptins' }; @@ -98,6 +102,7 @@ const RULE_HANDLES = []; // in JS we do not have access to the GVL; assume that everyone declares legitimate interest for basic ads const LI_PURPOSES = [2]; +const PUBLISHER_LI_PURPOSES = [2, 7, 9, 10]; /** * Retrieve a module's GVL ID. @@ -111,7 +116,7 @@ export function getGvlid(moduleType, moduleName, fallbackFn) { if (gvlMapping && gvlMapping[moduleName]) { return gvlMapping[moduleName]; } else if (moduleType === MODULE_TYPE_PREBID) { - return moduleName === 'cdep' ? FIRST_PARTY_GVLID : VENDORLESS_GVLID; + return VENDORLESS_GVLID; } else { let {gvlid, modules} = GDPR_GVLIDS.get(moduleName); if (gvlid == null && Object.keys(modules).length > 0) { @@ -163,15 +168,25 @@ export function shouldEnforce(consentData, purpose, name) { return consentData && consentData.gdprApplies; } -function getConsent(consentData, type, id, gvlId) { - let purpose = !!deepAccess(consentData, `vendorData.${CONSENT_PATHS[type]}.${id}`); - let vendor = !!deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`); +function getConsentOrLI(consentData, path, id, acceptLI) { + const data = deepAccess(consentData, `vendorData.${path}`); + return !!data?.consents?.[id] || (acceptLI && !!data?.legitimateInterests?.[id]); +} - if (type === 'purpose' && LI_PURPOSES.includes(id)) { - purpose ||= !!deepAccess(consentData, `vendorData.purpose.legitimateInterests.${id}`); - vendor ||= !!deepAccess(consentData, `vendorData.vendor.legitimateInterests.${gvlId}`); +function getConsent(consentData, type, purposeNo, gvlId) { + let purpose; + if (CONSENT_PATHS[type] !== false) { + purpose = !!deepAccess(consentData, `vendorData.${CONSENT_PATHS[type]}.${purposeNo}`); + } else { + const [path, liPurposes] = gvlId === VENDORLESS_GVLID + ? ['publisher', PUBLISHER_LI_PURPOSES] + : ['purpose', LI_PURPOSES]; + purpose = getConsentOrLI(consentData, path, purposeNo, liPurposes.includes(purposeNo)); + } + return { + purpose, + vendor: getConsentOrLI(consentData, 'vendor', gvlId, LI_PURPOSES.includes(purposeNo)) } - return {purpose, vendor}; } /** @@ -192,14 +207,7 @@ export function validateRules(rule, consentData, currentModule, gvlId) { } const vendorConsentRequred = rule.enforceVendor && !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); const {purpose, vendor} = getConsent(consentData, ruleOptions.type, ruleOptions.id, gvlId); - - let validation = (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor); - - if (gvlId === FIRST_PARTY_GVLID) { - validation = (!rule.enforcePurpose || !!deepAccess(consentData, `vendorData.publisher.consents.${ruleOptions.id}`)); - } - - return validation; + return (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor); } function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback = () => null) { diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index a3c8d3e24dc..97cfa273b1e 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,7 +1,9 @@ -import {getValue, logError, deepAccess, parseSizesInput, isArray, getBidIdParameter} from '../src/utils.js'; +import {logError, deepAccess, parseSizesInput, isArray, getBidIdParameter, getWinDimensions} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {isAutoplayEnabled} from '../libraries/autoplayDetection/autoplay.js'; +import {getDM, getHC, getHLen} from '../libraries/navigatorData/navigatorData.js'; +import {getTimeToFirstByte} from '../libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -17,7 +19,10 @@ const gdprStatus = { GDPR_DOESNT_APPLY: 0, CMP_NOT_FOUND_OR_ERROR: 22 }; + const FP_TEADS_ID_COOKIE_NAME = '_tfpvi'; +const OB_USER_TOKEN_KEY = 'OB-USER-TOKEN'; + export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { @@ -33,8 +38,8 @@ export const spec = { isBidRequestValid: function(bid) { let isValid = false; if (typeof bid.params !== 'undefined') { - let isValidPlacementId = _validateId(getValue(bid.params, 'placementId')); - let isValidPageId = _validateId(getValue(bid.params, 'pageId')); + let isValidPlacementId = _validateId(bid.params.placementId); + let isValidPageId = _validateId(bid.params.pageId); isValid = isValidPlacementId && isValidPageId; } @@ -59,19 +64,23 @@ export const spec = { pageTitle: getPageTitle().slice(0, 300), pageDescription: getPageDescription().slice(0, 300), networkBandwidth: getConnectionDownLink(window.navigator), + networkQuality: getNetworkQuality(window.navigator), timeToFirstByte: getTimeToFirstByte(window), data: bids, + domComplexity: getDomComplexity(document), + device: bidderRequest?.ortb2?.device || {}, deviceWidth: screen.width, deviceHeight: screen.height, devicePixelRatio: topWindow.devicePixelRatio, screenOrientation: screen.orientation?.type, - historyLength: topWindow.history?.length, - viewportHeight: topWindow.visualViewport?.height, - viewportWidth: topWindow.visualViewport?.width, - hardwareConcurrency: topWindow.navigator?.hardwareConcurrency, - deviceMemory: topWindow.navigator?.deviceMemory, + historyLength: getHLen(), + viewportHeight: getWinDimensions().visualViewport.height, + viewportWidth: getWinDimensions().visualViewport.width, + hardwareConcurrency: getHC(), + deviceMemory: getDM(), hb_version: '$prebid.version$', ...getSharedViewerIdParameters(validBidRequests), + outbrainId: storage.getDataFromLocalStorage(OB_USER_TOKEN_KEY), ...getFirstPartyTeadsIdParameter(validBidRequests) }; @@ -81,6 +90,18 @@ export const spec = { payload.schain = firstBidRequest.schain; } + let gpp = bidderRequest.gppConsent; + if (bidderRequest && gpp) { + let isValidConsentString = typeof gpp.gppString === 'string'; + let validateApplicableSections = + Array.isArray(gpp.applicableSections) && + gpp.applicableSections.every((section) => typeof (section) === 'number') + payload.gpp = { + consentString: isValidConsentString ? gpp.gppString : '', + applicableSectionIds: validateApplicableSections ? gpp.applicableSections : [], + }; + } + let gdpr = bidderRequest.gdprConsent; if (bidderRequest && gdpr) { let isCmp = typeof gdpr.gdprApplies === 'boolean'; @@ -99,12 +120,12 @@ export const spec = { payload.us_privacy = bidderRequest.uspConsent; } - const userAgentClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua'); + const userAgentClientHints = firstBidRequest?.ortb2?.device?.sua; if (userAgentClientHints) { payload.userAgentClientHints = userAgentClientHints; } - const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); + const dsa = bidderRequest?.ortb2?.regs?.ext?.dsa; if (dsa) { payload.dsa = dsa; } @@ -230,33 +251,14 @@ function getConnectionDownLink(nav) { return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; } -function getTimeToFirstByte(win) { - const performance = win.performance || win.webkitPerformance || win.msPerformance || win.mozPerformance; - - const ttfbWithTimingV2 = performance && - typeof performance.getEntriesByType === 'function' && - Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' && - performance.getEntriesByType('navigation')[0] && - performance.getEntriesByType('navigation')[0].responseStart && - performance.getEntriesByType('navigation')[0].requestStart && - performance.getEntriesByType('navigation')[0].responseStart > 0 && - performance.getEntriesByType('navigation')[0].requestStart > 0 && - Math.round( - performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart - ); - - if (ttfbWithTimingV2) { - return ttfbWithTimingV2.toString(); - } +function getNetworkQuality(navigator) { + const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; - const ttfbWithTimingV1 = performance && - performance.timing.responseStart && - performance.timing.requestStart && - performance.timing.responseStart > 0 && - performance.timing.requestStart > 0 && - performance.timing.responseStart - performance.timing.requestStart; + return connection?.effectiveType ?? ''; +} - return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; +function getDomComplexity(document) { + return document?.querySelectorAll('*')?.length ?? -1; } function findGdprStatus(gdprApplies, gdprData) { @@ -273,10 +275,10 @@ function findGdprStatus(gdprApplies, gdprData) { function buildRequestObject(bid) { const reqObj = {}; - let placementId = getValue(bid.params, 'placementId'); - let pageId = getValue(bid.params, 'pageId'); - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); - const videoPlcmt = deepAccess(bid, 'mediaTypes.video.plcmt'); + let placementId = bid.params.placementId; + let pageId = bid.params.pageId; + const gpid = bid?.ortb2Imp?.ext?.gpid; + const videoPlcmt = bid?.mediaTypes?.video?.plcmt; reqObj.sizes = getSizes(bid); reqObj.bidId = getBidIdParameter('bidId', bid); @@ -295,9 +297,9 @@ function getSizes(bid) { } function concatSizes(bid) { - let playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); - let videoSizes = deepAccess(bid, 'mediaTypes.video.sizes'); - let bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); + let playerSize = bid?.mediaTypes?.video?.playerSize; + let videoSizes = bid?.mediaTypes?.video?.sizes; + let bannerSizes = bid?.mediaTypes?.banner?.sizes; if (isArray(bannerSizes) || isArray(playerSize) || isArray(videoSizes)) { let mediaTypesSizes = [bannerSizes, videoSizes, playerSize]; @@ -329,7 +331,7 @@ function _validateId(id) { * @returns `{} | {firstPartyCookieTeadsId: string}` */ function getFirstPartyTeadsIdParameter(validBidRequests) { - const firstPartyTeadsIdFromUserIdModule = deepAccess(validBidRequests, '0.userId.teadsId'); + const firstPartyTeadsIdFromUserIdModule = validBidRequests?.[0]?.userId?.teadsId; if (firstPartyTeadsIdFromUserIdModule) { return {firstPartyCookieTeadsId: firstPartyTeadsIdFromUserIdModule}; diff --git a/modules/teadsIdSystem.js b/modules/teadsIdSystem.js index 8026fe77f87..16b8e97820a 100644 --- a/modules/teadsIdSystem.js +++ b/modules/teadsIdSystem.js @@ -9,7 +9,6 @@ import {isStr, isNumber, logError, logInfo, isEmpty, timestamp} from '../src/uti import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; -import {uspDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; /** @@ -108,9 +107,9 @@ export const teadsIdSubmodule = { export function buildAnalyticsTagUrl(submoduleConfig, consentData) { const pubId = getPublisherId(submoduleConfig); const teadsViewerId = getTeadsViewerId(); - const status = getGdprStatus(consentData); - const gdprConsentString = getGdprConsentString(consentData); - const ccpaConsentString = getCcpaConsentString(uspDataHandler?.getConsentData()); + const status = getGdprStatus(consentData?.gdpr); + const gdprConsentString = getGdprConsentString(consentData?.gdpr); + const ccpaConsentString = getCcpaConsentString(consentData?.usp); const gdprReason = getGdprReasonFromStatus(status); const params = { analytics_tag_id: pubId, diff --git a/modules/tealBidAdapter.js b/modules/tealBidAdapter.js new file mode 100644 index 00000000000..03b983d25d9 --- /dev/null +++ b/modules/tealBidAdapter.js @@ -0,0 +1,145 @@ +import {deepSetValue, deepAccess, triggerPixel, deepClone, isEmpty, logError, shuffle} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import {BANNER} from '../src/mediaTypes.js'; +import {pbsExtensions} from '../libraries/pbsExtensions/pbsExtensions.js' +const BIDDER_CODE = 'teal'; +const GVLID = 1378; +const DEFAULT_ENDPOINT = 'https://a.bids.ws/openrtb2/auction'; +const COOKIE_SYNC_ENDPOINT = 'https://a.bids.ws/cookie_sync'; +const COOKIE_SYNC_IFRAME = 'https://bids.ws/load-cookie.html'; +const MAX_SYNC_COUNT = 10; + +const converter = ortbConverter({ + processors: pbsExtensions, + context: { + netRevenue: true, + ttl: 30 + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const { placement, testMode } = bidRequest.params; + if (placement) { + deepSetValue(imp, 'ext.prebid.storedrequest.id', placement); + } + if (testMode) { + deepSetValue(imp, 'ext.prebid.storedauctionresponse.id', placement); + } + delete imp.ext.prebid.bidder; + return imp; + }, + overrides: { + bidResponse: { + bidderCode(orig, bidResponse, bid, { bidRequest }) { + let useSourceBidderCode = deepAccess(bidRequest, 'params.useSourceBidderCode', false); + if (useSourceBidderCode) { + orig.apply(this, [...arguments].slice(1)); + } + }, + }, + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + aliases: [], + + isBidRequestValid: function(bid) { + return Boolean(bid.params?.account); + }, + + buildRequests: function(bidRequests, bidderRequest) { + const { bidder } = bidRequests[0]; + const data = converter.toORTB({bidRequests, bidderRequest}); + const account = deepAccess(bidRequests[0], 'params.account', null); + const subAccount = deepAccess(bidRequests[0], 'params.subAccount', null); + deepSetValue(data, 'site.publisher.id', account); + deepSetValue(data, 'ext.prebid.storedrequest.id', subAccount || account); + data.ext.prebid.passthrough = { + ...data.ext.prebid.passthrough, + teal: { bidder }, + }; + data.tmax = (bidderRequest.timeout || 1500) - 100; + return { + method: 'POST', + url: deepAccess(bidRequests[0], 'params.endpoint', DEFAULT_ENDPOINT), + data + }; + }, + + interpretResponse: function(response, request) { + const resp = deepClone(response.body); + const { bidder } = request.data.ext.prebid.passthrough.teal; + const modifiers = { + responsetimemillis: (values) => Math.max(...values), + errors: (values) => [].concat(...values), + }; + Object.entries(modifiers).forEach(([field, combineFn]) => { + const obj = resp.ext?.[field]; + if (!isEmpty(obj)) { + resp.ext[field] = {[bidder]: combineFn(Object.values(obj))}; + } + }); + const bids = converter.fromORTB({response: resp, request: request.data}).bids; + return bids; + }, + + getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (!syncOptions.iframeEnabled) { + return []; + } + const syncs = []; + const { gdprApplies, consentString } = gdprConsent || {}; + let bidders = []; + serverResponses.forEach(({ body }) => { + const newBidders = Object.keys(body.ext?.responsetimemillis || {}); + newBidders.forEach(s => { + if (bidders.indexOf(s) === -1) { + bidders.push(s); + } + }); + }); + bidders = shuffle(bidders).slice(0, MAX_SYNC_COUNT); + if (!bidders.length) { + return; + } + const params = { + endpoint: COOKIE_SYNC_ENDPOINT, + max_sync_count: MAX_SYNC_COUNT, + gdpr: gdprApplies ? 1 : 0, + gdpr_consent: consentString, + us_privacy: uspConsent, + bidders: bidders.join(','), + coop_sync: 0 + }; + const qs = Object.entries(params) + .filter(([k, v]) => ![null, undefined, ''].includes(v)) + .map(([k, v]) => `${k}=${encodeURIComponent(v.toString())}`) + .join('&'); + syncs.push({ type: 'iframe', url: `${COOKIE_SYNC_IFRAME}?${qs}` }); + return syncs; + }, + + onBidWon: function(bid) { + if (bid.pbsWurl) { + triggerPixel(bid.pbsWurl); + } + if (bid.burl) { + triggerPixel(bid.burl); + } + }, + + onBidderError: function({ error, bidderRequest }) { + if (error.responseText && error.status) { + let id = error.responseText.match(/found for id: (.*)/); + if (Array.isArray(id) && id.length > 1 && error.status == 400) { + logError(`Placement: ${id[1]} not found on ${BIDDER_CODE} server. Please contact your account manager or email prebid@teal.works`, error); + return; + } + } + logError(`${BIDDER_CODE} bidder error`, error); + } +} +registerBidder(spec); diff --git a/modules/tealBidAdapter.md b/modules/tealBidAdapter.md new file mode 100644 index 00000000000..18b654c8108 --- /dev/null +++ b/modules/tealBidAdapter.md @@ -0,0 +1,46 @@ +Overview +======== + +``` +Module Name: Teal Adapter +Module Type: Bidder Adapter +Maintainer: prebid@teal.works +``` + +Description +=========== + +This module connects web publishers to Teal's server-side banner demand. + +# Bid Parameters + +| Key | Required | Example | Description | +| --- | -------- | ------- | ----------- | +| `account` | yes | `myaccount` | account name provided by your account manager - set to `test-account` for test mode | +| `placement` | no | `mysite300x250` | placement name provided by your account manager - set to `test-placement300x250` for test mode | +| `testMode` | no | `true` | activate test mode - 100% test bids - placement needs be set to `test-placement300x250` for this option to work | +| `useSourceBidderCode` | no | `true` | use seat bidder code as hb_bidder, instead of teal (or configured alias) | +| `subAccount` | no | `mysubaccount` | subAccount name, if provided by your account manager | + +### Notes + +- Specific ads.txt entries are required for the Teal bid adapter - please contact your account manager or for more details. +- This adapter requires iframe user syncs to be enabled to support uids. +- For full functionality in GDPR territories, please ensure Teal Digital Group Ltd is configured in your CMP. + +# Test Parameters + +``` +var adUnits = [{ + code: 'test-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'teal', + params: { + account: 'test-account', + placement: 'test-placement300x250', + testMode: true + }, + }] +}] +``` diff --git a/modules/telariaBidAdapter.js b/modules/telariaBidAdapter.js index 38eefd447a8..4ad544aaa50 100644 --- a/modules/telariaBidAdapter.js +++ b/modules/telariaBidAdapter.js @@ -1,6 +1,7 @@ import { logError, isEmpty, deepAccess, triggerPixel, logWarn, isArray } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {VIDEO} from '../src/mediaTypes.js'; +import {getSupplyChain} from '../libraries/riseUtils/index.js'; const BIDDER_CODE = 'telaria'; const DOMAIN = 'tremorhub.com'; @@ -127,37 +128,6 @@ function getDefaultSrcPageUrl() { return encodeURIComponent(document.location.href); } -function getEncodedValIfNotEmpty(val) { - return (val !== '' && val !== undefined) ? encodeURIComponent(val) : ''; -} - -/** - * Converts the schain object to a url param value. Please refer to - * https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md - * (schain for non ORTB section) for more information - * @param schainObject - * @returns {string} - */ -function getSupplyChainAsUrlParam(schainObject) { - if (isEmpty(schainObject)) { - return ''; - } - - let scStr = `&schain=${schainObject.ver},${schainObject.complete}`; - - schainObject.nodes.forEach((node) => { - scStr += '!'; - scStr += `${getEncodedValIfNotEmpty(node.asi)},`; - scStr += `${getEncodedValIfNotEmpty(node.sid)},`; - scStr += `${getEncodedValIfNotEmpty(node.hp)},`; - scStr += `${getEncodedValIfNotEmpty(node.rid)},`; - scStr += `${getEncodedValIfNotEmpty(node.name)},`; - scStr += `${getEncodedValIfNotEmpty(node.domain)}`; - }); - - return scStr; -} - function getUrlParams(params, schainFromBidRequest) { let urlSuffix = ''; @@ -167,7 +137,7 @@ function getUrlParams(params, schainFromBidRequest) { urlSuffix += `&${key}=${params[key]}`; } } - urlSuffix += getSupplyChainAsUrlParam(!isEmpty(schainFromBidRequest) ? schainFromBidRequest : params['schain']); + urlSuffix += getSupplyChain(!isEmpty(schainFromBidRequest) ? schainFromBidRequest : params['schain']); } return urlSuffix; diff --git a/modules/telariaBidAdapter.md b/modules/telariaBidAdapter.md index 6a5e24e9a5e..3a6c86ec2f6 100644 --- a/modules/telariaBidAdapter.md +++ b/modules/telariaBidAdapter.md @@ -10,7 +10,7 @@ Maintainer: github@telaria.com Connects to Telaria's exchange. -Telaria bid adapter supports insteream Video. +Telaria bid adapter supports instream Video. # Test Parameters ``` diff --git a/modules/terceptAnalyticsAdapter.js b/modules/terceptAnalyticsAdapter.js index 089f8d917d6..289a19beb53 100644 --- a/modules/terceptAnalyticsAdapter.js +++ b/modules/terceptAnalyticsAdapter.js @@ -3,7 +3,6 @@ import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { EVENTS } from '../src/constants.js'; -import {getGlobal} from '../src/prebidGlobal.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; @@ -124,7 +123,7 @@ function send(data, status) { search: { auctionTimestamp: auctionTimestamp, terceptAnalyticsVersion: terceptAnalyticsVersion, - prebidVersion: getGlobal().version + prebidVersion: 'v' + '$prebid.version$' } }); diff --git a/modules/theAdxBidAdapter.js b/modules/theAdxBidAdapter.js index f19f7cfe515..6d3c2e07b84 100644 --- a/modules/theAdxBidAdapter.js +++ b/modules/theAdxBidAdapter.js @@ -20,6 +20,7 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'theadx'; const ENDPOINT_URL = 'https://ssp.theadx.com/request'; +const ENDPOINT_TR_URL = 'https://ssptr.theadx.com/request'; const NATIVEASSETNAMES = { 0: 'title', @@ -125,7 +126,7 @@ const NATIVEPROBS = { export const spec = { code: BIDDER_CODE, - aliases: ['theadx'], // short code + aliases: ['theadx', 'theAdx'], // short code supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** @@ -160,10 +161,11 @@ export const spec = { if (!isEmpty(validBidRequests)) { results = validBidRequests.map( bidRequest => { + let url = `${getRegionEndPoint(bidRequest)}?tagid=${bidRequest.params.tagId}`; return { method: requestType, type: requestType, - url: `${ENDPOINT_URL}?tagid=${bidRequest.params.tagId}`, + url: url, options: { withCredentials: true, }, @@ -500,6 +502,14 @@ let generateImpBody = (bidRequest, bidderRequest) => { return result; } +let getRegionEndPoint = (bidRequest) => { + if (bidRequest && bidRequest.params && bidRequest.params.region) { + if (bidRequest.params.region.toLowerCase() == 'tr') { + return ENDPOINT_TR_URL; + } + } + return ENDPOINT_URL; +}; let generatePayload = (bidRequest, bidderRequest) => { // Generate the expected OpenRTB payload @@ -511,7 +521,38 @@ let generatePayload = (bidRequest, bidderRequest) => { imp: [generateImpBody(bidRequest, bidderRequest)], }; // return payload; + let eids = getEids(bidRequest); + if (Object.keys(eids).length > 0) { + payload.ext = eids; + } return JSON.stringify(payload); }; +function getEids(bidRequest) { + let eids = {} + + let uId2 = deepAccess(bidRequest, 'userId.uid2.id'); + if (uId2) { + eids['uid2'] = uId2; + } + + let id5 = deepAccess(bidRequest, 'userId.id5id.uid'); + if (id5) { + eids['id5id'] = id5; + let id5Linktype = deepAccess(bidRequest, 'userId.id5id.ext.linkType'); + if (id5Linktype) { + eids['id5_linktype'] = id5Linktype; + } + } + let netId = deepAccess(bidRequest, 'userId.netId'); + if (netId) { + eids['netid'] = netId; + } + let sharedId = deepAccess(bidRequest, 'userId.sharedid.id'); + if (sharedId) { + eids['sharedid'] = sharedId; + } + return eids; +}; + registerBidder(spec); diff --git a/modules/tncIdSystem.js b/modules/tncIdSystem.js index 59254a9430f..198cedd26bd 100644 --- a/modules/tncIdSystem.js +++ b/modules/tncIdSystem.js @@ -1,38 +1,114 @@ +/** + * This module adds TncId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/tncIdSystem + * @requires module:modules/userId + */ + import { submodule } from '../src/hook.js'; -import { logInfo } from '../src/utils.js'; +import { parseUrl, buildUrl, logInfo, logMessage, logError } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; import { loadExternalScript } from '../src/adloader.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ const MODULE_NAME = 'tncId'; -let url = null; +const TNC_API_URL = 'https://js.tncid.app/remote.js'; +const TNC_DEFAULT_NS = '__tnc'; +const TNC_PREBID_NS = '__tncPbjs'; +const TNC_PREBIDJS_PROVIDER_ID = 'c8549079-f149-4529-a34b-3fa91ef257d1'; +const TNC_LOCAL_VALUE_KEY = 'tncid'; +let moduleConfig = null; -const waitTNCScript = (tncNS) => { +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); + +function fixURL(config, ns) { + config.params = (config && config.params) ? config.params : {}; + config.params.url = config.params.url || TNC_API_URL; + let url = parseUrl(config.params.url); + url.search = url.search || {}; + let providerId = config.params.publisherId || config.params.providerId || url.search.publisherId || url.search.providerId || TNC_PREBIDJS_PROVIDER_ID; + delete url.search.publisherId; + url.search.providerId = providerId; + url.search.ns = ns; + return url; +} + +const loadRemoteScript = function(url) { return new Promise((resolve, reject) => { - var tnc = window[tncNS]; - if (!tnc) reject(new Error('No TNC Object')); - if (tnc.tncid) resolve(tnc.tncid); - tnc.ready(() => { - tnc = window[tncNS]; - if (tnc.tncid) resolve(tnc.tncid); - else tnc.on('data-sent', () => resolve(tnc.tncid)); - }); + let endpoint = buildUrl(url); + logMessage('TNC Endpoint', endpoint); + loadExternalScript(endpoint, MODULE_TYPE_UID, MODULE_NAME, resolve); }); } -const loadRemoteScript = () => { - return new Promise((resolve) => { - loadExternalScript(url, MODULE_NAME, resolve); - }) +function TNCObject(ns) { + let tnc = window[ns]; + tnc = typeof tnc !== 'undefined' && tnc !== null && typeof tnc.ready == 'function' ? tnc : { + ready: function(f) { this.ready.q = this.ready.q || []; return typeof f == 'function' ? (this.ready.q.push(f), this) : new Promise(resolve => this.ready.q.push(resolve)); }, + }; + window[ns] = tnc; + return tnc; } -const tncCallback = function (cb) { - let tncNS = '__tnc'; - let promiseArray = []; - if (!window[tncNS]) { - tncNS = '__tncPbjs'; - promiseArray.push(loadRemoteScript()); +function getlocalValue(key) { + let value; + if (storage.hasLocalStorage()) { + value = storage.getDataFromLocalStorage(key); + } + if (!value) { + value = storage.getCookie(key); + } + + if (typeof value === 'string') { + // if it's a json object parse it and return the tncid value, otherwise assume the value is the id + if (value.charAt(0) === '{') { + try { + const obj = JSON.parse(value); + if (obj) { + return obj.tncid; + } + } catch (e) { + logError(e); + } + } else { + return value; + } } + return null; +} + +const tncCallback = async function(cb) { + try { + let tncNS = TNC_DEFAULT_NS; + let tncid = getlocalValue(TNC_LOCAL_VALUE_KEY); - return Promise.all(promiseArray).then(() => waitTNCScript(tncNS)).then(cb).catch(() => cb()); + if (!window[tncNS] || typeof window[tncNS].ready !== 'function') { + tncNS = TNC_PREBID_NS; // Register a new namespace for TNC global object + let url = fixURL(moduleConfig, tncNS); + if (!url) return cb(); + TNCObject(tncNS); // create minimal TNC object + await loadRemoteScript(url); // load remote script + } + if (!tncid) { + await new Promise(resolve => window[tncNS].ready(resolve)); + tncid = await window[tncNS].getTNCID('prebid'); // working directly with (possibly) overridden TNC Object + logMessage('tncId Module - tncid retrieved from remote script', tncid); + } else { + logMessage('tncId Module - tncid already exists', tncid); + window[tncNS].ready(() => window[tncNS].getTNCID('prebid')); + } + return cb(tncid); + } catch (err) { + logMessage('tncId Module', err); + return cb(); + } } export const tncidSubModule = { @@ -43,19 +119,28 @@ export const tncidSubModule = { }; }, gvlid: 750, + /** + * performs action to obtain id + * Use a tncid cookie first if it is present, otherwise callout to get a new id + * @function + * @param {SubmoduleConfig} [config] Config object with params and storage properties + * @param {ConsentData} [consentData] GDPR consent + * @returns {IdResponse} + */ getId(config, consentData) { - const gdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - const consentString = gdpr ? consentData.consentString : ''; + const gdpr = (consentData?.gdpr?.gdprApplies === true) ? 1 : 0; + const consentString = gdpr ? consentData.gdpr.consentString : ''; if (gdpr && !consentString) { logInfo('Consent string is required for TNCID module'); return; } - if (config.params && config.params.url) { url = config.params.url; } + moduleConfig = config; return { callback: function (cb) { return tncCallback(cb); } + // callback: tncCallback } }, eids: { diff --git a/modules/tncIdSystem.md b/modules/tncIdSystem.md index f0f98e9098f..b806d545cc4 100644 --- a/modules/tncIdSystem.md +++ b/modules/tncIdSystem.md @@ -1,14 +1,18 @@ -# TNCID UserID Module +# Overview -### Prebid Configuration +Module Name: tncIdSystem + +## Prebid Configuration First, make sure to add the TNCID submodule to your Prebid.js package with: -``` +```bash gulp build --modules=tncIdSystem,userId ``` -### TNCIDIdSystem module Configuration +## TNCIdSystem module Configuration + +Disclosure: This module loads external script unreviewed by the prebid.js community You can configure this submodule in your `userSync.userIds[]` configuration: @@ -18,16 +22,26 @@ pbjs.setConfig({ userIds: [{ name: 'tncId', params: { - url: 'https://js.tncid.app/remote.min.js' //Optional + url: 'TNC-fallback-script-url' // Fallback url, not required if onpage tag is present (ask TNC for it) + }, + storage: { + type: "cookie", + name: "tncid", + expires: 365 // in days } }], syncDelay: 5000 } }); ``` -#### Configuration Params -| Param Name | Required | Type | Description | -| --- | --- | --- | --- | -| name | Required | String | ID value for the TNCID module: `"tncId"` | -| params.url | Optional | String | Provide TNC fallback script URL, this script is loaded if there is no TNC script on page | +## Configuration Params + +The following configuration parameters are available: + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this sub-module | `"tncId"` | +| params ||| Details for the sub-module initialization || +| params.url | Optional | String | TNC script fallback URL - This script is loaded if there is no TNC script on page | `"https://js.tncid.app/remote.min.js"` | +| params.publisherId | Optional | String | Publisher ID used in TNC fallback script - As default Prebid specific Publisher ID is used | `"c8549079-f149-4529-a34b-3fa91ef257d1"` | diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js new file mode 100644 index 00000000000..4337fbb1e6b --- /dev/null +++ b/modules/topLevelPaapi.js @@ -0,0 +1,214 @@ +import {submodule} from '../src/hook.js'; +import {config} from '../src/config.js'; +import {logError, logInfo, logWarn, mergeDeep} from '../src/utils.js'; +import {auctionStore} from '../libraries/weakStore/weakStore.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {emit} from '../src/events.js'; +import {BID_STATUS, EVENTS} from '../src/constants.js'; +import {PbPromise} from '../src/utils/promise.js'; +import {getBidToRender, getRenderingData, markWinningBid} from '../src/adRendering.js'; + +let getPAAPIConfig, expandFilters, moduleConfig; + +const paapiBids = auctionStore(); +const MODULE_NAME = 'topLevelPaapi'; + +config.getConfig('paapi', (cfg) => { + moduleConfig = cfg.paapi?.topLevelSeller; + if (moduleConfig) { + getBidToRender.before(renderPaapiHook); + getBidToRender.after(renderOverrideHook); + getRenderingData.before(getRenderingDataHook); + markWinningBid.before(markWinningBidHook); + } else { + getBidToRender.getHooks({hook: renderPaapiHook}).remove(); + getBidToRender.getHooks({hook: renderOverrideHook}).remove(); + getRenderingData.getHooks({hook: getRenderingDataHook}).remove(); + markWinningBid.getHooks({hook: markWinningBidHook}).remove(); + } +}); + +function isPaapiBid(bid) { + return bid?.source === 'paapi'; +} + +function bidIfRenderable(bid) { + if (bid && !bid.urn) { + logWarn(MODULE_NAME, 'rendering in fenced frames is not supported. Consider using resolveToConfig: false', bid); + return; + } + return bid; +} + +const forRenderStack = []; + +function renderPaapiHook(next, adId, forRender = true, override = PbPromise.resolve()) { + forRenderStack.push(forRender); + const ids = parsePaapiAdId(adId); + if (ids) { + override = override.then((bid) => { + if (bid) return bid; + const [auctionId, adUnitCode] = ids; + return paapiBids(auctionId)?.[adUnitCode]?.then(bid => { + if (!bid) { + logWarn(MODULE_NAME, `No PAAPI bid found for auctionId: "${auctionId}", adUnit: "${adUnitCode}"`); + } + return bidIfRenderable(bid); + }); + }); + } + next(adId, forRender, override); +} + +function renderOverrideHook(next, bidPm) { + const forRender = forRenderStack.pop(); + if (moduleConfig?.overrideWinner) { + bidPm = bidPm.then((bid) => { + if (isPaapiBid(bid) || bid?.status === BID_STATUS.RENDERED) return bid; + return getPAAPIBids({adUnitCode: bid.adUnitCode}).then(res => { + let paapiBid = bidIfRenderable(res[bid.adUnitCode]); + if (paapiBid) { + if (!forRender) return paapiBid; + if (forRender && paapiBid.status !== BID_STATUS.RENDERED) { + paapiBid.overriddenAdId = bid.adId; + logInfo(MODULE_NAME, 'overriding contextual bid with PAAPI bid', bid, paapiBid) + return paapiBid; + } + } + return bid; + }); + }); + } + next(bidPm); +} + +export function getRenderingDataHook(next, bid, options) { + if (isPaapiBid(bid)) { + next.bail({ + width: bid.width, + height: bid.height, + adUrl: bid.urn + }); + } else { + next(bid, options); + } +} + +export function markWinningBidHook(next, bid) { + if (isPaapiBid(bid)) { + emit(EVENTS.BID_WON, bid); + next.bail(); + } else { + next(bid); + } +} + +function getBaseAuctionConfig() { + if (moduleConfig?.auctionConfig) { + return Object.assign({ + resolveToConfig: false + }, moduleConfig.auctionConfig); + } +} + +function onAuctionConfig(auctionId, auctionConfigs) { + const base = getBaseAuctionConfig(); + if (base) { + Object.entries(auctionConfigs).forEach(([adUnitCode, auctionConfig]) => { + mergeDeep(auctionConfig, base); + if (moduleConfig.autorun ?? true) { + getPAAPIBids({adUnitCode, auctionId}); + } + }); + } +} + +export function parsePaapiSize(size) { + /* From https://github.com/WICG/turtledove/blob/main/FLEDGE.md#12-interest-group-attributes: + * Each size has the format {width: widthVal, height: heightVal}, + * where the values can have either pixel units (e.g. 100 or '100px') or screen dimension coordinates (e.g. 100sw or 100sh). + */ + if (typeof size === 'number') return size; + if (typeof size === 'string') { + const px = /^(\d+)(px)?$/.exec(size)?.[1]; + if (px) { + return parseInt(px, 10); + } + } + return null; +} + +export function getPaapiAdId(auctionId, adUnitCode) { + return `paapi:/${auctionId.replace(/:/g, '::')}/:/${adUnitCode.replace(/:/g, '::')}`; +} + +export function parsePaapiAdId(adId) { + const match = /^paapi:\/(.*)\/:\/(.*)$/.exec(adId); + if (match) { + return [match[1], match[2]].map(s => s.replace(/::/g, ':')); + } +} + +/** + * Returns the PAAPI runAdAuction result for the given filters, as a map from ad unit code to auction result + * (an object with `width`, `height`, and one of `urn` or `frameConfig`). + * + * @param filters + * @param raa + * @return {Promise<{[p: string]: any}>} + */ +export function getPAAPIBids(filters, raa = (...args) => navigator.runAdAuction(...args)) { + return Promise.all( + Object.entries(expandFilters(filters)) + .map(([adUnitCode, auctionId]) => { + const bids = paapiBids(auctionId); + if (bids && !bids.hasOwnProperty(adUnitCode)) { + const auctionConfig = getPAAPIConfig({adUnitCode, auctionId})[adUnitCode]; + if (auctionConfig) { + emit(EVENTS.RUN_PAAPI_AUCTION, { + auctionId, + adUnitCode, + auctionConfig + }); + bids[adUnitCode] = new Promise((resolve, reject) => raa(auctionConfig).then(resolve, reject)) + .then(result => { + if (result) { + const bid = { + source: 'paapi', + adId: getPaapiAdId(auctionId, adUnitCode), + width: parsePaapiSize(auctionConfig.requestedSize?.width), + height: parsePaapiSize(auctionConfig.requestedSize?.height), + adUnitCode, + auctionId, + [typeof result === 'string' ? 'urn' : 'frameConfig']: result, + auctionConfig, + }; + emit(EVENTS.PAAPI_BID, bid); + return bid; + } else { + emit(EVENTS.PAAPI_NO_BID, {auctionId, adUnitCode, auctionConfig}); + return null; + } + }).catch(error => { + logError(MODULE_NAME, `error (auction "${auctionId}", adUnit "${adUnitCode}"):`, error); + emit(EVENTS.PAAPI_ERROR, {auctionId, adUnitCode, error, auctionConfig}); + return null; + }); + } + } + return bids?.[adUnitCode]?.then(res => [adUnitCode, res]); + }).filter(e => e) + ).then(result => Object.fromEntries(result)); +} + +getGlobal().getPAAPIBids = (filters) => getPAAPIBids(filters); + +export const topLevelPAAPI = { + name: MODULE_NAME, + init(params) { + getPAAPIConfig = params.getPAAPIConfig; + expandFilters = params.expandFilters; + }, + onAuctionConfig +}; +submodule('paapi', topLevelPAAPI); diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 72f4068af7f..c3b2c69394b 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -1,7 +1,7 @@ import {isEmpty, logError, logWarn, mergeDeep, safeJSONParse} from '../src/utils.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {submodule} from '../src/hook.js'; -import {GreedyPromise} from '../src/utils/promise.js'; +import {PbPromise} from '../src/utils/promise.js'; import {config} from '../src/config.js'; import {getCoreStorageManager} from '../src/storageManager.js'; import {includes} from '../src/polyfill.js'; @@ -21,35 +21,6 @@ export function reset() { iframeLoadedURL = []; } -const bidderIframeList = { - maxTopicCaller: 4, - bidders: [{ - bidder: 'pubmatic', - iframeURL: 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html' - }, { - bidder: 'rtbhouse', - iframeURL: 'https://topics.authorizedvault.com/topicsapi.html' - }, { - bidder: 'openx', - iframeURL: 'https://pa.openx.net/topics_frame.html' - }, { - bidder: 'improvedigital', - iframeURL: 'https://hb.360yield.com/privacy-sandbox/topics.html' - }, { - bidder: 'onetag', - iframeURL: 'https://onetag-sys.com/static/topicsapi.html' - }, { - bidder: 'taboola', - iframeURL: 'https://cdn.taboola.com/libtrc/static/topics/taboola-prebid-browsing-topics.html' - }, { - bidder: 'discovery', - iframeURL: 'https://api.popin.cc/topic/prebid-topics-frame.html' - }, { - bidder: 'undertone', - iframeURL: 'https://creative-p.undertone.com/spk-public/topics_frame.html' - }] -} - export const coreStorage = getCoreStorageManager(MODULE_NAME); export const topicStorageName = 'prebid:topics'; export const lastUpdated = 'lastUpdated'; @@ -121,13 +92,13 @@ export function getTopics(doc = document) { try { if (isTopicsSupported(doc)) { - topics = GreedyPromise.resolve(doc.browsingTopics()); + topics = PbPromise.resolve(doc.browsingTopics()); } } catch (e) { logError('Could not call topics API', e); } if (topics == null) { - topics = GreedyPromise.resolve([]); + topics = PbPromise.resolve([]); } return topics; @@ -158,8 +129,8 @@ export function processFpd(config, {global}, {data = topicsData} = {}) { */ export function getCachedTopics() { let cachedTopicData = []; - const topics = config.getConfig('userSync.topics') || bidderIframeList; - const bidderList = topics.bidders || []; + const topics = config.getConfig('userSync.topics'); + const bidderList = topics?.bidders || []; let storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); storedSegments && storedSegments.forEach((value, cachedBidder) => { // Check bidder exist in config for cached bidder data and then only retrieve the cached data @@ -198,7 +169,8 @@ export function receiveMessage(evt) { /** Function to store Topics data received from iframe in storage(name: "prebid:topics") - * @param {Topics} topics + * @param {string} bidder + * @param {object} topics */ export function storeInLocalStorage(bidder, topics) { const storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); @@ -240,7 +212,7 @@ function listenMessagesFromTopicIframe() { */ export function loadTopicsForBidders(doc = document) { if (!isTopicsSupported(doc)) return; - const topics = config.getConfig('userSync.topics') || bidderIframeList; + const topics = config.getConfig('userSync.topics'); if (topics) { listenMessagesFromTopicIframe(); diff --git a/modules/topicsFpdModule.md b/modules/topicsFpdModule.md index d187645f520..1f299a9eabe 100644 --- a/modules/topicsFpdModule.md +++ b/modules/topicsFpdModule.md @@ -68,6 +68,10 @@ pbjs.setConfig({ bidder: 'undertone', iframeURL: 'https://creative-p.undertone.com/spk-public/topics_frame.html', expiry: 7 // Configurable expiry days + },{ + bidder: 'vidazoo', + iframeURL: 'https://static.vidazoo.com/topics_api/topics_frame.html', + expiry: 7 // Configurable expiry days }] } .... diff --git a/modules/tpmnBidAdapter.js b/modules/tpmnBidAdapter.js index 3edc89c90ae..ab9b64977e2 100644 --- a/modules/tpmnBidAdapter.js +++ b/modules/tpmnBidAdapter.js @@ -1,4 +1,4 @@ -/* eslint-disable no-tabs */ + import { registerBidder } from '../src/adapters/bidderFactory.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { getStorageManager } from '../src/storageManager.js'; diff --git a/modules/trafficgateBidAdapter.js b/modules/trafficgateBidAdapter.js index fcd84306099..d30d79ef3a6 100644 --- a/modules/trafficgateBidAdapter.js +++ b/modules/trafficgateBidAdapter.js @@ -2,7 +2,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; import {deepAccess, mergeDeep} from '../src/utils.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'trafficgate'; const URL = 'https://[HOST].bc-plugin.com/prebidjs' @@ -13,7 +12,6 @@ export const spec = { isBidRequestValid, buildRequests, interpretResponse, - transformBidParams, isBannerBid }; @@ -88,14 +86,6 @@ const converter = ortbConverter({ } }); -function transformBidParams(params, isOpenRtb) { - return convertTypes({ - 'customFloor': 'number', - 'placementId': 'number', - 'host': 'string' - }, params); -} - function isBidRequestValid(bidRequest) { const isValid = bidRequest.params.placementId && bidRequest.params.host; if (!isValid) { diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index fc0d73dc44b..0c8bd330e11 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -58,8 +58,8 @@ export const tripleliftAdapterSpec = { tlCall = tryAppendQueryString(tlCall, 'us_privacy', bidderRequest.uspConsent); } - if (bidderRequest && bidderRequest.fledgeEnabled) { - tlCall = tryAppendQueryString(tlCall, 'fledge', bidderRequest.fledgeEnabled); + if (bidderRequest?.paapi?.enabled) { + tlCall = tryAppendQueryString(tlCall, 'fledge', bidderRequest.paapi.enabled); } if (config.getConfig('coppa') === true) { @@ -96,7 +96,7 @@ export const tripleliftAdapterSpec = { logMessage('Response with FLEDGE:', { bids, fledgeAuctionConfigs }); return { bids, - fledgeAuctionConfigs + paapi: fledgeAuctionConfigs }; } else { return bids; @@ -236,20 +236,7 @@ function _getORTBVideo(bidRequest) { } catch (err) { logWarn('Video size not defined', err); } - // honor existing publisher settings - if (video.context === 'instream') { - if (!video.placement) { - video.placement = 1; - } - } - if (video.context === 'outstream') { - if (!video.placement) { - video.placement = 3 - } else if ([3, 4, 5].indexOf(video.placement) === -1) { - logMessage(`video.placement value of ${video.placement} is invalid for outstream context. Setting placement to 3`) - video.placement = 3 - } - } + if (video.playbackmethod && Number.isInteger(video.playbackmethod)) { video.playbackmethod = Array.from(String(video.playbackmethod), Number); } @@ -268,7 +255,7 @@ function _getFloor (bid) { mediaType: _isVideoBidRequest(bid) ? 'video' : 'banner', size: '*' }); - if (typeof floorInfo === 'object' && + if (utils.isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = parseFloat(floorInfo.floor); } diff --git a/modules/truereachBidAdapter.js b/modules/truereachBidAdapter.js index 8b1656ec7a2..9dda76f6518 100755 --- a/modules/truereachBidAdapter.js +++ b/modules/truereachBidAdapter.js @@ -11,7 +11,7 @@ export const spec = { supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { - return (bidRequest.params.site_id && bidRequest.params.bidfloor && + return (bidRequest.params.site_id && deepAccess(bidRequest, 'mediaTypes.banner') && (deepAccess(bidRequest, 'mediaTypes.banner.sizes.length') > 0)); }, @@ -116,8 +116,6 @@ function buildCommonQueryParamsFromBids(validBidRequests, bidderRequest) { adH = adSizes[0][1]; } - let bidFloor = Number(0); - let domain = window.location.host; let page = window.location.host + window.location.pathname + location.search + location.hash; @@ -129,8 +127,7 @@ function buildCommonQueryParamsFromBids(validBidRequests, bidderRequest) { banner: { w: adW, h: adH - }, - bidfloor: bidFloor + } } ], site: { diff --git a/modules/ttdBidAdapter.js b/modules/ttdBidAdapter.js index d7705f2f5df..4bc0a6c2ded 100644 --- a/modules/ttdBidAdapter.js +++ b/modules/ttdBidAdapter.js @@ -2,7 +2,8 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import {isNumber} from '../src/utils.js'; +import { isNumber } from '../src/utils.js'; +import { getConnectionType } from '../libraries/connectionInfo/connectionUtils.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -13,10 +14,11 @@ import {isNumber} from '../src/utils.js'; * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync */ -const BIDADAPTERVERSION = 'TTD-PREBID-2023.09.05'; +const BIDADAPTERVERSION = 'TTD-PREBID-2024.07.28'; const BIDDER_CODE = 'ttd'; const BIDDER_CODE_LONG = 'thetradedesk'; const BIDDER_ENDPOINT = 'https://direct.adsrvr.org/bid/bidder/'; +const BIDDER_ENDPOINT_HTTP2 = 'https://d2.adsrvr.org/bid/bidder/'; const USER_SYNC_ENDPOINT = 'https://match.adsrvr.org'; const MEDIA_TYPE = { @@ -99,33 +101,6 @@ function getDevice(firstPartyData) { return device; }; -function getConnectionType() { - const connection = navigator.connection || navigator.webkitConnection; - if (!connection) { - return 0; - } - switch (connection.type) { - case 'ethernet': - return 1; - case 'wifi': - return 2; - case 'cellular': - switch (connection.effectiveType) { - case 'slow-2g': - case '2g': - return 4; - case '3g': - return 5; - case '4g': - return 6; - default: - return 3; - } - default: - return 0; - } -} - function getUser(bidderRequest, firstPartyData) { let user = {}; if (bidderRequest.gdprConsent) { @@ -241,7 +216,7 @@ function banner(bid) { }, optionalParams); - const battr = utils.deepAccess(bid, 'ortb2Imp.battr'); + const battr = utils.deepAccess(bid, 'ortb2Imp.banner.battr'); if (battr) { banner.battr = battr; } @@ -318,7 +293,7 @@ function video(bid) { video.maxbitrate = maxbitrate; } - const battr = utils.deepAccess(bid, 'ortb2Imp.battr'); + const battr = utils.deepAccess(bid, 'ortb2Imp.video.battr'); if (battr) { video.battr = battr; } @@ -327,6 +302,13 @@ function video(bid) { } } +function selectEndpoint(params) { + if (params.useHttp2) { + return BIDDER_ENDPOINT_HTTP2; + } + return BIDDER_ENDPOINT; +} + export const spec = { code: BIDDER_CODE, gvlid: 21, @@ -408,12 +390,13 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {BidRequest[]} an array of validBidRequests - * @param {*} bidderRequest - * @return {ServerRequest} Info describing the request to the server. + * @param {BidRequest[]} validBidRequests - An array of valid bid requests + * @param {*} bidderRequest - The current bidder request object + * @returns {ServerRequest} - Info describing the request to the server */ buildRequests: function (validBidRequests, bidderRequest) { const firstPartyData = bidderRequest.ortb2 || {}; + const firstPartyImpData = bidderRequest.ortb2Imp || {}; let topLevel = { id: bidderRequest.bidderRequestId, imp: validBidRequests.map(bidRequest => getImpression(bidRequest)), @@ -436,21 +419,28 @@ export const spec = { } if (firstPartyData && firstPartyData.app) { - topLevel.app = firstPartyData.app + topLevel.app = firstPartyData.app; } - if (firstPartyData && firstPartyData.pmp) { - topLevel.pmp = firstPartyData.pmp + if ((firstPartyData && firstPartyData.pmp) || (firstPartyImpData && firstPartyImpData.pmp)) { + topLevel.imp.forEach(imp => { + imp.pmp = utils.mergeDeep( + {}, + imp.pmp || {}, + firstPartyData?.pmp || {}, + firstPartyImpData?.pmp || {} + ); + }); } - let url = BIDDER_ENDPOINT + bidderRequest.bids[0].params.supplySourceId; + let url = selectEndpoint(bidderRequest.bids[0].params) + bidderRequest.bids[0].params.supplySourceId; let serverRequest = { method: 'POST', url: url, data: topLevel, options: { - withCredentials: true + withCredentials: true, } }; @@ -475,7 +465,7 @@ export const spec = { * - vastXml * - dealId * - * @param {ttdResponseObj} bidResponse A successful response from ttd. + * @param {Object} response A successful response from ttd. * @param {ServerRequest} serverRequest The result of buildRequests() that lead to this response. * @return {Bid[]} An array of formatted bids. */ diff --git a/modules/twistDigitalBidAdapter.js b/modules/twistDigitalBidAdapter.js new file mode 100644 index 00000000000..bed9e531a26 --- /dev/null +++ b/modules/twistDigitalBidAdapter.js @@ -0,0 +1,39 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import { + isBidRequestValid, createInterpretResponseFn, createUserSyncGetter, createBuildRequestsFn, onBidWon +} from '../libraries/vidazooUtils/bidderUtils.js'; + +const GVLID = 1292; +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'twistdigital'; +const BIDDER_VERSION = '1.0.0'; + +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.twist.win`; +} + +const buildRequests = createBuildRequestsFn(createDomain, null, storage, BIDDER_CODE, BIDDER_VERSION, true); + +const interpretResponse = createInterpretResponseFn(BIDDER_CODE, true); + +const getUserSyncs = createUserSyncGetter({ + iframeSyncUrl: 'https://sync.twist.win/api/sync/iframe', imageSyncUrl: 'https://sync.twist.win/api/sync/image' +}); + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onBidWon +}; + +registerBidder(spec); diff --git a/modules/minutemediaplusBidAdapter.md b/modules/twistDigitalBidAdapter.md similarity index 72% rename from modules/minutemediaplusBidAdapter.md rename to modules/twistDigitalBidAdapter.md index 410c00e7017..8722608c5dd 100644 --- a/modules/minutemediaplusBidAdapter.md +++ b/modules/twistDigitalBidAdapter.md @@ -1,14 +1,21 @@ # Overview -**Module Name:** MinuteMediaPlus Bidder Adapter +**Module Name:** Twist Digital Bidder Adapter **Module Type:** Bidder Adapter -**Maintainer:** hb@minutemedia.com +**Maintainer:** yoni@twist.win + + + + + + + # Description -Module that connects to MinuteMediaPlus's demand sources. +Module that connects to Twist Digital demand sources. # Test Parameters ```js @@ -18,7 +25,7 @@ var adUnits = [ sizes: [[300, 250]], bids: [ { - bidder: 'mmplus', + bidder: 'twistdigital', params: { cId: '562524b21b1c1f08117fc7f9', pId: '59ac17c192832d0011283fe3', diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index 19b933a8666..d5017db0705 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -235,7 +235,7 @@ function getFloor(bid, size, mediaTypes) { mediaType: getMediaType(mediaTypes), size: (size) ? [ size[0], size[1] ] : '*', }); - if (bidFloor.currency === CURRENCY) { + if (bidFloor?.currency === CURRENCY) { return bidFloor.floor; } } diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js index 32d2322e9bd..4b5c4da7396 100644 --- a/modules/uid2IdSystem.js +++ b/modules/uid2IdSystem.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ /** * This module adds uid2 ID support to the User ID module * The {@link module:modules/userId} module is required. @@ -69,12 +68,12 @@ export const uid2IdSubmodule = { /** * performs action to obtain id and return a value. * @function - * @param {SubmoduleConfig} [configparams] + * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @returns {uid2Id} */ getId(config, consentData) { - if (consentData?.gdprApplies === true) { + if (consentData?.gdpr?.gdprApplies === true) { _logWarn('UID2 is not intended for use where GDPR applies. The UID2 module will not run.'); return; } @@ -109,6 +108,10 @@ function decodeImpl(value) { const result = { uid2: { id: value } }; return result; } + if (value.latestToken === 'optout') { + _logInfo('Found optout token. Refresh is unavailable for this token.'); + return { uid2: { optout: true } }; + } if (Date.now() < value.latestToken.identity_expires) { return { uid2: { id: value.latestToken.advertising_token } }; } diff --git a/modules/uid2IdSystem_shared.js b/modules/uid2IdSystem_shared.js index f3702069d10..a9acfd6291e 100644 --- a/modules/uid2IdSystem_shared.js +++ b/modules/uid2IdSystem_shared.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { ajax } from '../src/ajax.js'; import { cyrb53Hash } from '../src/utils.js'; @@ -8,12 +7,22 @@ function isValidIdentity(identity) { return !!(typeof identity === 'object' && identity !== null && identity.advertising_token && identity.identity_expires && identity.refresh_from && identity.refresh_token && identity.refresh_expires); } +// Helper function to prepend message +function prependMessage(message) { + return `UID2 shared library - ${message}`; +} + +// Wrapper function for logInfo +function logInfoWrapper(logInfo, ...args) { + logInfo(prependMessage(args[0]), ...args.slice(1)); +} + // This is extracted from an in-progress API client. Once it's available via NPM, this class should be replaced with the NPM package. export class Uid2ApiClient { constructor(opts, clientId, logInfo, logWarn) { this._baseUrl = opts.baseUrl; this._clientVersion = clientId; - this._logInfo = logInfo; + this._logInfo = (...args) => logInfoWrapper(logInfo, ...args); this._logWarn = logWarn; } @@ -36,7 +45,7 @@ export class Uid2ApiClient { if (this.isValidRefreshResponse(response)) { if (response.status === 'success') { return { status: response.status, identity: response.body }; } return response; - } else { return `Response didn't contain a valid status`; } + } else { return prependMessage(`Response didn't contain a valid status`); } } callRefreshApi(refreshDetails) { const url = this._baseUrl + '/v2/token/refresh'; @@ -54,7 +63,7 @@ export class Uid2ApiClient { this._logInfo('No response decryption key available, assuming unencrypted JSON'); const response = JSON.parse(responseText); const result = this.ResponseToRefreshResult(response); - if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } + if (typeof result === 'string') { rejectPromise(prependMessage(result)); } else { resolvePromise(result); } } else { this._logInfo('Decrypting refresh API response'); const encodeResp = this.createArrayBuffer(atob(responseText)); @@ -70,12 +79,12 @@ export class Uid2ApiClient { this._logInfo('Decrypted to:', decryptedResponse); const response = JSON.parse(decryptedResponse); const result = this.ResponseToRefreshResult(response); - if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } - }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); - }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + if (typeof result === 'string') { rejectPromise(prependMessage(result)); } else { resolvePromise(result); } + }, (reason) => this._logWarn(prependMessage(`Call to UID2 API failed`), reason)); + }, (reason) => this._logWarn(prependMessage(`Call to UID2 API failed`), reason)); } } catch (_err) { - rejectPromise(responseText); + rejectPromise(prependMessage(responseText)); } }, error: (error, xhr) => { @@ -83,9 +92,9 @@ export class Uid2ApiClient { this._logInfo('Error status, assuming unencrypted JSON'); const response = JSON.parse(xhr.responseText); const result = this.ResponseToRefreshResult(response); - if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } + if (typeof result === 'string') { rejectPromise(prependMessage(result)); } else { resolvePromise(result); } } catch (_e) { - rejectPromise(error) + rejectPromise(prependMessage(error)); } } }, refreshDetails.refresh_token, { method: 'POST', @@ -100,7 +109,7 @@ export class Uid2StorageManager { this._storage = storage; this._preferLocalStorage = preferLocalStorage; this._storageName = storageName; - this._logInfo = logInfo; + this._logInfo = (...args) => logInfoWrapper(logInfo, ...args); } readCookie(cookieName) { return this._storage.cookiesAreEnabled() ? this._storage.getCookie(cookieName) : null; @@ -187,11 +196,15 @@ if (FEATURES.UID2_CSTG) { clientSideTokenGenerator = { isCSTGOptionsValid(maybeOpts, _logWarn) { if (typeof maybeOpts !== 'object' || maybeOpts === null) { - _logWarn('CSTG opts must be an object'); + _logWarn('CSTG is not being used, but is included in the Prebid.js bundle. You can reduce the bundle size by passing "--disable UID2_CSTG" to the Prebid.js build.'); return false; } const opts = maybeOpts; + if (!opts.serverPublicKey && !opts.subscriptionId) { + _logWarn('CSTG has been enabled but its parameters have not been set.'); + return false; + } if (typeof opts.serverPublicKey !== 'string') { _logWarn('CSTG opts.serverPublicKey must be a string'); return false; @@ -389,8 +402,7 @@ if (FEATURES.UID2_CSTG) { this._baseUrl = opts.baseUrl; this._serverPublicKey = opts.cstg.serverPublicKey; this._subscriptionId = opts.cstg.subscriptionId; - this._optoutCheck = opts.cstg.optoutCheck; - this._logInfo = logInfo; + this._logInfo = (...args) => logInfoWrapper(logInfo, ...args); this._logWarn = logWarn; } @@ -447,8 +459,7 @@ if (FEATURES.UID2_CSTG) { } async generateToken(cstgIdentity) { - const requestIdentity = await this.generateCstgRequest(cstgIdentity); - const request = { optout_check: this._optoutCheck, ...requestIdentity }; + const request = await this.generateCstgRequest(cstgIdentity); this._logInfo('Building CSTG request for', request); const box = await UID2CstgBox.build( this.stripPublicKeyPrefix(this._serverPublicKey) @@ -511,11 +522,11 @@ if (FEATURES.UID2_CSTG) { // A 200 should always be a success response. // Something has gone wrong. rejectPromise( - `API error: Response body was invalid for HTTP status 200: ${decryptedResponse}` + prependMessage(`API error: Response body was invalid for HTTP status 200: ${decryptedResponse}`) ); } } catch (err) { - rejectPromise(err); + rejectPromise(prependMessage(err)); } }, error: (error, xhr) => { @@ -523,32 +534,32 @@ if (FEATURES.UID2_CSTG) { if (xhr.status === 400) { const response = JSON.parse(xhr.responseText); if (this.isCstgApiClientErrorResponse(response)) { - rejectPromise(`Client error: ${response.message}`); + rejectPromise(prependMessage(`Client error: ${response.message}`)); } else { // A 400 should always be a client error. // Something has gone wrong. rejectPromise( - `API error: Response body was invalid for HTTP status 400: ${xhr.responseText}` + prependMessage(`UID2 API error: Response body was invalid for HTTP status 400: ${xhr.responseText}`) ); } } else if (xhr.status === 403) { const response = JSON.parse(xhr.responseText); if (this.isCstgApiForbiddenResponse(xhr)) { - rejectPromise(`Forbidden: ${response.message}`); + rejectPromise(prependMessage(`Forbidden: ${response.message}`)); } else { // A 403 should always be a forbidden response. // Something has gone wrong. rejectPromise( - `API error: Response body was invalid for HTTP status 403: ${xhr.responseText}` + prependMessage(`UID2 API error: Response body was invalid for HTTP status 403: ${xhr.responseText}`) ); } } else { rejectPromise( - `API error: Unexpected HTTP status ${xhr.status}: ${error}` + prependMessage(`UID2 API error: Unexpected HTTP status ${xhr.status}: ${error}`) ); } } catch (_e) { - rejectPromise(error); + rejectPromise(prependMessage(error)); } }, }, @@ -677,43 +688,45 @@ if (FEATURES.UID2_CSTG) { } export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { + const logInfo = (...args) => logInfoWrapper(_logInfo, ...args); + let suppliedToken = null; const preferLocalStorage = (config.storage !== 'cookie'); - const storageManager = new Uid2StorageManager(prebidStorageManager, preferLocalStorage, config.internalStorage, _logInfo); - _logInfo(`Module is using ${preferLocalStorage ? 'local storage' : 'cookies'} for internal storage.`); + const storageManager = new Uid2StorageManager(prebidStorageManager, preferLocalStorage, config.internalStorage, logInfo); + logInfo(`Module is using ${preferLocalStorage ? 'local storage' : 'cookies'} for internal storage.`); const isCstgEnabled = clientSideTokenGenerator && clientSideTokenGenerator.isCSTGOptionsValid(config.cstg, _logWarn); if (isCstgEnabled) { - _logInfo(`Module is using client-side token generation.`); + logInfo(`Module is using client-side token generation.`); // Ignores config.paramToken and config.serverCookieName if any is provided suppliedToken = null; } else if (config.paramToken) { suppliedToken = config.paramToken; - _logInfo('Read token from params', suppliedToken); + logInfo('Read token from params', suppliedToken); } else if (config.serverCookieName) { suppliedToken = storageManager.readProvidedCookie(config.serverCookieName); - _logInfo('Read token from server-supplied cookie', suppliedToken); + logInfo('Read token from server-supplied cookie', suppliedToken); } let storedTokens = storageManager.getStoredValueWithFallback(); - _logInfo('Loaded module-stored tokens:', storedTokens); + logInfo('Loaded module-stored tokens:', storedTokens); if (storedTokens && typeof storedTokens === 'string') { // Stored value is a plain token - if no token is supplied, just use the stored value. if (!suppliedToken && !isCstgEnabled) { - _logInfo('Returning legacy cookie value.'); + logInfo('Returning legacy cookie value.'); return { id: storedTokens }; } // Otherwise, ignore the legacy value - it should get over-written later anyway. - _logInfo('Discarding superseded legacy cookie.'); + logInfo('Discarding superseded legacy cookie.'); storedTokens = null; } if (suppliedToken && storedTokens) { if (storedTokens.originalToken?.advertising_token !== suppliedToken.advertising_token) { - _logInfo('Server supplied new token - ignoring stored value.', storedTokens.originalToken?.advertising_token, suppliedToken.advertising_token); + logInfo('Server supplied new token - ignoring stored value.', storedTokens.originalToken?.advertising_token, suppliedToken.advertising_token); // Stored token wasn't originally sourced from the provided token - ignore the stored value. A new user has logged in? storedTokens = null; } @@ -722,16 +735,16 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { if (FEATURES.UID2_CSTG && isCstgEnabled) { const cstgIdentity = clientSideTokenGenerator.getValidIdentity(config.cstg, _logWarn); if (cstgIdentity) { - if (storedTokens && clientSideTokenGenerator.isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn)) { + if (storedTokens && clientSideTokenGenerator.isStoredTokenInvalid(cstgIdentity, storedTokens, logInfo, _logWarn)) { storedTokens = null; } if (!storedTokens || Date.now() > storedTokens.latestToken.refresh_expires) { - const promise = clientSideTokenGenerator.generateTokenAndStore(config.apiBaseUrl, config.cstg, cstgIdentity, storageManager, _logInfo, _logWarn); - _logInfo('Generate token using CSTG'); + const promise = clientSideTokenGenerator.generateTokenAndStore(config.apiBaseUrl, config.cstg, cstgIdentity, storageManager, logInfo, _logWarn); + logInfo('Generate token using CSTG'); return { callback: (cb) => { promise.then((result) => { - _logInfo('Token generation responded, passing the new token on.', result); + logInfo('Token generation responded, passing the new token on.', result); cb(result); }); } }; @@ -741,25 +754,25 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { const useSuppliedToken = !(storedTokens?.latestToken) || (suppliedToken && suppliedToken.identity_expires > storedTokens.latestToken.identity_expires); const newestAvailableToken = useSuppliedToken ? suppliedToken : storedTokens.latestToken; - _logInfo('UID2 module selected latest token', useSuppliedToken, newestAvailableToken); + logInfo('UID2 module selected latest token', useSuppliedToken, newestAvailableToken); if ((!newestAvailableToken || Date.now() > newestAvailableToken.refresh_expires)) { - _logInfo('Newest available token is expired and not refreshable.'); + logInfo('Newest available token is expired and not refreshable.'); return { id: null }; } if (Date.now() > newestAvailableToken.identity_expires) { - const promise = refreshTokenAndStore(config.apiBaseUrl, newestAvailableToken, config.clientId, storageManager, _logInfo, _logWarn); - _logInfo('Token is expired but can be refreshed, attempting refresh.'); + const promise = refreshTokenAndStore(config.apiBaseUrl, newestAvailableToken, config.clientId, storageManager, logInfo, _logWarn); + logInfo('Token is expired but can be refreshed, attempting refresh.'); return { callback: (cb) => { promise.then((result) => { - _logInfo('Refresh reponded, passing the updated token on.', result); + logInfo('Refresh reponded, passing the updated token on.', result); cb(result); }); } }; } // If should refresh (but don't need to), refresh in the background. if (Date.now() > newestAvailableToken.refresh_from) { - _logInfo(`Refreshing token in background with low priority.`); - refreshTokenAndStore(config.apiBaseUrl, newestAvailableToken, config.clientId, storageManager, _logInfo, _logWarn); + logInfo(`Refreshing token in background with low priority.`); + refreshTokenAndStore(config.apiBaseUrl, newestAvailableToken, config.clientId, storageManager, logInfo, _logWarn); } const tokens = { originalToken: suppliedToken ?? storedTokens?.originalToken, diff --git a/modules/underdogmediaBidAdapter.js b/modules/underdogmediaBidAdapter.js index 54b74c7ccd4..612df51a3d3 100644 --- a/modules/underdogmediaBidAdapter.js +++ b/modules/underdogmediaBidAdapter.js @@ -12,6 +12,7 @@ import { import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import { percentInView } from '../libraries/percentInView/percentInView.js'; const BIDDER_CODE = 'underdogmedia'; const UDM_ADAPTER_VERSION = '7.30V'; @@ -262,106 +263,13 @@ function _getViewability(element, topWin, { h } = {}) { return topWin.document.visibilityState === 'visible' - ? _getPercentInView(element, topWin, { + ? percentInView(element, { w, h }) : 0 } -function _getPercentInView(element, topWin, { - w, - h -} = {}) { - const elementBoundingBox = _getBoundingBox(element, { - w, - h - }); - - // Obtain the intersection of the element and the viewport - const elementInViewBoundingBox = _getIntersectionOfRects([{ - left: 0, - top: 0, - right: topWin.innerWidth, - bottom: topWin.innerHeight - }, elementBoundingBox]); - - let elementInViewArea, - elementTotalArea; - - if (elementInViewBoundingBox !== null) { - // Some or all of the element is in view - elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; - elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; - - return ((elementInViewArea / elementTotalArea) * 100); - } - - // No overlap between element and the viewport; therefore, the element - // lies completely out of view - return 0; -} - -function _getBoundingBox(element, { - w, - h -} = {}) { - let { - width, - height, - left, - top, - right, - bottom - } = element.getBoundingClientRect(); - - if ((width === 0 || height === 0) && w && h) { - width = w; - height = h; - right = left + w; - bottom = top + h; - } - - return { - width, - height, - left, - top, - right, - bottom - }; -} - -function _getIntersectionOfRects(rects) { - const bbox = { - left: rects[0].left, - right: rects[0].right, - top: rects[0].top, - bottom: rects[0].bottom - }; - - for (let i = 1; i < rects.length; ++i) { - bbox.left = Math.max(bbox.left, rects[i].left); - bbox.right = Math.min(bbox.right, rects[i].right); - - if (bbox.left >= bbox.right) { - return null; - } - - bbox.top = Math.max(bbox.top, rects[i].top); - bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); - - if (bbox.top >= bbox.bottom) { - return null; - } - } - - bbox.width = bbox.right - bbox.left; - bbox.height = bbox.bottom - bbox.top; - - return bbox; -} - function makeNotification(bid, mid, bidParam) { let url = mid.notification_url; diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index c7e8102ffc9..a3e5fbbf728 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -2,7 +2,7 @@ * Adapter to send bids to Undertone */ -import {deepAccess, parseUrl} from '../src/utils.js'; +import {deepAccess, parseUrl, extractDomainFromHost, getWinDimensions} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; @@ -26,24 +26,6 @@ function getBidFloor(bidRequest, mediaType) { return (floor && floor.currency === 'USD' && floor.floor) || 0; } -function extractDomainFromHost(pageHost) { - let domain = null; - try { - let domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); - if (domains != null && domains.length > 0) { - domain = domains[0]; - for (let i = 1; i < domains.length; i++) { - if (domains[i].length > domain.length) { - domain = domains[i]; - } - } - } - } catch (e) { - domain = null; - } - return domain; -} - function getGdprQueryParams(gdprConsent) { if (!gdprConsent) { return null; @@ -85,8 +67,9 @@ export const spec = { } }, buildRequests: function(validBidRequests, bidderRequest) { - const vw = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); - const vh = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); + const windowDimensions = getWinDimensions(); + const vw = Math.max(windowDimensions.document.documentElement.clientWidth, windowDimensions.innerWidth || 0); + const vh = Math.max(windowDimensions.document.documentElement.clientHeight, windowDimensions.innerHeight || 0); const pageSizeArray = vw == 0 || vh == 0 ? null : [vw, vh]; const commons = { 'adapterVersion': '$prebid.version$', diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index 43eb943f6d5..ac81e124b66 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -135,7 +135,7 @@ const interpretResponse = (serverResponse, request) => { ad: b.adm, ttl: 1000, creativeId: b.crid, - netRevenue: false, + netRevenue: true, currency: res.cur } diff --git a/modules/uniquestAnalyticsAdapter.js b/modules/uniquestAnalyticsAdapter.js new file mode 100644 index 00000000000..f91d74986e4 --- /dev/null +++ b/modules/uniquestAnalyticsAdapter.js @@ -0,0 +1,108 @@ +import {logError} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import adapterManager from '../src/adapterManager.js'; +import {EVENTS} from '../src/constants.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; + +const ADAPTER_CODE = 'uniquest'; +const BASE_URL = 'https://rcvp.ust-ad.com/'; +const AUCTION_END_URI = 'pbaae'; +const AD_RENDERED_URI = 'pbaars'; + +let sid; + +function sendEvent(event, uri) { + ajax( + BASE_URL + uri, + null, + JSON.stringify(event) + ); +} + +function adRenderSucceededHandler(eventType, args, pageUrl) { + const event = { + event_type: eventType, + url: pageUrl, + slot_id: sid, + bid: { + auction_id: args.bid?.auctionId, + creative_id: args.bid?.creativeId, + bidder: args.bid?.bidderCode, + media_type: args.bid?.mediaType, + size: args.bid?.size, + cpm: String(args.bid?.cpm), + currency: args.bid?.currency, + original_cpm: String(args.bid?.originalCpm), + original_currency: args.bid?.originalCurrency, + hb_pb: String(args.bid?.adserverTargeting.hb_pb), + bidding_time: args.bid?.timeToRespond, + ad_unit_code: args.bid?.adUnitCode + } + }; + sendEvent(event, AD_RENDERED_URI); +} + +function auctionEndHandler(eventType, args, pageUrl) { + if (args.bidsReceived.length > 0) { + const event = { + event_type: eventType, + url: pageUrl, + slot_id: sid, + bids: args.bidsReceived?.map(br => ({ + auction_id: br?.auctionId, + creative_id: br?.creativeId, + bidder: br?.bidder, + media_type: br?.mediaType, + size: br?.size, + cpm: String(br?.cpm), + currency: br?.currency, + original_cpm: String(br?.originalCpm), + original_currency: br?.originalCurrency, + hb_pb: String(br?.adserverTargeting.hb_pb), + bidding_time: br?.timeToRespond, + ad_unit_code: br?.adUnitCode + })) + }; + sendEvent(event, AUCTION_END_URI); + } +} + +let baseAdapter = adapter({analyticsType: 'endpoint'}); +let uniquestAdapter = Object.assign({}, baseAdapter, { + + enableAnalytics(config = {}) { + if (config.options && config.options.sid) { + sid = config.options.sid; + baseAdapter.enableAnalytics.call(this, config); + } else { + logError('Config not found. Analytics is disabled due.'); + } + }, + + disableAnalytics() { + sid = undefined; + baseAdapter.disableAnalytics.apply(this, arguments); + }, + + track({eventType, args}) { + const refererInfo = getRefererInfo(); + let pageUrl = refererInfo.page; + + switch (eventType) { + case EVENTS.AD_RENDER_SUCCEEDED: + adRenderSucceededHandler(eventType, args, pageUrl); + break; + case EVENTS.AUCTION_END: + auctionEndHandler(eventType, args, pageUrl); + break; + } + } +}); + +adapterManager.registerAnalyticsAdapter({ + adapter: uniquestAdapter, + code: ADAPTER_CODE +}); + +export default uniquestAdapter; diff --git a/modules/uniquestAnalyticsAdapter.md b/modules/uniquestAnalyticsAdapter.md new file mode 100644 index 00000000000..73e220ee926 --- /dev/null +++ b/modules/uniquestAnalyticsAdapter.md @@ -0,0 +1,22 @@ +# Overview + +``` +Module Name: UNIQUEST Analytics Adapter +Module Type: Analytics Adapter +Maintainer: prebid_info@muneee.co.jp +``` + +# Description + +Analytics exchange for UNIQUEST + +# Test Parameters + +``` +{ + provider: 'uniquest', + options: { + sid: 'ONhFoaQn', + } +} +``` diff --git a/modules/uniquestBidAdapter.js b/modules/uniquestBidAdapter.js new file mode 100644 index 00000000000..fa4b7c0e347 --- /dev/null +++ b/modules/uniquestBidAdapter.js @@ -0,0 +1,97 @@ +import {getBidIdParameter} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory').BidRequest} BidRequest + * @typedef {import('../src/auction').BidderRequest} BidderRequest + */ + +const BIDDER_CODE = 'uniquest'; +const ENDPOINT = 'https://adpb.ust-ad.com/hb/prebid'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * 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. + */ + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.sid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const bidRequests = []; + + for (let i = 0; i < validBidRequests.length; i++) { + let queryString = ''; + const request = validBidRequests[i]; + + const bid = request.bidId; + const sid = getBidIdParameter('sid', request.params); + const widths = request.sizes.map(size => size[0]).join(','); + const heights = request.sizes.map(size => size[1]).join(','); + const timeout = bidderRequest.timeout + + queryString = tryAppendQueryString(queryString, 'bid', bid); + queryString = tryAppendQueryString(queryString, 'sid', sid); + queryString = tryAppendQueryString(queryString, 'widths', widths); + queryString = tryAppendQueryString(queryString, 'heights', heights); + queryString = tryAppendQueryString(queryString, 'timeout', timeout); + + bidRequests.push({ + method: 'GET', + url: ENDPOINT, + data: queryString, + }); + } + return bidRequests; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, requests) { + const response = serverResponse.body; + + if (!response || Object.keys(response).length === 0) { + return [] + } + + const bid = { + requestId: response.request_id, + cpm: response.cpm, + currency: response.currency, + width: response.width, + height: response.height, + ad: response.ad, + creativeId: response.bid_id, + netRevenue: response.net_revenue, + mediaType: response.media_type, + ttl: response.ttl, + meta: { + advertiserDomains: response.meta && response.meta.advertiser_domains ? response.meta.advertiser_domains : [], + }, + }; + + return [bid]; + }, +}; + +registerBidder(spec); diff --git a/modules/uniquestBidAdapter.md b/modules/uniquestBidAdapter.md new file mode 100644 index 00000000000..699816f96e1 --- /dev/null +++ b/modules/uniquestBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: UNIQUEST Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid_info@muneee.co.jp +``` + +# Description +Connects to UNIQUEST exchange for bids. + +# Test Parameters +```js +var adUnits = [ + // Banner adUnit + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [ + [300, 300], + [300, 250], + [320, 100] + ] + } + }, + bids: [{ + bidder: 'uniquest', + params: { + sid: 'ONhFoaQn', // device is smartphone only + } + }] + } +]; +``` diff --git a/modules/unrulyBidAdapter.js b/modules/unrulyBidAdapter.js index b825003f36f..53e6629d8cc 100644 --- a/modules/unrulyBidAdapter.js +++ b/modules/unrulyBidAdapter.js @@ -31,7 +31,7 @@ const addBidFloorInfo = (validBid) => { currency: 'USD', mediaType: key, size: '*' - }).floor || 0; + })?.floor || 0; } else { floor = validBid.params.floor || 0; } @@ -226,7 +226,7 @@ export const adapter = { 'options': { 'contentType': 'application/json' }, - 'protectedAudienceEnabled': bidderRequest.fledgeEnabled + 'protectedAudienceEnabled': bidderRequest.paapi?.enabled }, validBidRequests, bidderRequest); }, @@ -261,7 +261,7 @@ export const adapter = { return { bids, - fledgeAuctionConfigs + paapi: fledgeAuctionConfigs }; } }; diff --git a/modules/userId/eids.js b/modules/userId/eids.js index e5f7e3b8fb2..0b336e7b00d 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -1,27 +1,30 @@ -import {deepAccess, deepClone, isFn, isPlainObject, isStr} from '../../src/utils.js'; +import {logError, deepClone, isFn, isStr} from '../../src/utils.js'; + +/** + * @typedef {import('./index.js').SubmodulePriorityMap} SubmodulePriorityMap + */ export const EID_CONFIG = new Map(); // this function will create an eid object for the given UserId sub-module -function createEidObject(userIdData, subModuleKey) { - const conf = EID_CONFIG.get(subModuleKey); - if (conf && userIdData) { +function createEidObject(userIdData, subModuleKey, eidConf) { + if (eidConf && userIdData) { let eid = {}; - eid.source = isFn(conf['getSource']) ? conf['getSource'](userIdData) : conf['source']; - const value = isFn(conf['getValue']) ? conf['getValue'](userIdData) : userIdData; + eid.source = isFn(eidConf['getSource']) ? eidConf['getSource'](userIdData) : eidConf['source']; + const value = isFn(eidConf['getValue']) ? eidConf['getValue'](userIdData) : userIdData; if (isStr(value)) { - const uid = { id: value, atype: conf['atype'] }; + const uid = { id: value, atype: eidConf['atype'] }; // getUidExt - if (isFn(conf['getUidExt'])) { - const uidExt = conf['getUidExt'](userIdData); + if (isFn(eidConf['getUidExt'])) { + const uidExt = eidConf['getUidExt'](userIdData); if (uidExt) { uid.ext = uidExt; } } eid.uids = [uid]; // getEidExt - if (isFn(conf['getEidExt'])) { - const eidExt = conf['getEidExt'](userIdData); + if (isFn(eidConf['getEidExt'])) { + const eidExt = eidConf['getEidExt'](userIdData); if (eidExt) { eid.ext = eidExt; } @@ -32,10 +35,13 @@ function createEidObject(userIdData, subModuleKey) { return null; } -export function createEidsArray(bidRequestUserId) { +export function createEidsArray(bidRequestUserId, eidConfigs = EID_CONFIG) { const allEids = {}; function collect(eid) { - const key = JSON.stringify([eid.source?.toLowerCase(), eid.ext]); + const key = JSON.stringify([ + eid.source?.toLowerCase(), + ...Object.keys(eid).filter(k => !['uids', 'source'].includes(k)).sort().map(k => eid[k]) + ]); if (allEids.hasOwnProperty(key)) { allEids[key].uids.push(...eid.uids); } else { @@ -45,31 +51,48 @@ export function createEidsArray(bidRequestUserId) { Object.entries(bidRequestUserId).forEach(([name, values]) => { values = Array.isArray(values) ? values : [values]; - const eids = name === 'pubProvidedId' ? deepClone(values) : values.map(value => createEidObject(value, name)); - eids.filter(eid => eid != null).forEach(collect); + const eidConf = eidConfigs.get(name); + let eids; + if (name === 'pubProvidedId') { + eids = deepClone(values); + } else if (typeof eidConf === 'function') { + try { + eids = eidConf(values); + if (!Array.isArray(eids)) { + eids = [eids]; + } + eids.forEach(eid => eid.uids = eid.uids.filter(({id}) => isStr(id))) + eids = eids.filter(({uids}) => uids?.length > 0); + } catch (e) { + logError(`Could not generate EID for "${name}"`, e); + } + } else { + eids = values.map(value => createEidObject(value, name, eidConf)); + } + if (Array.isArray(eids)) { + eids.filter(eid => eid != null).forEach(collect); + } }) return Object.values(allEids); } /** - * @param {SubmoduleContainer[]} submodules + * @param {SubmodulePriorityMap} priorityMap */ -export function buildEidPermissions(submodules) { - let eidPermissions = []; - submodules.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length) - .forEach(i => { - Object.keys(i.idObj).forEach(key => { - const eidConf = EID_CONFIG.get(key) || {}; - if (deepAccess(i, 'config.bidders') && Array.isArray(i.config.bidders) && - eidConf.source) { - eidPermissions.push( - { - source: eidConf.source, - bidders: i.config.bidders - } - ); - } - }); - }); - return eidPermissions; +export function getEids(priorityMap) { + const eidConfigs = new Map(); + const idValues = {}; + Object.entries(priorityMap).forEach(([key, getActiveModule]) => { + const submodule = getActiveModule(); + if (submodule) { + idValues[key] = submodule.idObj[key]; + let eidConf = submodule.submodule.eids?.[key]; + if (typeof eidConf === 'function') { + // if eid config is given as a function, append the active module configuration to its args + eidConf = ((orig) => (...args) => orig(...args, submodule.config))(eidConf); + } + eidConfigs.set(key, eidConf); + } + }) + return createEidsArray(idValues, eidConfigs); } diff --git a/modules/userId/eids.md b/modules/userId/eids.md index c10ecde9c30..53567032175 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -81,14 +81,6 @@ userIdAsEids = [ }] }, - { - source: 'parrable.com', - uids: [{ - id: 'some-random-id-value', - atype: 1 - }] - }, - { source: 'liveramp.com', uids: [{ @@ -107,7 +99,7 @@ userIdAsEids = [ segments: ['s1', 's2'] } }, - + { source: 'bidswitch.net', uids: [{ @@ -118,7 +110,7 @@ userIdAsEids = [ } }] }, - + { source: 'liveintent.indexexchange.com', uids: [{ @@ -161,7 +153,7 @@ userIdAsEids = [ provider: 'liveintent.com' } }] - }, + }, { source: 'media.net', @@ -185,6 +177,17 @@ userIdAsEids = [ }] }, + { + source: 'fpid.liveintent.com', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + provider: 'liveintent.com' + } + }] + }, + { source: 'merkleinc.com', uids: [{ diff --git a/modules/userId/index.js b/modules/userId/index.js index 90d377a816e..455366f76f7 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -4,63 +4,47 @@ */ /** - * @interface Submodule + * @typedef Submodule + * @property {string} name - used to link submodule with config + * @property {decode} decode + * @property {getId} getId + * @property {Object} eids + * @property {number} [gvlid] - vendor ID + * @property {extendId} [extendId] + * @property {function} [domainOverride] - use a predefined domain override for cookies or provide your own + * @property {function(): string} [findRootDomain] - returns the root domain */ /** - * @function - * @summary performs action to obtain id and return a value in the callback's response argument. - * If IdResponse#id is defined, then it will be written to the current active storage. - * If IdResponse#callback is defined, then it'll called at the end of auction. - * It's permissible to return neither, one, or both fields. - * @name Submodule#getId + * Performs action to obtain id and return a value in the callback's response argument. + * If IdResponse#id is defined, then it will be written to the current active storage. + * If IdResponse#callback is defined, then it'll called at the end of auction. + * It's permissible to return neither, one, or both fields. + * @callback getId * @param {SubmoduleConfig} config - * @param {ConsentData|undefined} consentData - * @param {(Object|undefined)} cacheIdObj - * @return {(IdResponse|undefined)} A response object that contains id and/or callback. + * @param {ConsentData|undefined} [consentData] + * @param {Object|undefined} [cacheIdObj] + * @returns {IdResponse|undefined} A response object that contains id and/or callback. */ /** - * @function - * @summary Similar to Submodule#getId, this optional method returns response to for id that exists already. - * If IdResponse#id is defined, then it will be written to the current active storage even if it exists already. - * If IdResponse#callback is defined, then it'll called at the end of auction. - * It's permissible to return neither, one, or both fields. - * @name Submodule#extendId + * Similar to `getId`, this optional method returns response to for id that exists already. + * If IdResponse#id is defined, then it will be written to the current active storage even if it exists already. + * If IdResponse#callback is defined, then it'll called at the end of auction. + * It's permissible to return neither, one, or both fields. + * @callback extendId * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @param {Object} storedId - existing id, if any - * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. + * @returns {IdResponse|function(callback:function)} A response object that contains id and/or callback. */ /** - * @function - * @summary decode a stored value for passing to bid requests - * @name Submodule#decode + * Decode a stored value for passing to bid requests + * @callback decode * @param {Object|string} value - * @param {SubmoduleConfig|undefined} config - * @return {(Object|undefined)} - */ - -/** - * @property - * @summary used to link submodule with config - * @name Submodule#name - * @type {string} - */ - -/** - * @property - * @summary use a predefined domain override for cookies or provide your own - * @name Submodule#domainOverride - * @type {(undefined|function)} - */ - -/** - * @function - * @summary Returns the root domain - * @name Submodule#findRootDomain - * @returns {string} + * @param {SubmoduleConfig|undefined} [config] + * @returns {Object|undefined} */ /** @@ -69,6 +53,7 @@ * @property {(SubmoduleStorage|undefined)} storage - browser storage config * @property {(SubmoduleParams|undefined)} params - params config for use by the submodule.getId function * @property {(Object|undefined)} value - if not empty, this value is added to bid requests for access in adapters + * @property {string[]} [enabledStorageTypes] */ /** @@ -111,29 +96,35 @@ * @property {(Object|undefined)} idObj - cache decoded id value (this is copied to every adUnit bid) * @property {(function|undefined)} callback - holds reference to submodule.getId() result if it returned a function. Will be set to undefined after callback executes * @property {StorageManager} storageMgr + * @property {string[]} [enabledStorageTypes] */ /** * @typedef {Object} ConsentData - * @property {(string|undefined)} consentString - * @property {(Object|undefined)} vendorData - * @property {(boolean|undefined)} gdprApplies + * @property {Object} gdpr + * @property {Object} gpp + * @property {Object} usp + * @property {Object} coppa */ /** * @typedef {Object} IdResponse - * @property {(Object|undefined)} id - id data - * @property {(function|undefined)} callback - function that will return an id + * @property {Object} [id] - id data + * @property {function} [callback] - function that will return an id */ -import {find, includes} from '../../src/polyfill.js'; +/** + * @typedef {{[idKey: string]: () => SubmoduleContainer[]}} SubmodulePriorityMap + */ + +import {find} from '../../src/polyfill.js'; import {config} from '../../src/config.js'; import * as events from '../../src/events.js'; import {getGlobal} from '../../src/prebidGlobal.js'; -import adapterManager, {gdprDataHandler} from '../../src/adapterManager.js'; -import { EVENTS } from '../../src/constants.js'; +import adapterManager from '../../src/adapterManager.js'; +import {EVENTS} from '../../src/constants.js'; import {module, ready as hooksReady} from '../../src/hook.js'; -import {buildEidPermissions, createEidsArray, EID_CONFIG} from './eids.js'; +import {EID_CONFIG, getEids} from './eids.js'; import { getCoreStorageManager, getStorageManager, @@ -141,13 +132,10 @@ import { STORAGE_TYPE_LOCALSTORAGE } from '../../src/storageManager.js'; import { - deepAccess, deepSetValue, delayExecution, - getPrebidInternal, isArray, isEmpty, - isEmptyStr, isFn, isGptPubadsDefined, isNumber, @@ -157,8 +145,7 @@ import { logWarn } from '../../src/utils.js'; import {getPPID as coreGetPPID} from '../../src/adserver.js'; -import {defer, GreedyPromise} from '../../src/utils/promise.js'; -import {registerOrtbProcessor, REQUEST} from '../../src/pbjsORTB.js'; +import {defer, PbPromise, delay} from '../../src/utils/promise.js'; import {newMetrics, timedAuctionHook, useMetrics} from '../../src/utils/perfMetrics.js'; import {findRootDomain} from '../../src/fpd/rootDomain.js'; import {allConsent, GDPR_GVLIDS} from '../../src/consentHandler.js'; @@ -166,25 +153,22 @@ import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_ENRICH_EIDS} from '../../src/activities/activities.js'; import {activityParams} from '../../src/activities/activityParams.js'; +import {USERSYNC_DEFAULT_CONFIG} from '../../src/userSync.js'; +import {startAuction} from '../../src/prebid.js'; const MODULE_NAME = 'User ID'; const COOKIE = STORAGE_TYPE_COOKIES; const LOCAL_STORAGE = STORAGE_TYPE_LOCALSTORAGE; -const DEFAULT_SYNC_DELAY = 500; -const NO_AUCTION_DELAY = 0; export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; export const coreStorage = getCoreStorageManager('userId'); export const dep = { isAllowed: isActivityAllowed } -/** @type {boolean} */ -let addedUserIdHook = false; - /** @type {SubmoduleContainer[]} */ let submodules = []; -/** @type {SubmoduleContainer[]} */ +/** @type {PriorityMaps} */ let initializedSubmodules; /** @type {SubmoduleConfig[]} */ @@ -239,6 +223,29 @@ function cookieSetter(submodule, storageMgr) { } } +function setValueInCookie(submodule, valueStr, expiresStr) { + const storage = submodule.config.storage; + const setCookie = cookieSetter(submodule); + + setCookie(null, valueStr, expiresStr); + setCookie('_cst', getConsentHash(), expiresStr); + if (typeof storage.refreshInSeconds === 'number') { + setCookie('_last', new Date().toUTCString(), expiresStr); + } +} + +function setValueInLocalStorage(submodule, valueStr, expiresStr) { + const storage = submodule.config.storage; + const mgr = submodule.storageMgr; + + mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); + mgr.setDataInLocalStorage(`${storage.name}_cst`, getConsentHash()); + mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); + if (typeof storage.refreshInSeconds === 'number') { + mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); + } +} + /** * @param {SubmoduleContainer} submodule * @param {(Object|string)} value @@ -248,60 +255,78 @@ export function setStoredValue(submodule, value) { * @type {SubmoduleStorage} */ const storage = submodule.config.storage; - const mgr = submodule.storageMgr; try { const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); const valueStr = isPlainObject(value) ? JSON.stringify(value) : value; - if (storage.type === COOKIE) { - const setCookie = cookieSetter(submodule); - setCookie(null, valueStr, expiresStr); - setCookie('_cst', getConsentHash(), expiresStr); - if (typeof storage.refreshInSeconds === 'number') { - setCookie('_last', new Date().toUTCString(), expiresStr); - } - } else if (storage.type === LOCAL_STORAGE) { - mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); - mgr.setDataInLocalStorage(`${storage.name}_cst`, getConsentHash()); - mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); - if (typeof storage.refreshInSeconds === 'number') { - mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); + + submodule.enabledStorageTypes.forEach(storageType => { + switch (storageType) { + case COOKIE: + setValueInCookie(submodule, valueStr, expiresStr); + break; + case LOCAL_STORAGE: + setValueInLocalStorage(submodule, valueStr, expiresStr); + break; } - } + }); } catch (error) { logError(error); } } +function deleteValueFromCookie(submodule) { + const setCookie = cookieSetter(submodule, coreStorage); + const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString(); + + ['', '_last', '_cst'].forEach(suffix => { + try { + setCookie(suffix, '', expiry); + } catch (e) { + logError(e); + } + }) +} + +function deleteValueFromLocalStorage(submodule) { + ['', '_last', '_exp', '_cst'].forEach(suffix => { + try { + coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix); + } catch (e) { + logError(e); + } + }); +} + export function deleteStoredValue(submodule) { - let deleter, suffixes; - switch (submodule.config?.storage?.type) { - case COOKIE: - const setCookie = cookieSetter(submodule, coreStorage); - const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString(); - deleter = (suffix) => setCookie(suffix, '', expiry) - suffixes = ['', '_last', '_cst']; - break; - case LOCAL_STORAGE: - deleter = (suffix) => coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix) - suffixes = ['', '_last', '_exp', '_cst']; - break; - } - if (deleter) { - suffixes.forEach(suffix => { - try { - deleter(suffix) - } catch (e) { - logError(e); - } - }); - } + populateEnabledStorageTypes(submodule); + + submodule.enabledStorageTypes.forEach(storageType => { + switch (storageType) { + case COOKIE: + deleteValueFromCookie(submodule); + break; + case LOCAL_STORAGE: + deleteValueFromLocalStorage(submodule); + break; + } + }); } -function setPrebidServerEidPermissions(initializedSubmodules) { - let setEidPermissions = getPrebidInternal().setEidPermissions; - if (typeof setEidPermissions === 'function' && isArray(initializedSubmodules)) { - setEidPermissions(buildEidPermissions(initializedSubmodules)); +function getValueFromCookie(submodule, storedKey) { + return submodule.storageMgr.getCookie(storedKey) +} + +function getValueFromLocalStorage(submodule, storedKey) { + const mgr = submodule.storageMgr; + const storage = submodule.config.storage; + const storedValueExp = mgr.getDataFromLocalStorage(`${storage.name}_exp`); + + // empty string means no expiration set + if (storedValueExp === '') { + return mgr.getDataFromLocalStorage(storedKey); + } else if (storedValueExp && ((new Date(storedValueExp)).getTime() - Date.now() > 0)) { + return decodeURIComponent(mgr.getDataFromLocalStorage(storedKey)); } } @@ -311,24 +336,23 @@ function setPrebidServerEidPermissions(initializedSubmodules) { * @returns {string} */ function getStoredValue(submodule, key = undefined) { - const mgr = submodule.storageMgr; const storage = submodule.config.storage; const storedKey = key ? `${storage.name}_${key}` : storage.name; let storedValue; try { - if (storage.type === COOKIE) { - storedValue = mgr.getCookie(storedKey); - } else if (storage.type === LOCAL_STORAGE) { - const storedValueExp = mgr.getDataFromLocalStorage(`${storage.name}_exp`); - // empty string means no expiration set - if (storedValueExp === '') { - storedValue = mgr.getDataFromLocalStorage(storedKey); - } else if (storedValueExp) { - if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { - storedValue = decodeURIComponent(mgr.getDataFromLocalStorage(storedKey)); - } + submodule.enabledStorageTypes.find(storageType => { + switch (storageType) { + case COOKIE: + storedValue = getValueFromCookie(submodule, storedKey); + break; + case LOCAL_STORAGE: + storedValue = getValueFromLocalStorage(submodule, storedKey); + break; } - } + + return !!storedValue; + }); + // support storing a string or a stringified object if (typeof storedValue === 'string' && storedValue.trim().charAt(0) === '{') { storedValue = JSON.parse(storedValue); @@ -342,8 +366,9 @@ function getStoredValue(submodule, key = undefined) { /** * @param {SubmoduleContainer[]} submodules * @param {function} cb - callback for after processing is done. + * @param {PriorityMaps} priorityMaps */ -function processSubmoduleCallbacks(submodules, cb, allModules) { +function processSubmoduleCallbacks(submodules, cb, priorityMaps) { cb = uidMetrics().fork().startTiming('userId.callbacks.total').stopBefore(cb); const done = delayExecution(() => { clearTimeout(timeoutID); @@ -359,7 +384,8 @@ function processSubmoduleCallbacks(submodules, cb, allModules) { } // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.submodule.decode(idObj, submodule.config); - updatePPID(getCombinedSubmoduleIds(allModules)); + priorityMaps.refresh(); + updatePPID(priorityMaps); } else { logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } @@ -377,95 +403,187 @@ function processSubmoduleCallbacks(submodules, cb, allModules) { } /** - * This function will create a combined object for all subModule Ids - * @param {SubmoduleContainer[]} submodules + * @param {SubmodulePriorityMap} priorityMap + * @returns {{}} */ -function getCombinedSubmoduleIds(submodules) { - if (!Array.isArray(submodules) || !submodules.length) { - return {}; +function getIds(priorityMap) { + return Object.fromEntries( + Object.entries(priorityMap) + .map(([key, getActiveModule]) => [key, getActiveModule()?.idObj?.[key]]) + .filter(([_, value]) => value != null) + ) +} + +function getPrimaryIds(submodule) { + if (submodule.primaryIds) return submodule.primaryIds; + const ids = Object.keys(submodule.eids ?? {}); + if (ids.length > 1) { + throw new Error(`ID submodule ${submodule.name} can provide multiple IDs, but does not specify 'primaryIds'`) } - return getPrioritizedCombinedSubmoduleIds(submodules) + return ids; } /** - * This function will return a submodule ID object for particular source name - * @param {SubmoduleContainer[]} submodules - * @param {string} sourceName + * Given a collection of items, where each item maps to any number of IDs (getKeys) and an ID module (getIdMod), + * return a map from ID key to all items that map to that ID key, in order of priority (highest priority first). + * + * @template T + * @param {T[]} items + * @param {(item: T) => string[]} getKeys + * @param {(item: T) => Submodule} getIdMod + * @returns {{[key: string]: T[]}} */ -function getSubmoduleId(submodules, sourceName) { - if (!Array.isArray(submodules) || !submodules.length) { - return {}; - } - - const prioritisedIds = getPrioritizedCombinedSubmoduleIds(submodules); - const eligibleIdName = Object.keys(prioritisedIds).find(idName => { - const config = EID_CONFIG.get(idName); - return config?.source === sourceName || (isFn(config?.getSource) && config.getSource() === sourceName); - }); - - return eligibleIdName ? {[eligibleIdName]: prioritisedIds[eligibleIdName]} : []; +function orderByPriority(items, getKeys, getIdMod) { + const tally = {}; + items.forEach(item => { + const module = getIdMod(item); + const primaryIds = getPrimaryIds(module); + getKeys(item).forEach(key => { + const keyItems = tally[key] = tally[key] ?? [] + const keyPriority = idPriority[key]?.indexOf(module.name) ?? (primaryIds.includes(key) ? 0 : -1); + const pos = keyItems.findIndex(([priority]) => priority < keyPriority); + keyItems.splice(pos === -1 ? keyItems.length : pos, 0, [keyPriority, item]) + }) + }) + return Object.fromEntries(Object.entries(tally).map(([key, items]) => [key, items.map(([_, item]) => item)])) } /** - * This function will create a combined object for bidder with allowed subModule Ids - * @param {SubmoduleContainer[]} submodules - * @param {string} bidder + * @typedef {Object} PriorityMaps + * @property {SubmoduleContainer[]} submodules all active submodules + * @property {SubmodulePriorityMap} global priority map for global (not bidder-specific) submodules + * @property {SubmodulePriorityMap} combined priority map for ALL submodules, disregarding bidder filters + * @property {{[bidder: string]: SubmodulePriorityMap}} bidder priority maps for each bidder's specific submodules + * @property {(submodules: SubmoduleContainer[]) => void} refresh refresh priority maps, optionally adding or updating some submodules. + * Should be called every time a submodule's ID is updated. */ -function getCombinedSubmoduleIdsForBidder(submodules, bidder) { - if (!Array.isArray(submodules) || !submodules.length || !bidder) { - return {}; - } - const eligibleSubmodules = submodules - .filter(i => !i.config.bidders || !isArray(i.config.bidders) || includes(i.config.bidders, bidder)) - - return getPrioritizedCombinedSubmoduleIds(eligibleSubmodules); -} -function collectByPriority(submodules, getIds, getName) { - return Object.fromEntries(Object.entries(submodules.reduce((carry, submod) => { - const ids = getIds(submod); - ids && Object.keys(ids).forEach(key => { - const maybeCurrentIdPriority = idPriority[key]?.indexOf(getName(submod)); - const currentIdPriority = isNumber(maybeCurrentIdPriority) ? maybeCurrentIdPriority : -1; - const currentIdState = {priority: currentIdPriority, value: ids[key]}; - if (carry[key]) { - const winnerIdState = currentIdState.priority > carry[key].priority ? currentIdState : carry[key]; - carry[key] = winnerIdState; - } else { - carry[key] = currentIdState; +/** + * @returns PriorityMaps + */ +function mkPriorityMaps() { + const map = { + submodules: [], + global: {}, + bidder: {}, + combined: {}, + /** + * @param {SubmoduleContainer[]} addtlModules + */ + refresh(addtlModules = []) { + const refreshing = new Set(addtlModules.map(mod => mod.submodule)); + map.submodules = map.submodules.filter((mod) => !refreshing.has(mod.submodule)).concat(addtlModules); + update(); + } + } + function update() { + const modulesById = orderByPriority( + map.submodules, + (submod) => Object.keys(submod.idObj ?? {}), + (submod) => submod.submodule, + ) + const global = {}; + const bidder = {}; + + function activeModuleGetter(key, useGlobals, modules) { + return function () { + for (const {allowed, bidders, module} of modules) { + const value = module.idObj?.[key]; + if (value != null) { + if (allowed) { + return module; + } else if (useGlobals) { + // value != null, allowed = false, useGlobals = true: + // this module has the preferred ID but it cannot be used (because it's restricted to only some bidders + // and we are calculating global IDs). + // since we don't (yet) have a way to express "global except for these bidders" in FPD, + // do not keep looking for alternative IDs in other (lower priority) modules; the ID will be provided only + // to the bidders this module is configured for. + const listModules = (modules) => modules.map(mod => mod.module.submodule.name).join(', '); + logWarn(`userID modules ${listModules(modules)} provide the same ID ('${key}'); ${module.submodule.name} is the preferred source, but it's configured only for some bidders, unlike ${listModules(modules.filter(mod => mod.bidders == null))}. Other bidders will not see the "${key}" ID.`) + return null; + } else if (bidders == null) { + // value != null, allowed = false, useGlobals = false, bidders == null: + // this module has the preferred ID but it should not be used because it's not bidder-restricted and + // we are calculating bidder-specific ids. Do not keep looking in other lower priority modules, as the ID + // will be set globally. + return null; + } + } + } + return null; } - }); - return carry; - }, {})).map(([k, v]) => [k, v.value])); + } + + Object.entries(modulesById) + .forEach(([key, modules]) => { + let allNonGlobal = true; + const bidderFilters = new Set(); + modules = modules.map(module => { + let bidders = null; + if (Array.isArray(module.config.bidders) && module.config.bidders.length > 0) { + bidders = module.config.bidders; + bidders.forEach(bidder => bidderFilters.add(bidder)); + } else { + allNonGlobal = false; + } + return { + module, + bidders + } + }) + if (!allNonGlobal) { + global[key] = activeModuleGetter(key, true, modules.map(({bidders, module}) => ({allowed: bidders == null, bidders, module}))); + } + bidderFilters.forEach(bidderCode => { + bidder[bidderCode] = bidder[bidderCode] ?? {}; + bidder[bidderCode][key] = activeModuleGetter(key, false, modules.map(({bidders, module}) => ({allowed: bidders?.includes(bidderCode), bidders, module}))); + }) + }); + const combined = Object.values(bidder).concat([global]).reduce((combo, map) => Object.assign(combo, map), {}); + Object.assign(map, {global, bidder, combined}); + } + return map; } -/** - * @param {SubmoduleContainer[]} submodules - */ -function getPrioritizedCombinedSubmoduleIds(submodules) { - return collectByPriority( - submodules.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length), - (submod) => submod.idObj, - (submod) => submod.submodule.name - ) +export function enrichEids(ortb2Fragments) { + const {global: globalFpd, bidder: bidderFpd} = ortb2Fragments; + const {global: globalMods, bidder: bidderMods} = initializedSubmodules; + const globalEids = getEids(globalMods); + if (globalEids.length > 0) { + deepSetValue(globalFpd, 'user.ext.eids', (globalFpd.user?.ext?.eids ?? []).concat(globalEids)); + } + Object.entries(bidderMods).forEach(([bidder, moduleMap]) => { + const bidderEids = getEids(moduleMap); + if (bidderEids.length > 0) { + deepSetValue( + bidderFpd, + `${bidder}.user.ext.eids`, + (bidderFpd[bidder]?.user?.ext?.eids ?? []).concat(bidderEids) + ); + } + }) + return ortb2Fragments; } -/** - * @param {AdUnit[]} adUnits - * @param {SubmoduleContainer[]} submodules - */ -function addIdDataToAdUnitBids(adUnits, submodules) { +function addIdData({adUnits, ortb2Fragments}) { + ortb2Fragments = ortb2Fragments ?? {global: {}, bidder: {}} + enrichEids(ortb2Fragments); if ([adUnits].some(i => !Array.isArray(i) || !i.length)) { return; } + const globalIds = getIds(initializedSubmodules.global); + const globalEids = ortb2Fragments.global.user?.ext?.eids || []; adUnits.forEach(adUnit => { if (adUnit.bids && isArray(adUnit.bids)) { adUnit.bids.forEach(bid => { - const combinedSubmoduleIds = getCombinedSubmoduleIdsForBidder(submodules, bid.bidder); - if (Object.keys(combinedSubmoduleIds).length) { - // create a User ID object on the bid, - bid.userId = combinedSubmoduleIds; - bid.userIdAsEids = createEidsArray(combinedSubmoduleIds); + const bidderIds = Object.assign({}, globalIds, getIds(initializedSubmodules.bidder[bid.bidder] ?? {})); + const bidderEids = globalEids.concat(ortb2Fragments.bidder?.[bid.bidder]?.user?.ext?.eids || []); + if (Object.keys(bidderIds).length > 0) { + bid.userId = bidderIds; + } + if (bidderEids.length > 0) { + bid.userIdAsEids = bidderEids; } }); } @@ -474,7 +592,7 @@ function addIdDataToAdUnitBids(adUnits, submodules) { const INIT_CANCELED = {}; -function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { +function idSystemInitializer({mkDelay = delay} = {}) { const startInit = defer(); const startCallbacks = defer(); let cancel; @@ -487,7 +605,7 @@ function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { cancel.reject(INIT_CANCELED); } cancel = defer(); - return GreedyPromise.race([promise, cancel.promise]) + return PbPromise.race([promise, cancel.promise]) .finally(initMetrics.startTiming('userId.total')) } @@ -511,16 +629,16 @@ function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { } let done = cancelAndTry( - GreedyPromise.all([hooksReady, startInit.promise]) + PbPromise.all([hooksReady, startInit.promise]) .then(timeConsent) .then(checkRefs(() => { initSubmodules(initModules, allModules); })) .then(() => startCallbacks.promise.finally(initMetrics.startTiming('userId.callbacks.pending'))) .then(checkRefs(() => { - const modWithCb = initModules.filter(item => isFn(item.callback)); + const modWithCb = initModules.submodules.filter(item => isFn(item.callback)); if (modWithCb.length) { - return new GreedyPromise((resolve) => processSubmoduleCallbacks(modWithCb, resolve, initModules)); + return new PbPromise((resolve) => processSubmoduleCallbacks(modWithCb, resolve, initModules)); } })) ); @@ -540,7 +658,7 @@ function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { } else { events.on(EVENTS.AUCTION_END, function auctionEndHandler() { events.off(EVENTS.AUCTION_END, auctionEndHandler); - delay(syncDelay).then(startCallbacks.resolve); + mkDelay(syncDelay).then(startCallbacks.resolve); }); } } @@ -558,7 +676,7 @@ function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { return sm.callback != null; }); if (cbModules.length) { - return new GreedyPromise((resolve) => processSubmoduleCallbacks(cbModules, resolve, initModules)); + return new PbPromise((resolve) => processSubmoduleCallbacks(cbModules, resolve, initModules)); } })) ); @@ -572,7 +690,7 @@ let initIdSystem; function getPPID(eids = getUserIdsAsEids() || []) { // userSync.ppid should be one of the 'source' values in getUserIdsAsEids() eg pubcid.org or id5-sync.com const matchingUserId = ppidSource && eids.find(userID => userID.source === ppidSource); - if (matchingUserId && typeof deepAccess(matchingUserId, 'uids.0.id') === 'string') { + if (matchingUserId && typeof matchingUserId?.uids?.[0]?.id === 'string') { const ppidValue = matchingUserId.uids[0].id.replace(/[\W_]/g, ''); if (ppidValue.length >= 32 && ppidValue.length <= 150) { return ppidValue; @@ -591,25 +709,43 @@ function getPPID(eids = getUserIdsAsEids() || []) { * @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. * @param {function} fn required; The next function in the chain, used by hook.js */ -export const requestBidsHook = timedAuctionHook('userId', function requestBidsHook(fn, reqBidsConfigObj, {delay = GreedyPromise.timeout, getIds = getUserIdsAsync} = {}) { - GreedyPromise.race([ +export const startAuctionHook = timedAuctionHook('userId', function requestBidsHook(fn, reqBidsConfigObj, {mkDelay = delay, getIds = getUserIdsAsync} = {}) { + PbPromise.race([ getIds().catch(() => null), - delay(auctionDelay) + mkDelay(auctionDelay) ]).then(() => { - // pass available user id data to bid adapters - addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); + addIdData(reqBidsConfigObj); uidMetrics().join(useMetrics(reqBidsConfigObj.metrics), {propagate: false, includeGroups: true}); // calling fn allows prebid to continue processing fn.call(this, reqBidsConfigObj); }); }); +/** + * Append user id data from config to bids to be accessed in adapters when there are no submodules. + * @param {function} fn required; The next function in the chain, used by hook.js + * @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + */ +export const addUserIdsHook = timedAuctionHook('userId', function requestBidsHook(fn, reqBidsConfigObj) { + addIdData(reqBidsConfigObj); + // calling fn allows prebid to continue processing + fn.call(this, reqBidsConfigObj); +}); + +/** + * Is startAuctionHook added + * @returns {boolean} + */ +function addedStartAuctionHook() { + return !!startAuction.getHooks({hook: startAuctionHook}).length; +} + /** * This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well. * Simple use case will be passing these UserIds to A9 wrapper solution */ function getUserIds() { - return getCombinedSubmoduleIds(initializedSubmodules) + return getIds(initializedSubmodules.combined) } /** @@ -617,7 +753,7 @@ function getUserIds() { * Simple use case will be passing these UserIds to A9 wrapper solution */ function getUserIdsAsEids() { - return createEidsArray(getUserIds()) + return getEids(initializedSubmodules.combined) } /** @@ -626,7 +762,7 @@ function getUserIdsAsEids() { */ function getUserIdsAsEidBySource(sourceName) { - return createEidsArray(getSubmoduleId(initializedSubmodules, sourceName))[0]; + return getUserIdsAsEids().filter(eid => eid.source === sourceName)[0]; } /** @@ -634,7 +770,7 @@ function getUserIdsAsEidBySource(sourceName) { * Sample use case is exposing this function to ESP */ function getEncryptedEidsForSource(source, encrypt, customFunction) { - return initIdSystem().then(() => { + return retryOnCancel().then(() => { let eidsSignals = {}; if (isFn(customFunction)) { @@ -693,6 +829,23 @@ function registerSignalSources() { } } +function retryOnCancel(initParams) { + return initIdSystem(initParams).then( + () => getUserIds(), + (e) => { + if (e === INIT_CANCELED) { + // there's a pending refresh - because GreedyPromise runs this synchronously, we are now in the middle + // of canceling the previous init, before the refresh logic has had a chance to run. + // Use a "normal" Promise to clear the stack and let it complete (or this will just recurse infinitely) + return Promise.resolve().then(getUserIdsAsync) + } else { + logError('Error initializing userId', e) + return PbPromise.reject(e) + } + } + ); +} + /** * Force (re)initialization of ID submodules. * @@ -704,12 +857,12 @@ function registerSignalSources() { * @param callback? called when the refresh is complete */ function refreshUserIds({submoduleNames} = {}, callback) { - return initIdSystem({refresh: true, submoduleNames}) - .then(() => { + return retryOnCancel({refresh: true, submoduleNames}) + .then((userIds) => { if (callback && isFn(callback)) { callback(); } - return getUserIds(); + return userIds; }); } @@ -725,20 +878,7 @@ function refreshUserIds({submoduleNames} = {}, callback) { */ function getUserIdsAsync() { - return initIdSystem().then( - () => getUserIds(), - (e) => { - if (e === INIT_CANCELED) { - // there's a pending refresh - because GreedyPromise runs this synchronously, we are now in the middle - // of canceling the previous init, before the refresh logic has had a chance to run. - // Use a "normal" Promise to clear the stack and let it complete (or this will just recurse infinitely) - return Promise.resolve().then(getUserIdsAsync) - } else { - logError('Error initializing userId', e) - return GreedyPromise.reject(e) - } - } - ); + return retryOnCancel(); } export function getConsentHash() { @@ -757,10 +897,8 @@ function consentChanged(submodule) { return !storedConsent || storedConsent !== getConsentHash(); } -function populateSubmoduleId(submodule, forceRefresh, allSubmodules) { - // TODO: the ID submodule API only takes GDPR consent; it should be updated now that GDPR - // is only a tiny fraction of a vast consent universe - const gdprConsent = gdprDataHandler.getConsentData(); +function populateSubmoduleId(submodule, forceRefresh) { + const consentData = allConsent.getConsentData(); // There are two submodule configuration types to handle: storage or value // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method @@ -776,11 +914,13 @@ function populateSubmoduleId(submodule, forceRefresh, allSubmodules) { } if (!storedId || refreshNeeded || forceRefresh || consentChanged(submodule)) { + const extendedConfig = Object.assign({ enabledStorageTypes: submodule.enabledStorageTypes }, submodule.config); + // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. - response = submodule.submodule.getId(submodule.config, gdprConsent, storedId); + response = submodule.submodule.getId(extendedConfig, consentData, storedId); } else if (typeof submodule.submodule.extendId === 'function') { // If the id exists already, give submodule a chance to decide additional actions that need to be taken - response = submodule.submodule.extendId(submodule.config, gdprConsent, storedId); + response = submodule.submodule.extendId(submodule.config, consentData, storedId); } if (isPlainObject(response)) { @@ -804,18 +944,18 @@ function populateSubmoduleId(submodule, forceRefresh, allSubmodules) { // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.config.value; } else { - const response = submodule.submodule.getId(submodule.config, gdprConsent, undefined); + const response = submodule.submodule.getId(submodule.config, consentData); if (isPlainObject(response)) { if (typeof response.callback === 'function') { submodule.callback = response.callback; } if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); } } } - updatePPID(getCombinedSubmoduleIds(allSubmodules)); } -function updatePPID(userIds = getUserIds()) { - if (userIds && ppidSource) { - const ppid = getPPID(createEidsArray(userIds)); +function updatePPID(priorityMaps) { + const eids = getEids(priorityMaps.combined); + if (eids.length && ppidSource) { + const ppid = getPPID(eids); if (ppid) { if (isGptPubadsDefined()) { window.googletag.pubads().setPublisherProvidedId(ppid); @@ -830,10 +970,12 @@ function updatePPID(userIds = getUserIds()) { } } -function initSubmodules(dest, submodules, forceRefresh = false) { +function initSubmodules(priorityMaps, submodules, forceRefresh = false) { return uidMetrics().fork().measureTime('userId.init.modules', function () { if (!submodules.length) return []; // to simplify log messages from here on + submodules.forEach(submod => populateEnabledStorageTypes(submod)); + /** * filter out submodules that: * @@ -853,7 +995,7 @@ function initSubmodules(dest, submodules, forceRefresh = false) { const initialized = submodules.reduce((carry, submodule) => { return submoduleMetrics(submodule.submodule.name).measureTime('init', () => { try { - populateSubmoduleId(submodule, forceRefresh, submodules); + populateSubmoduleId(submodule, forceRefresh); carry.push(submodule); } catch (e) { logError(`Error in userID module '${submodule.submodule.name}':`, e); @@ -861,27 +1003,20 @@ function initSubmodules(dest, submodules, forceRefresh = false) { return carry; }) }, []); - if (initialized.length) { - setPrebidServerEidPermissions(initialized); - } - initialized.forEach(updateInitializedSubmodules.bind(null, dest)); + priorityMaps.refresh(initialized); + updatePPID(priorityMaps); return initialized; }) } -function updateInitializedSubmodules(dest, submodule) { - let updated = false; - for (let i = 0; i < dest.length; i++) { - if (submodule.config.name.toLowerCase() === dest[i].config.name.toLowerCase()) { - updated = true; - dest[i] = submodule; - break; - } - } +function getConfiguredStorageTypes(config) { + return config?.storage?.type?.trim().split(/\s*&\s*/) || []; +} - if (!updated) { - dest.push(submodule); - } +function hasValidStorageTypes(config) { + const storageTypes = getConfiguredStorageTypes(config); + + return storageTypes.every(storageType => ALL_STORAGE_TYPES.has(storageType)); } /** @@ -891,64 +1026,102 @@ function updateInitializedSubmodules(dest, submodule) { * @param {SubmoduleConfig[]} configRegistry * @returns {SubmoduleConfig[]} */ -function getValidSubmoduleConfigs(configRegistry) { +export function getValidSubmoduleConfigs(configRegistry) { + function err(msg, ...args) { + logWarn(`Invalid userSync.userId config: ${msg}`, ...args) + } if (!Array.isArray(configRegistry)) { + if (configRegistry != null) { + err('must be an array', configRegistry); + } return []; } - return configRegistry.reduce((carry, config) => { - // every submodule config obj must contain a valid 'name' - if (!config || isEmptyStr(config.name)) { - return carry; - } - // Validate storage config contains 'type' and 'name' properties with non-empty string values - // 'type' must be one of html5, cookies - if (config.storage && - !isEmptyStr(config.storage.type) && - !isEmptyStr(config.storage.name) && - ALL_STORAGE_TYPES.has(config.storage.type)) { - carry.push(config); - } else if (isPlainObject(config.value)) { - carry.push(config); - } else if (!config.storage && !config.value) { - carry.push(config); + return configRegistry.filter(config => { + if (!config?.name) { + return err('must specify "name"', config); + } else if (config.storage) { + if (!config.storage.name || !config.storage.type) { + return err('must specify "storage.name" and "storage.type"', config); + } else if (!hasValidStorageTypes(config)) { + return err('invalid "storage.type"', config) + } + ['expires', 'refreshInSeconds'].forEach(param => { + let value = config.storage[param]; + if (value != null && typeof value !== 'number') { + value = Number(value) + if (isNaN(value)) { + err(`storage.${param} must be a number and will be ignored`, config); + delete config.storage[param]; + } else { + config.storage[param] = value; + } + } + }); } - return carry; - }, []); + return true; + }) } const ALL_STORAGE_TYPES = new Set([LOCAL_STORAGE, COOKIE]); -function canUseStorage(submodule) { - switch (submodule.config?.storage?.type) { - case LOCAL_STORAGE: - if (submodule.storageMgr.localStorageIsEnabled()) { - if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { - logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`); - return false - } - return true; - } - break; - case COOKIE: - if (submodule.storageMgr.cookiesAreEnabled()) { - if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { - logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`); - return false; - } - return true - } - break; +function canUseLocalStorage(submodule) { + if (!submodule.storageMgr.localStorageIsEnabled()) { + return false; + } + + if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { + logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`); + return false } - return false; + + return true; +} + +function canUseCookies(submodule) { + if (!submodule.storageMgr.cookiesAreEnabled()) { + return false; + } + + if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { + logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`); + return false; + } + + return true +} + +function populateEnabledStorageTypes(submodule) { + if (submodule.enabledStorageTypes) { + return; + } + + const storageTypes = getConfiguredStorageTypes(submodule.config); + + submodule.enabledStorageTypes = storageTypes.filter(type => { + switch (type) { + case LOCAL_STORAGE: + return canUseLocalStorage(submodule); + case COOKIE: + return canUseCookies(submodule); + } + + return false; + }); +} + +function canUseStorage(submodule) { + return !!submodule.enabledStorageTypes.length; } function updateEIDConfig(submodules) { EID_CONFIG.clear(); - Object.entries(collectByPriority( - submodules, - (mod) => mod.eids, - (mod) => mod.name - )).forEach(([id, conf]) => EID_CONFIG.set(id, conf)); + Object.entries( + orderByPriority( + submodules, + (mod) => Object.keys(mod.eids || {}), + (mod) => mod + ) + ).forEach(([key, submodules]) => EID_CONFIG.set(key, submodules[0].eids[key])) } /** @@ -971,7 +1144,6 @@ function updateSubmodules() { const submoduleConfig = find(configs, j => j.name && (j.name.toLowerCase() === i.name.toLowerCase() || (i.aliasName && j.name.toLowerCase() === i.aliasName.toLowerCase()))); if (submoduleConfig && i.name !== submoduleConfig.name) submoduleConfig.name = i.name; - i.findRootDomain = findRootDomain; return submoduleConfig ? { submodule: i, config: submoduleConfig, @@ -982,25 +1154,26 @@ function updateSubmodules() { }).filter(submodule => submodule !== null) .forEach((sm) => submodules.push(sm)); - if (!addedUserIdHook && submodules.length) { - // priority value 40 will load after consentManagement with a priority of 50 - getGlobal().requestBids.before(requestBidsHook, 40); - adapterManager.callDataDeletionRequest.before(requestDataDeletion); - coreGetPPID.after((next) => next(getPPID())); + if (submodules.length) { + if (!addedStartAuctionHook()) { + startAuction.getHooks({hook: addUserIdsHook}).remove(); + startAuction.before(startAuctionHook, 100) // use higher priority than dataController / rtd + adapterManager.callDataDeletionRequest.before(requestDataDeletion); + coreGetPPID.after((next) => next(getPPID())); + } logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name)); - addedUserIdHook = true; } } /** * This function will update the idPriority according to the provided configuration * @param {Object} idPriorityConfig - * @param {SubmoduleContainer[]} submodules + * @param {Submodule[]} submodules */ function updateIdPriority(idPriorityConfig, submodules) { if (idPriorityConfig) { const result = {}; - const aliasToName = new Map(submodules.map(s => s.submodule.aliasName ? [s.submodule.aliasName, s.submodule.name] : [])); + const aliasToName = new Map(submodules.map(s => s.aliasName ? [s.aliasName, s.name] : [])); Object.keys(idPriorityConfig).forEach(key => { const priority = isArray(idPriorityConfig[key]) ? [...idPriorityConfig[key]].reverse() : [] result[key] = priority.map(s => aliasToName.has(s) ? aliasToName.get(s) : s); @@ -1009,6 +1182,8 @@ function updateIdPriority(idPriorityConfig, submodules) { } else { idPriority = {}; } + initializedSubmodules.refresh(); + updateEIDConfig(submodules) } export function requestDataDeletion(next, ...args) { @@ -1031,6 +1206,7 @@ export function requestDataDeletion(next, ...args) { * @param {Submodule} submodule */ export function attachIdSystem(submodule) { + submodule.findRootDomain = findRootDomain; if (!find(submoduleRegistry, i => i.name === submodule.name)) { submoduleRegistry.push(submodule); GDPR_GVLIDS.register(MODULE_TYPE_UID, submodule.name, submodule.gvlid) @@ -1054,13 +1230,12 @@ function normalizePromise(fn) { * so a callback is added to fire after the consentManagement module. * @param {{getConfig:function}} config */ -export function init(config, {delay = GreedyPromise.timeout} = {}) { +export function init(config, {mkDelay = delay} = {}) { ppidSource = undefined; submodules = []; configRegistry = []; - addedUserIdHook = false; - initializedSubmodules = []; - initIdSystem = idSystemInitializer({delay}); + initializedSubmodules = mkPriorityMaps(); + initIdSystem = idSystemInitializer({mkDelay}); if (configListener != null) { configListener(); } @@ -1074,10 +1249,10 @@ export function init(config, {delay = GreedyPromise.timeout} = {}) { ppidSource = userSync.ppid; if (userSync.userIds) { configRegistry = userSync.userIds; - syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; - auctionDelay = isNumber(userSync.auctionDelay) ? userSync.auctionDelay : NO_AUCTION_DELAY; + syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : USERSYNC_DEFAULT_CONFIG.syncDelay + auctionDelay = isNumber(userSync.auctionDelay) ? userSync.auctionDelay : USERSYNC_DEFAULT_CONFIG.auctionDelay; updateSubmodules(); - updateIdPriority(userSync.idPriority, submodules); + updateIdPriority(userSync.idPriority, submoduleRegistry); initIdSystem({ready: true}); } } @@ -1091,17 +1266,18 @@ export function init(config, {delay = GreedyPromise.timeout} = {}) { (getGlobal()).refreshUserIds = normalizePromise(refreshUserIds); (getGlobal()).getUserIdsAsync = normalizePromise(getUserIdsAsync); (getGlobal()).getUserIdsAsEidBySource = getUserIdsAsEidBySource; + if (!addedStartAuctionHook()) { + // Add ortb2.user.ext.eids even if 0 submodules are added + startAuction.before(addUserIdsHook, 100); // use higher priority than dataController / rtd + } +} + +export function resetUserIds() { + config.setConfig({userSync: {}}) + init(config); } // init config update listener to start the application init(config); module('userId', attachIdSystem, { postInstallAllowed: true }); - -export function setOrtbUserExtEids(ortbRequest, bidderRequest, context) { - const eids = deepAccess(context, 'bidRequests.0.userIdAsEids'); - if (eids && Object.keys(eids).length > 0) { - deepSetValue(ortbRequest, 'user.ext.eids', eids); - } -} -registerOrtbProcessor({type: REQUEST, name: 'userExtEids', fn: setOrtbUserExtEids}); diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 7a01e128814..9aea3e8d533 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -70,12 +70,6 @@ pbjs.setConfig({ params: { url: 'https://d9.flashtalking.com/d9core', // required, if not populated ftrack will not run } - }, { - name: 'parrableId', - params: { - // Replace partner with comma-separated (if more than one) Parrable Partner Client ID(s) for Parrable-aware bid adapters in use - partner: "30182847-e426-4ff9-b2b5-9ca1324ea09b" - } },{ name: 'identityLink', params: { @@ -358,6 +352,9 @@ pbjs.setConfig({ }, { name: 'naveggId', + }, + { + name: 'lmpid', }], syncDelay: 5000 } @@ -369,16 +366,15 @@ pbjs.setConfig({ Example showing how to configure a `params` object to pass directly to bid adapters ``` - pbjs.setConfig({ -userSync: { -userIds: [{ -name: 'tncId', -params: { -providerId: "c8549079-f149-4529-a34b-3fa91ef257d1" -} -}], -syncDelay: 5000 -} + userSync: { + userIds: [{ + name: 'tncId', + params: { + url: 'https://js.tncid.app/remote.min.js' //Optional + } + }], + syncDelay: 5000 + } }); ``` diff --git a/modules/utiqSystem.js b/modules/utiqIdSystem.js similarity index 83% rename from modules/utiqSystem.js rename to modules/utiqIdSystem.js index 473dc5854a9..b5da87627a5 100644 --- a/modules/utiqSystem.js +++ b/modules/utiqIdSystem.js @@ -1,7 +1,7 @@ /** * This module adds Utiq provided by Utiq SA/NV to the User ID module * The {@link module:modules/userId} module is required - * @module modules/utiqSystem + * @module modules/utiqIdSystem * @requires module:modules/userId */ import { logInfo } from '../src/utils.js'; @@ -9,7 +9,7 @@ import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; -const MODULE_NAME = 'utiq'; +const MODULE_NAME = 'utiqId'; const LOG_PREFIX = 'Utiq module'; export const storage = getStorageManager({ @@ -27,11 +27,17 @@ function getUtiqFromStorage() { let utiqPassStorage = JSON.parse( storage.getDataFromLocalStorage('utiqPass') ); - logInfo( - `${LOG_PREFIX}: Local storage utiqPass: ${JSON.stringify( - utiqPassStorage - )}` - ); + + const netIdAdtechpass = storage.getDataFromLocalStorage('netid_utiq_adtechpass'); + + if (netIdAdtechpass) { + logInfo( + `${LOG_PREFIX}: Local storage netid_utiq_adtechpass: ${netIdAdtechpass}` + ); + return { + utiq: netIdAdtechpass, + } + } if ( utiqPassStorage && @@ -40,12 +46,19 @@ function getUtiqFromStorage() { utiqPassStorage.connectId.idGraph.length > 0 ) { utiqPass = utiqPassStorage.connectId.idGraph[0]; + + logInfo( + `${LOG_PREFIX}: Local storage utiqPass: ${JSON.stringify( + utiqPassStorage + )}` + ); + + logInfo( + `${LOG_PREFIX}: Graph of utiqPass: ${JSON.stringify( + utiqPass + )}` + ); } - logInfo( - `${LOG_PREFIX}: Graph of utiqPass: ${JSON.stringify( - utiqPass - )}` - ); return { utiq: @@ -56,7 +69,7 @@ function getUtiqFromStorage() { } /** @type {Submodule} */ -export const utiqSubmodule = { +export const utiqIdSubmodule = { /** * Used to link submodule with config * @type {string} @@ -135,4 +148,4 @@ export const utiqSubmodule = { } }; -submodule('userId', utiqSubmodule); +submodule('userId', utiqIdSubmodule); diff --git a/modules/utiqSystem.md b/modules/utiqIdSystem.md similarity index 54% rename from modules/utiqSystem.md rename to modules/utiqIdSystem.md index d2c53480383..c7f4f95827f 100644 --- a/modules/utiqSystem.md +++ b/modules/utiqIdSystem.md @@ -5,7 +5,7 @@ Utiq ID Module. First, make sure to add the utiq submodule to your Prebid.js package with: ``` -gulp build --modules=userId,adfBidAdapter,ixBidAdapter,prebidServerBidAdapter,utiqSystem +gulp build --modules=userId,adfBidAdapter,ixBidAdapter,prebidServerBidAdapter,utiqIdSystem ``` ## Parameter Descriptions @@ -15,8 +15,3 @@ gulp build --modules=userId,adfBidAdapter,ixBidAdapter,prebidServerBidAdapter,ut | name | String | The name of the module | `"utiq"` | | params | Object | Object with configuration parameters for utiq User Id submodule | - | | params.maxDelayTime | Integer | Max amount of time (in seconds) before looking into storage for data | 2500 | -| bidders | Array of Strings | An array of bidder codes to which this user ID may be sent. Currently required and supporting AdformOpenRTB | [`"adf"`, `"adformPBS"`, `"ix"`] | -| storage | Object | Local storage configuration object | - | -| storage.type | String | Type of the storage that would be used to store user ID. Must be `"html5"` to utilise HTML5 local storage. | `"html5"` | -| storage.name | String | The name of the key in local storage where the user ID will be stored. | `"utiq"` | -| storage.expires | Integer | How long (in days) the user ID information will be stored. For safety reasons, this information is required. | `1` | diff --git a/modules/validationFpdModule/index.js b/modules/validationFpdModule/index.js index 70af9d30ec3..2330c41099c 100644 --- a/modules/validationFpdModule/index.js +++ b/modules/validationFpdModule/index.js @@ -13,8 +13,8 @@ let optout; /** * Check if data passed is empty - * @param {*} value to test against - * @returns {Boolean} is value empty + * @param {*} data to test against + * @returns {Boolean} is data empty */ function isEmptyData(data) { let check = true; @@ -30,10 +30,10 @@ function isEmptyData(data) { /** * Check if required keys exist in data object - * @param {Object} data object - * @param {Array} array of required keys - * @param {String} object path (for printing warning) - * @param {Number} index of object value in the data array (for printing warning) + * @param {Object} obj data object + * @param {Array} required array of required keys + * @param {String} parent object path (for printing warning) + * @param {Number} i index of object value in the data array (for printing warning) * @returns {Boolean} is requirements fulfilled */ function getRequiredData(obj, required, parent, i) { @@ -51,8 +51,8 @@ function getRequiredData(obj, required, parent, i) { /** * Check if data type is valid - * @param {*} value to test against - * @param {Object} object containing type definition and if should be array bool + * @param {*} data value to test against + * @param {Object} mapping object containing type definition and if should be array bool * @returns {Boolean} is type fulfilled */ function typeValidation(data, mapping) { @@ -77,10 +77,10 @@ function typeValidation(data, mapping) { /** * Validates ortb2 data arrays and filters out invalid data - * @param {Array} ortb2 data array - * @param {Object} object defining child type and if array - * @param {String} config path of data array - * @param {String} parent path for logging warnings + * @param {Array} arr ortb2 data array + * @param {Object} child object defining child type and if array + * @param {String} path config path of data array + * @param {String} parent parent path for logging warnings * @returns {Array} validated/filtered data */ export function filterArrayData(arr, child, path, parent) { @@ -136,9 +136,9 @@ export function filterArrayData(arr, child, path, parent) { /** * Validates ortb2 object and filters out invalid data - * @param {Object} ortb2 object - * @param {String} config path of data array - * @param {String} parent path for logging warnings + * @param {Object} fpd ortb2 object + * @param {String} path config path of data array + * @param {String} parent parent path for logging warnings * @returns {Object} validated/filtered data */ export function validateFpd(fpd, path = '', parent = '') { @@ -190,6 +190,8 @@ export function validateFpd(fpd, path = '', parent = '') { /** * Run validation on global and bidder config data for ortb2 + * @param {Object} data global and bidder config data + * @returns {Object} validated data */ function runValidations(data) { return { @@ -200,6 +202,9 @@ function runValidations(data) { /** * Sets default values to ortb2 if exists and adds currency and ortb2 setConfig callbacks on init + * @param {Object} fpdConf configuration object + * @param {Object} data ortb2 data + * @returns {Object} processed data */ export function processFpd(fpdConf, data) { // Checks for existsnece of pubcid optout cookie/storage @@ -210,11 +215,11 @@ export function processFpd(fpdConf, data) { return (!fpdConf.skipValidations) ? runValidations(data) : data; } -/** @type {firstPartyDataSubmodule} */ +/** @type {{name: string, queue: number, processFpd: function}} */ export const validationSubmodule = { name: 'validation', queue: 1, processFpd } -submodule('firstPartyData', validationSubmodule) +submodule('firstPartyData', validationSubmodule); diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index ada843a6e45..6ef5b6ebb3e 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -1,6 +1,7 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import { logMessage, groupBy, flatten, uniques } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ajax } from '../src/ajax.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -9,130 +10,171 @@ import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; */ const BIDDER_CODE = 'vdoai'; -const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; + +/** + * Determines whether or not the given bid response is valid. + * + * @param {object} vdoresponse The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function vdoIsBidResponseValid(vdoresponse) { + if (!vdoresponse.requestId || !vdoresponse.cpm || !vdoresponse.creativeId || !vdoresponse.ttl || !vdoresponse.currency || !vdoresponse.meta.advertiserDomains) { + return false; + } + switch (vdoresponse.meta.mediaType) { + case BANNER: + return Boolean(vdoresponse.width && vdoresponse.height && vdoresponse.ad); + case VIDEO: + return Boolean(vdoresponse.vastXml || vdoresponse.vastUrl); + } + return false; +} export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], + /** * Determines whether or not the given bid request is valid. * - * @param {BidRequest} bid The bid params to validate. + * @param {BidRequest} vdobid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ - isBidRequestValid: function (bid) { - return !!(bid.params.placementId); + isBidRequestValid: (vdobid) => { + logMessage('vdobid', vdobid); + return Boolean(vdobid.bidId && vdobid.params && vdobid.params.host && vdobid.params.adUnitType && + (vdobid.params.adUnitId || vdobid.params.adUnitId === 0)); }, /** * Make a server request from the list of BidRequests. * - * @return Array Info describing the request to the server. - * @param validBidRequests - * @param bidderRequest + * @return ServerRequest Info describing the request to the server. */ - buildRequests: function (validBidRequests, bidderRequest) { - if (validBidRequests.length === 0) { - return []; + buildRequests: (vdoValidBidRequests, bidderRequest) => { + let winTop; + try { + winTop = window.top; + winTop.location.toString(); + } catch (e) { + logMessage(e); + winTop = window; } + const placements = groupBy(vdoValidBidRequests.map(bidRequest => vdoBuildPlacement(bidRequest)), 'host') + return Object.keys(placements) + .map(host => vdoBuildRequest(winTop, host, placements[host].map(placement => placement.adUnit), bidderRequest)); + }, - return validBidRequests.map(bidRequest => { - const sizes = getAdUnitSizes(bidRequest); - const payload = { - placementId: bidRequest.params.placementId, - sizes: sizes, - bidId: bidRequest.bidId, - // TODO: is 'page' the right value here? - referer: bidderRequest.refererInfo.page, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - id: bidRequest.auctionId, - mediaType: bidRequest.mediaTypes.video ? 'video' : 'banner' - }; - bidRequest.params.bidFloor && (payload['bidFloor'] = bidRequest.params.bidFloor); - return { - method: 'POST', - url: ENDPOINT_URL, - data: payload - }; - }); + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} vdobid The bid that won the auction + */ + onBidWon: (vdobid) => { + const cpm = vdobid.pbMg; + if (vdobid.nurl !== '') { + vdobid.nurl = vdobid.nurl.replace( + /\$\{AUCTION_PRICE\}/, + cpm + ); + ajax(vdobid.nurl, null); + } }, /** * Unpack the response from the server into a list of bids. * - * @param {ServerResponse} serverResponse A successful response from the server. - * @param bidRequest + * @param {ServerResponse} vdoServerResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function (serverResponse, bidRequest) { + interpretResponse: (vdoServerResponse, vdoBidRequest) => { const bidResponses = []; - const response = serverResponse.body; - const creativeId = response.adid || 0; - // const width = response.w || 0; - const width = response.width; - // const height = response.h || 0; - const height = response.height; - const cpm = response.price || 0; - - response.rWidth = width; - response.rHeight = height; - - const adCreative = response.vdoCreative; - - if (width !== 0 && height !== 0 && cpm !== 0 && creativeId !== 0) { - // const dealId = response.dealid || ''; - const currency = response.cur || 'USD'; - const netRevenue = true; - // const referrer = bidRequest.data.referer; - const bidResponse = { - requestId: response.bidId, - cpm: cpm, - width: width, - height: height, - creativeId: creativeId, - // dealId: dealId, - currency: currency, - netRevenue: netRevenue, - ttl: 60, - // referrer: referrer, - // ad: response.adm - // ad: adCreative, - mediaType: response.mediaType - }; - - if (response.mediaType == 'video') { - bidResponse.vastXml = adCreative; - } else { - bidResponse.ad = adCreative; - } - if (response.adDomain) { - bidResponse.meta = { - advertiserDomains: response.adDomain - }; + const serverBody = vdoServerResponse.body; + const len = serverBody.length; + for (let i = 0; i < len; i++) { + const bidResponse = serverBody[i]; + if (vdoIsBidResponseValid(bidResponse)) { + bidResponses.push(bidResponse); } - bidResponses.push(bidResponse); } - return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponse) { - let syncUrls = serverResponse[0] && serverResponse[0].body && serverResponse[0].body.cookiesync && serverResponse[0].body.cookiesync.bidder_status; + getUserSyncs: (userSyncOptions, vdoServerResponses, userGdprConsent, UserUspConsent) => { + const allIframeSyncs = []; + const allImageSyncs = []; + for (let i = 0; i < vdoServerResponses.length; i++) { + const serverResponseHeaders = vdoServerResponses[i].headers; + const vdoImgSync = (serverResponseHeaders != null && userSyncOptions.pixelEnabled) ? serverResponseHeaders.get('X-PLL-UserSync-Image') : null + const vdoIframeSync = (serverResponseHeaders != null && userSyncOptions.iframeEnabled) ? serverResponseHeaders.get('X-PLL-UserSync-Iframe') : null + if (vdoIframeSync != null) { + allIframeSyncs.push(vdoIframeSync) + } else if (vdoImgSync != null) { + allImageSyncs.push(vdoImgSync) + } + } + return [allIframeSyncs.filter(uniques).map(it => { return { type: 'iframe', url: it } }), + allImageSyncs.filter(uniques).map(it => { return { type: 'image', url: it } })].reduce(flatten, []).filter(uniques); + } +}; + +registerBidder(spec); + +function vdoBuildRequest(windowTop, hostname, vdoAdUnits, bidderRequest) { + return { + url: `https://${hostname}/hb`, + method: 'POST', + data: { + secure: (location.protocol === 'https:'), + deviceWidth: windowTop.screen.width, + deviceHeight: windowTop.screen.height, + adUnits: vdoAdUnits, + ortb2: bidderRequest?.ortb2, + refererInfo: bidderRequest?.refererInfo, + sua: bidderRequest?.ortb2?.device?.sua, + page: bidderRequest?.ortb2?.site?.page || bidderRequest?.refererInfo?.page + } + } +} - if (syncOptions.iframeEnabled && syncUrls && syncUrls.length > 0) { - let prebidSyncUrls = syncUrls.map(syncObj => { +function vdoBuildPlacement(vdoBidRequest) { + let sizes; + if (vdoBidRequest.mediaTypes) { + switch (vdoBidRequest.params.adUnitType) { + case BANNER: + if (vdoBidRequest.mediaTypes.banner && vdoBidRequest.mediaTypes.banner.sizes) { + sizes = vdoBidRequest.mediaTypes.banner.sizes; + } + break; + case VIDEO: + if (vdoBidRequest.mediaTypes.video && vdoBidRequest.mediaTypes.video.playerSize) { + sizes = [vdoBidRequest.mediaTypes.video.playerSize]; + } + break; + } + } + sizes = (sizes || []).concat(vdoBidRequest.sizes || []); + return { + host: vdoBidRequest.params.host, + adUnit: { + id: vdoBidRequest.params.adUnitId, + bidId: vdoBidRequest.bidId, + transactionId: vdoBidRequest.ortb2Imp?.ext?.tid, + sizes: sizes.map(size => { return { - url: syncObj.usersync.url, - type: 'iframe' + width: size[0], + height: size[1] } - }) - return prebidSyncUrls; + }), + type: vdoBidRequest.params.adUnitType.toUpperCase(), + ortb2Imp: vdoBidRequest.ortb2Imp, + publisherId: vdoBidRequest.params.publisherId, + userIdAsEids: vdoBidRequest.userIdAsEids, + supplyChain: vdoBidRequest.schain, + custom1: vdoBidRequest.params.custom1, + custom2: vdoBidRequest.params.custom2, + custom3: vdoBidRequest.params.custom3, + custom4: vdoBidRequest.params.custom4, + custom5: vdoBidRequest.params.custom5 } - return []; - }, - - onTImeout: function(data) {}, - onBidWon: function(bid) {}, - onSetTargeting: function(bid) {} -}; -registerBidder(spec); + } +} diff --git a/modules/vdoaiBidAdapter.md b/modules/vdoaiBidAdapter.md index 04200cc9be0..a1f856b651a 100644 --- a/modules/vdoaiBidAdapter.md +++ b/modules/vdoaiBidAdapter.md @@ -10,48 +10,61 @@ Maintainer: arjit@z1media.com Module that connects to VDO.AI's demand sources -# Test Parameters -``` - var adUnits = [ - { - code: 'test-div', - mediaTypes: { - banner: { - sizes: [[300, 250]] // a display size - } - }, - bids: [ - { - bidder: "vdoai", - params: { - placementId: 'newsdv77', - bidFloor: 0.01 // Optional - } - } - ] +# Test Parameters for banner +``` +var adUnits = [{ + code: 'placementCode', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'vdoai', + params: { + host: 'exchange-9qao.ortb.net', + adUnitId: 0, + adUnitType: 'banner', + custom1: 'custom1', + custom2: 'custom2', + custom3: 'custom3', + custom4: 'custom4', + custom5: 'custom5' } - ]; + }] +}]; ``` - -# Video Test Parameters +# Test Parameters for video ``` -var videoAdUnit = { - code: 'test-div', - sizes: [[640, 480]], - mediaTypes: { - video: { - playerSize: [[640, 480]], - context: 'instream' - }, - }, - bids: [ - { - bidder: "vdoai", +var videoAdUnit = [{ + code: 'video1', + sizes: [[300, 250]], + bids: [{ + bidder: 'vdoai', params: { - placementId: 'newsdv77' + host: 'exchange-9qao.ortb.net', + adUnitId: 0, + adUnitType: 'video', + custom1: 'custom1', + custom2: 'custom2', + custom3: 'custom3', + custom4: 'custom4', + custom5: 'custom5' } + }] +}]; +``` + +# Configuration + +The VDO.AI Bidder Adapter expects Prebid Cache(for video) to be enabled so that we can store and retrieve a single vastXml. + +``` +pbjs.setConfig({ + usePrebidCache: true, + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache' } - ] -}; -``` \ No newline at end of file +}); +``` diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js index 26fa89cfe03..f2ab448aec8 100644 --- a/modules/verizonMediaIdSystem.js +++ b/modules/verizonMediaIdSystem.js @@ -67,7 +67,7 @@ export const verizonMediaIdSubmodule = { he: params.he, gdpr: isEUConsentRequired(consentData) ? '1' : '0', gdpr_consent: isEUConsentRequired(consentData) ? consentData.gdpr.consentString : '', - us_privacy: consentData && consentData.uspConsent ? consentData.uspConsent : '' + us_privacy: consentData && consentData.usp ? consentData.usp : '' }; if (params.pixelId) { diff --git a/modules/viantOrtbBidAdapter.js b/modules/viantOrtbBidAdapter.js index d056dfeb2eb..cfc4c450db8 100644 --- a/modules/viantOrtbBidAdapter.js +++ b/modules/viantOrtbBidAdapter.js @@ -5,7 +5,8 @@ import {ortbConverter} from '../libraries/ortbConverter/converter.js' import {deepAccess, getBidIdParameter, logError} from '../src/utils.js'; const BIDDER_CODE = 'viant'; -const ENDPOINT = 'https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder' +const ENDPOINT = 'https://bidders-us.adelphic.net/d/rtb/v25/prebid/bidder' +const ADAPTER_VERSION = '2.0.0'; const DEFAULT_BID_TTL = 300; const DEFAULT_CURRENCY = 'USD'; @@ -85,6 +86,25 @@ function createRequest(bidRequests, bidderRequest, mediaType) { if (!data.regs.ext) data.regs.ext = {}; data.regs.ext.us_privacy = bidderRequest.uspConsent; } + let imp = data.imp || []; + let dealsMap = new Map(); + if (bidderRequest.bids) { + bidderRequest.bids.forEach(bid => { + if (bid.ortb2Imp && bid.ortb2Imp.pmp) { + dealsMap.set(bid.bidId, bid.ortb2Imp.pmp); + } + }); + } + imp.forEach((element) => { + let deals = dealsMap.get(element.id); + if (deals) { + element.pmp = deals; + } + }); + data.ext = data.ext || {}; + data.ext.viant = { + adapterVersion: ADAPTER_VERSION + }; return { method: 'POST', url: ENDPOINT, diff --git a/modules/vibrantmediaBidAdapter.js b/modules/vibrantmediaBidAdapter.js index 8809aae32bd..1e5f0b0136b 100644 --- a/modules/vibrantmediaBidAdapter.js +++ b/modules/vibrantmediaBidAdapter.js @@ -7,7 +7,7 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; * Note: Only BANNER and VIDEO are currently supported by the prebid server. */ -import {logError, triggerPixel} from '../src/utils.js'; +import {getWinDimensions, logError, triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {OUTSTREAM} from '../src/video.js'; @@ -97,17 +97,6 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: SUPPORTED_MEDIA_TYPES, - /** - * Transforms the 'raw' bid params into ones that this adapter can use, prior to creating the bid request. - * - * @param {object} bidParams the params to transform. - * - * @returns {object} the bid params. - */ - transformBidParams: function(bidParams) { - return bidParams; - }, - /** * Determines whether or not the given bid request is valid. For all bid requests passed to the buildRequests * function, each will have been passed to this function and this function will have returned true. @@ -149,8 +138,8 @@ export const spec = { gdpr: bidderRequest.gdprConsent, usp: bidderRequest.uspConsent, window: { - width: window.innerWidth, - height: window.innerHeight, + width: getWinDimensions().innerWidth, + height: getWinDimensions().innerHeight, }, biddata: transformedBidRequests, }; diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index ea1555e5327..ed732a4814a 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -1,494 +1,46 @@ -import { - _each, - deepAccess, - isFn, - parseSizesInput, - parseUrl, - uniques, - isArray, - formatQS, - triggerPixel -} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; -import {bidderSettings} from '../src/bidderSettings.js'; -import {config} from '../src/config.js'; -import {chunk} from '../libraries/chunk/chunk.js'; +import { + createSessionId, + isBidRequestValid, + getCacheOpt, + getNextDealId, + onBidWon, + createUserSyncGetter, + getVidazooSessionId, + createBuildRequestsFn, + createInterpretResponseFn +} from '../libraries/vidazooUtils/bidderUtils.js'; +import {OPT_CACHE_KEY, OPT_TIME_KEY} from '../libraries/vidazooUtils/constants.js'; const GVLID = 744; const DEFAULT_SUB_DOMAIN = 'prebid'; const BIDDER_CODE = 'vidazoo'; const BIDDER_VERSION = '1.0.0'; -const CURRENCY = 'USD'; -const TTL_SECONDS = 60 * 5; -const DEAL_ID_EXPIRY = 1000 * 60 * 15; -const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 60; -const SESSION_ID_KEY = 'vidSid'; -const OPT_CACHE_KEY = 'vdzwopt'; -export const webSessionId = 'wsid_' + parseInt(Date.now() * Math.random()); -const storage = getStorageManager({bidderCode: BIDDER_CODE}); - -function getTopWindowQueryParams() { - try { - const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); - return parsedUrl.search; - } catch (e) { - return ''; - } -} +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); +export const webSessionId = createSessionId(); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.cootlogix.com`; } -export function extractCID(params) { - return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; -} - -export function extractPID(params) { - return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; -} - -export function extractSubDomain(params) { - return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; -} - -function isBidRequestValid(bid) { - const params = bid.params || {}; - return !!(extractCID(params) && extractPID(params)); -} - -function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { - const { - params, - bidId, - userId, - adUnitCode, - schain, - mediaTypes, - ortb2Imp, - bidderRequestId, - bidRequestsCount, - bidderRequestsCount, - bidderWinsCount - } = bid; - const {ext} = params; - let {bidFloor} = params; - const hashUrl = hashCode(topWindowUrl); - const dealId = getNextDealId(hashUrl); - const uniqueDealId = getUniqueDealId(hashUrl); - const sId = getVidazooSessionId(); - const pId = extractPID(params); - const ptrace = getCacheOpt(); - const isStorageAllowed = bidderSettings.get(BIDDER_CODE, 'storageAllowed'); - - const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); - const cat = deepAccess(bidderRequest, 'ortb2.site.cat', []); - const pagecat = deepAccess(bidderRequest, 'ortb2.site.pagecat', []); - const contentData = deepAccess(bidderRequest, 'ortb2.site.content.data', []); - const userData = deepAccess(bidderRequest, 'ortb2.user.data', []); - - if (isFn(bid.getFloor)) { - const floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - - if (floorInfo.currency === 'USD') { - bidFloor = floorInfo.floor; - } - } - - let data = { - url: encodeURIComponent(topWindowUrl), - uqs: getTopWindowQueryParams(), - cb: Date.now(), - bidFloor: bidFloor, - bidId: bidId, - referrer: bidderRequest.refererInfo.ref, - adUnitCode: adUnitCode, - publisherId: pId, - sessionId: sId, - sizes: sizes, - dealId: dealId, - uniqueDealId: uniqueDealId, - bidderVersion: BIDDER_VERSION, - prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}`, - schain: schain, - mediaTypes: mediaTypes, - ptrace: ptrace, - isStorageAllowed: isStorageAllowed, - gpid: gpid, - cat: cat, - contentData, - userData: userData, - pagecat: pagecat, - transactionId: ortb2Imp?.ext?.tid, - bidderRequestId: bidderRequestId, - bidRequestsCount: bidRequestsCount, - bidderRequestsCount: bidderRequestsCount, - bidderWinsCount: bidderWinsCount, - bidderTimeout: bidderTimeout, - webSessionId: webSessionId - }; - - appendUserIdsToRequestPayload(data, userId); - - const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); - - if (sua) { - data.sua = sua; - } - - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.consentString) { - data.gdprConsent = bidderRequest.gdprConsent.consentString; - } - if (bidderRequest.gdprConsent.gdprApplies !== undefined) { - data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - } - if (bidderRequest.uspConsent) { - data.usPrivacy = bidderRequest.uspConsent; - } - - if (bidderRequest.gppConsent) { - data.gppString = bidderRequest.gppConsent.gppString; - data.gppSid = bidderRequest.gppConsent.applicableSections; - } else if (bidderRequest.ortb2?.regs?.gpp) { - data.gppString = bidderRequest.ortb2.regs.gpp; - data.gppSid = bidderRequest.ortb2.regs.gpp_sid; - } - - if (bidderRequest.fledgeEnabled) { - const fledge = deepAccess(bidderRequest, 'ortb2Imp.ext.ae'); - if (fledge) { - data.fledge = fledge; - } - } - - _each(ext, (value, key) => { - data['ext.' + key] = value; - }); - - return data; -} - -function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { - const {params} = bid; - const cId = extractCID(params); - const subDomain = extractSubDomain(params); - const data = buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout); - const dto = { - method: 'POST', - url: `${createDomain(subDomain)}/prebid/multi/${cId}`, - data: data - }; - return dto; -} - -function buildSingleRequest(bidRequests, bidderRequest, topWindowUrl, bidderTimeout) { - const {params} = bidRequests[0]; - const cId = extractCID(params); - const subDomain = extractSubDomain(params); - const data = bidRequests.map(bid => { - const sizes = parseSizesInput(bid.sizes); - return buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) - }); - const chunkSize = Math.min(20, config.getConfig('vidazoo.chunkSize') || 10); - - const chunkedData = chunk(data, chunkSize); - return chunkedData.map(chunk => { - return { - method: 'POST', - url: `${createDomain(subDomain)}/prebid/multi/${cId}`, - data: { - bids: chunk - } - }; - }); -} - -function appendUserIdsToRequestPayload(payloadRef, userIds) { - let key; - _each(userIds, (userId, idSystemProviderName) => { - key = `uid.${idSystemProviderName}`; - switch (idSystemProviderName) { - case 'digitrustid': - payloadRef[key] = deepAccess(userId, 'data.id'); - break; - case 'lipb': - payloadRef[key] = userId.lipbid; - break; - case 'parrableId': - payloadRef[key] = userId.eid; - break; - case 'id5id': - payloadRef[key] = userId.uid; - break; - default: - payloadRef[key] = userId; - } - }); -} - -function buildRequests(validBidRequests, bidderRequest) { - // TODO: does the fallback make sense here? - const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - const bidderTimeout = config.getConfig('bidderTimeout'); - - const singleRequestMode = config.getConfig('vidazoo.singleRequest'); - - const requests = []; - - if (singleRequestMode) { - // banner bids are sent as a single request - const bannerBidRequests = validBidRequests.filter(bid => isArray(bid.mediaTypes) ? bid.mediaTypes.includes(BANNER) : bid.mediaTypes[BANNER] !== undefined); - if (bannerBidRequests.length > 0) { - const singleRequests = buildSingleRequest(bannerBidRequests, bidderRequest, topWindowUrl, bidderTimeout); - requests.push(...singleRequests); - } - - // video bids are sent as a single request for each bid - - const videoBidRequests = validBidRequests.filter(bid => bid.mediaTypes[VIDEO] !== undefined); - videoBidRequests.forEach(validBidRequest => { - const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); - requests.push(request); - }); - } else { - validBidRequests.forEach(validBidRequest => { - const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); - requests.push(request); - }); - } - return requests; -} - -function interpretResponse(serverResponse, request) { - if (!serverResponse || !serverResponse.body) { - return []; - } - - const singleRequestMode = config.getConfig('vidazoo.singleRequest'); - const reqBidId = deepAccess(request, 'data.bidId'); - const {results} = serverResponse.body; - - let output = []; - - try { - results.forEach((result, i) => { - const { - creativeId, - ad, - price, - exp, - width, - height, - currency, - bidId, - nurl, - advertiserDomains, - metaData, - mediaType = BANNER - } = result; - if (!ad || !price) { - return; - } - - const response = { - requestId: (singleRequestMode && bidId) ? bidId : reqBidId, - cpm: price, - width: width, - height: height, - creativeId: creativeId, - currency: currency || CURRENCY, - netRevenue: true, - ttl: exp || TTL_SECONDS, - }; - - if (nurl) { - response.nurl = nurl; - } - - if (metaData) { - Object.assign(response, { - meta: metaData - }) - } else { - Object.assign(response, { - meta: { - advertiserDomains: advertiserDomains || [] - } - }) - } - - if (mediaType === BANNER) { - Object.assign(response, { - ad: ad, - }); - } else { - Object.assign(response, { - vastXml: ad, - mediaType: VIDEO - }); - } - output.push(response); - }); - - return output; - } catch (e) { - return []; - } -} - -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { - let syncs = []; - const {iframeEnabled, pixelEnabled} = syncOptions; - const {gdprApplies, consentString = ''} = gdprConsent; - const {gppString, applicableSections} = gppConsent; - - const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}`; +function createUniqueRequestData(hashUrl) { + const dealId = getNextDealId(storage, hashUrl); + const sessionId = getVidazooSessionId(storage); + const ptrace = getCacheOpt(storage, OPT_CACHE_KEY); + const vdzhum = getCacheOpt(storage, OPT_TIME_KEY); - if (gppString && applicableSections?.length) { - params += '&gpp=' + encodeURIComponent(gppString); - params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); - } - - if (iframeEnabled) { - syncs.push({ - type: 'iframe', - url: `https://sync.cootlogix.com/api/sync/iframe/${params}` - }); - } - if (pixelEnabled) { - syncs.push({ - type: 'image', - url: `https://sync.cootlogix.com/api/sync/image/${params}` - }); - } - return syncs; -} - -/** - * @param {Bid} bid - */ -function onBidWon(bid) { - if (!bid.nurl) { - return; - } - const wonBid = { - adId: bid.adId, - creativeId: bid.creativeId, - auctionId: bid.auctionId, - transactionId: bid.transactionId, - adUnitCode: bid.adUnitCode, - cpm: bid.cpm, - currency: bid.currency, - originalCpm: bid.originalCpm, - originalCurrency: bid.originalCurrency, - netRevenue: bid.netRevenue, - mediaType: bid.mediaType, - timeToRespond: bid.timeToRespond, - status: bid.status, + return { + dealId: dealId, sessionId: sessionId, ptrace: ptrace, vdzhum: vdzhum, webSessionId: webSessionId }; - const qs = formatQS(wonBid); - const url = bid.nurl + (bid.nurl.indexOf('?') === -1 ? '?' : '&') + qs; - triggerPixel(url); -} - -export function hashCode(s, prefix = '_') { - const l = s.length; - let h = 0 - let i = 0; - if (l > 0) { - while (i < l) { - h = (h << 5) - h + s.charCodeAt(i++) | 0; - } - } - return prefix + h; } -export function getNextDealId(key, expiry = DEAL_ID_EXPIRY) { - try { - const data = getStorageItem(key); - let currentValue = 0; - let timestamp; - - if (data && data.value && Date.now() - data.created < expiry) { - currentValue = data.value; - timestamp = data.created; - } - - const nextValue = currentValue + 1; - setStorageItem(key, nextValue, timestamp); - return nextValue; - } catch (e) { - return 0; - } -} - -export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { - const storageKey = `u_${key}`; - const now = Date.now(); - const data = getStorageItem(storageKey); - let uniqueId; - - if (!data || !data.value || now - data.created > expiry) { - uniqueId = `${key}_${now.toString()}`; - setStorageItem(storageKey, uniqueId); - } else { - uniqueId = data.value; - } - - return uniqueId; -} - -export function getVidazooSessionId() { - return getStorageItem(SESSION_ID_KEY) || ''; -} - -export function getCacheOpt() { - let data = storage.getDataFromLocalStorage(OPT_CACHE_KEY); - if (!data) { - data = String(Date.now()); - storage.setDataInLocalStorage(OPT_CACHE_KEY, data); - } - - return data; -} - -export function getStorageItem(key) { - try { - return tryParseJSON(storage.getDataFromLocalStorage(key)); - } catch (e) { - } - - return null; -} - -export function setStorageItem(key, value, timestamp) { - try { - const created = timestamp || Date.now(); - const data = JSON.stringify({value, created}); - storage.setDataInLocalStorage(key, data); - } catch (e) { - } -} - -export function tryParseJSON(value) { - try { - return JSON.parse(value); - } catch (e) { - return value; - } -} +const buildRequests = createBuildRequestsFn(createDomain, createUniqueRequestData, storage, BIDDER_CODE, BIDDER_VERSION, true); +const interpretResponse = createInterpretResponseFn(BIDDER_CODE, true); +const getUserSyncs = createUserSyncGetter({ + iframeSyncUrl: 'https://sync.cootlogix.com/api/sync/iframe', imageSyncUrl: 'https://sync.cootlogix.com/api/sync/image' +}); export const spec = { code: BIDDER_CODE, diff --git a/modules/videoModule/adQueue.js b/modules/videoModule/adQueue.js index 54cad4befc0..a98bd742294 100644 --- a/modules/videoModule/adQueue.js +++ b/modules/videoModule/adQueue.js @@ -9,7 +9,7 @@ export function AdQueueCoordinator(videoCore, pbEvents) { videoCore.onEvents([SETUP_COMPLETE], onSetupComplete, divId); } - function queueAd(adTagUrl, divId, options) { + function queueAd(adTagUrl, divId, options = {}) { const queue = storage[divId]; if (queue) { queue.push({adTagUrl, options}); @@ -53,7 +53,11 @@ export function AdQueueCoordinator(videoCore, pbEvents) { function loadAd(divId, adTagUrl, options) { triggerEvent(AUCTION_AD_LOAD_ATTEMPT, adTagUrl, options); - videoCore.setAdTagUrl(adTagUrl, divId, options); + if (options.prefetchedVastXml) { + videoCore.setAdXml(options.prefetchedVastXml, divId, options); + } else { + videoCore.setAdTagUrl(adTagUrl, divId, options); + } } function triggerEvent(eventName, adTagUrl, options) { diff --git a/modules/videoModule/coreVideo.js b/modules/videoModule/coreVideo.js index fc54d0d0b98..1c18a77839c 100644 --- a/modules/videoModule/coreVideo.js +++ b/modules/videoModule/coreVideo.js @@ -104,12 +104,11 @@ import { ParentModule, SubmoduleBuilder } from '../../libraries/video/shared/par /** * @summary Maps a Video Provider factory to the video player's vendor code. - * @type {vendorSubmoduleDirectory} */ const videoVendorDirectory = {}; /** - * @constructor + * @class * @param {ParentModule} parentModule_ * @returns {VideoCore} */ @@ -166,6 +165,18 @@ export function VideoCore(parentModule_) { submodule && submodule.setAdTagUrl(adTagUrl, options); } + /** + * @name VideoCore#setAdXml + * @summary Requests that a player render the ad in the provided ad tag + * @param {string} vastXml - VAST content in xml format + * @param {string} divId - unique identifier of the player instance + * @param {Object} options - additional params + */ + function setAdXml(vastXml, divId, options) { + const submodule = parentModule.getSubmodule(divId); + submodule && submodule.setAdXml(vastXml, options); + } + /** * @name VideoCore#onEvents * @summary attaches event listeners @@ -217,6 +228,7 @@ export function VideoCore(parentModule_) { getOrtbVideo, getOrtbContent, setAdTagUrl, + setAdXml, onEvents, offEvents, hasProviderFor(divId) { diff --git a/modules/videoModule/gamAdServerSubmodule.js b/modules/videoModule/gamAdServerSubmodule.js index 87db71ae38b..046ea37d5bd 100644 --- a/modules/videoModule/gamAdServerSubmodule.js +++ b/modules/videoModule/gamAdServerSubmodule.js @@ -2,19 +2,23 @@ import { GAM_VENDOR } from '../../libraries/video/constants/vendorCodes.js'; import { getGlobal } from '../../src/prebidGlobal.js'; /** - * @constructor + * @class * @param {Object} dfpModule_ - the DFP ad server module - * @returns {AdServerProvider} */ function GamAdServerProvider(dfpModule_) { const dfp = dfpModule_; - function getAdTagUrl(adUnit, baseAdTag, params) { - return dfp.buildVideoUrl({ adUnit: adUnit, url: baseAdTag, params }); + function getAdTagUrl(adUnit, baseAdTag, params, bid) { + return dfp.buildVideoUrl({ adUnit: adUnit, url: baseAdTag, params, bid }); + } + + async function getVastXml(adUnit, baseAdTag, params, bid) { + return dfp.getVastXml({ adUnit: adUnit, url: baseAdTag, params, bid }); } return { - getAdTagUrl + getAdTagUrl, + getVastXml } } diff --git a/modules/videoModule/index.js b/modules/videoModule/index.js index c84d98a6d5f..9e675170888 100644 --- a/modules/videoModule/index.js +++ b/modules/videoModule/index.js @@ -88,7 +88,8 @@ export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvent const adUrl = bid.vastUrl; options.adXml = bid.vastXml; options.winner = bid.bidder; - loadAdTag(adUrl, divId, options); + + loadAd(adUrl, divId, options); } function getOrtbVideo(divId) { @@ -204,39 +205,49 @@ export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvent return mergeDeep({}, globalVideoConfig.adServer, globalProviderConfig.adServer, adUnitVideoConfig.adServer); } - function renderWinningBid(adUnit) { + async function renderWinningBid(adUnit) { const adUnitCode = adUnit.code; - const options = { adUnitCode }; const videoConfig = adUnit.video; const divId = videoConfig.divId; + const adServerConfig = getAdServerConfig(videoConfig); - let adUrl; - if (adServerConfig) { - adUrl = gamSubmodule.getAdTagUrl(adUnit, adServerConfig.baseAdTagUrl, adServerConfig.params); + const winningBid = getWinningBid(adUnitCode); + + const options = { adUnitCode }; + + async function prefetchVast() { + const gamVastWrapper = await gamSubmodule.getVastXml( + adUnit, adServerConfig.baseAdTagUrl, adServerConfig.params, winningBid + ); + options.prefetchedVastXml = gamVastWrapper; } - if (adUrl) { - loadAdTag(adUrl, divId, options); - return; + if (adServerConfig) { + if (config.getConfig('cache.useLocal')) { + await prefetchVast(); + } else { + const adTagUrl = gamSubmodule.getAdTagUrl( + adUnit, adServerConfig.baseAdTagUrl, adServerConfig.params + ); + loadAd(adTagUrl, divId, options); + return; + } } + renderBid(divId, winningBid, options); + } + + function getWinningBid(adUnitCode) { const highestCpmBids = pbGlobal.getHighestCpmBids(adUnitCode); if (!highestCpmBids.length) { - pbEvents.emit(getExternalVideoEventName(AUCTION_AD_LOAD_ABORT), getExternalVideoEventPayload(AUCTION_AD_LOAD_ABORT, options)); + pbEvents.emit(getExternalVideoEventName(AUCTION_AD_LOAD_ABORT), getExternalVideoEventPayload(AUCTION_AD_LOAD_ABORT, {adUnitCode})); return; } - - const highestBid = highestCpmBids.shift(); - if (!highestBid) { - return; - } - - renderBid(divId, highestBid, options); + return highestCpmBids.shift(); } - // options: adXml, winner, adUnitCode, - function loadAdTag(adTagUrl, divId, options) { + function loadAd(adTagUrl, divId, options) { adQueueCoordinator.queueAd(adTagUrl, divId, options); } diff --git a/modules/videobyteBidAdapter.js b/modules/videobyteBidAdapter.js index 8cedf9ac16a..c34d3ecb097 100644 --- a/modules/videobyteBidAdapter.js +++ b/modules/videobyteBidAdapter.js @@ -19,6 +19,7 @@ const VIDEO_ORTB_PARAMS = [ 'minduration', 'maxduration', 'placement', + 'plcmt', 'protocols', 'startdelay', 'skip', @@ -191,16 +192,6 @@ function buildRequestData(bidRequest, bidderRequest) { } }); - // Placement Inference Rules: - // - If no placement is defined then default to 1 (In Stream) - video.placement = video.placement || 2; - - // - If product is instream (for instream context) then override placement to 1 - if (params.context === 'instream') { - video.startdelay = video.startdelay || 0; - video.placement = 1; - } - // bid floor const bidFloorRequest = { currency: bidRequest.params.cur || 'USD', @@ -223,8 +214,8 @@ function buildRequestData(bidRequest, bidderRequest) { id: '1', video: video, secure: isSecure() ? 1 : 0, - bidfloor: floorData.floor, - bidfloorcur: floorData.currency + bidfloor: floorData?.floor, + bidfloorcur: floorData?.currency } ], site: { diff --git a/modules/videoheroesBidAdapter.js b/modules/videoheroesBidAdapter.js index ee2c2deef8b..4adabacc33b 100644 --- a/modules/videoheroesBidAdapter.js +++ b/modules/videoheroesBidAdapter.js @@ -1,8 +1,8 @@ -import { isEmpty, parseUrl, isStr, triggerPixel } from '../src/utils.js'; +import { triggerPixel, isStr } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { parseNative } from '../libraries/braveUtils/index.js'; +import { buildRequests, interpretResponse } from '../libraries/braveUtils/buildAndInterpret.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -13,158 +13,15 @@ const BIDDER_CODE = 'videoheroes'; const DEFAULT_CUR = 'USD'; const ENDPOINT_URL = `https://point.contextualadv.com/?t=2&partner=hash`; -const NATIVE_ASSETS_IDS = { 1: 'title', 2: 'icon', 3: 'image', 4: 'body', 5: 'sponsoredBy', 6: 'cta' }; -const NATIVE_ASSETS = { - title: { id: 1, name: 'title' }, - icon: { id: 2, type: 1, name: 'img' }, - image: { id: 3, type: 3, name: 'img' }, - body: { id: 4, type: 2, name: 'data' }, - sponsoredBy: { id: 5, type: 1, name: 'data' }, - cta: { id: 6, type: 12, name: 'data' } -}; - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - /** - * Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: (bid) => { - return !!(bid.params.placementId && bid.params.placementId.toString().length === 32); - }, - - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - if (validBidRequests.length === 0 || !bidderRequest) return []; - - const endpointURL = ENDPOINT_URL.replace('hash', validBidRequests[0].params.placementId); - - let imp = validBidRequests.map(br => { - let impObject = { - id: br.bidId, - secure: 1 - }; - - if (br.mediaTypes.banner) { - impObject.banner = createBannerRequest(br); - } else if (br.mediaTypes.video) { - impObject.video = createVideoRequest(br); - } else if (br.mediaTypes.native) { - impObject.native = { - // TODO: fix transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - // Also, `id` is not in the ORTB native spec - id: br.transactionId, - ver: '1.2', - request: createNativeRequest(br) - }; - } - return impObject; - }); - - let page = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - - let data = { - id: bidderRequest.bidderRequestId, - cur: [ DEFAULT_CUR ], - device: { - w: screen.width, - h: screen.height, - language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', - ua: navigator.userAgent, - }, - site: { - domain: parseUrl(page).hostname, - page: page, - }, - tmax: bidderRequest.timeout, - imp - }; - - if (bidderRequest.refererInfo.ref) { - data.site.ref = bidderRequest.refererInfo.ref; - } - - if (bidderRequest.gdprConsent) { - data['regs'] = {'ext': {'gdpr': bidderRequest.gdprConsent.gdprApplies ? 1 : 0}}; - data['user'] = {'ext': {'consent': bidderRequest.gdprConsent.consentString ? bidderRequest.gdprConsent.consentString : ''}}; - } - - if (bidderRequest.uspConsent !== undefined) { - if (!data['regs'])data['regs'] = {'ext': {}}; - data['regs']['ext']['us_privacy'] = bidderRequest.uspConsent; - } - - if (config.getConfig('coppa') === true) { - if (!data['regs'])data['regs'] = {'coppa': 1}; - else data['regs']['coppa'] = 1; - } - - if (validBidRequests[0].schain) { - data['source'] = {'ext': {'schain': validBidRequests[0].schain}}; - } - - return { - method: 'POST', - url: endpointURL, - data: data - }; - }, - - /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: (serverResponse) => { - if (!serverResponse || isEmpty(serverResponse.body)) return []; - - let bids = []; - serverResponse.body.seatbid.forEach(response => { - response.bid.forEach(bid => { - let mediaType = bid.ext && bid.ext.mediaType ? bid.ext.mediaType : 'banner'; - - let bidObj = { - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - ttl: 1200, - currency: DEFAULT_CUR, - netRevenue: true, - creativeId: bid.crid, - dealId: bid.dealid || null, - mediaType: mediaType - }; - - switch (mediaType) { - case 'video': - bidObj.vastUrl = bid.adm; - break; - case 'native': - bidObj.native = parseNative(bid.adm); - break; - default: - bidObj.ad = bid.adm; - } + isBidRequestValid: (bid) => !!(bid.params.placementId && bid.params.placementId.toString().length === 32), - bids.push(bidObj); - }); - }); + buildRequests: (validBidRequests, bidderRequest) => buildRequests(validBidRequests, bidderRequest, ENDPOINT_URL, DEFAULT_CUR), - return bids; - }, + interpretResponse: (serverResponse) => interpretResponse(serverResponse, DEFAULT_CUR, parseNative), onBidWon: (bid) => { if (isStr(bid.nurl) && bid.nurl !== '') { @@ -173,89 +30,4 @@ export const spec = { } }; -const parseNative = adm => { - let bid = { - clickUrl: adm.native.link && adm.native.link.url, - impressionTrackers: adm.native.imptrackers || [], - clickTrackers: (adm.native.link && adm.native.link.clicktrackers) || [], - jstracker: adm.native.jstracker || [] - }; - adm.native.assets.forEach(asset => { - let kind = NATIVE_ASSETS_IDS[asset.id]; - let content = kind && asset[NATIVE_ASSETS[kind].name]; - if (content) { - bid[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; - } - }); - - return bid; -} - -const createNativeRequest = br => { - let impObject = { - ver: '1.2', - assets: [] - }; - - let keys = Object.keys(br.mediaTypes.native); - - for (let key of keys) { - const props = NATIVE_ASSETS[key]; - if (props) { - const asset = { - required: br.mediaTypes.native[key].required ? 1 : 0, - id: props.id, - [props.name]: {} - }; - - if (props.type) asset[props.name]['type'] = props.type; - if (br.mediaTypes.native[key].len) asset[props.name]['len'] = br.mediaTypes.native[key].len; - if (br.mediaTypes.native[key].sizes && br.mediaTypes.native[key].sizes[0]) { - asset[props.name]['w'] = br.mediaTypes.native[key].sizes[0]; - asset[props.name]['h'] = br.mediaTypes.native[key].sizes[1]; - } - - impObject.assets.push(asset); - } - } - - return impObject; -} - -const createBannerRequest = br => { - let size = []; - - if (br.mediaTypes.banner.sizes && Array.isArray(br.mediaTypes.banner.sizes)) { - if (Array.isArray(br.mediaTypes.banner.sizes[0])) { size = br.mediaTypes.banner.sizes[0]; } else { size = br.mediaTypes.banner.sizes; } - } else size = [300, 250]; - - return { id: br.transactionId, w: size[0], h: size[1] }; -}; - -const createVideoRequest = br => { - let videoObj = {id: br.transactionId}; - let supportParamsList = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'skip', 'minbitrate', 'maxbitrate', 'api', 'linearity']; - - for (let param of supportParamsList) { - if (br.mediaTypes.video[param] !== undefined) { - videoObj[param] = br.mediaTypes.video[param]; - } - } - - if (br.mediaTypes.video.playerSize && Array.isArray(br.mediaTypes.video.playerSize)) { - if (Array.isArray(br.mediaTypes.video.playerSize[0])) { - videoObj.w = br.mediaTypes.video.playerSize[0][0]; - videoObj.h = br.mediaTypes.video.playerSize[0][1]; - } else { - videoObj.w = br.mediaTypes.video.playerSize[0]; - videoObj.h = br.mediaTypes.video.playerSize[1]; - } - } else { - videoObj.w = 640; - videoObj.h = 480; - } - - return videoObj; -} - registerBidder(spec); diff --git a/modules/videojsVideoProvider.js b/modules/videojsVideoProvider.js index 7764e8af995..066a026d597 100644 --- a/modules/videojsVideoProvider.js +++ b/modules/videojsVideoProvider.js @@ -6,13 +6,17 @@ import { } from '../libraries/video/constants/events.js'; // missing events: , AD_BREAK_START, , AD_BREAK_END, VIEWABLE, BUFFER, CAST, PLAYLIST_COMPLETE, RENDITION_UPDATE, PLAY_ATTEMPT_FAILED, AUTOSTART_BLOCKED import { - PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE, AD_POSITION, PLAYBACK_END + PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLCMT, VPAID_MIME_TYPE, AD_POSITION, PLAYBACK_END } from '../libraries/video/constants/ortb.js'; import { VIDEO_JS_VENDOR } from '../libraries/video/constants/vendorCodes.js'; import { submodule } from '../src/hook.js'; import stateFactory from '../libraries/video/shared/state.js'; import { PLAYBACK_MODE } from '../libraries/video/constants/constants.js'; import { getEventHandler } from '../libraries/video/shared/eventHandler.js'; +import { getWinDimensions } from '../src/utils.js'; +/** + * @typedef {import('../libraries/video/shared/state.js').State} State + */ /* Plugins of interest: @@ -146,8 +150,9 @@ export function VideojsProvider(providerConfig, vjs_, adState_, timeState_, call // ~ Sort of resolved check if the player has a source to tell if the placement is instream // Still cannot reliably check what type of placement the player is if its outstream // i.e. we can't tell if its interstitial, in article, etc. + // update: cannot infer instream ever, always need declarations if (player.src()) { - video.placement = PLACEMENT.INSTREAM; + video.plcmt = PLCMT.ACCOMPANYING_CONTENT; } // Placement according to IQG Guidelines 4.2.8 @@ -212,6 +217,22 @@ export function VideojsProvider(providerConfig, vjs_, adState_, timeState_, call } } + function setAdXml(vastXml) { + if (!player.ima || !vastXml) { + return; + } + + // The VideoJS IMA plugin version 1.11.0 will throw when the ad is empty. + try { + player.ima.controller.settings.adsResponse = vastXml; + player.ima.requestAds(); + } catch (e) { + /* + Handling is not required; ad errors are emitted automatically by video.js + */ + } + } + function onEvent(type, callback, payload) { registerSetupListeners(type, callback, payload); @@ -497,6 +518,7 @@ export function VideojsProvider(providerConfig, vjs_, adState_, timeState_, call getOrtbVideo, getOrtbContent, setAdTagUrl, + setAdXml, onEvent, offEvent, destroy @@ -593,8 +615,8 @@ export const utils = { }, getPositionCode: function({left, top, width, height}) { - const bottom = window.innerHeight - top - height; - const right = window.innerWidth - left - width; + const bottom = getWinDimensions().innerHeight - top - height; + const right = getWinDimensions().innerWidth - left - width; if (left < 0 || right < 0 || top < 0) { return AD_POSITION.UNKNOWN; diff --git a/modules/vidoomyBidAdapter.js b/modules/vidoomyBidAdapter.js index c9ac9fae0f4..894d3191311 100644 --- a/modules/vidoomyBidAdapter.js +++ b/modules/vidoomyBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, logError, parseSizesInput} from '../src/utils.js'; +import {deepAccess, isPlainObject, logError, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -88,7 +88,7 @@ function getBidFloor(bid, mediaType, sizes, bidfloor) { var size = sizes && sizes.length > 0 ? sizes[0] : '*'; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({currency: 'USD', mediaType, size}); - if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + if (isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = Math.max(bidfloor, parseFloat(floorInfo.floor)); } } @@ -120,7 +120,10 @@ const buildRequests = (validBidRequests, bidderRequest) => { sizes = bid.mediaTypes[VIDEO].playerSize; adType = VIDEO; } - const [w, h] = (parseSizesInput(sizes)[0] || '0x0').split('x'); + + const parsedSizes = (sizes ? parseSizesInput(sizes) : []).map(size => size.split('x')); + const widths = parsedSizes.length ? parsedSizes.map(size => size[0]).join(',') : '0'; + const heights = parsedSizes.length ? parsedSizes.map(size => size[1]).join(',') : '0'; // TODO: is 'domain' the right value here? const hostname = bidderRequest.refererInfo.domain || window.location.hostname; @@ -147,8 +150,8 @@ const buildRequests = (validBidRequests, bidderRequest) => { id: bid.params.id, adtype: adType, auc: bid.adUnitCode, - w, - h, + w: widths, + h: heights, pos: parseInt(bid.params.position) || 1, ua: navigator.userAgent, l: navigator.language && navigator.language.indexOf('-') !== -1 ? navigator.language.split('-')[0] : '', diff --git a/modules/viewdeosDXBidAdapter.js b/modules/viewdeosDXBidAdapter.js index 7afd23cbde7..4f920fbd91f 100644 --- a/modules/viewdeosDXBidAdapter.js +++ b/modules/viewdeosDXBidAdapter.js @@ -1,61 +1,29 @@ import {deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import {findIndex} from '../src/polyfill.js'; +import { + getUserSyncsFn, + isBidRequestValid, + supportedMediaTypes +} from '../libraries/adtelligentUtils/adtelligentUtils.js'; const URL = 'https://ghb.sync.viewdeos.com/auction/'; const OUTSTREAM_SRC = 'https://player.sync.viewdeos.com/outstream-unit/2.01/outstream.min.js'; const BIDDER_CODE = 'viewdeosDX'; const OUTSTREAM = 'outstream'; const DISPLAY = 'display'; +const syncsCache = {}; export const spec = { code: BIDDER_CODE, aliases: ['viewdeos'], gvlid: 924, - supportedMediaTypes: [VIDEO, BANNER], - isBidRequestValid: function (bid) { - return !!deepAccess(bid, 'params.aid'); - }, + supportedMediaTypes, + isBidRequestValid, getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; - - function addSyncs(bid) { - const uris = bid.cookieURLs; - const types = bid.cookieURLSTypes || []; - - if (Array.isArray(uris)) { - uris.forEach((uri, i) => { - const type = types[i] || 'image'; - - if ((!syncOptions.pixelEnabled && type === 'image') || - (!syncOptions.iframeEnabled && type === 'iframe')) { - return; - } - - syncs.push({ - type: type, - url: uri - }) - }) - } - } - - if (syncOptions.pixelEnabled || syncOptions.iframeEnabled) { - isArray(serverResponses) && serverResponses.forEach((response) => { - if (response.body) { - if (isArray(response.body)) { - response.body.forEach(b => { - addSyncs(b); - }) - } else { - addSyncs(response.body) - } - } - }) - } - return syncs; + return getUserSyncsFn(syncOptions, serverResponses, syncsCache) }, /** * Make a server request from the list of BidRequests @@ -221,6 +189,7 @@ function createBid(bidResponse, mediaType, bidderParams) { /** * Create renderer * @param requestId + * @param bidderParams * @returns {*} */ function newRenderer(requestId, bidderParams) { diff --git a/modules/viouslyBidAdapter.js b/modules/viouslyBidAdapter.js index 5ccca7590dd..e474a8de93c 100644 --- a/modules/viouslyBidAdapter.js +++ b/modules/viouslyBidAdapter.js @@ -2,7 +2,7 @@ import { deepAccess, logError, parseUrl, parseSizesInput, triggerPixel } from '. import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; // eslint-disable-line prebid/validate-imports +import {find} from '../src/polyfill.js'; const BIDDER_CODE = 'viously'; const GVLID = 1028; diff --git a/modules/viqeoBidAdapter.js b/modules/viqeoBidAdapter.js index 28f4de1fd52..aac9ee69a24 100644 --- a/modules/viqeoBidAdapter.js +++ b/modules/viqeoBidAdapter.js @@ -1,7 +1,15 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {logError, logInfo, _each, mergeDeep, isFn, isNumber, isPlainObject} from '../src/utils.js' -import {VIDEO} from '../src/mediaTypes.js'; -import {Renderer} from '../src/Renderer.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { + logError, + logInfo, + _each, + mergeDeep, + isFn, + isNumber, + isPlainObject, +} from '../src/utils.js'; +import { VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -12,36 +20,48 @@ import {Renderer} from '../src/Renderer.js'; */ const BIDDER_CODE = 'viqeo'; -const DEFAULT_MIMES = ['application/javascript']; -const VIQEO_ENDPOINT = 'https://ads.betweendigital.com/openrtb_bid'; +const DEFAULT_MIMES = [ + 'video/3gpp', + 'video/mp4', + 'video/mpeg', + 'video/webm', + 'application/javascript', +]; +const VIQEO_ENDPOINT = 'https://ad.vqserve.com/ads/prebid'; const RENDERER_URL = 'https://cdn.viqeo.tv/js/vq_starter.js'; const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_SSPID = 44697; function getBidFloor(bid) { - const {floor, currency} = bid.params; + const { floor, currency } = bid.params; const curr = currency || DEFAULT_CURRENCY; if (!isFn(bid.getFloor)) { - return {floor: isNumber(floor) ? floor : 0, currency: curr}; + return { floor: isNumber(floor) ? floor : 0, currency: curr }; } - const floorInfo = bid.getFloor({currency: curr, mediaType: VIDEO, size: '*'}); - if (isPlainObject(floorInfo) && isNumber(floorInfo.floor) && floorInfo.currency === curr) { + const floorInfo = bid.getFloor({ + currency: curr, + mediaType: VIDEO, + size: '*', + }); + if ( + isPlainObject(floorInfo) && + isNumber(floorInfo.floor) && + floorInfo.currency === curr + ) { return floorInfo; } - return {floor: floor || 0, currency: currency || DEFAULT_CURRENCY}; + return { floor: floor || 0, currency: currency || DEFAULT_CURRENCY }; } -function getVideoTargetingParams({mediaTypes: {video}}) { +function getVideoTargetingParams({ mediaTypes: { video } }) { const result = {}; - Object.keys(Object(video)) - .forEach(key => { - if (key === 'playerSize') { - result.w = video.playerSize[0][0]; - result.h = video.playerSize[0][1]; - } else if (key !== 'context') { - result[key] = video[key]; - } - }) + Object.keys(Object(video)).forEach((key) => { + if (key === 'playerSize') { + result.w = video.playerSize[0][0]; + result.h = video.playerSize[0][1]; + } else if (key !== 'context') { + result[key] = video[key]; + } + }); return result; } @@ -55,20 +75,20 @@ export const spec = { * @param {BidRequest} bidRequest The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ - isBidRequestValid: ({params}) => { + isBidRequestValid: ({ params }) => { if (!params) { logError('failed validation: params not declared'); return false; } - if (!params.user && !params.user?.buyeruid) { - logError('failed validation: user.buyeruid not declared'); + if (!params.tagId) { + logError('failed validation: tagId not declared'); return false; } if (!params.playerOptions) { logError('failed validation: playerOptions not declared'); return false; } - const {profileId, videoId, playerId} = params.playerOptions; + const { profileId, videoId, playerId } = params.playerOptions; if (!profileId) { logError('failed validation: profileId not declared'); return false; @@ -88,46 +108,42 @@ export const spec = { const bidRequests = []; _each(validBidRequests, (bid, i) => { const { - params: {test, sspId, endpointUrl}, - mediaTypes: {video}, + params: { test, tagId, endpointUrl, bcat, badv }, + mediaTypes: { video }, } = bid; const ortb2 = bid.ortb2 || {}; const user = bid.params.user || {}; const device = bid.params.device || {}; const site = bid.params.site || {}; - const w = window; const floorInfo = getBidFloor(bid); + const data = { id: bid.bidId, test, - imp: [{ - id: `${i}`, - tagid: bid.adUnitCode, - video: { - ...getVideoTargetingParams(bid), - mimes: video.mimes || DEFAULT_MIMES, + imp: [ + { + id: `${i}`, + video: { + ...getVideoTargetingParams(bid), + mimes: video.mimes || DEFAULT_MIMES, + }, + tagid: `${tagId}`, + bidfloor: floorInfo.floor, + bidfloorcur: floorInfo.currency, + secure: 1, }, - bidfloor: floorInfo.floor, - bidfloorcur: floorInfo.currency, - secure: 1 - }], - site: test === 1 ? { - page: 'https://viqeo.tv', - domain: 'viqeo.tv' - } : mergeDeep({ - domain: w.location.hostname, - page: w.location.href - }, ortb2.site, site), - device: mergeDeep({ - w: w.screen.width, - h: w.screen.height, - ua: w.navigator.userAgent, - }, ortb2.device, device), - user: mergeDeep({...user}, ortb2.user), - app: bid.params.app, + ], + site: mergeDeep( + site, + ortb2.site + ), + device: mergeDeep(device, ortb2.device), + user: mergeDeep(user, ortb2.user), + bcat: ortb2.bcat || bcat, + badv: ortb2.badv || badv }; bidRequests.push({ - url: endpointUrl || `${VIQEO_ENDPOINT}/?sspId=${sspId || DEFAULT_SSPID}`, + url: endpointUrl || `${VIQEO_ENDPOINT}`, method: 'POST', data, bids: validBidRequests, @@ -148,24 +164,24 @@ export const spec = { return []; } try { - const {id, seatbid, cur} = serverResponse.body; + const { id, seatbid, cur } = serverResponse.body; _each(seatbid, (sb) => { - const {bid} = sb; + const { bid } = sb; _each(bid, (b) => { - const bidRequest = bidRequests.bids.find(({bidId}) => bidId === id); + const bidRequest = bidRequests.bids.find(({ bidId }) => bidId === id); const renderer = Renderer.install({ url: bidRequest?.params?.renderUrl || RENDERER_URL, }); - renderer.setRender((bid) => { + renderer.setRender((bid, doc) => { if (window.VIQEO) { - window.VIQEO.renderPrebid(bid); + window.VIQEO.renderPrebid(bid, doc); } else { logError('failed get window.VIQEO'); } }); bidResponses.push({ requestId: id, - currency: cur, + currency: cur || DEFAULT_CURRENCY, cpm: b.price, ttl: b.exp, netRevenue: true, @@ -176,13 +192,18 @@ export const spec = { vastUrl: b.nurl, mediaType: VIDEO, renderer, - }) - }) + meta: { + secondaryCatIds: b.cat, + attr: b.attr, + advertiserDomains: b.adomain, + }, + }); + }); }); } catch (error) { logError(error); } return bidResponses; }, -} +}; registerBidder(spec); diff --git a/modules/viqeoBidAdapter.md b/modules/viqeoBidAdapter.md index b4a020bf057..b3a0c59d3a5 100644 --- a/modules/viqeoBidAdapter.md +++ b/modules/viqeoBidAdapter.md @@ -11,23 +11,21 @@ Viqeo Bidder Adapter for Prebid.js. About: https://viqeo.tv/ ### Bid params {: .table .table-bordered .table-striped } -| Name | Scope | Description | Example | Type | -|-----------------------------|----------|----------------------------------------------------------------------------------------------------------------------------|--------------------------|-----------| -| `user` | required | The object containing user data (See OpenRTB spec) | `user: {}` | `object` | -| `user.buyeruid` | required | User id | `"12345"` | `string` | -| `playerOptions` | required | The object containing Viqeo player options | `playerOptions: {}` | `object` | -| `playerOptions.profileId` | required | Viqeo profile id | `1382` | `number` | -| `playerOptions.videId` | optional | Viqeo video id | `"ed584da454c7205ca7e4"` | `string` | -| `playerOptions.playerId` | optional | Viqeo player id | `1` | `number` | -| `device` | optional | The object containing device data (See OpenRTB spec) | `device: {}` | `object` | -| `site` | optional | The object containing site data (See OpenRTB spec) | `site: {}` | `object` | -| `app` | optional | The object containing app data (See OpenRTB spec) | `app: {}` | `object` | -| `floor` | optional | Bid floor price | `0.5` | `number` | -| `currency` | optional | 3-letter ISO 4217 code defining the currency of the bid. | `EUR` | `string` | -| `test` | optional | Flag which will induce a sample bid response when true; only set to true for testing purposes (1 = true, 0 = false) | `1` | `integer` | -| `sspId` | optional | For debug, request id | `1` | `number` | -| `renderUrl` | optional | For debug, script player url | `"https://viqeo.tv"` | `string` | -| `endpointUrl` | optional | For debug, api endpoint | `"https://viqeo.tv"` | `string` | +| Name | Scope | Description | Example | Type | +|-----------------------------|----------|---------------------------------------------------------------------------------------------------------------------|--------------------------|-----------| +| `tagid` | required | The unique identifier of the ad placement. Could be obtained from the Viqeo UI or from your account manager. | `2` | `string` | +| `playerOptions` | required | The object containing Viqeo player options | `playerOptions: {}` | `object` | +| `playerOptions.profileId` | required | Viqeo profile id | `1382` | `number` | +| `playerOptions.videId` | optional | Viqeo video id | `"ed584da454c7205ca7e4"` | `string` | +| `playerOptions.playerId` | optional | Viqeo player id | `1` | `number` | +| `user` | optional | The object containing user data (See OpenRTB spec) | `user: {}` | `object` | +| `device` | optional | The object containing device data (See OpenRTB spec) | `device: {}` | `object` | +| `site` | optional | The object containing site data (See OpenRTB spec) | `site: {}` | `object` | +| `floor` | optional | Bid floor price | `0.5` | `number` | +| `currency` | optional | 3-letter ISO 4217 code defining the currency of the bid. | `EUR` | `string` | +| `test` | optional | Flag which will induce a sample bid response when true; only set to true for testing purposes (1 = true, 0 = false) | `1` | `integer` | +| `renderUrl` | optional | For debug, script player url | `"https://viqeo.tv"` | `string` | +| `endpointUrl` | optional | For debug, api endpoint | `"https://viqeo.tv"` | `string` | # Test Parameters ``` @@ -42,9 +40,7 @@ Viqeo Bidder Adapter for Prebid.js. About: https://viqeo.tv/ bids: [{ bidder: 'viqeo', params: { - user: { - buyeruid: '1', - }, + tagId: '2', playerOptions: { videoId: 'ed584da454c7205ca7e4', profileId: 1382, diff --git a/modules/visiblemeasuresBidAdapter.js b/modules/visiblemeasuresBidAdapter.js index e77477c812b..1c51944c538 100644 --- a/modules/visiblemeasuresBidAdapter.js +++ b/modules/visiblemeasuresBidAdapter.js @@ -1,212 +1,19 @@ -import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'visiblemeasures'; const AD_URL = 'https://us-e.visiblemeasures.com/pbjs'; const SYNC_URL = 'https://cs.visiblemeasures.com'; -function isBidResponseValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; - } - - switch (bid.mediaType) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad); - case VIDEO: - return Boolean(bid.vastUrl || bid.vastXml); - case NATIVE: - return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); - default: - return false; - } -} - -function getPlacementReqData(bid) { - const { params, bidId, mediaTypes } = bid; - const schain = bid.schain || {}; - const { placementId, endpointId } = params; - const bidfloor = getBidFloor(bid); - - const placement = { - bidId, - schain, - bidfloor - }; - - if (placementId) { - placement.placementId = placementId; - placement.type = 'publisher'; - } else if (endpointId) { - placement.endpointId = endpointId; - placement.type = 'network'; - } - - if (mediaTypes && mediaTypes[BANNER]) { - placement.adFormat = BANNER; - placement.sizes = mediaTypes[BANNER].sizes; - } else if (mediaTypes && mediaTypes[VIDEO]) { - placement.adFormat = VIDEO; - placement.playerSize = mediaTypes[VIDEO].playerSize; - placement.minduration = mediaTypes[VIDEO].minduration; - placement.maxduration = mediaTypes[VIDEO].maxduration; - placement.mimes = mediaTypes[VIDEO].mimes; - placement.protocols = mediaTypes[VIDEO].protocols; - placement.startdelay = mediaTypes[VIDEO].startdelay; - placement.placement = mediaTypes[VIDEO].placement; - placement.skip = mediaTypes[VIDEO].skip; - placement.skipafter = mediaTypes[VIDEO].skipafter; - placement.minbitrate = mediaTypes[VIDEO].minbitrate; - placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; - placement.delivery = mediaTypes[VIDEO].delivery; - placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; - placement.api = mediaTypes[VIDEO].api; - placement.linearity = mediaTypes[VIDEO].linearity; - } else if (mediaTypes && mediaTypes[NATIVE]) { - placement.native = mediaTypes[NATIVE]; - placement.adFormat = NATIVE; - } - - return placement; -} - -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.bidfloor', 0); - } - - try { - const bidFloor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*', - }); - return bidFloor.floor; - } catch (err) { - logError(err); - return 0; - } -} - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: (bid = {}) => { - const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); - - if (mediaTypes && mediaTypes[BANNER]) { - valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); - } else if (mediaTypes && mediaTypes[VIDEO]) { - valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); - } else if (mediaTypes && mediaTypes[NATIVE]) { - valid = valid && Boolean(mediaTypes[NATIVE]); - } else { - valid = false; - } - return valid; - }, - - buildRequests: (validBidRequests = [], bidderRequest = {}) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let deviceWidth = 0; - let deviceHeight = 0; - - let winLocation; - try { - const winTop = window.top; - deviceWidth = winTop.screen.width; - deviceHeight = winTop.screen.height; - winLocation = winTop.location; - } catch (e) { - logMessage(e); - winLocation = window.location; - } - - const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; - let refferLocation; - try { - refferLocation = refferUrl && new URL(refferUrl); - } catch (e) { - logMessage(e); - } - // TODO: does the fallback make sense here? - let location = refferLocation || winLocation; - const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - const host = location.host; - const page = location.pathname; - const secure = location.protocol === 'https:' ? 1 : 0; - const placements = []; - const request = { - deviceWidth, - deviceHeight, - language, - secure, - host, - page, - placements, - coppa: config.getConfig('coppa') === true ? 1 : 0, - ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, - tmax: bidderRequest.timeout - }; - - const len = validBidRequests.length; - for (let i = 0; i < len; i++) { - const bid = validBidRequests[i]; - placements.push(getPlacementReqData(bid)); - } - - return { - method: 'POST', - url: AD_URL, - data: request - }; - }, - - interpretResponse: (serverResponse) => { - let response = []; - for (let i = 0; i < serverResponse.body.length; i++) { - let resItem = serverResponse.body[i]; - if (isBidResponseValid(resItem)) { - const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; - resItem.meta = { ...resItem.meta, advertiserDomains }; - - response.push(resItem); - } - } - return response; - }, - - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; - } - - const coppa = config.getConfig('coppa') ? 1 : 0; - syncUrl += `&coppa=${coppa}`; - - return [{ - type: syncType, - url: syncUrl - }]; - } + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) }; registerBidder(spec); diff --git a/modules/vistarsBidAdapter.js b/modules/vistarsBidAdapter.js new file mode 100644 index 00000000000..f77586ff8bf --- /dev/null +++ b/modules/vistarsBidAdapter.js @@ -0,0 +1,111 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { hasPurpose1Consent } from '../src/utils/gdpr.js'; +import { deepSetValue, replaceAuctionPrice, deepClone, deepAccess } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; + +const BIDDER_CODE = 'vistars'; +const DEFAULT_ENDPOINT = 'ex-asr.vistarsagency.com'; +const SYNC_ENDPOINT = 'sync.vistarsagency.com'; +const ADOMAIN = 'vistarsagency.com'; +const TIME_TO_LIVE = 360; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'ext.prebid', true); + + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.adm = replaceAuctionPrice(bidResponse.adm, bidResponse.price); + bidResponse.burl = replaceAuctionPrice(bidResponse.burl, bidResponse.price); + bidResponse.nurl = replaceAuctionPrice(bidResponse.nurl, bidResponse.price); + + return bidResponse; + } +}); + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + let valid = bid.params.source; + + return !!valid; + }, + + buildRequests: function(bids, bidderRequest) { + return bids.map((bid) => { + let endpoint = bid.params.endpoint || DEFAULT_ENDPOINT; + return { + method: 'POST', + url: `https://${endpoint}/bid?source=${bid.params.source}`, + data: converter.toORTB({ + bidRequests: [bid], + bidderRequest: deepClone(bidderRequest), + context: { + mediaType: deepAccess(bid, 'mediaTypes.video') ? VIDEO : BANNER + }, + }), + }; + }); + }, + + interpretResponse: function(response, request) { + if (!response?.body) { + return []; + } + + const bids = converter.fromORTB({response: response.body, request: request.data}).bids; + bids.forEach((bid) => { + bid.meta = bid.meta || {}; + bid.meta.advertiserDomains = bid.meta.advertiserDomains || []; + if (bid.meta.advertiserDomains.length == 0) { + bid.meta.advertiserDomains.push(ADOMAIN); + } + + bid.ttl = bid.ttl || TIME_TO_LIVE; + }); + + return bids; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + if (!hasPurpose1Consent(gdprConsent)) { + return syncs; + } + + let params = `us_privacy=${uspConsent || ''}&gdpr_consent=${gdprConsent?.consentString ? gdprConsent.consentString : ''}`; + if (typeof gdprConsent?.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}`; + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `//${SYNC_ENDPOINT}/match/sp.ifr?${params}` + }); + } + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `//${SYNC_ENDPOINT}/match/sp?${params}` + }); + } + + return syncs; + }, + + supportedMediaTypes: [ BANNER, VIDEO ] +} + +registerBidder(spec); diff --git a/modules/vistarsBidAdapter.md b/modules/vistarsBidAdapter.md new file mode 100644 index 00000000000..6252aaaa180 --- /dev/null +++ b/modules/vistarsBidAdapter.md @@ -0,0 +1,44 @@ +# Overview + +``` +Module Name: Vistars Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@vistarsagency.com +``` + +# Description +Connects to Vistars server for bids. +Module supports banner and video mediaType. + +# Test Parameters + +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'vistars', + params: { + source: 'ssp1', + } + }] + }, + { + code: '/test/div', + mediaTypes: { + video: { + playerSize: [[640, 360]] + } + }, + bids: [{ + bidder: 'vistars', + params: { + source: 'ssp1', + } + }] + },]; +``` diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index a86c958392e..510106845db 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -1,10 +1,12 @@ -import {deepAccess, logError, parseSizesInput, triggerPixel} from '../src/utils.js'; +import {deepAccess, logError, mergeDeep, parseSizesInput, sizeTupleToRtbSize, sizesToSizeTuples, triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {INSTREAM as VIDEO_INSTREAM} from '../src/video.js'; import {getStorageManager} from '../src/storageManager.js'; import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import { getBidFromResponse } from '../libraries/processResponse/index.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; const BIDDER_CODE = 'visx'; const GVLID = 154; @@ -32,7 +34,7 @@ const LOG_ERROR_MESS = { onlyVideoInstream: `Only video ${VIDEO_INSTREAM} supported`, videoMissing: 'Bid request videoType property is missing - ' }; -const currencyWhiteList = ['EUR', 'USD', 'GBP', 'PLN']; +const currencyWhiteList = ['EUR', 'USD', 'GBP', 'PLN', 'CHF', 'SEK']; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); const _bidResponseTimeLogged = []; export const spec = { @@ -56,14 +58,19 @@ export const spec = { const bids = validBidRequests || []; const currency = config.getConfig(`currency.bidderCurrencyDefault.${BIDDER_CODE}`) || - config.getConfig('currency.adServerCurrency') || + getCurrencyFromBidderRequest(bidderRequest) || DEFAULT_CUR; - + let request; let reqId; let payloadSchain; let payloadUserId; let payloadUserEids; let timeout; + let payloadDevice; + let payloadSite; + let payloadRegs; + let payloadContent; + let payloadUser; if (currencyWhiteList.indexOf(currency) === -1) { logError(LOG_ERROR_MESS.notAllowedCurrency + currency); @@ -80,9 +87,7 @@ export const spec = { imp.push(impObj); bidsMap[bid.bidId] = bid; } - const { params: { uid }, schain, userId, userIdAsEids } = bid; - if (!payloadSchain && schain) { payloadSchain = schain; } @@ -93,6 +98,7 @@ export const spec = { if (!payloadUserId && userId) { payloadUserId = userId; } + auids.push(uid); }); @@ -100,10 +106,7 @@ export const spec = { if (bidderRequest) { timeout = bidderRequest.timeout; - if (bidderRequest.refererInfo && bidderRequest.refererInfo.page) { - // TODO: is 'page' the right value here? - payload.u = bidderRequest.refererInfo.page; - } + if (bidderRequest.gdprConsent) { if (bidderRequest.gdprConsent.consentString) { payload.gdpr_consent = bidderRequest.gdprConsent.consentString; @@ -112,10 +115,45 @@ export const spec = { (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; } + + const { ortb2 } = bidderRequest; + const { device, site, regs, content } = ortb2; + const userOrtb2 = ortb2.user; + let user; + let userReq; + const vads = _getUserId(); + if (payloadUserEids || payload.gdpr_consent || vads) { + user = { + ext: { + ...(payloadUserEids && { eids: payloadUserEids }), + ...(payload.gdpr_consent && { consent: payload.gdpr_consent }), + ...(vads && { vads }) + } + }; + } + if (user) { + userReq = mergeDeep(user, userOrtb2); + } else { + userReq = userOrtb2; + } + if (device) { + payloadDevice = device; + } + if (site) { + payloadSite = site; + } + if (regs) { + payloadRegs = regs; + } + if (content) { + payloadContent = content; + } + if (userReq) { + payloadUser = userReq; + } } - const bidderTimeout = Number(config.getConfig('bidderTimeout')) || timeout; - const tmax = timeout ? Math.min(bidderTimeout, timeout) : bidderTimeout; + const tmax = timeout; const source = { ext: { wrapperType: 'Prebid_js', @@ -124,29 +162,25 @@ export const spec = { } }; - const vads = _getUserId(); - const user = { - ext: { - ...(payloadUserEids && { eids: payloadUserEids }), - ...(payload.gdpr_consent && { consent: payload.gdpr_consent }), - ...(vads && { vads }) - } - }; - const regs = ('gdpr_applies' in payload) && { - ext: { - gdpr: payload.gdpr_applies - } - }; + if (payloadRegs === undefined) { + payloadRegs = ('gdpr_applies' in payload) && { + ext: { + gdpr: payload.gdpr_applies + } + }; + } - const request = { + request = { id: reqId, imp, tmax, cur: [currency], source, - site: { page: payload.u }, - ...(Object.keys(user.ext).length && { user }), - ...(regs && { regs }) + ...(payloadUser && { user: payloadUser }), + ...(payloadRegs && {regs: payloadRegs}), + ...(payloadDevice && { device: payloadDevice }), + ...(payloadSite && { site: payloadSite }), + ...(payloadContent && { content: payloadContent }), }; return { @@ -171,7 +205,7 @@ export const spec = { if (!errorMessage && serverResponse.seatbid) { serverResponse.seatbid.forEach(respItem => { - _addBidResponse(_getBidFromResponse(respItem), bidsMap, currency, bidResponses); + _addBidResponse(getBidFromResponse(respItem, LOG_ERROR_MESS), bidsMap, currency, bidResponses); }); } if (errorMessage) logError(errorMessage); @@ -241,13 +275,7 @@ function makeBanner(bannerParams) { if (bannerSizes) { const sizes = parseSizesInput(bannerSizes); if (sizes.length) { - const format = sizes.map(size => { - const [ width, height ] = size.split('x'); - const w = parseInt(width, 10); - const h = parseInt(height, 10); - return { w, h }; - }); - + const format = sizesToSizeTuples(bannerSizes).map(sizeTupleToRtbSize); return { format }; } } @@ -287,17 +315,6 @@ function buildImpObject(bid) { } } -function _getBidFromResponse(respItem) { - if (!respItem) { - logError(LOG_ERROR_MESS.emptySeatbid); - } else if (!respItem.bid) { - logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); - } else if (!respItem.bid[0]) { - logError(LOG_ERROR_MESS.noBid); - } - return respItem && respItem.bid && respItem.bid[0]; -} - function _addBidResponse(serverBid, bidsMap, currency, bidResponses) { if (!serverBid) return; let errorMessage; diff --git a/modules/voxBidAdapter.js b/modules/voxBidAdapter.js index da72b975717..b0cfdabee9f 100644 --- a/modules/voxBidAdapter.js +++ b/modules/voxBidAdapter.js @@ -3,7 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find} from '../src/polyfill.js'; import {Renderer} from '../src/Renderer.js'; -import {config} from '../src/config.js'; +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -12,17 +12,15 @@ import {config} from '../src/config.js'; * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests */ -const { getConfig } = config; - const BIDDER_CODE = 'vox'; const SSP_ENDPOINT = 'https://ssp.hybrid.ai/auction/prebid'; const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const TTL = 60; const GVLID = 206; -function buildBidRequests(validBidRequests) { +function buildBidRequests(validBidRequests, bidderRequest) { return _map(validBidRequests, function(bid) { - const currency = getConfig('currency.adServerCurrency'); + const currency = getCurrencyFromBidderRequest(bidderRequest); const floorInfo = bid.getFloor ? bid.getFloor({ currency: currency || 'USD' }) : {}; @@ -218,7 +216,7 @@ export const spec = { // TODO: is 'page' the right value here? url: bidderRequest.refererInfo.page, cmp: !!bidderRequest.gdprConsent, - bidRequests: buildBidRequests(validBidRequests) + bidRequests: buildBidRequests(validBidRequests, bidderRequest) }; if (payload.cmp) { diff --git a/modules/waardexBidAdapter.js b/modules/waardexBidAdapter.js index 92b7fc26e4c..c4ca5d299bc 100644 --- a/modules/waardexBidAdapter.js +++ b/modules/waardexBidAdapter.js @@ -147,7 +147,6 @@ const createVideoObject = (videoMediaTypes, videoParams) => { maxduration: getBidIdParameter('maxduration', videoParams) || 500, protocols: getBidIdParameter('protocols', videoParams) || [2, 3, 5, 6], startdelay: getBidIdParameter('startdelay', videoParams) || 0, - placement: getBidIdParameter('placement', videoParams) || videoMediaTypes.context === 'outstream' ? 3 : 1, skip: getBidIdParameter('skip', videoParams) || 1, skipafter: getBidIdParameter('skipafter', videoParams) || 0, minbitrate: getBidIdParameter('minbitrate', videoParams) || 0, diff --git a/modules/waardexBidAdapter.md b/modules/waardexBidAdapter.md index 6978281d40e..165321c2057 100644 --- a/modules/waardexBidAdapter.md +++ b/modules/waardexBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Waardex Bid Adapter Module Type: Bidder Adapter -Maintainer: info@prebid.org +Maintainer: ``` # Description diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 82feec252eb..f9286e8b9aa 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -100,9 +100,7 @@ * @typedef {WeboCtxConf|WeboUserDataConf|SfbxLiteDataConf} CommonConf */ -import { - getGlobal -} from '../src/prebidGlobal.js'; +import { getGlobal } from '../src/prebidGlobal.js'; import { deepAccess, deepClone, @@ -111,28 +109,18 @@ import { isBoolean, isEmpty, isFn, + isNumber, isPlainObject, isStr, - logWarn, mergeDeep, prefixLog, } from '../src/utils.js'; -import { - submodule -} from '../src/hook.js'; -import { - ajax -} from '../src/ajax.js'; -import { - getStorageManager -} from '../src/storageManager.js'; -import { - MODULE_TYPE_RTD -} from '../src/activities/modules.js'; +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import adapterManager from '../src/adapterManager.js'; -import { - tryAppendQueryString -} from '../libraries/urlUtils/urlUtils.js'; +import { tryAppendQueryString } from '../libraries/urlUtils/urlUtils.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -167,7 +155,7 @@ const logger = prefixLog('[WeboramaRTD]'); export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, - moduleName: SUBMODULE_NAME + moduleName: SUBMODULE_NAME, }); /** @@ -202,7 +190,7 @@ class WeboramaRtdProvider { } /** * Initialize module - * @method + * @function * @param {Object} moduleConfig * @param {?ModuleParams} moduleConfig.params * @param {Object} userConsent @@ -216,10 +204,14 @@ class WeboramaRtdProvider { sendToBidders: true, onData: () => { /* do nothing */ - } + }, }; /** @type {ModuleParams} */ - const moduleParams = Object.assign({}, globalDefaults, moduleConfig?.params || {}); + const moduleParams = Object.assign( + {}, + globalDefaults, + moduleConfig?.params || {} + ); // reset profiles @@ -229,20 +221,31 @@ class WeboramaRtdProvider { const weboCtxRequiredFields = ['token']; - this.#components.WeboCtx.initialized = this.#initSubSection(moduleParams, WEBO_CTX_CONF_SECTION, { - requiredFields: weboCtxRequiredFields, - }); - this.#components.WeboUserData.initialized = this.#initSubSection(moduleParams, WEBO_USER_DATA_CONF_SECTION, { - userConsent: userConsent || {}, - }); - this.#components.SfbxLiteData.initialized = this.#initSubSection(moduleParams, SFBX_LITE_DATA_CONF_SECTION); + this.#components.WeboCtx.initialized = this.#initSubSection( + moduleParams, + WEBO_CTX_CONF_SECTION, + { + requiredFields: weboCtxRequiredFields, + } + ); + this.#components.WeboUserData.initialized = this.#initSubSection( + moduleParams, + WEBO_USER_DATA_CONF_SECTION, + { + userConsent: userConsent || {}, + } + ); + this.#components.SfbxLiteData.initialized = this.#initSubSection( + moduleParams, + SFBX_LITE_DATA_CONF_SECTION + ); return Object.values(this.#components).some((c) => c.initialized); } /** * function that will allow RTD sub-modules to modify the AdUnit object for each auction - * @method + * @function * @param {Object} reqBidsConfigObj * @param {doneCallback} onDone * @param {Object} moduleConfig @@ -264,20 +267,26 @@ class WeboramaRtdProvider { /** @type {WeboCtxConf} */ const weboCtxConf = moduleParams.weboCtxConf || {}; - this.#fetchContextualProfile(weboCtxConf, (data) => { - logger.logMessage('fetchContextualProfile on getBidRequestData is done'); + this.#fetchContextualProfile( + weboCtxConf, + (data) => { + logger.logMessage( + 'fetchContextualProfile on getBidRequestData is done' + ); - this.#setWeboContextualProfile(data); - }, () => { - this.#handleBidRequestData(reqBidsConfigObj, moduleParams); + this.#setWeboContextualProfile(data); + }, + () => { + this.#handleBidRequestData(reqBidsConfigObj, moduleParams); - onDone(); - }); + onDone(); + } + ); } /** * function that provides ad server targeting data to RTD-core - * @method + * @function * @param {string[]} adUnitsCodes * @param {Object} moduleConfig * @param {?ModuleParams} moduleConfig.params @@ -319,7 +328,7 @@ class WeboramaRtdProvider { /** * Initialize subsection module - * @method + * @function * @private * @param {ModuleParams} moduleParams * @param {string} subSection subsection name to initialize @@ -344,17 +353,23 @@ class WeboramaRtdProvider { extra = extra || {}; const requiredFields = extra?.requiredFields || []; - requiredFields.forEach(field => { + requiredFields.forEach((field) => { if (!(field in weboSectionConf)) { throw `missing required field '${field}'`; } }); - if (isPlainObject(extra?.userConsent?.gdpr) && !this.#checkTCFv2(extra.userConsent.gdpr)) { + if ( + isPlainObject(extra?.userConsent?.gdpr) && + !this.#checkTCFv2(extra.userConsent.gdpr) + ) { throw 'gdpr consent not ok'; } } catch (e) { - logger.logError(`unable to initialize: error on '${subSection}' configuration:`, e); + logger.logError( + `unable to initialize: error on '${subSection}' configuration:`, + e + ); return false; } @@ -365,7 +380,7 @@ class WeboramaRtdProvider { /** * check gdpr consent data - * @method + * @function * @private * @param {Object} gdpr * @param {?boolean} gdpr.gdprApplies @@ -376,34 +391,38 @@ class WeboramaRtdProvider { * @param {?Object.} gdpr.vendorData.vendor.consents * @return {boolean} */ - // eslint-disable-next-line no-dupe-class-members + #checkTCFv2(gdpr) { if (gdpr?.gdprApplies !== true) { return true; } - if (deepAccess(gdpr, 'vendorData.vendor.consents') && - deepAccess(gdpr, 'vendorData.purpose.consents')) { - return gdpr.vendorData.vendor.consents[GVLID] === true && // check weborama vendor id + if ( + deepAccess(gdpr, 'vendorData.vendor.consents') && + deepAccess(gdpr, 'vendorData.purpose.consents') + ) { + return ( + gdpr.vendorData.vendor.consents[GVLID] === true && // check weborama vendor id gdpr.vendorData.purpose.consents[1] === true && // info storage access gdpr.vendorData.purpose.consents[3] === true && // create personalized ads gdpr.vendorData.purpose.consents[4] === true && // select personalized ads gdpr.vendorData.purpose.consents[5] === true && // create personalized content - gdpr.vendorData.purpose.consents[6] === true; // select personalized content + gdpr.vendorData.purpose.consents[6] === true + ); // select personalized content } return true; } /** * normalize submodule configuration - * @method + * @function * @private * @param {ModuleParams} moduleParams * @param {CommonConf} submoduleParams * @return {void} * @throws will throw an error in case of invalid configuration */ - // eslint-disable-next-line no-dupe-class-members + #normalizeConf(moduleParams, submoduleParams) { submoduleParams.defaultProfile = submoduleParams.defaultProfile || {}; @@ -430,16 +449,18 @@ class WeboramaRtdProvider { /** * coerce setPrebidTargeting to a callback - * @method + * @function * @private * @param {CommonConf} submoduleParams * @return {void} * @throws will throw an error in case of invalid configuration */ - // eslint-disable-next-line no-dupe-class-members + #coerceSetPrebidTargeting(submoduleParams) { try { - submoduleParams.setPrebidTargeting = this.#wrapValidatorCallback(submoduleParams.setPrebidTargeting); + submoduleParams.setPrebidTargeting = this.#wrapValidatorCallback( + submoduleParams.setPrebidTargeting + ); } catch (e) { throw `invalid setPrebidTargeting: ${e}`; } @@ -447,21 +468,24 @@ class WeboramaRtdProvider { /** * coerce sendToBidders to a callback - * @method + * @function * @private * @param {CommonConf} submoduleParams * @return {void} * @throws will throw an error in case of invalid configuration */ - // eslint-disable-next-line no-dupe-class-members + #coerceSendToBidders(submoduleParams) { let sendToBidders = submoduleParams.sendToBidders; if (isPlainObject(sendToBidders)) { - const sendToBiddersMap = Object.entries(sendToBidders).reduce((map, [key, value]) => { - map[key] = this.#wrapValidatorCallback(value); - return map; - }, {}); + const sendToBiddersMap = Object.entries(sendToBidders).reduce( + (map, [key, value]) => { + map[key] = this.#wrapValidatorCallback(value); + return map; + }, + {} + ); submoduleParams.sendToBidders = (bid, adUnitCode) => { const bidder = bid.bidder; @@ -482,8 +506,10 @@ class WeboramaRtdProvider { } try { - submoduleParams.sendToBidders = this.#wrapValidatorCallback(submoduleParams.sendToBidders, - (bid) => bid.bidder); + submoduleParams.sendToBidders = this.#wrapValidatorCallback( + submoduleParams.sendToBidders, + (bid) => bid.bidder + ); } catch (e) { throw `invalid sendToBidders: ${e}`; } @@ -495,7 +521,7 @@ class WeboramaRtdProvider { */ /** * function that handles bid request data - * @method + * @function * @private * @param {Object} reqBidsConfigObj * @param {AdUnit[]} reqBidsConfigObj.adUnits @@ -504,7 +530,7 @@ class WeboramaRtdProvider { * @param {ModuleParams} moduleParams * @returns {void} */ - // eslint-disable-next-line no-dupe-class-members + #handleBidRequestData(reqBidsConfigObj, moduleParams) { const profileHandlers = this.#buildProfileHandlers(moduleParams); @@ -516,9 +542,9 @@ class WeboramaRtdProvider { const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; try { - adUnits.forEach( - adUnit => adUnit.bids?.forEach( - bid => profileHandlers.forEach(ph => { + adUnits.forEach((adUnit) => + adUnit.bids?.forEach((bid) => + profileHandlers.forEach((ph) => { // logger.logMessage(`check if bidder '${bid.bidder}' and adunit '${adUnit.code} are share ${ph.metadata.source} data`); const [data, metadata] = this.#copyDataAndMetadata(ph); @@ -534,12 +560,15 @@ class WeboramaRtdProvider { logger.logError('unable to send data to bidders:', e); } - profileHandlers.forEach(ph => { + profileHandlers.forEach((ph) => { try { const [data, metadata] = this.#copyDataAndMetadata(ph); ph.onData(data, metadata); } catch (e) { - logger.logError(`error while execute onData callback with ${ph.metadata.source}-based data:`, e); + logger.logError( + `error while execute onData callback with ${ph.metadata.source}-based data:`, + e + ); } }); } @@ -559,17 +588,18 @@ class WeboramaRtdProvider { /** * Fetch Bigsea Contextual Profile - * @method + * @function * @private * @param {WeboCtxConf} weboCtxConf * @param {successCallback} onSuccess callback * @param {doneCallback} onDone callback * @returns {void} */ - // eslint-disable-next-line no-dupe-class-members + #fetchContextualProfile(weboCtxConf, onSuccess, onDone) { const token = weboCtxConf.token; - const baseURLProfileAPI = weboCtxConf.baseURLProfileAPI || BASE_URL_CONTEXTUAL_PROFILE_API; + const baseURLProfileAPI = + weboCtxConf.baseURLProfileAPI || BASE_URL_CONTEXTUAL_PROFILE_API; let path = '/profile'; let queryString = ''; @@ -583,7 +613,10 @@ class WeboramaRtdProvider { try { assetID = weboCtxConf.assetID(); } catch (e) { - logger.logError('unexpected error while fetching asset id from callback', e); + logger.logError( + 'unexpected error while fetching asset id from callback', + e + ); onDone(); @@ -639,12 +672,12 @@ class WeboramaRtdProvider { /** * set bigsea contextual profile on module state - * @method + * @function * @private * @param {?Object} data * @returns {void} */ - // eslint-disable-next-line no-dupe-class-members + #setWeboContextualProfile(data) { if (data && isPlainObject(data) && isValidProfile(data) && !isEmpty(data)) { this.#components.WeboCtx.data = data; @@ -653,37 +686,50 @@ class WeboramaRtdProvider { /** * function that provides data handlers based on the configuration - * @method + * @function * @private * @param {ModuleParams} moduleParams * @returns {ProfileHandler[]} */ - // eslint-disable-next-line no-dupe-class-members + #buildProfileHandlers(moduleParams) { - const steps = [{ - component: this.#components.WeboCtx, - conf: moduleParams?.weboCtxConf, - }, { - component: this.#components.WeboUserData, - conf: moduleParams?.weboUserDataConf, - }, { - component: this.#components.SfbxLiteData, - conf: moduleParams?.sfbxLiteDataConf, - }]; - - return steps.filter(step => step.component.initialized).reduce((ph, { component, conf }) => { - const user = component.user; - const source = component.source; - const callback = component.callbackBuilder(component /* equivalent to this */); - const profileHandler = this.#buildProfileHandler(conf, callback, user, source); - if (profileHandler) { - ph.push(profileHandler); - } else { - logger.logMessage(`skip ${source} profile: no data`); - } + const steps = [ + { + component: this.#components.WeboCtx, + conf: moduleParams?.weboCtxConf, + }, + { + component: this.#components.WeboUserData, + conf: moduleParams?.weboUserDataConf, + }, + { + component: this.#components.SfbxLiteData, + conf: moduleParams?.sfbxLiteDataConf, + }, + ]; + + return steps + .filter((step) => step.component.initialized) + .reduce((ph, { component, conf }) => { + const user = component.user; + const source = component.source; + const callback = component.callbackBuilder( + component /* equivalent to this */ + ); + const profileHandler = this.#buildProfileHandler( + conf, + callback, + user, + source + ); + if (profileHandler) { + ph.push(profileHandler); + } else { + logger.logMessage(`skip ${source} profile: no data`); + } - return ph; - }, []); + return ph; + }, []); } /** @@ -709,7 +755,7 @@ class WeboramaRtdProvider { /** * return specific profile handler - * @method + * @function * @private * @param {CommonConf} dataConf * @param {buildProfileHandlerCallback} callback @@ -717,7 +763,7 @@ class WeboramaRtdProvider { * @param {string} source * @returns {ProfileHandler} */ - // eslint-disable-next-line no-dupe-class-members + #buildProfileHandler(dataConf, callback, user, source) { if (!dataConf) { return; @@ -742,7 +788,7 @@ class WeboramaRtdProvider { } /** * handle individual bid - * @method + * @function * @private * @param {Object} reqBidsConfigObj * @param {Object} reqBidsConfigObj.ortb2Fragments @@ -753,7 +799,7 @@ class WeboramaRtdProvider { * @param {dataCallbackMetadata} metadata * @returns {void} */ - // eslint-disable-next-line no-dupe-class-members + #handleBid(reqBidsConfigObj, bid, profile, metadata) { this.#handleBidViaORTB2(reqBidsConfigObj, bid.bidder, profile, metadata); @@ -767,31 +813,31 @@ class WeboramaRtdProvider { /** * return adapter name based on alias, if any - * @method + * @function * @private * @param {string} aliasName * @returns {string} */ - // eslint-disable-next-line no-dupe-class-members + #getAdapterNameForAlias(aliasName) { return adapterManager.aliasRegistry[aliasName] || aliasName; } /** * function that handles bid request data - * @method + * @function * @private * @param {ProfileHandler} ph profile handler * @returns {[Profile,dataCallbackMetadata]} deeply copy data + metadata */ - // eslint-disable-next-line no-dupe-class-members + #copyDataAndMetadata(ph) { return [deepClone(ph.data), deepClone(ph.metadata)]; } /** * handle appnexus/xandr bid - * @method + * @function * @private * @param {Object} reqBidsConfigObj * @param {Object} reqBidsConfigObj.ortb2Fragments @@ -801,7 +847,7 @@ class WeboramaRtdProvider { * @param {Profile} profile * @returns {void} */ - // eslint-disable-next-line no-dupe-class-members + #handleAppnexusBid(reqBidsConfigObj, bid, profile) { const base = 'params.keywords'; this.#assignProfileToObject(bid, base, profile); @@ -810,7 +856,7 @@ class WeboramaRtdProvider { /** * handle generic bid via ortb2 arbitrary data - * @method + * @function * @private * @param {Object} reqBidsConfigObj * @param {Object} reqBidsConfigObj.ortb2Fragments @@ -820,21 +866,30 @@ class WeboramaRtdProvider { * @param {dataCallbackMetadata} metadata * @returns {void} */ - // eslint-disable-next-line no-dupe-class-members + #handleBidViaORTB2(reqBidsConfigObj, bidder, profile, metadata) { if (isBoolean(metadata.user)) { - logger.logMessage(`bidder '${bidder}' is not directly supported, trying set data via bidder ortb2 fpd`); + logger.logMessage( + `bidder '${bidder}' is not directly supported, trying set data via bidder ortb2 fpd` + ); const section = metadata.user ? 'user' : 'site'; const path = `${section}.ext.data`; - this.#setBidderOrtb2(reqBidsConfigObj.ortb2Fragments?.bidder, bidder, path, profile) + this.#setBidderOrtb2( + reqBidsConfigObj.ortb2Fragments?.bidder, + bidder, + path, + profile + ); } else { - logger.logMessage(`SKIP unsupported bidder '${bidder}', data from '${metadata.source}' is not defined as user or site-centric`); + logger.logMessage( + `SKIP unsupported bidder '${bidder}', data from '${metadata.source}' is not defined as user or site-centric` + ); } } /** * set bidder ortb2 data - * @method + * @function * @private * @param {Object} bidderOrtb2Fragments * @param {string} bidder @@ -842,26 +897,26 @@ class WeboramaRtdProvider { * @param {Profile} profile * @returns {void} */ - // eslint-disable-next-line no-dupe-class-members + #setBidderOrtb2(bidderOrtb2Fragments, bidder, path, profile) { const base = `${bidder}.${path}`; - this.#assignProfileToObject(bidderOrtb2Fragments, base, profile) + this.#assignProfileToObject(bidderOrtb2Fragments, base, profile); } /** * assign profile to object - * @method + * @function * @private * @param {Object} destination * @param {string} base * @param {Profile} profile * @returns {void} */ - // eslint-disable-next-line no-dupe-class-members + #assignProfileToObject(destination, base, profile) { Object.entries(profile).forEach(([key, values]) => { const path = `${base}.${key}`; deepSetValue(destination, path, values); - }) + }); } /** @@ -878,14 +933,14 @@ class WeboramaRtdProvider { /** * wrap value into validator - * @method + * @function * @private * @param {*} value * @param {coerceCallback} coerce * @returns {validatorCallback} * @throws will throw an error in case of unsupported type */ - // eslint-disable-next-line no-dupe-class-members + #wrapValidatorCallback(value, coerce = (x) => x) { if (isFn(value)) { return value; @@ -913,6 +968,7 @@ class WeboramaRtdProvider { /** * check if profile is valid + * a valid profile must be a plain object and every value should be an array of strings or numbers * @param {*} profile * @returns {boolean} */ @@ -921,7 +977,18 @@ export function isValidProfile(profile) { return false; } - return Object.values(profile).every((field) => isArray(field) && field.every(isStr)); + return Object.values(profile).every( + (field) => isArray(field) && field.every(isStrOrNumber) + ); +} + +/** + * Return if the object is a string or number + * @param {*} object object to test + * @return {Boolean} if object is a string or number + */ +function isStrOrNumber(object) { + return isStr(object) || isNumber(object); } /** @@ -943,7 +1010,7 @@ function getContextualProfile(component /* equivalent to this */) { const defaultContextualProfile = weboCtxConf.defaultProfile || {}; return [defaultContextualProfile, true]; - } + }; } /** @@ -958,13 +1025,15 @@ function getWeboUserDataProfile(component /* equivalent to this */) { * @returns {[Profile,boolean]} weboUserData profile + isDefault boolean flag */ return function (weboUserDataConf) { - return getDataFromLocalStorage(weboUserDataConf, + return getDataFromLocalStorage( + weboUserDataConf, () => component.data, - (data) => component.data = data, + (data) => (component.data = data), DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY, LOCAL_STORAGE_USER_TARGETING_SECTION, - WEBO_USER_DATA_SOURCE_LABEL); - } + WEBO_USER_DATA_SOURCE_LABEL + ); + }; } /** @@ -979,13 +1048,15 @@ function getSfbxLiteDataProfile(component /* equivalent to this */) { * @returns {[Profile,boolean]} sfbxLiteData profile + isDefault boolean flag */ return function getSfbxLiteDataProfile(sfbxLiteDataConf) { - return getDataFromLocalStorage(sfbxLiteDataConf, + return getDataFromLocalStorage( + sfbxLiteDataConf, () => component.data, - (data) => component.data = data, + (data) => (component.data = data), DEFAULT_LOCAL_STORAGE_LITE_PROFILE_KEY, LOCAL_STORAGE_LITE_TARGETING_SECTION, - SFBX_LITE_DATA_SOURCE_LABEL); - } + SFBX_LITE_DATA_SOURCE_LABEL + ); + }; } /** @@ -1008,11 +1079,23 @@ function getSfbxLiteDataProfile(component /* equivalent to this */) { * @param {string} source * @returns {[Profile,boolean]} webo (user|lite) data profile + isDefault boolean flag */ -function getDataFromLocalStorage(weboDataConf, cacheGet, cacheSet, defaultLocalStorageProfileKey, targetingSection, source) { +function getDataFromLocalStorage( + weboDataConf, + cacheGet, + cacheSet, + defaultLocalStorageProfileKey, + targetingSection, + source +) { const defaultProfile = weboDataConf.defaultProfile || {}; - if (storage.hasLocalStorage() && storage.localStorageIsEnabled() && !cacheGet()) { - const localStorageProfileKey = weboDataConf.localStorageProfileKey || defaultLocalStorageProfileKey; + if ( + storage.hasLocalStorage() && + storage.localStorageIsEnabled() && + !cacheGet() + ) { + const localStorageProfileKey = + weboDataConf.localStorageProfileKey || defaultLocalStorageProfileKey; const entry = storage.getDataFromLocalStorage(localStorageProfileKey); if (entry) { @@ -1022,9 +1105,12 @@ function getDataFromLocalStorage(weboDataConf, cacheGet, cacheSet, defaultLocalS const profile = data[targetingSection]; const valid = isValidProfile(profile); if (!valid) { - logWarn(`found invalid ${source} profile on local storage key ${localStorageProfileKey}, section ${targetingSection}`); + logger.logMessage( + `WARNING: found invalid ${source} profile on local storage key ${localStorageProfileKey}, section ${targetingSection}: `, + profile + ); - return; + return [defaultProfile, true]; } if (!isEmpty(data)) { diff --git a/modules/weboramaRtdProvider.md b/modules/weboramaRtdProvider.md index a8fc692ba74..11b4368a502 100644 --- a/modules/weboramaRtdProvider.md +++ b/modules/weboramaRtdProvider.md @@ -67,7 +67,7 @@ pbjs.setConfig({ enabled: true, }, sfbxLiteDataConf: { // sfbx-lite site-centric configuration, *omit if not needed* - enabled: true, + enabled: true, }, } }, @@ -215,7 +215,7 @@ A better look on the `Object` type ```javascript sendToBidders: { appnexus: true, // send profile to appnexus on all ad units - pubmatic: ['adUnitCode1'],// send profile to pubmatic on this ad units + pubmatic: ['adUnitCode1'],// send profile to pubmatic on this ad units } ``` @@ -253,7 +253,7 @@ To be possible customize the way we send data to bidders via this callback: sendToBidders: function(bid, adUnitCode, data, metadata){ if (bid.bidder == 'other'){ /* use bid object to store data based on this specific logic, like in the example below */ - + bid.params = bid.params || {}; bid.params['some_specific_key'] = data; @@ -374,7 +374,7 @@ pbjs.que.push(function () { sendToBidders: true, // override param.sendToBidders. default is true defaultProfile: { // optional, used if nothing is found webo_cs: [...], // wam custom segments - webo_audiences: [...], // wam audiences + webo_audiences: [...], // wam audiences }, enabled: true, }, @@ -485,18 +485,18 @@ pbjs.que.push(function () { params: { weboCtxConf: { token: "to-be-defined", // mandatory - setPrebidTargeting: ['adUnitCode1',...], // set target only on certain adunits + setPrebidTargeting: ['adUnitCode1',...], // set target only on certain adunits sendToBidders: ['appnexus',...], // overide, send to only some bidders enabled: true, }, weboUserDataConf: { accountId: 12345, // recommended - setPrebidTargeting: ['adUnitCode2',...], // set target only on certain adunits + setPrebidTargeting: ['adUnitCode2',...], // set target only on certain adunits sendToBidders: ['rubicon',...], // overide, send to only some bidders enabled: true, }, sfbxLiteDataConf: { - setPrebidTargeting: ['adUnitCode3',...], // set target only on certain adunits + setPrebidTargeting: ['adUnitCode3',...], // set target only on certain adunits sendToBidders: ['smartadserver',...], // overide, send to only some bidders enabled: true, } @@ -546,15 +546,15 @@ pbjs.que.push(function () { }, weboUserDataConf: { accountId: 12345, // recommended - setPrebidTargeting: ['adUnitCode1',...], // set target only on certain adunits + setPrebidTargeting: ['adUnitCode1',...], // set target only on certain adunits sendToBidders: { // send to only some bidders and adunits - 'appnexus': true, // all adunits for appnexus + 'appnexus': true, // all adunits for appnexus 'pubmatic': ['adUnitCode1',...] // some adunits for pubmatic // other bidders will be ignored }, defaultProfile: { // optional - webo_cs: ['Red'], - webo_audiences: ['bam'] + webo_cs: ['Red'], // using label + webo_audiences: [12345] // using id }, localStorageProfileKey: 'webo_wam2gam_entry', // default enabled: true, diff --git a/modules/widespaceBidAdapter.js b/modules/widespaceBidAdapter.js index ea6f1bce793..aa939d4584a 100644 --- a/modules/widespaceBidAdapter.js +++ b/modules/widespaceBidAdapter.js @@ -1,8 +1,9 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {parseQueryStringParameters, parseSizesInput} from '../src/utils.js'; +import {deepClone, parseQueryStringParameters, parseSizesInput} from '../src/utils.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; const BIDDER_CODE = 'widespace'; const WS_ADAPTER_VERSION = '2.0.1'; @@ -194,8 +195,8 @@ function pixelSyncPossibility() { } function visibleOnLoad(element) { - if (element && element.getBoundingClientRect) { - const topPos = element.getBoundingClientRect().top; + if (element) { + const topPos = getBoundingClientRect(element).top; return topPos < screen.height && topPos >= window.top.pageYOffset ? 1 : 0; } return ''; @@ -212,7 +213,7 @@ function getLcuid() { } function encodedParamValue(value) { - const requiredStringify = typeof JSON.parse(JSON.stringify(value)) === 'object'; + const requiredStringify = typeof deepClone(value) === 'object'; return encodeURIComponent(requiredStringify ? JSON.stringify(value) : value); } diff --git a/modules/winrBidAdapter.js b/modules/winrBidAdapter.js index cf1158474b4..b29a854cbab 100644 --- a/modules/winrBidAdapter.js +++ b/modules/winrBidAdapter.js @@ -13,10 +13,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import { transformSizes } from '../libraries/sizeUtils/tranformSize.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -312,39 +312,6 @@ export const spec = { ]; } }, - - transformBidParams: function (params, isOpenRtb) { - params = convertTypes( - { - member: 'string', - invCode: 'string', - placementId: 'number', - keywords: transformBidderParamKeywords, - publisherId: 'number', - }, - params - ); - - if (isOpenRtb) { - params.use_pmt_rule = - typeof params.usePaymentRule === 'boolean' - ? params.usePaymentRule - : false; - if (params.usePaymentRule) { - delete params.usePaymentRule; - } - - Object.keys(params).forEach((paramKey) => { - let convertedKey = convertCamelToUnderscore(paramKey); - if (convertedKey !== paramKey) { - params[convertedKey] = params[paramKey]; - delete params[paramKey]; - } - }); - } - - return params; - }, }; function formatRequest(payload, bidderRequest) { @@ -490,7 +457,7 @@ function bidToTag(bid) { } tag.keywords = getANKeywordParam(bid.ortb2, bid.params.keywords) - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + let gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { tag.gpid = gpid; } @@ -504,32 +471,6 @@ function bidToTag(bid) { return tag; } -/* Turn bid request sizes into ut-compatible format */ -function transformSizes(requestSizes) { - let sizes = []; - let sizeObj = {}; - - if ( - isArray(requestSizes) && - requestSizes.length === 2 && - !isArray(requestSizes[0]) - ) { - sizeObj.width = parseInt(requestSizes[0], 10); - sizeObj.height = parseInt(requestSizes[1], 10); - sizes.push(sizeObj); - } else if (typeof requestSizes === 'object') { - for (let i = 0; i < requestSizes.length; i++) { - let size = requestSizes[i]; - sizeObj = {}; - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - sizes.push(sizeObj); - } - } - - return sizes; -} - function hasUserInfo(bid) { return !!bid.params.user; } diff --git a/modules/wurflRtdProvider.js b/modules/wurflRtdProvider.js new file mode 100644 index 00000000000..7dfb68f298c --- /dev/null +++ b/modules/wurflRtdProvider.js @@ -0,0 +1,306 @@ +import { submodule } from '../src/hook.js'; +import { fetch, sendBeacon } from '../src/ajax.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { + mergeDeep, + prefixLog, +} from '../src/utils.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +// Constants +const REAL_TIME_MODULE = 'realTimeData'; +const MODULE_NAME = 'wurfl'; + +// WURFL_JS_HOST is the host for the WURFL service endpoints +const WURFL_JS_HOST = 'https://prebid.wurflcloud.com'; +// WURFL_JS_ENDPOINT_PATH is the path for the WURFL.js endpoint used to load WURFL data +const WURFL_JS_ENDPOINT_PATH = '/wurfl.js'; +// STATS_ENDPOINT_PATH is the path for the stats endpoint used to send analytics data +const STATS_ENDPOINT_PATH = '/v1/prebid/stats'; + +const logger = prefixLog('[WURFL RTD Submodule]'); + +// enrichedBidders holds a list of prebid bidder names, of bidders which have been +// injected with WURFL data +const enrichedBidders = new Set(); + +/** + * init initializes the WURFL RTD submodule + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Object} userConsent User consent data + */ +const init = (config, userConsent) => { + logger.logMessage('initialized'); + return true; +} + +/** + * getBidRequestData enriches the OpenRTB 2.0 device data with WURFL data + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} callback Called on completion + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Object} userConsent User consent data + */ +const getBidRequestData = (reqBidsConfigObj, callback, config, userConsent) => { + const altHost = config.params?.altHost ?? null; + const isDebug = config.params?.debug ?? false; + + const bidders = new Set(); + reqBidsConfigObj.adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + bidders.add(bid.bidder); + }); + }); + + let host = WURFL_JS_HOST; + if (altHost) { + host = altHost; + } + + const url = new URL(host); + url.pathname = WURFL_JS_ENDPOINT_PATH; + + if (isDebug) { + url.searchParams.set('debug', 'true') + } + + url.searchParams.set('mode', 'prebid') + url.searchParams.set('wurfl_id', 'true') + + try { + loadExternalScript(url.toString(), MODULE_TYPE_RTD, MODULE_NAME, () => { + logger.logMessage('script injected'); + window.WURFLPromises.complete.then((res) => { + logger.logMessage('received data', res); + if (!res.wurfl_pbjs) { + logger.logError('invalid WURFL.js for Prebid response'); + } else { + enrichBidderRequests(reqBidsConfigObj, bidders, res); + } + callback(); + }); + }); + } catch (err) { + logger.logError(err); + callback(); + } +} + +/** + * enrichBidderRequests enriches the OpenRTB 2.0 device data with WURFL data for Business Edition + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Array} bidders List of bidders + * @param {Object} wjsResponse WURFL.js response + */ +function enrichBidderRequests(reqBidsConfigObj, bidders, wjsResponse) { + const authBidders = wjsResponse.wurfl_pbjs?.authorized_bidders ?? {}; + const caps = wjsResponse.wurfl_pbjs?.caps ?? []; + + bidders.forEach((bidderCode) => { + if (bidderCode in authBidders) { + // inject WURFL data + enrichedBidders.add(bidderCode); + const data = bidderData(wjsResponse.WURFL, caps, authBidders[bidderCode]); + data['enrich_device'] = true; + enrichBidderRequest(reqBidsConfigObj, bidderCode, data); + return; + } + // inject WURFL low entropy data + const data = lowEntropyData(wjsResponse.WURFL, wjsResponse.wurfl_pbjs?.low_entropy_caps); + enrichBidderRequest(reqBidsConfigObj, bidderCode, data); + }); +} + +/** + * bidderData returns the WURFL data for a bidder + * @param {Object} wurflData WURFL data + * @param {Array} caps Capability list + * @param {Array} filter Filter list + * @returns {Object} Bidder data + */ +export const bidderData = (wurflData, caps, filter) => { + const data = {}; + if ('wurfl_id' in wurflData) { + data['wurfl_id'] = wurflData.wurfl_id; + } + caps.forEach((cap, index) => { + if (!filter.includes(index)) { + return; + } + if (cap in wurflData) { + data[cap] = wurflData[cap]; + } + }); + return data; +} + +/** + * lowEntropyData returns the WURFL low entropy data + * @param {Object} wurflData WURFL data + * @param {Array} lowEntropyCaps Low entropy capability list + * @returns {Object} Bidder data + */ +export const lowEntropyData = (wurflData, lowEntropyCaps) => { + const data = {}; + lowEntropyCaps.forEach((cap, _) => { + let value = wurflData[cap]; + if (cap == 'complete_device_name') { + value = value.replace(/Apple (iP(hone|ad|od)).*/, 'Apple iP$2'); + } + data[cap] = value; + }); + if ('model_name' in wurflData) { + data['model_name'] = wurflData.model_name.replace(/(iP(hone|ad|od)).*/, 'iP$2'); + } + if ('brand_name' in wurflData) { + data['brand_name'] = wurflData.brand_name; + } + if ('wurfl_id' in wurflData) { + data['wurfl_id'] = wurflData.wurfl_id; + } + return data; +} +/** + * enrichBidderRequest enriches the bidder request with WURFL data + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {String} bidderCode Bidder code + * @param {Object} wurflData WURFL data + */ +export const enrichBidderRequest = (reqBidsConfigObj, bidderCode, wurflData) => { + const ortb2data = { + 'device': { + 'ext': {}, + }, + }; + + const device = reqBidsConfigObj.ortb2Fragments.global.device; + enrichOrtb2DeviceData('make', wurflData.brand_name, device, ortb2data); + enrichOrtb2DeviceData('model', wurflData.model_name, device, ortb2data); + if (wurflData.enrich_device) { + delete wurflData.enrich_device; + enrichOrtb2DeviceData('devicetype', makeOrtb2DeviceType(wurflData), device, ortb2data); + enrichOrtb2DeviceData('os', wurflData.advertised_device_os, device, ortb2data); + enrichOrtb2DeviceData('osv', wurflData.advertised_device_os_version, device, ortb2data); + enrichOrtb2DeviceData('hwv', wurflData.model_name, device, ortb2data); + enrichOrtb2DeviceData('h', wurflData.resolution_height, device, ortb2data); + enrichOrtb2DeviceData('w', wurflData.resolution_width, device, ortb2data); + enrichOrtb2DeviceData('ppi', wurflData.pixel_density, device, ortb2data); + enrichOrtb2DeviceData('pxratio', toNumber(wurflData.density_class), device, ortb2data); + enrichOrtb2DeviceData('js', toNumber(wurflData.ajax_support_javascript), device, ortb2data); + } + ortb2data.device.ext['wurfl'] = wurflData + mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { [bidderCode]: ortb2data }); +} + +/** + * makeOrtb2DeviceType returns the ortb2 device type based on WURFL data + * @param {Object} wurflData WURFL data + * @returns {Number} ortb2 device type + * @see https://www.scientiamobile.com/how-to-populate-iab-openrtb-device-object/ + */ +export function makeOrtb2DeviceType(wurflData) { + if (wurflData.is_mobile) { + if (!('is_phone' in wurflData) || !('is_tablet' in wurflData)) { + return undefined; + } + if (wurflData.is_phone || wurflData.is_tablet) { + return 1; + } + return 6; + } + if (wurflData.is_full_desktop) { + return 2; + } + if (wurflData.is_connected_tv) { + return 3; + } + if (wurflData.is_phone) { + return 4; + } + if (wurflData.is_tablet) { + return 5; + } + if (wurflData.is_ott) { + return 7; + } + return undefined; +} + +/** + * enrichOrtb2DeviceData enriches the ortb2data device data with WURFL data. + * Note: it does not overrides properties set by Prebid.js + * @param {String} key the device property key + * @param {any} value the value of the device property + * @param {Object} device the ortb2 device object from Prebid.js + * @param {Object} ortb2data the ortb2 device data enrchiced with WURFL data + */ +function enrichOrtb2DeviceData(key, value, device, ortb2data) { + if (device?.[key] !== undefined) { + // value already defined by Prebid.js, do not overrides + return; + } + if (value === undefined) { + return; + } + ortb2data.device[key] = value; +} + +/** + * toNumber converts a given value to a number. + * Returns `undefined` if the conversion results in `NaN`. + * @param {any} value - The value to convert to a number. + * @returns {number|undefined} The converted number, or `undefined` if the conversion fails. + */ +export function toNumber(value) { + if (value === '' || value === null) { + return undefined; + } + const num = Number(value); + return Number.isNaN(num) ? undefined : num; +} + +/** + * onAuctionEndEvent is called when the auction ends + * @param {Object} auctionDetails Auction details + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Object} userConsent User consent data + */ +function onAuctionEndEvent(auctionDetails, config, userConsent) { + const altHost = config.params?.altHost ?? null; + + let host = WURFL_JS_HOST; + if (altHost) { + host = altHost; + } + + const url = new URL(host); + url.pathname = STATS_ENDPOINT_PATH; + + if (enrichedBidders.size === 0) { + return; + } + + var payload = JSON.stringify({ bidders: [...enrichedBidders] }); + const sentBeacon = sendBeacon(url.toString(), payload); + if (sentBeacon) { + return; + } + + fetch(url.toString(), { + method: 'POST', + body: payload, + mode: 'no-cors', + keepalive: true + }); +} + +// The WURFL submodule +export const wurflSubmodule = { + name: MODULE_NAME, + init, + getBidRequestData, + onAuctionEndEvent, +} + +// Register the WURFL submodule as submodule of realTimeData +submodule(REAL_TIME_MODULE, wurflSubmodule); diff --git a/modules/wurflRtdProvider.md b/modules/wurflRtdProvider.md new file mode 100644 index 00000000000..c7993a67364 --- /dev/null +++ b/modules/wurflRtdProvider.md @@ -0,0 +1,67 @@ +# WURFL Real-time Data Submodule + +## Overview + + Module Name: WURFL Rtd Provider + Module Type: Rtd Provider + Maintainer: prebid@scientiamobile.com + +## Description + +The WURFL RTD module enriches the OpenRTB 2.0 device data with [WURFL data](https://www.scientiamobile.com/wurfl-js-business-edition-at-the-intersection-of-javascript-and-enterprise/). +The module sets the WURFL data in `device.ext.wurfl` and all the bidder adapters will always receive the low entry capabilities like `is_mobile`, `complete_device_name` and `form_factor`, and the `wurfl_id`. + +For a more detailed analysis bidders can subscribe to detect iPhone and iPad models and receive additional [WURFL device capabilities](https://www.scientiamobile.com/capabilities/?products%5B%5D=wurfl-js). + +## User-Agent Client Hints + +WURFL.js is fully compatible with Chromium's User-Agent Client Hints (UA-CH) initiative. If User-Agent Client Hints are absent in the HTTP headers that WURFL.js receives, the service will automatically fall back to using the User-Agent Client Hints' JS API to fetch [high entropy client hint values](https://wicg.github.io/ua-client-hints/#getHighEntropyValues) from the client device. However, we recommend that you explicitly opt-in/advertise support for User-Agent Client Hints on your website and delegate them to the WURFL.js service for the fastest detection experience. Our documentation regarding implementing User-Agent Client Hint support [is available here](https://docs.scientiamobile.com/guides/implementing-useragent-clienthints). + +## Usage + +### Build +``` +gulp build --modules="wurflRtdProvider,appnexusBidAdapter,..." +``` + +### Configuration + +Use `setConfig` to instruct Prebid.js to initilize the WURFL RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +var TIMEOUT = 1000; +pbjs.setConfig({ + realTimeData: { + auctionDelay: TIMEOUT, + dataProviders: [{ + name: 'wurfl', + waitForIt: true, + params: { + debug: false + } + }] + } +}); +``` + +### Parameters + +| Name | Type | Description | Default | +| :------------------------ | :------------ | :--------------------------------------------------------------- |:----------------- | +| name | String | Real time data module name | Always 'wurfl' | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | | +| params.altHost | String | Alternate host to connect to WURFL.js | | +| params.debug | Boolean | Enable debug | `false` | + +## Testing + +To view an example of how the WURFL RTD module works : + +`gulp serve --modules=wurflRtdProvider,appnexusBidAdapter` + +and then point your browser at: + +`http://localhost:9999/integrationExamples/gpt/wurflRtdProvider_example.html` diff --git a/modules/xeBidAdapter.js b/modules/xeBidAdapter.js index a813b9aa2a3..9bd6f8adbac 100644 --- a/modules/xeBidAdapter.js +++ b/modules/xeBidAdapter.js @@ -1,214 +1,16 @@ -import { config } from '../src/config.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import {parseSizesInput, isFn, deepAccess, logError, isArray, getBidIdParameter} from '../src/utils.js'; -import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {buildRequests, getUserSyncs, interpretResponse, isBidRequestValid} from '../libraries/xeUtils/bidderUtils.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 - */ - -const CUR = 'USD'; const BIDDER_CODE = 'xe'; const ENDPOINT = 'https://pbjs.xe.works/bid'; -/** - * 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(req) { - if (req && typeof req.params !== 'object') { - logError('Params is not defined or is incorrect in the bidder settings'); - return false; - } - - if (!getBidIdParameter('env', req.params) || !getBidIdParameter('placement', req.params)) { - logError('Env or placement is not present in bidder params'); - return false; - } - - if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { - logError('mediaTypes.video.playerSize is required for video'); - return false; - } - - return true; -} - -/** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ -function buildRequests(validBidRequests, bidderRequest) { - const { refererInfo = {}, gdprConsent = {}, uspConsent } = bidderRequest; - const requests = validBidRequests.map(req => { - const request = {}; - request.bidId = req.bidId; - request.banner = deepAccess(req, 'mediaTypes.banner'); - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - request.auctionId = req.auctionId; - request.transactionId = req.ortb2Imp?.ext?.tid; - request.sizes = parseSizesInput(getAdUnitSizes(req)); - request.schain = req.schain; - request.location = { - page: refererInfo.page, - location: refererInfo.location, - domain: refererInfo.domain, - whost: window.location.host, - ref: refererInfo.ref, - isAmp: refererInfo.isAmp - }; - request.device = { - ua: navigator.userAgent, - lang: navigator.language - }; - request.env = { - env: req.params.env, - placement: req.params.placement - }; - request.ortb2 = req.ortb2; - request.ortb2Imp = req.ortb2Imp; - request.tz = new Date().getTimezoneOffset(); - request.ext = req.params.ext; - request.bc = req.bidRequestsCount; - request.floor = getBidFloor(req); - - if (req.userIdAsEids && req.userIdAsEids.length !== 0) { - request.userEids = req.userIdAsEids; - } else { - request.userEids = []; - } - if (gdprConsent.gdprApplies) { - request.gdprApplies = Number(gdprConsent.gdprApplies); - request.consentString = gdprConsent.consentString; - } else { - request.gdprApplies = 0; - request.consentString = ''; - } - if (uspConsent) { - request.usPrivacy = uspConsent; - } else { - request.usPrivacy = ''; - } - if (config.getConfig('coppa')) { - request.coppa = 1; - } else { - request.coppa = 0; - } - - const video = deepAccess(req, 'mediaTypes.video'); - if (video) { - request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); - request.video = video; - } - - return request; - }); - - return { - method: 'POST', - url: ENDPOINT, - data: JSON.stringify(requests), - withCredentials: true, - bidderRequest, - options: { - contentType: 'application/json' - } - }; -} - -/** - * 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, { bidderRequest }) { - const response = []; - if (!isArray(deepAccess(serverResponse, 'body.data'))) { - return response; - } - - serverResponse.body.data.forEach(serverBid => { - const bid = { - requestId: bidderRequest.bidId, - dealId: bidderRequest.dealId || null, - ...serverBid - }; - response.push(bid); - }); - - return response; -} - -/** - * 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. - */ -function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { - const syncs = []; - const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); - - if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { - const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; - const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; - const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; - - pixels.forEach(pixel => { - const [ type, url ] = pixel; - const sync = { type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}` }; - if (type === 'iframe' && syncOptions.iframeEnabled) { - syncs.push(sync) - } else if (type === 'image' && syncOptions.pixelEnabled) { - syncs.push(sync) - } - }); - } - - return syncs; -} - -/** - * Get valid floor value from getFloor fuction. - * - * @param {Object} bid Current bid request. - * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. - */ -export function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return null; - } - - let floor = bid.getFloor({ - currency: CUR, - mediaType: '*', - size: '*' - }); - - if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { - return floor.floor; - } - - return null; -} - export const spec = { code: BIDDER_CODE, - aliases: [ 'xeworks', 'lunamediax' ], - supportedMediaTypes: [ BANNER, VIDEO ], + aliases: ['xeworks', 'lunamediax'], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, - buildRequests, + buildRequests: (validBidRequests, bidderRequest) => buildRequests(validBidRequests, bidderRequest, ENDPOINT), interpretResponse, getUserSyncs } diff --git a/modules/yahoosspBidAdapter.js b/modules/yahooAdsBidAdapter.js similarity index 97% rename from modules/yahoosspBidAdapter.js rename to modules/yahooAdsBidAdapter.js index 0453350a85a..7622a5b7587 100644 --- a/modules/yahoosspBidAdapter.js +++ b/modules/yahooAdsBidAdapter.js @@ -3,12 +3,15 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { deepAccess, isFn, isStr, isNumber, isArray, isEmpty, isPlainObject, generateUUID, logInfo, logWarn } from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; const INTEGRATION_METHOD = 'prebid.js'; const BIDDER_CODE = 'yahooAds'; -const BIDDER_ALIASES = ['yahoossp', 'yahooAdvertising'] const GVLID = 25; +const BIDDER_ALIASES = [ + { code: 'yahoossp', gvlid: GVLID }, + { code: 'yahooAdvertising', gvlid: GVLID } +]; const ADAPTER_VERSION = '1.1.0'; const PREBID_VERSION = '$prebid.version$'; const DEFAULT_BID_TTL = 300; @@ -45,7 +48,6 @@ const SUPPORTED_USER_ID_SOURCES = [ 'neustar.biz', 'nextroll.com', 'novatiq.com', - 'parrable.com', 'pubcid.org', 'quantcast.com', 'tapad.com', @@ -148,7 +150,7 @@ function getSupportedEids(bid) { } function isSecure(bid) { - return deepAccess(bid, 'params.bidOverride.imp.secure') || (document.location.protocol === 'https:') ? 1 : 0; + return deepAccess(bid, 'params.bidOverride.imp.secure') ?? bid.ortb2Imp?.secure ?? 1; }; function getPubIdMode(bid) { @@ -344,7 +346,7 @@ function appendImpObject(bid, openRtbObject) { const impObject = { id: bid.bidId, secure: isSecure(bid), - bidfloor: getFloorModuleData(bid).floor || deepAccess(bid, 'params.bidOverride.imp.bidfloor') + bidfloor: getFloorModuleData(bid)?.floor || deepAccess(bid, 'params.bidOverride.imp.bidfloor') }; if (bid.mediaTypes.banner && (typeof mediaTypeMode === 'undefined' || mediaTypeMode === BANNER || mediaTypeMode === '*')) { @@ -371,6 +373,7 @@ function appendImpObject(bid, openRtbObject) { pos: deepAccess(bid, 'params.bidOverride.imp.video.pos') || bid.mediaTypes.video.pos || undefined, playbackmethod: deepAccess(bid, 'params.bidOverride.imp.video.playbackmethod') || bid.mediaTypes.video.playbackmethod || undefined, placement: deepAccess(bid, 'params.bidOverride.imp.video.placement') || bid.mediaTypes.video.placement || undefined, + plcmt: deepAccess(bid, 'params.bidOverride.imp.video.plcmt') || bid.mediaTypes.video.plcmt || undefined, linearity: deepAccess(bid, 'params.bidOverride.imp.video.linearity') || bid.mediaTypes.video.linearity || 1, protocols: deepAccess(bid, 'params.bidOverride.imp.video.protocols') || bid.mediaTypes.video.protocols || [2, 5], startdelay: deepAccess(bid, 'params.bidOverride.imp.video.startdelay') || bid.mediaTypes.video.startdelay || 0, @@ -657,13 +660,9 @@ export const spec = { bidResponse.mediaType = VIDEO; bidResponse.meta.mediaType = VIDEO; bidResponse.vastXml = bid.adm; - - if (bid.nurl) { - bidResponse.vastUrl = bid.nurl; - }; } - if (deepAccess(bidderRequest, 'mediaTypes.video.context') === 'outstream' && !bidderRequest.renderer) { + if (deepAccess(bidderRequest, 'mediaTypes.video.context') === 'outstream' && !bidderRequest.renderer && bidResponse.mediaType === VIDEO) { bidResponse.renderer = createRenderer(bidderRequest, bidResponse) || undefined; } diff --git a/modules/yahoosspBidAdapter.md b/modules/yahooAdsBidAdapter.md similarity index 99% rename from modules/yahoosspBidAdapter.md rename to modules/yahooAdsBidAdapter.md index 62fe0f22a55..df9b71b2314 100644 --- a/modules/yahoosspBidAdapter.md +++ b/modules/yahooAdsBidAdapter.md @@ -581,6 +581,7 @@ Currently the bidOverride object only accepts the following: * pos * playbackmethod * placement + * plcmt * linearity * protocols * rewarded @@ -619,6 +620,7 @@ const adUnits = [{ pos: 1, playbackmethod: 0, placement: 1, + plcmt: 1, linearity: 1, protocols: [2,5], startdelay: 0, diff --git a/modules/yandexAnalyticsAdapter.js b/modules/yandexAnalyticsAdapter.js index 8afe7298c13..6c44bea7cd2 100644 --- a/modules/yandexAnalyticsAdapter.js +++ b/modules/yandexAnalyticsAdapter.js @@ -3,6 +3,7 @@ import adapterManager from '../src/adapterManager.js'; import { logError, logInfo } from '../src/utils.js'; import { EVENTS } from '../src/constants.js'; import * as events from '../src/events.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const timeoutIds = {}; const tryUntil = (operationId, conditionCb, cb) => { @@ -23,6 +24,7 @@ const clearTryUntilTimeouts = (timeouts) => { }); }; +export const PBJS_INIT_EVENT_NAME = 'pbjsInit'; const SEND_EVENTS_BUNDLE_TIMEOUT = 1500; const { BID_REQUESTED, @@ -122,6 +124,9 @@ const yandexAnalytics = Object.assign(buildAdapter({ analyticsType: 'endpoint' } logError('Aborting yandex analytics provider initialization.'); }, 25000); + yandexAnalytics.onEvent(PBJS_INIT_EVENT_NAME, { + 'version': getGlobal().version, + }); events.getEvents().forEach((event) => { if (event && EVENTS_TO_TRACK.indexOf(event.eventType) >= 0) { yandexAnalytics.onEvent(event.eventType, event); diff --git a/modules/yandexAnalyticsAdapter.md b/modules/yandexAnalyticsAdapter.md index 43460550471..0863cc1dfb6 100644 --- a/modules/yandexAnalyticsAdapter.md +++ b/modules/yandexAnalyticsAdapter.md @@ -20,7 +20,7 @@ Disclosure: The adapter utilizes the Metrica Tag build based on [github.com/yand 2. **Insert Counter Initialization Code:** - Retrieve the counter initialization code from the Yandex Metrica settings page at `https://metrica.yandex.com/settings?id={counterId}`, where `{counterId}` is your counter ID, and embed it into your website's HTML. + Retrieve the counter initialization code from the Yandex Metrica settings page at [metrica.yandex.com/r/settings](https://metrica.yandex.com/r/settings), and embed it into your website's HTML. 3. **Initialize the Adapter in Prebid.js:** @@ -43,4 +43,4 @@ Disclosure: The adapter utilizes the Metrica Tag build based on [github.com/yand ## Accessing Analytics Data -You can view the collected analytics data in the Yandex Metrica dashboard. Navigate to [metrika.yandex.com/dashboard](https://metrika.yandex.com/dashboard) and look for the Prebid Analytics section to analyze your data. +You can view the collected analytics data in the Yandex Metrica dashboard. Navigate to [metrika.yandex.com/r/stat/prebid_events](https://metrika.yandex.com/r/stat/prebid_events) to analyze your data. diff --git a/modules/yandexBidAdapter.js b/modules/yandexBidAdapter.js index e694dbcfe03..ffddfa0c2dd 100644 --- a/modules/yandexBidAdapter.js +++ b/modules/yandexBidAdapter.js @@ -1,8 +1,8 @@ +import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { _each, _map, deepAccess, deepSetValue, formatQS, triggerPixel } from '../src/utils.js'; +import { _each, _map, deepAccess, deepSetValue, formatQS, triggerPixel, logInfo } from '../src/utils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid @@ -128,7 +128,7 @@ export const spec = { timeout = bidderRequest.timeout; } - const adServerCurrency = config.getConfig('currency.adServerCurrency'); + const adServerCurrency = getCurrencyFromBidderRequest(bidderRequest); return validBidRequests.map((bidRequest) => { const { params } = bidRequest; @@ -175,13 +175,20 @@ export const spec = { device: ortb2?.device, }; + if (!data?.site?.content?.language) { + const documentLang = deepAccess(ortb2, 'site.ext.data.documentLang'); + if (documentLang) { + deepSetValue(data, 'site.content.language', documentLang); + } + } + const eids = deepAccess(bidRequest, 'userIdAsEids'); if (eids && eids.length) { deepSetValue(data, 'user.ext.eids', eids); } const queryParamsString = formatQS(queryParams); - return { + const request = { method: 'POST', url: BIDDER_URL + `/${pageId}?${queryParamsString}`, data, @@ -190,6 +197,10 @@ export const spec = { }, bidRequest, }; + + logInfo('ServerRequest', request); + + return request; }); }, diff --git a/modules/yandexIdSystem.js b/modules/yandexIdSystem.js new file mode 100644 index 00000000000..e21642abf0b --- /dev/null +++ b/modules/yandexIdSystem.js @@ -0,0 +1,165 @@ +/** + * The {@link module:modules/userId} module is required + * @module modules/yandexIdSystem + * @requires module:modules/userId + */ + +// @ts-check + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager, STORAGE_TYPE_COOKIES } from '../src/storageManager.js'; +import { logError, logInfo } from '../src/utils.js'; + +// .com suffix is just a convention for naming the bidder eids +// See https://github.com/prebid/Prebid.js/pull/11196#discussion_r1591165139 +export const BIDDER_EID_KEY = 'yandex.com'; +export const YANDEX_ID_KEY = 'yandexId'; +export const YANDEX_EXT_COOKIE_NAMES = ['_ym_fa']; +export const BIDDER_CODE = 'yandex'; +export const YANDEX_USER_ID_KEY = '_ym_uid'; +export const YANDEX_STORAGE_TYPE = STORAGE_TYPE_COOKIES; +export const YANDEX_MIN_EXPIRE_DAYS = 30; + +export const PREBID_STORAGE = getStorageManager({ + moduleType: MODULE_TYPE_UID, + moduleName: BIDDER_CODE, + bidderCode: undefined +}); + +/** @type {Submodule} */ +export const yandexIdSubmodule = { + name: BIDDER_CODE, + /** + * Decodes the stored id value for passing to bid requests. + * @param {string} value + */ + decode(value) { + logInfo(`Decoded ${YANDEX_ID_KEY}`, value); + + return { [YANDEX_ID_KEY]: value }; + }, + getId(submoduleConfig, _consentData, storedId) { + if (checkConfigHasErrorsAndReport(submoduleConfig)) { + return; + } + + if (storedId) { + logInfo('Got storedId', storedId); + return { + id: storedId + }; + } + + return { + id: new YandexIdGenerator().generate(), + }; + }, + eids: { + [YANDEX_ID_KEY]: { + source: BIDDER_EID_KEY, + /** + * Agent Type 1 means that it is an ID + * which is tied to a specific web browser or device (cookie-based, probabilistic, or other). + * @see https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/main/AdCOM%20v1.0%20FINAL.md#list--agent-types- + */ + atype: 1, + getUidExt() { + if (PREBID_STORAGE.cookiesAreEnabled()) { + return YANDEX_EXT_COOKIE_NAMES.reduce((acc, cookieName) => ({ + ...acc, + [cookieName]: PREBID_STORAGE.getCookie(cookieName), + }), {}); + } + }, + }, + }, +}; + +/** + * @param {SubmoduleConfig} submoduleConfig + * @returns {boolean} `true` - when there are errors, `false` - otherwise. + */ +function checkConfigHasErrorsAndReport(submoduleConfig) { + let error = false; + + const READABLE_MODULE_NAME = 'Yandex ID module'; + + if (submoduleConfig.storage == null) { + logError(`Misconfigured ${READABLE_MODULE_NAME}. "storage" is required.`); + return true; + } + + if (submoduleConfig.storage.name !== YANDEX_USER_ID_KEY) { + logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.name" is expected to be "${YANDEX_USER_ID_KEY}", actual is "${submoduleConfig.storage.name}"`); + error = true; + } + + if (submoduleConfig.storage.type !== YANDEX_STORAGE_TYPE) { + logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.type" is expected to be "${YANDEX_STORAGE_TYPE}", actual is "${submoduleConfig.storage.type}"`); + error = true; + } + + if ((submoduleConfig.storage.expires ?? 0) < YANDEX_MIN_EXPIRE_DAYS) { + logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.expires" is expected not to be less than "${YANDEX_MIN_EXPIRE_DAYS}", actual is "${submoduleConfig.storage.expires}"`); + error = true; + } + + return error; +} + +/** + * Yandex-specific generator for uid. Needs to be compatible with Yandex Metrica tag. + * @see https://github.com/yandex/metrica-tag/blob/main/src/utils/uid/uid.ts#L51 + */ +class YandexIdGenerator { + generate() { + const yandexId = [ + this._getCurrentSecTimestamp(), + this._getRandomInteger(1000000, 999999999), + ].join(''); + + logInfo(`Generated ${YANDEX_ID_KEY}`, yandexId); + + return yandexId; + } + + _getCurrentSecTimestamp() { + return Math.round(Date.now() / 1000); + } + + /** + * @param {number} min + * @param {number} max + */ + _getRandomInteger(min, max) { + const generateRandom = this._getRandomGenerator(); + + /** + * Needs to be compatible with Yandex Metrica `getRandom` function. + * @see https://github.com/yandex/metrica-tag/blob/main/src/utils/number/random.ts#L12 + */ + return Math.floor(generateRandom() * (max - min)) + min; + } + + _getRandomGenerator() { + if (window.crypto) { + return () => { + const buffer = new Uint32Array(1); + crypto.getRandomValues(buffer); + + return buffer[0] / 0xffffffff; + }; + } + + // Polyfill for environments that don't support Crypto API + return () => Math.random(); + } +} + +submodule('userId', yandexIdSubmodule); diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index 23ff94f62a1..fda54328b85 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -100,7 +100,7 @@ export const spec = { if (bidderRequest.gdprConsent) { query.gdpr = (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true; if (query.gdpr) { - query.consent = bidderRequest.gdprConsent.consentString; + query.gdpr_consent = bidderRequest.gdprConsent.consentString; } } @@ -184,7 +184,7 @@ export const spec = { const extId = bidRequest.params.extId !== undefined ? '&id=' + bidRequest.params.extId : ''; const adType = matchedBid.adtype !== undefined ? matchedBid.adtype : ''; const gdprApplies = reqParams.gdpr ? '&gdpr=' + reqParams.gdpr : ''; - const gdprConsent = reqParams.consent ? '&consent=' + reqParams.consent : ''; + const gdprConsent = reqParams.gdpr_consent ? '&gdpr_consent=' + reqParams.gdpr_consent : ''; const pvId = matchedBid.pvid !== undefined ? '&pvid=' + matchedBid.pvid : ''; const iabContent = reqParams.iab_content ? '&iab_content=' + reqParams.iab_content : ''; @@ -201,7 +201,7 @@ export const spec = { referrer: '', ad: ``, meta: { - advertiserDomains: (matchedBid.advertiser) ? matchedBid.advertiser : 'n/a', + advertiserDomains: [(matchedBid.advertiser) ? matchedBid.advertiser : 'n/a'], }, }; @@ -555,7 +555,7 @@ function getBidFloor(bid, sizes) { mediaType: mediaType !== undefined && spec.supportedMediaTypes.includes(mediaType) ? mediaType : '*', size: sizes.length !== 1 ? '*' : sizes[0].split(DIMENSION_SIGN), }); - if (floor.currency === CURRENCY_CODE) { + if (floor?.currency === CURRENCY_CODE) { return (floor.floor * 100).toFixed(0); } return undefined; diff --git a/modules/yieldloveBidAdapter.js b/modules/yieldloveBidAdapter.js index 4568206b20a..fadcf51cc85 100644 --- a/modules/yieldloveBidAdapter.js +++ b/modules/yieldloveBidAdapter.js @@ -45,8 +45,8 @@ export const spec = { const s2sRequest = { device: { ua: window.navigator.userAgent, - w: window.innerWidth, - h: window.innerHeight, + w: utils.getWinDimensions().innerWidth, + h: utils.getWinDimensions().innerHeight, }, site: { ver: '1.9.0', diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index fe99adcec5f..ae91d39e2cc 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -1,6 +1,7 @@ import { deepAccess, deepSetValue, + getWinDimensions, getWindowTop, isArray, isArrayOfNums, @@ -114,8 +115,8 @@ export const spec = { serverRequest.pr = (LOCAL_WINDOW.document && LOCAL_WINDOW.document.referrer) || ''; serverRequest.scrd = LOCAL_WINDOW.devicePixelRatio || 0; serverRequest.title = LOCAL_WINDOW.document.title || ''; - serverRequest.w = LOCAL_WINDOW.innerWidth; - serverRequest.h = LOCAL_WINDOW.innerHeight; + serverRequest.w = getWinDimensions().innerWidth; + serverRequest.h = getWinDimensions().innerHeight; } const mtp = window.navigator.maxTouchPoints; @@ -177,7 +178,7 @@ export const spec = { serverRequest.topics = topicsData; } if (eids.length) { - serverRequest.user = { eids }; + deepSetValue(serverRequest, 'user.ext.eids', eids); }; serverRequests.push({ method: 'POST', @@ -257,7 +258,9 @@ function hasVideoMediaType(bidRequest) { * @param request bid request */ function addPlacement(request) { - const gpid = deepAccess(request, 'ortb2Imp.ext.data.pbadslot'); + const gpid = deepAccess(request, 'ortb2Imp.ext.gpid') || deepAccess(request, 'ortb2Imp.ext.data.pbadslot'); + const tagid = deepAccess(request, 'ortb2Imp.ext.tagid'); + const divid = deepAccess(request, 'ortb2Imp.ext.divid'); const placementInfo = { placement_id: request.adUnitCode, callback_id: request.bidId, @@ -275,6 +278,12 @@ function addPlacement(request) { if (gpid) { placementInfo.gpid = gpid; } + if (tagid) { + placementInfo.tagid = tagid; + } + if (divid) { + placementInfo.divid = divid; + } // get the transaction id for the banner bid. const transactionId = deepAccess(request, 'ortb2Imp.ext.tid'); @@ -337,8 +346,7 @@ function createNewVideoBid(response, bidRequest) { mediaType: VIDEO, }, }; - - if (imp.video.placement && imp.video.placement !== 1) { + if (imp.video.plcmt && imp.video.plcmt !== 1) { const renderer = Renderer.install({ url: OUTSTREAM_VIDEO_PLAYER_URL, config: { @@ -349,7 +357,7 @@ function createNewVideoBid(response, bidRequest) { allowVpaid: true, autoPlay: true, preload: true, - mute: true + mute: true, }, id: imp.tagid, loaded: false, @@ -471,7 +479,9 @@ function getTopics(bidderRequest) { * @return Object OpenRTB's 'imp' (impression) object */ function openRtbImpression(bidRequest) { - const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); + const tagid = deepAccess(bidRequest, 'ortb2Imp.ext.tagid'); + const divid = deepAccess(bidRequest, 'ortb2Imp.ext.divid'); const size = extractPlayerSize(bidRequest); const imp = { id: bidRequest.bidId, @@ -509,6 +519,12 @@ function openRtbImpression(bidRequest) { if (gpid) { imp.ext.gpid = gpid; } + if (tagid) { + imp.ext.tagid = tagid; + } + if (divid) { + imp.ext.divid = divid; + } return imp; } @@ -653,19 +669,11 @@ function validateVideoParams(bid) { validate('video.mimes', val => isDefined(val), paramRequired); validate('video.mimes', val => isArray(val) && val.every(v => isStr(v)), paramInvalid, 'array of strings, ex: ["video/mp4"]'); - - const placement = validate('video.placement', val => isDefined(val), paramRequired); - validate('video.placement', val => val >= 1 && val <= 5, paramInvalid); - if (placement === 1) { - validate('video.startdelay', val => isDefined(val), - (field, v) => paramRequired(field, v, 'placement == 1')); - validate('video.startdelay', val => isNumber(val), paramInvalid, 'number, ex: 5'); - } - validate('video.protocols', val => isDefined(val), paramRequired); validate('video.api', val => isDefined(val), paramRequired); - validate('video.api', val => isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + // PS-6597 - Allow video.api to be any number greater than 0 + validate('video.api', val => isArrayOfNums(val) && val.every(v => (v >= 1)), paramInvalid, 'array of numbers, ex: [2,3]'); validate('video.playbackmethod', val => !isDefined(val) || isArrayOfNums(val), paramInvalid, diff --git a/modules/yieldoneBidAdapter.js b/modules/yieldoneBidAdapter.js index 8ad7e69aa6e..655b331c7c3 100644 --- a/modules/yieldoneBidAdapter.js +++ b/modules/yieldoneBidAdapter.js @@ -2,6 +2,8 @@ import {deepAccess, isEmpty, isStr, logWarn, parseSizesInput} from '../src/utils import {registerBidder} from '../src/adapters/bidderFactory.js'; 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'; /** * @typedef {import('../src/adapters/bidderFactory').Bid} Bid @@ -111,6 +113,12 @@ export const spec = { payload.id5Id = id5id; } + // UID2.0 + const uid2 = deepAccess(bidRequest, 'userId.uid2.id'); + if (isStr(uid2) && !isEmpty(uid2)) { + payload.uid2id = uid2; + } + return { method: 'GET', url: ENDPOINT_URL, @@ -219,10 +227,12 @@ export const spec = { /** * 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. + * @param {Object} gdprConsent Is the GDPR Consent object wrapping gdprApplies {boolean} and consentString {string} attributes. * @returns {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs: function(syncOptions) { - if (syncOptions.iframeEnabled) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { + if (syncOptions.iframeEnabled && !skipSync(gdprConsent)) { return [{ type: 'iframe', url: USER_SYNC_URL @@ -393,4 +403,20 @@ function cmerRender(bid) { }); } +/** + * Stop sending push_sync requests in case it's either Safari browser OR iOS device OR GDPR applies. + * 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); +} + +/** + * Check if GDPR applies. + */ +function gdprApplies(gdprConsent) { + return gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies; +} + registerBidder(spec); diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index 25e4dc73b74..19825041589 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -1,12 +1,11 @@ import {buildUrl, generateUUID, getWindowLocation, logError, logInfo, parseSizesInput, parseUrl} from '../src/utils.js'; -import {ajax} from '../src/ajax.js'; +import {ajax, fetch} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { EVENTS, STATUS } from '../src/constants.js'; import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {includes as strIncludes} from '../src/polyfill.js'; -import {getGlobal} from '../src/prebidGlobal.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_CODE = 'yuktamedia'; @@ -37,7 +36,7 @@ const _pageInfo = { referer: referer, refererDomain: parseUrl(referer).host, yuktamediaAnalyticsVersion: yuktamediaAnalyticsVersion, - prebidVersion: getGlobal().version + prebidVersion: 'v' + 'prebid.version$' }; function getParameterByName(param) { @@ -51,10 +50,6 @@ function getParameterByName(param) { return vars[param] ? vars[param] : ''; } -function isNavigatorSendBeaconSupported() { - return ('navigator' in window) && ('sendBeacon' in window.navigator); -} - function updateSessionId() { if (isSessionIdTimeoutExpired()) { let newSessionId = generateUUID(); @@ -89,11 +84,14 @@ function send(data, status) { hostname: 'analytics-prebid.yuktamedia.com', pathname: '/api/bids' }); - if (isNavigatorSendBeaconSupported()) { - window.navigator.sendBeacon(yuktamediaAnalyticsRequestUrl, JSON.stringify(data)); - } else { + fetch(yuktamediaAnalyticsRequestUrl, { + body: JSON.stringify(data), + keepalive: true, + withCredentials: true, + method: 'POST' + }).catch((_e) => { ajax(yuktamediaAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/plain' }); - } + }); } var yuktamediaAnalyticsAdapter = Object.assign(adapter({ analyticsType: 'endpoint' }), { diff --git a/modules/zeta_global_sspAnalyticsAdapter.js b/modules/zeta_global_sspAnalyticsAdapter.js index f0933d2f62f..03e3f8f556e 100644 --- a/modules/zeta_global_sspAnalyticsAdapter.js +++ b/modules/zeta_global_sspAnalyticsAdapter.js @@ -1,22 +1,20 @@ -import {logInfo, logError} from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; +import {logError} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; import adapterManager from '../src/adapterManager.js'; -import { EVENTS } from '../src/constants.js'; +import {EVENTS} from '../src/constants.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import {config} from '../src/config.js'; +import {parseDomain} from '../src/refererDetection.js'; const ZETA_GVL_ID = 833; const ADAPTER_CODE = 'zeta_global_ssp'; const BASE_URL = 'https://ssp.disqus.com/prebid/event'; const LOG_PREFIX = 'ZetaGlobalSsp-Analytics: '; -const cache = { - auctions: {} -}; - /// /////////// VARIABLES //////////////////////////////////// -let publisherId; // int +let zetaParams; /// /////////// HELPER FUNCTIONS ///////////////////////////// @@ -28,154 +26,107 @@ function sendEvent(eventType, event) { ); } -function getZetaParams(event) { - if (event.adUnits) { - for (const i in event.adUnits) { - const unit = event.adUnits[i]; - if (unit.bids) { - for (const j in unit.bids) { - const bid = unit.bids[j]; - if (bid.bidder === ADAPTER_CODE && bid.params) { - return bid.params; - } - } - } - } - } - return null; -} - /// /////////// ADAPTER EVENT HANDLER FUNCTIONS ////////////// function adRenderSucceededHandler(args) { - let eventType = EVENTS.AD_RENDER_SUCCEEDED - logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); - + const page = config.getConfig('pageUrl') || args.doc?.location?.host + args.doc?.location?.pathname; const event = { - adId: args.adId, + zetaParams: zetaParams, + domain: parseDomain(page, {noLeadingWww: true}), + page: page, bid: { adId: args.bid?.adId, - auctionId: args.bid?.auctionId, - adUnitCode: args.bid?.adUnitCode, - bidId: args.bid?.bidId, requestId: args.bid?.requestId, - bidderCode: args.bid?.bidderCode, - mediaTypes: args.bid?.mediaTypes, - sizes: args.bid?.sizes, - adserverTargeting: args.bid?.adserverTargeting, - cpm: args.bid?.cpm, + auctionId: args.bid?.auctionId, creativeId: args.bid?.creativeId, + bidder: args.bid?.bidderCode, mediaType: args.bid?.mediaType, - renderer: args.bid?.renderer, size: args.bid?.size, + adomain: args.bid?.adserverTargeting?.hb_adomain, timeToRespond: args.bid?.timeToRespond, - params: args.bid?.params + cpm: args.bid?.cpm, + adUnitCode: args.bid?.adUnitCode }, - doc: { - location: args.doc?.location + device: { + ua: navigator.userAgent } } - - // set zetaParams from cache - if (event.bid && event.bid.auctionId) { - const zetaParams = cache.auctions[event.bid.auctionId]; - if (zetaParams) { - event.bid.params = [ zetaParams ]; - } - } - - sendEvent(eventType, event); + sendEvent(EVENTS.AD_RENDER_SUCCEEDED, event); } function auctionEndHandler(args) { - let eventType = EVENTS.AUCTION_END; - logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); - const event = { - auctionId: args.auctionId, - adUnits: args.adUnits, + zetaParams: zetaParams, bidderRequests: args.bidderRequests?.map(br => ({ bidderCode: br?.bidderCode, - refererInfo: br?.refererInfo, + domain: br?.refererInfo?.domain, + page: br?.refererInfo?.page, bids: br?.bids?.map(b => ({ - adUnitCode: b?.adUnitCode, - auctionId: b?.auctionId, bidId: b?.bidId, - requestId: b?.requestId, - bidderCode: b?.bidderCode, - mediaTypes: b?.mediaTypes, - sizes: b?.sizes, + auctionId: b?.auctionId, bidder: b?.bidder, - params: b?.params + mediaType: b?.mediaTypes?.video ? 'VIDEO' : (b?.mediaTypes?.banner ? 'BANNER' : undefined), + size: b?.sizes?.filter(s => s && s.length === 2).filter(s => Number.isInteger(s[0]) && Number.isInteger(s[1])).map(s => s[0] + 'x' + s[1]).find(s => s), + device: b?.ortb2?.device, + adUnitCode: b?.adUnitCode })) })), bidsReceived: args.bidsReceived?.map(br => ({ adId: br?.adId, - adserverTargeting: { - hb_adomain: br?.adserverTargeting?.hb_adomain - }, - cpm: br?.cpm, + requestId: br?.requestId, creativeId: br?.creativeId, + bidder: br?.bidder, mediaType: br?.mediaType, - renderer: br?.renderer, size: br?.size, + adomain: br?.adserverTargeting?.hb_adomain, timeToRespond: br?.timeToRespond, - adUnitCode: br?.adUnitCode, - auctionId: br?.auctionId, - bidId: br?.bidId, - requestId: br?.requestId, - bidderCode: br?.bidderCode, - mediaTypes: br?.mediaTypes, - sizes: br?.sizes, - bidder: br?.bidder, - params: br?.params + cpm: br?.cpm, + adUnitCode: br?.adUnitCode })) } + sendEvent(EVENTS.AUCTION_END, event); +} - // save zetaParams to cache - const zetaParams = getZetaParams(event); - if (zetaParams && event.auctionId) { - cache.auctions[event.auctionId] = zetaParams; +function bidTimeoutHandler(args) { + const event = { + zetaParams: zetaParams, + domain: args.find(t => t?.ortb2?.site?.domain)?.ortb2?.site?.domain, + page: args.find(t => t?.ortb2?.site?.page)?.ortb2?.site?.page, + timeouts: args.map(t => ({ + bidId: t?.bidId, + auctionId: t?.auctionId, + bidder: t?.bidder, + mediaType: t?.mediaTypes?.video ? 'VIDEO' : (t?.mediaTypes?.banner ? 'BANNER' : undefined), + size: t?.sizes?.filter(s => s && s.length === 2).filter(s => Number.isInteger(s[0]) && Number.isInteger(s[1])).map(s => s[0] + 'x' + s[1]).find(s => s), + timeout: t?.timeout, + device: t?.ortb2?.device, + adUnitCode: t?.adUnitCode + })) } - - sendEvent(eventType, event); + sendEvent(EVENTS.BID_TIMEOUT, event); } /// /////////// ADAPTER DEFINITION /////////////////////////// -let baseAdapter = adapter({ analyticsType: 'endpoint' }); +let baseAdapter = adapter({analyticsType: 'endpoint'}); let zetaAdapter = Object.assign({}, baseAdapter, { enableAnalytics(config = {}) { - let error = false; - - if (typeof config.options === 'object') { - if (config.options.sid) { - publisherId = Number(config.options.sid); - } + if (config.options && config.options.sid) { + zetaParams = config.options; + baseAdapter.enableAnalytics.call(this, config); } else { logError(LOG_PREFIX + 'Config not found'); - error = true; - } - - if (!publisherId) { - logError(LOG_PREFIX + 'Missing sid (publisher id)'); - error = true; - } - - if (error) { logError(LOG_PREFIX + 'Analytics is disabled due to error(s)'); - } else { - baseAdapter.enableAnalytics.call(this, config); } }, disableAnalytics() { - publisherId = undefined; + zetaParams = undefined; baseAdapter.disableAnalytics.apply(this, arguments); }, - track({ eventType, args }) { + track({eventType, args}) { switch (eventType) { case EVENTS.AD_RENDER_SUCCEEDED: adRenderSucceededHandler(args); @@ -183,6 +134,9 @@ let zetaAdapter = Object.assign({}, baseAdapter, { case EVENTS.AUCTION_END: auctionEndHandler(args); break; + case EVENTS.BID_TIMEOUT: + bidTimeoutHandler(args); + break; } } }); diff --git a/modules/zeta_global_sspBidAdapter.js b/modules/zeta_global_sspBidAdapter.js index f3e2e12c143..62e60db020f 100644 --- a/modules/zeta_global_sspBidAdapter.js +++ b/modules/zeta_global_sspBidAdapter.js @@ -3,7 +3,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {parseDomain} from '../src/refererDetection.js'; -import {ajax} from '../src/ajax.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -15,7 +14,6 @@ import {ajax} from '../src/ajax.js'; const BIDDER_CODE = 'zeta_global_ssp'; const ENDPOINT_URL = 'https://ssp.disqus.com/bid/prebid'; -const TIMEOUT_URL = 'https://ssp.disqus.com/timeout/prebid'; const USER_SYNC_URL_IFRAME = 'https://ssp.disqus.com/sync?type=iframe'; const USER_SYNC_URL_IMAGE = 'https://ssp.disqus.com/sync?type=image'; const DEFAULT_CUR = 'USD'; @@ -130,8 +128,8 @@ export const spec = { id: bidderRequest.bidderRequestId, cur: [DEFAULT_CUR], imp: imps, - site: params.site ? params.site : {}, - device: {...(bidderRequest.ortb2?.device || {}), ...params.device}, + site: {...bidderRequest?.ortb2?.site, ...params?.site}, + device: {...bidderRequest?.ortb2?.device, ...params?.device}, user: params.user ? params.user : {}, app: params.app ? params.app : {}, ext: { @@ -149,6 +147,18 @@ export const spec = { payload.device.w = screen.width; payload.device.h = screen.height; + if (bidderRequest.ortb2?.user?.geo && bidderRequest.ortb2?.device?.geo) { + payload.device.geo = { ...payload.device.geo, ...bidderRequest.ortb2?.device.geo }; + payload.user.geo = { ...payload.user.geo, ...bidderRequest.ortb2?.user.geo }; + } else { + if (bidderRequest.ortb2?.user?.geo) { + payload.user.geo = payload.device.geo = { ...payload.user.geo, ...bidderRequest.ortb2?.user.geo }; + } + if (bidderRequest.ortb2?.device?.geo) { + payload.user.geo = payload.device.geo = { ...payload.user.geo, ...bidderRequest.ortb2?.device.geo }; + } + } + if (bidderRequest?.ortb2?.device?.sua) { payload.device.sua = bidderRequest.ortb2.device.sua; } @@ -181,6 +191,14 @@ export const spec = { payload.tmax = bidderRequest.timeout; } + if (bidderRequest?.ortb2?.bcat) { + payload.bcat = bidderRequest.ortb2.bcat; + } + + if (bidderRequest?.ortb2?.badv) { + payload.badv = bidderRequest.ortb2.badv; + } + provideEids(validBidRequests[0], payload); provideSegments(bidderRequest, payload); const url = params.sid ? ENDPOINT_URL.concat('?sid=', params.sid) : ENDPOINT_URL; @@ -268,25 +286,6 @@ export const spec = { url: USER_SYNC_URL_IMAGE + syncurl }]; } - }, - - onTimeout: function(timeoutData) { - if (timeoutData) { - const payload = timeoutData.map(d => ({ - bidder: d?.bidder, - shortname: d?.params?.map(p => p?.tags?.shortname).find(p => p), - sid: d?.params?.map(p => p?.sid).find(p => p), - country: d?.ortb2?.device?.geo?.country, - devicetype: d?.ortb2?.device?.devicetype - })); - ajax(TIMEOUT_URL, null, JSON.stringify(payload), { - method: 'POST', - options: { - withCredentials: false, - contentType: 'application/json' - } - }); - } } } diff --git a/package-lock.json b/package-lock.json index 53ceaded15a..c0cbb0bdd1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,69 +1,65 @@ { "name": "prebid.js", - "version": "8.47.0", - "lockfileVersion": 3, + "version": "9.40.0", + "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "8.47.0", + "version": "9.40.0", "license": "Apache-2.0", "dependencies": { - "@babel/core": "^7.16.7", + "@babel/core": "^7.25.2", "@babel/plugin-transform-runtime": "^7.18.9", "@babel/preset-env": "^7.16.8", "@babel/runtime": "^7.18.9", "core-js": "^3.13.0", "core-js-pure": "^3.13.0", - "criteo-direct-rsa-validate": "^1.1.0", "crypto-js": "^4.2.0", "dlv": "1.1.3", - "dset": "3.1.2", + "dset": "3.1.4", "express": "^4.15.4", "fun-hooks": "^0.9.9", "gulp-wrap": "^0.15.0", - "just-clone": "^1.0.2", - "live-connect-js": "^6.3.4" + "klona": "^2.0.6", + "live-connect-js": "^7.2.0" }, "devDependencies": { "@babel/eslint-parser": "^7.16.5", - "@wdio/browserstack-service": "^8.29.0", - "@wdio/cli": "^8.29.0", + "@babel/register": "^7.24.6", + "@eslint/compat": "^1.2.7", + "@wdio/browserstack-service": "^9.0.5", + "@wdio/cli": "^9.0.5", "@wdio/concise-reporter": "^8.29.0", - "@wdio/local-runner": "^8.29.0", + "@wdio/local-runner": "^9.0.5", "@wdio/mocha-framework": "^8.29.0", "@wdio/spec-reporter": "^8.29.0", "ajv": "6.12.3", "assert": "^2.0.0", "babel-loader": "^8.0.5", "babel-plugin-istanbul": "^6.1.1", - "babel-register": "^6.26.0", "body-parser": "^1.19.0", "chai": "^4.2.0", "coveralls": "^3.1.0", "deep-equal": "^2.0.3", "documentation": "^14.0.0", "es5-shim": "^4.5.14", - "eslint": "^7.27.0", - "eslint-config-standard": "^10.2.1", - "eslint-plugin-import": "^2.20.2", - "eslint-plugin-jsdoc": "^38.1.6", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prebid": "file:./plugins/eslint", - "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-standard": "^3.0.1", + "eslint": "^9.22.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.6", "execa": "^1.0.0", "faker": "^5.5.3", "fs.extra": "^1.3.2", - "gulp": "^4.0.0", + "globals": "^16.0.0", + "gulp": "^4.0.2", "gulp-clean": "^0.4.0", "gulp-concat": "^2.6.0", "gulp-connect": "^5.7.0", - "gulp-eslint": "^6.0.0", "gulp-if": "^3.0.0", "gulp-js-escape": "^1.0.1", "gulp-rename": "^2.0.0", "gulp-replace": "^1.0.0", + "gulp-run-command": "^0.0.10", "gulp-shell": "^0.8.0", "gulp-sourcemaps": "^3.0.0", "gulp-terser": "^2.0.1", @@ -90,20 +86,21 @@ "karma-spec-reporter": "^0.0.32", "karma-webpack": "^5.0.0", "lodash": "^4.17.21", - "mocha": "^10.0.0", + "mocha": "^10.7.3", "morgan": "^1.10.0", + "neostandard": "^0.12.1", "node-html-parser": "^6.1.5", "opn": "^5.4.0", "resolve-from": "^5.0.0", - "sinon": "^4.1.3", + "sinon": "^4.5.0", "through2": "^4.0.2", "url": "^0.11.0", "url-parse": "^1.0.5", "video.js": "^7.17.0", "videojs-contrib-ads": "^6.9.0", - "videojs-ima": "^1.11.0", + "videojs-ima": "^2.3.0", "videojs-playlist": "^5.0.0", - "webdriverio": "^7.6.1", + "webdriverio": "^9.0.9", "webpack": "^5.70.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-manifest-plugin": "^5.0.0", @@ -111,64 +108,66 @@ "yargs": "^1.3.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=20.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz", - "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==", - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.6", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helpers": "^7.19.4", - "@babel/parser": "^7.19.6", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.6", - "@babel/types": "^7.19.4", - "convert-source-map": "^1.7.0", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -179,102 +178,89 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", - "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", + "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": "^10.13.0 || ^12.13.0 || >=14.0.0" }, "peerDependencies": { - "@babel/core": ">=7.11.0", - "eslint": "^7.5.0 || ^8.0.0" + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", - "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dependencies": { - "@babel/compat-data": "^7.20.0", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz", - "integrity": "sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -284,12 +270,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", - "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -299,131 +286,122 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", + "@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.14.2", - "semver": "^6.1.2" + "resolve": "^1.14.2" }, "peerDependencies": { - "@babel/core": "^7.4.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", - "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", "dependencies": { - "@babel/types": "^7.18.9" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz", - "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.19.4", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.6", - "@babel/types": "^7.19.4" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", - "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -433,121 +411,132 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", - "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", - "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dependencies": { - "@babel/types": "^7.19.4" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dependencies": { - "@babel/types": "^7.20.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", - "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "license": "MIT", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.10" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -555,12 +544,13 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -569,248 +559,55 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", - "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz", - "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", - "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.19.4.tgz", - "integrity": "sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", - "dependencies": { - "@babel/compat-data": "^7.19.4", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.18.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", - "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "engines": { "node": ">=6.9.0" }, @@ -818,22 +615,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -893,11 +674,25 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -906,6 +701,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", @@ -1011,28 +817,27 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1041,12 +846,15 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { "node": ">=6.9.0" @@ -1055,12 +863,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.0.tgz", - "integrity": "sha512-sXOohbpHZSk7GjxK9b3dKB7CfqUD5DwOH+DggKzOQ7TXYP+RCSbRykfjQmn/zq+rBjycVRtLf9pYhAaEJA786w==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1069,20 +879,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz", - "integrity": "sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1091,12 +893,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", - "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1105,12 +907,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.0.tgz", - "integrity": "sha512-1dIhvZfkDVx/zn2S1aFwlruspTt4189j7fEkH0Y0VyuDM6bQt7bD6kLcz3l4IlLG+e5OReaBz9ROAbttRtUHqA==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1119,27 +922,35 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.12.0" } }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" @@ -1148,13 +959,21 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1163,12 +982,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1177,14 +996,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1193,12 +1011,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1207,12 +1025,132 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1222,12 +1160,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", - "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", "dependencies": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1237,13 +1175,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", - "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", "dependencies": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-simple-access": "^7.19.4" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1253,14 +1191,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", - "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.19.1" + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1270,12 +1208,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1285,12 +1223,12 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", - "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1300,11 +1238,58 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1314,12 +1299,43 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1329,11 +1345,43 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.1.tgz", - "integrity": "sha512-nDvKLrAvl+kf6BOy1UJ3MGwzzfTMgppxwiD2Jb4LO3xjYyZq30oQzDNJbCQpMdG9+j2IXHoiMrw5Cm/L6ZoxXQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -1343,11 +1391,11 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1357,12 +1405,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" }, "engines": { "node": ">=6.9.0" @@ -1372,11 +1420,11 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1386,16 +1434,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", - "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "^6.3.0" + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", + "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1405,11 +1453,11 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1419,12 +1467,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", - "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1434,11 +1482,11 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1448,11 +1496,11 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1462,11 +1510,11 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1476,11 +1524,26 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1490,12 +1553,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1504,38 +1567,43 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/preset-env": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.4.tgz", - "integrity": "sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg==", - "dependencies": { - "@babel/compat-data": "^7.19.4", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.19.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.19.4", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", + "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.18.6", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", @@ -1545,45 +1613,61 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.19.4", - "@babel/plugin-transform-classes": "^7.19.0", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.19.4", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.18.6", - "@babel/plugin-transform-modules-commonjs": "^7.18.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.0", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.18.8", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.19.4", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1593,3126 +1677,3093 @@ } }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", + "node_modules/@babel/register": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.24.6.tgz", + "integrity": "sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==", + "dev": true, "dependencies": { - "regenerator-runtime": "^0.13.10" + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "node_modules/@babel/register/node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=6" } }, - "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" + "node_modules/@babel/register/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=6" } }, - "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "node_modules/@babel/register/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=6" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "node_modules/@babel/register/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, "engines": { - "node": ">=0.1.90" + "node": ">=6" } }, - "node_modules/@es-joy/jsdoccomment": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.22.2.tgz", - "integrity": "sha512-pM6WQKcuAtdYoqCsXSvVSu3Ij8K0HY50L8tIheOKHDl0wH1uA4zbP88etY8SIeP16NVCMCTFU+Q2DahSKheGGQ==", + "node_modules/@babel/register/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "dependencies": { - "comment-parser": "1.3.1", - "esquery": "^1.4.0", - "jsdoc-type-pratt-parser": "~2.2.5" + "p-limit": "^2.0.0" }, "engines": { - "node": "^12 || ^14 || ^16 || ^17" + "node": ">=6" } }, - "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "node_modules/@babel/register/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=4" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@babel/register/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "find-up": "^3.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "node_modules/@babel/register/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "semver": "bin/semver" } }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/@babel/register/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/@babel/register/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "engines": { - "node": ">=10" + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@gulp-sourcemaps/identity-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", - "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", - "dev": true, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "license": "MIT", "dependencies": { - "acorn": "^6.4.1", - "normalize-path": "^3.0.0", - "postcss": "^7.0.16", - "source-map": "^0.6.0", - "through2": "^3.0.1" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { - "node": ">= 0.10" + "node": ">=6.9.0" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true, - "bin": { - "acorn": "bin/acorn" + "node_modules/@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" }, "engines": { - "node": ">=0.4.0" + "node": ">=6.9.0" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, + "node_modules/@babel/types": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "license": "MIT", "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">=6.9.0" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/@browserstack/ai-sdk-node": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@browserstack/ai-sdk-node/-/ai-sdk-node-1.5.8.tgz", + "integrity": "sha512-34snogSnvskHxUZYOX61ga1/oTlyXwneRtd7Epu2bEdSsRR1rMm8xXhO6DVrLsHPwPHz+ljAlwVwhcE2uKysxw==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "axios": "^1.6.2", + "uuid": "9.0.1" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" + "engines": { + "node": ">=0.1.90" } }, - "node_modules/@gulp-sourcemaps/map-sources": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", - "integrity": "sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, - "dependencies": { - "normalize-path": "^2.0.1", - "through2": "^2.0.3" - }, "engines": { - "node": ">= 0.10" + "node": ">=10.0.0" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "node_modules/@es-joy/jsdoccomment": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", + "integrity": "sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==", "dev": true, "dependencies": { - "remove-trailing-separator": "^1.0.1" + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=16" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@hapi/hoek": "9.x.x" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@hapi/cryptiles": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-5.1.0.tgz", - "integrity": "sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==", + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10.10.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=7.0.0" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.27.8" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@eslint/compat": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.7.tgz", + "integrity": "sha512-xvv7hJE32yhegJ8xNAnb62ggiAwTYHBpUCWhRxEj/ksvgDJuSXfoDkBcRYaYNFiJ+jH0IE3K16hd+xXzhBgNbg==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, "engines": { - "node": ">=7.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", "dev": true, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@jest/types/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==", + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "node_modules/@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=6.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "engines": { - "node": ">=6.0.0" + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, "engines": { - "node": ">=6.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "engines": { + "node": ">= 4" } }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "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==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=6.0.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ljharb/through": { - "version": "2.3.12", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", - "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", + "node_modules/@eslint/js": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", + "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", "dev": true, - "dependencies": { - "call-bind": "^1.0.5" - }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "dependencies": { - "eslint-scope": "5.1.1" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@open-draft/until": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz", - "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==", - "dev": true - }, - "node_modules/@percy/appium-app": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@percy/appium-app/-/appium-app-2.0.3.tgz", - "integrity": "sha512-6INeUJSyK2LzWV4Cc9bszNqKr3/NLcjFelUC2grjPnm6+jLA29inBF4ZE3PeTfLeCSw/0jyCGWV5fr9AyxtzCA==", + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, "dependencies": { - "@percy/sdk-utils": "^1.27.0-beta.0", - "tmp": "^0.2.1" + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" }, "engines": { - "node": ">=14" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@percy/appium-app/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "node_modules/@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", + "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", "dev": true, "dependencies": { - "rimraf": "^3.0.0" + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" }, "engines": { - "node": ">=8.17.0" + "node": ">= 0.10" } }, - "node_modules/@percy/sdk-utils": { - "version": "1.27.7", - "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.27.7.tgz", - "integrity": "sha512-E21dIEQ9wwGDno41FdMDYf6jJow5scbWGClqKE/ptB+950W4UF5C4hxhVVQoEJxDdLE/Gy/8ZJR7upvPHShWDg==", + "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true, + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=14" + "node": ">=0.4.0" } }, - "node_modules/@percy/selenium-webdriver": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@percy/selenium-webdriver/-/selenium-webdriver-2.0.3.tgz", - "integrity": "sha512-JfLJVRkwNfqVofe7iGKtoQbOcKSSj9t4pWFbSUk95JfwAA7b9/c+dlBsxgIRrdrMYzLRjnJkYAFSZkJ4F4A19A==", + "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", "dev": true, "dependencies": { - "@percy/sdk-utils": "^1.27.2", - "node-request-interceptor": "^0.6.3" + "picocolors": "^0.2.1", + "source-map": "^0.6.1" }, "engines": { - "node": ">=14" + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@gulp-sourcemaps/identity-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "optional": true, "engines": { - "node": ">=14" + "node": ">=0.10.0" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", - "dev": true + "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } }, - "node_modules/@puppeteer/browsers": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", - "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "node_modules/@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", "dev": true, "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.3.1", - "tar-fs": "3.0.4", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" + "normalize-path": "^2.0.1", + "through2": "^2.0.3" }, "engines": { - "node": ">=16.3.0" + "node": ">= 0.10" } }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "dev": true, "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/@puppeteer/browsers/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", "dev": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/cryptiles": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-5.1.0.tgz", + "integrity": "sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==", + "dev": true, + "dependencies": { + "@hapi/boom": "9.x.x" }, "engines": { - "node": ">=12" + "node": ">=12.0.0" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", "dev": true }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "node": ">=18.18.0" } }, - "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "dependencies": { - "type-detect": "4.0.8" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" } }, - "node_modules/@sinonjs/formatio": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", - "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, - "dependencies": { - "samsam": "1.3.0" + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@sinonjs/samsam": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", - "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.3.0", - "array-from": "^2.1.1", - "lodash": "^4.17.15" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "node_modules/@inquirer/checkbox": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.4.7.tgz", + "integrity": "sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==", "dev": true, "dependencies": { - "defer-to-connect": "^2.0.0" + "@inquirer/core": "^9.0.10", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/aria-query": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", - "dev": true - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", - "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "node_modules/@inquirer/confirm": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", + "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", "dev": true, "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "*", - "@types/node": "*", - "@types/responselike": "*" + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "dev": true - }, - "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "node_modules/@inquirer/core": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.10.tgz", + "integrity": "sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==", "dev": true, "dependencies": { - "@types/node": "*" + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.2", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.1.0", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "22.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", + "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==", "dev": true, "dependencies": { - "@types/ms": "*" + "undici-types": "~6.19.2" } }, - "node_modules/@types/eslint": { - "version": "8.4.9", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz", - "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==", + "node_modules/@inquirer/core/node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "node_modules/@types/expect": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", - "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", - "dev": true - }, - "node_modules/@types/extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.1.tgz", - "integrity": "sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw==", - "dev": true - }, - "node_modules/@types/gitconfiglocal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/gitconfiglocal/-/gitconfiglocal-2.0.3.tgz", - "integrity": "sha512-W6hyZux6TrtKfF2I9XNLVcsFr4xRr0T+S6hrJ9nDkhA2vzsFPIEAbnY4vgb6v2yKXQ9MJVcbLsARNlMfg4EVtQ==", - "dev": true - }, - "node_modules/@types/github-slugger": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz", - "integrity": "sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==", - "dev": true - }, - "node_modules/@types/hast": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", - "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "@types/unist": "*" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "node_modules/@inquirer/core/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "node_modules/@inquirer/editor": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.1.22.tgz", + "integrity": "sha512-K1QwTu7GCK+nKOVRBp5HY9jt3DXOfPGPr6WRDrPImkcJRelG9UTx2cAtK1liXmibRrzJlTWOwqgWT3k2XnS62w==", "dev": true, "dependencies": { - "@types/istanbul-lib-coverage": "*" + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "node_modules/@inquirer/expand": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.1.22.tgz", + "integrity": "sha512-wTZOBkzH+ItPuZ3ZPa9lynBsdMp6kQ9zbjVPYEtSBG7UulGjg2kQiAnUjgyG4SlntpTce5bOmXAPvE4sguXjpA==", "dev": true, "dependencies": { - "@types/istanbul-lib-report": "*" + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "node_modules/@inquirer/figures": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", + "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", + "dev": true, + "engines": { + "node": ">=18" + } }, - "node_modules/@types/keyv": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-4.2.0.tgz", - "integrity": "sha512-xoBtGl5R9jeKUhc8ZqeYaRDx04qqJ10yhhXYGmJ4Jr8qKpvMsDQQrNUvF/wUJ4klOtmJeJM+p2Xo3zp9uaC3tw==", - "deprecated": "This is a stub types definition. keyv provides its own type definitions, so you do not need this installed.", + "node_modules/@inquirer/input": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.2.9.tgz", + "integrity": "sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==", "dev": true, "dependencies": { - "keyv": "*" + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/mdast": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", - "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "node_modules/@inquirer/number": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.0.10.tgz", + "integrity": "sha512-kWTxRF8zHjQOn2TJs+XttLioBih6bdc5CcosXIzZsrTY383PXI35DuhIllZKu7CdXFi2rz2BWPN9l0dPsvrQOA==", "dev": true, "dependencies": { - "@types/unist": "*" + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/mocha": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", - "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", - "dev": true - }, - "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", - "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", + "node_modules/@inquirer/password": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.1.22.tgz", + "integrity": "sha512-5Fxt1L9vh3rAKqjYwqsjU4DZsEvY/2Gll+QkqR4yEpy6wvzLxdSgFhUcxfDAOtO4BEoTreWoznC0phagwLU5Kw==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true + "node_modules/@inquirer/prompts": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" + }, + "engines": { + "node": ">=18" + } }, - "node_modules/@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "node_modules/@inquirer/rawlist": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.2.4.tgz", + "integrity": "sha512-pb6w9pWrm7EfnYDgQObOurh2d2YH07+eDo3xQBsNAM2GRhliz6wFXGi1thKQ4bN6B0xDd6C3tBsjdr3obsCl3Q==", "dev": true, "dependencies": { - "@types/node": "*" + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "node_modules/@types/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw==", - "dev": true - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "dev": true - }, - "node_modules/@types/ua-parser-js": { - "version": "0.7.36", - "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz", - "integrity": "sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==", - "dev": true - }, - "node_modules/@types/unist": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", - "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", - "dev": true - }, - "node_modules/@types/vinyl": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz", - "integrity": "sha512-ayJ0iOCDNHnKpKTgBG6Q6JOnHTj9zFta+3j2b8Ejza0e4cvRyMn0ZoLEmbPrTHe5YYRlDYPvPWVdV4cTaRyH7g==", - "dev": true, - "dependencies": { - "@types/expect": "^1.20.4", - "@types/node": "*" - } - }, - "node_modules/@types/which": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-1.3.2.tgz", - "integrity": "sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA==", - "dev": true - }, - "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "node_modules/@inquirer/search": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.0.7.tgz", + "integrity": "sha512-p1wpV+3gd1eST/o5N3yQpYEdFNCzSP0Klrl+5bfD3cTTz8BGG6nf4Z07aBW0xjlKIj1Rp0y3x/X4cZYi6TfcLw==", "dev": true, - "optional": true, "dependencies": { - "@types/node": "*" + "@inquirer/core": "^9.0.10", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@videojs/http-streaming": { - "version": "2.14.3", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.14.3.tgz", - "integrity": "sha512-2tFwxCaNbcEZzQugWf8EERwNMyNtspfHnvxRGRABQs09W/5SqmkWFuGWfUAm4wQKlXGfdPyAJ1338ASl459xAA==", + "node_modules/@inquirer/select": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.4.7.tgz", + "integrity": "sha512-JH7XqPEkBpNWp3gPCqWqY8ECbyMoFcCZANlL6pV9hf59qK6dGmkOlx1ydyhY+KZ0c5X74+W6Mtp+nm2QX0/MAQ==", "dev": true, "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "3.0.5", - "aes-decrypter": "3.1.3", - "global": "^4.4.0", - "m3u8-parser": "4.7.1", - "mpd-parser": "0.21.1", - "mux.js": "6.0.1", - "video.js": "^6 || ^7" + "@inquirer/core": "^9.0.10", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=8", - "npm": ">=5" - }, - "peerDependencies": { - "video.js": "^6 || ^7" + "node": ">=18" } }, - "node_modules/@videojs/vhs-utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", - "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "node_modules/@inquirer/type": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.2.tgz", + "integrity": "sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==", "dev": true, "dependencies": { - "@babel/runtime": "^7.12.5", - "global": "^4.4.0", - "url-toolkit": "^2.2.1" + "mute-stream": "^1.0.0" }, "engines": { - "node": ">=8", - "npm": ">=5" + "node": ">=18" } }, - "node_modules/@videojs/xhr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz", - "integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "dependencies": { - "@babel/runtime": "^7.5.5", - "global": "~4.4.0", - "is-function": "^1.0.1" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@vitest/snapshot": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.1.tgz", - "integrity": "sha512-Tmp/IcYEemKaqAYCS08sh0vORLJkMr0NRV76Gl8sHGxXT5151cITJCET20063wk0Yr/1koQ6dnmP6eEqezmd/Q==", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "engines": { + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@vitest/snapshot/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, - "node_modules/@vitest/snapshot/node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vue/compiler-core": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.41.tgz", - "integrity": "sha512-oA4mH6SA78DT+96/nsi4p9DX97PHcNROxs51lYk7gb9Z4BPKQ3Mh+BLn6CQZBw857Iuhu28BfMSRHAlPvD4vlw==", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "optional": true, "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.41", - "estree-walker": "^2.0.2", - "source-map": "^0.6.1" - } - }, - "node_modules/@vue/compiler-core/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@vue/compiler-dom": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz", - "integrity": "sha512-xe5TbbIsonjENxJsYRbDJvthzqxLNk+tb3d/c47zgREDa/PCp6/Y4gC/skM4H6PIuX5DAxm7fFJdbjjUH2QTMw==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "optional": true, "dependencies": { - "@vue/compiler-core": "3.2.41", - "@vue/shared": "3.2.41" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@vue/compiler-sfc": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz", - "integrity": "sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "optional": true, - "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.41", - "@vue/compiler-dom": "3.2.41", - "@vue/compiler-ssr": "3.2.41", - "@vue/reactivity-transform": "3.2.41", - "@vue/shared": "3.2.41", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", - "postcss": "^8.1.10", - "source-map": "^0.6.1" + "engines": { + "node": ">=8" } }, - "node_modules/@vue/compiler-sfc/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, - "optional": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@vue/compiler-ssr": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.41.tgz", - "integrity": "sha512-Y5wPiNIiaMz/sps8+DmhaKfDm1xgj6GrH99z4gq2LQenfVQcYXmHIOBcs5qPwl7jaW3SUQWjkAPKMfQemEQZwQ==", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, - "optional": true, "dependencies": { - "@vue/compiler-dom": "3.2.41", - "@vue/shared": "3.2.41" + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@vue/reactivity-transform": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.41.tgz", - "integrity": "sha512-mK5+BNMsL4hHi+IR3Ft/ho6Za+L3FA5j8WvreJ7XzHrqkPq8jtF/SMo7tuc9gHjLDwKZX1nP1JQOKo9IEAn54A==", + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "optional": true, "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.41", - "@vue/shared": "3.2.41", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@vue/shared": { - "version": "3.2.41", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.41.tgz", - "integrity": "sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw==", - "dev": true, - "optional": true - }, - "node_modules/@wdio/browserstack-service": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/browserstack-service/-/browserstack-service-8.29.1.tgz", - "integrity": "sha512-dLEJcdVF0Cu+2REByVOfLUzx9FvMias1VsxSCZpKXeIAGAIWBBdNdooK6Vdc9QdS36S5v/mk0/rTTQhYn4nWjQ==", + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "@percy/appium-app": "^2.0.1", - "@percy/selenium-webdriver": "^2.0.3", - "@types/gitconfiglocal": "^2.0.1", - "@wdio/logger": "8.28.0", - "@wdio/reporter": "8.29.1", - "@wdio/types": "8.29.1", - "browserstack-local": "^1.5.1", - "chalk": "^5.3.0", - "csv-writer": "^1.6.0", - "formdata-node": "5.0.1", - "git-repo-info": "^2.1.1", - "gitconfiglocal": "^2.1.0", - "got": "^12.6.1", - "uuid": "^9.0.0", - "webdriverio": "8.29.1", - "winston-transport": "^4.5.0", - "yauzl": "^2.10.0" + "color-convert": "^2.0.1" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=8" }, - "peerDependencies": { - "@wdio/cli": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@wdio/browserstack-service/node_modules/@puppeteer/browsers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", - "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "http-proxy-agent": "5.0.0", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wdio/browserstack-service/node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "dev": true, "engines": { - "node": ">=14.16" + "node": ">=10" }, "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@wdio/browserstack-service/node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "defer-to-connect": "^2.0.1" + "color-name": "~1.1.4" }, "engines": { - "node": ">=14.16" + "node": ">=7.0.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/@types/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "optional": true, - "peer": true + "engines": { + "node": ">=8" + } }, - "node_modules/@wdio/browserstack-service/node_modules/archiver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.1.tgz", - "integrity": "sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==", + "node_modules/@jest/types/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, "dependencies": { - "archiver-utils": "^4.0.1", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^5.0.1" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">=8" } }, - "node_modules/@wdio/browserstack-service/node_modules/archiver-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz", - "integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==", - "dev": true, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "glob": "^8.0.0", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": ">= 12.0.0" + "node": ">=6.0.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/@wdio/browserstack-service/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { - "node": ">=14.16" + "node": ">=6.0.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/@wdio/browserstack-service/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "dependencies": { + "eslint-scope": "5.1.1" } }, - "node_modules/@wdio/browserstack-service/node_modules/chrome-launcher": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.0.tgz", - "integrity": "sha512-rJYWeEAERwWIr3c3mEVXwNiODPEdMRlRxHc47B1qHPOolHZnkj7rMv1QSUfPoG6MgatWj5AxSpnKKR4QEwEQIQ==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^2.0.1" - }, - "bin": { - "print-chrome-path": "bin/print-chrome-path.js" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=12.13.0" + "node": ">= 8" } }, - "node_modules/@wdio/browserstack-service/node_modules/compress-commons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", - "integrity": "sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^5.0.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, "engines": { - "node": ">= 12.0.0" + "node": ">= 8" } }, - "node_modules/@wdio/browserstack-service/node_modules/crc32-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.0.tgz", - "integrity": "sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 8" } }, - "node_modules/@wdio/browserstack-service/node_modules/cross-fetch": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", - "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "node-fetch": "^2.6.11" + "engines": { + "node": ">=12.4.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/devtools": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/devtools/-/devtools-8.29.1.tgz", - "integrity": "sha512-fbH0Z7CPK4OZSgUw2QcAppczowxtSyvFztPUmiFyi99cUadjEOwlg0aL3pBVlIDo67olYjGb8GD1M5Z4yI/P6w==", + "node_modules/@open-draft/until": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz", + "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==", + "dev": true + }, + "node_modules/@percy/appium-app": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@percy/appium-app/-/appium-app-2.0.6.tgz", + "integrity": "sha512-0NT8xgaq4UOhcqVc4H3D440M7H5Zko8mDpY5j30TRpjOQ3ctLPJalmUVKOCFv4rSzjd2LmyE2F9KXTPA3zqQsw==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "chrome-launcher": "^1.0.0", - "edge-paths": "^3.0.5", - "import-meta-resolve": "^4.0.0", - "puppeteer-core": "20.3.0", - "query-selector-shadow-dom": "^1.0.0", - "ua-parser-js": "^1.0.1", - "uuid": "^9.0.0", - "which": "^4.0.0" + "@percy/sdk-utils": "^1.28.2", + "tmp": "^0.2.1" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=14" } }, - "node_modules/@wdio/browserstack-service/node_modules/devtools-protocol": { - "version": "0.0.1120988", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", - "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@wdio/browserstack-service/node_modules/devtools/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "node_modules/@percy/sdk-utils": { + "version": "1.28.7", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.28.7.tgz", + "integrity": "sha512-LIhfHnkcS0fyIdo3gvKn7rwodZjbEtyLkgiDRSRulcBOatI2mhn2Bh269sXXiiFTyAW2BDQjyE3DWc4hkGbsbQ==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">=14" } }, - "node_modules/@wdio/browserstack-service/node_modules/edge-paths": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", - "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "node_modules/@percy/selenium-webdriver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@percy/selenium-webdriver/-/selenium-webdriver-2.0.5.tgz", + "integrity": "sha512-bNj52xQm02dY872loFa+8OwyuGcdYHYvCKflmSEsF9EDRiSDj0Wr+XP+DDIgDAl9xXschA7OOdXCLTWV4zEQWA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/which": "^2.0.1", - "which": "^2.0.2" + "@percy/sdk-utils": "^1.28.0", + "node-request-interceptor": "^0.6.3" }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/shirshak55" + "node": ">=14" } }, - "node_modules/@wdio/browserstack-service/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "optional": true, - "peer": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=14" } }, - "node_modules/@wdio/browserstack-service/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/unts" } }, - "node_modules/@wdio/browserstack-service/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "node_modules/@polka/url": { + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "dev": true }, - "node_modules/@wdio/browserstack-service/node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/@promptbook/utils": { + "version": "0.50.0-10", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.50.0-10.tgz", + "integrity": "sha512-Z94YoY/wcZb5m1QoXgmIC1rVeDguGK5bWmUTYdWCqh/LHVifRdJ1C+tBzS0h+HMOD0XzMjZhBQ/mBgTZ/QNW/g==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" + } + ], "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "moment": "2.30.1", + "prettier": "2.8.1", + "spacetrim": "0.11.25" } }, - "node_modules/@wdio/browserstack-service/node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", "dev": true, "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.1", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.3.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "ms": "2.1.2" }, "engines": { - "node": ">= 6" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@wdio/browserstack-service/node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10.19.0" + "node": ">=12" } }, - "node_modules/@wdio/browserstack-service/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=16" - } + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true }, - "node_modules/@wdio/browserstack-service/node_modules/lighthouse-logger": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", - "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" - } + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true }, - "node_modules/@wdio/browserstack-service/node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true }, - "node_modules/@wdio/browserstack-service/node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@wdio/browserstack-service/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "type-detect": "4.0.8" } }, - "node_modules/@wdio/browserstack-service/node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "node_modules/@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "samsam": "1.3.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" } }, - "node_modules/@wdio/browserstack-service/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "optional": true, - "peer": true + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true }, - "node_modules/@wdio/browserstack-service/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/@stylistic/eslint-plugin": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.11.0.tgz", + "integrity": "sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==", "dev": true, "dependencies": { - "whatwg-url": "^5.0.0" + "@typescript-eslint/utils": "^8.13.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "eslint": ">=8.40.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "engines": { - "node": ">=14.16" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@wdio/browserstack-service/node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "node_modules/@stylistic/eslint-plugin/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { - "node": ">=12.20" + "node": ">=4.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" - }, "engines": { - "node": ">= 14" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@wdio/browserstack-service/node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "@types/node": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dev": true, "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "@types/ms": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", + "dev": true + }, + "node_modules/@types/extend": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.4.tgz", + "integrity": "sha512-ArMouDUTJEz1SQRpFsT2rIw7DeqICFv5aaVzLSIYMYQSLcwcGOfT3VyglQs/p7K3F7fT4zxr0NWxYZIdifD6dA==", + "dev": true + }, + "node_modules/@types/gitconfiglocal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/gitconfiglocal/-/gitconfiglocal-2.0.3.tgz", + "integrity": "sha512-W6hyZux6TrtKfF2I9XNLVcsFr4xRr0T+S6hrJ9nDkhA2vzsFPIEAbnY4vgb6v2yKXQ9MJVcbLsARNlMfg4EVtQ==", + "dev": true + }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", "dev": true, "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "@types/unist": "^2" } }, - "node_modules/@wdio/browserstack-service/node_modules/puppeteer-core": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.3.0.tgz", - "integrity": "sha512-264pBrIui5bO6NJeOcbJrLa0OCwmA4+WK00JMrLIKTfRiqe2gx8KWTzLsjyw/bizErp3TKS7vt/I0i5fTC+mAw==", + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@puppeteer/browsers": "1.3.0", - "chromium-bidi": "0.4.9", - "cross-fetch": "3.1.6", - "debug": "4.3.4", - "devtools-protocol": "0.0.1120988", - "ws": "8.13.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@types/istanbul-lib-coverage": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "@types/istanbul-lib-report": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dev": true, - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, - "node_modules/@wdio/browserstack-service/node_modules/serialize-error": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", "dev": true, "dependencies": { - "type-fest": "^2.12.2" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@types/unist": "^2" } }, - "node_modules/@wdio/browserstack-service/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "node_modules/@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "@types/node": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "undici-types": "~5.26.4" } }, - "node_modules/@wdio/browserstack-service/node_modules/ua-parser-js": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", - "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "dev": true + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/supports-color": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", + "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "dev": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "node_modules/@types/vinyl": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", + "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "optional": true, - "peer": true, - "engines": { - "node": "*" + "dependencies": { + "@types/expect": "^1.20.4", + "@types/node": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" + "dependencies": { + "@types/node": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.29.1.tgz", - "integrity": "sha512-NZK95ivXCqdPraB3FHMw6ByxnCvtgFXkjzG2l3Oq5z0IuJS2aMow3AKFIyiuG6is/deGCe+Tb8eFTCqak7UV+w==", + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/repl": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "archiver": "^6.0.0", - "aria-query": "^5.0.0", - "css-shorthand-properties": "^1.1.1", - "css-value": "^0.0.1", - "devtools-protocol": "^0.0.1249869", - "grapheme-splitter": "^1.0.2", - "import-meta-resolve": "^4.0.0", - "is-plain-obj": "^4.1.0", - "lodash.clonedeep": "^4.5.0", - "lodash.zip": "^4.2.0", - "minimatch": "^9.0.0", - "puppeteer-core": "^20.9.0", - "query-selector-shadow-dom": "^1.0.0", - "resq": "^1.9.1", - "rgb2hex": "0.2.5", - "serialize-error": "^11.0.1", - "webdriver": "8.29.1" - }, - "engines": { - "node": "^16.13 || >=18" - }, - "peerDependencies": { - "devtools": "^8.14.0" - }, - "peerDependenciesMeta": { - "devtools": { - "optional": true - } + "@types/yargs-parser": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio/node_modules/@puppeteer/browsers": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", - "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, + "optional": true, "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.3.0", - "tar-fs": "3.0.4", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@types/node": "*" } }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio/node_modules/chromium-bidi": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", - "integrity": "sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", + "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", "dev": true, "dependencies": { - "mitt": "3.0.0" + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/type-utils": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "devtools-protocol": "*" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "dependencies": { - "node-fetch": "^2.6.12" + "engines": { + "node": ">= 4" } }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio/node_modules/devtools-protocol": { - "version": "0.0.1249869", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1249869.tgz", - "integrity": "sha512-Ctp4hInA0BEavlUoRy9mhGq0i+JSo/AwVyX2EFgZmV1kYB+Zq+EMBAn52QWu6FbRr10hRb6pBl420upbp4++vg==", - "dev": true - }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio/node_modules/puppeteer-core": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.9.0.tgz", - "integrity": "sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==", + "node_modules/@typescript-eslint/parser": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", "dev": true, "dependencies": { - "@puppeteer/browsers": "1.4.6", - "chromium-bidi": "0.4.16", - "cross-fetch": "4.0.0", - "debug": "4.3.4", - "devtools-protocol": "0.0.1147663", - "ws": "8.13.0" + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4" }, "engines": { - "node": ">=16.3.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "typescript": ">= 4.7.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio/node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1147663", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", - "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", - "dev": true - }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", + "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", "dev": true, "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@wdio/browserstack-service/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", + "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, "engines": { - "node": ">=10.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", + "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@wdio/browserstack-service/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", + "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", "dev": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/zip-stream": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.1.tgz", - "integrity": "sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "archiver-utils": "^4.0.1", - "compress-commons": "^5.0.1", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 12.0.0" + "balanced-match": "^1.0.0" } }, - "node_modules/@wdio/cli": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.29.1.tgz", - "integrity": "sha512-WWRTf0g0O+ovTTvS1kEhZ/svX32M7jERuuMF1MaldKCi7rZwHsQqOyJD+fO1UDjuxqS96LHSGsZn0auwUfCTXA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { - "@types/node": "^20.1.1", - "@vitest/snapshot": "^1.2.1", - "@wdio/config": "8.29.1", - "@wdio/globals": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "async-exit-hook": "^2.0.1", - "chalk": "^5.2.0", - "chokidar": "^3.5.3", - "cli-spinners": "^2.9.0", - "dotenv": "^16.3.1", - "ejs": "^3.1.9", - "execa": "^8.0.1", - "import-meta-resolve": "^4.0.0", - "inquirer": "9.2.12", - "lodash.flattendeep": "^4.4.0", - "lodash.pickby": "^4.6.0", - "lodash.union": "^4.6.0", - "read-pkg-up": "^10.0.0", - "recursive-readdir": "^2.2.3", - "webdriverio": "8.29.1", - "yargs": "^17.7.2" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, "bin": { - "wdio": "bin/wdio.js" + "semver": "bin/semver.js" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=10" } }, - "node_modules/@wdio/cli/node_modules/@puppeteer/browsers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", - "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", + "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "http-proxy-agent": "5.0.0", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1" }, "engines": { - "node": ">=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "typescript": ">= 4.7.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@wdio/cli/node_modules/@puppeteer/browsers/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", + "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "@typescript-eslint/types": "8.26.1", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@wdio/cli/node_modules/@types/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "optional": true, - "peer": true + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/@wdio/cli/node_modules/archiver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.1.tgz", - "integrity": "sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==", + "node_modules/@videojs/http-streaming": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.16.3.tgz", + "integrity": "sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==", "dev": true, "dependencies": { - "archiver-utils": "^4.0.1", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^5.0.1" + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "3.0.5", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "^0.22.1", + "mux.js": "6.0.1", + "video.js": "^6 || ^7" }, "engines": { - "node": ">= 12.0.0" + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^6 || ^7" } }, - "node_modules/@wdio/cli/node_modules/archiver-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz", - "integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==", + "node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", "dev": true, "dependencies": { - "glob": "^8.0.0", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" }, "engines": { - "node": ">= 12.0.0" + "node": ">=8", + "npm": ">=5" } }, - "node_modules/@wdio/cli/node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/@wdio/cli/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@videojs/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" } }, - "node_modules/@wdio/cli/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@wdio/cli/node_modules/chrome-launcher": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.0.tgz", - "integrity": "sha512-rJYWeEAERwWIr3c3mEVXwNiODPEdMRlRxHc47B1qHPOolHZnkj7rMv1QSUfPoG6MgatWj5AxSpnKKR4QEwEQIQ==", + "node_modules/@vitest/snapshot": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^2.0.1" - }, - "bin": { - "print-chrome-path": "bin/print-chrome-path.js" + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" }, - "engines": { - "node": ">=12.13.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@wdio/cli/node_modules/compress-commons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", - "integrity": "sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==", + "node_modules/@vue/compiler-core": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", + "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", "dev": true, + "optional": true, "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^5.0.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 12.0.0" + "@babel/parser": "^7.24.4", + "@vue/shared": "3.4.27", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" } }, - "node_modules/@wdio/cli/node_modules/crc32-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.0.tgz", - "integrity": "sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==", + "node_modules/@vue/compiler-dom": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", + "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", "dev": true, + "optional": true, "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 12.0.0" + "@vue/compiler-core": "3.4.27", + "@vue/shared": "3.4.27" } }, - "node_modules/@wdio/cli/node_modules/cross-fetch": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", - "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "node_modules/@vue/compiler-sfc": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", + "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", "dev": true, "optional": true, - "peer": true, "dependencies": { - "node-fetch": "^2.6.11" + "@babel/parser": "^7.24.4", + "@vue/compiler-core": "3.4.27", + "@vue/compiler-dom": "3.4.27", + "@vue/compiler-ssr": "3.4.27", + "@vue/shared": "3.4.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" } }, - "node_modules/@wdio/cli/node_modules/devtools": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/devtools/-/devtools-8.29.1.tgz", - "integrity": "sha512-fbH0Z7CPK4OZSgUw2QcAppczowxtSyvFztPUmiFyi99cUadjEOwlg0aL3pBVlIDo67olYjGb8GD1M5Z4yI/P6w==", + "node_modules/@vue/compiler-ssr": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", + "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", "dev": true, "optional": true, - "peer": true, "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "chrome-launcher": "^1.0.0", - "edge-paths": "^3.0.5", - "import-meta-resolve": "^4.0.0", - "puppeteer-core": "20.3.0", - "query-selector-shadow-dom": "^1.0.0", - "ua-parser-js": "^1.0.1", - "uuid": "^9.0.0", - "which": "^4.0.0" - }, - "engines": { - "node": "^16.13 || >=18" + "@vue/compiler-dom": "3.4.27", + "@vue/shared": "3.4.27" } }, - "node_modules/@wdio/cli/node_modules/devtools-protocol": { - "version": "0.0.1120988", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", - "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==", + "node_modules/@vue/shared": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", + "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", "dev": true, - "optional": true, - "peer": true + "optional": true }, - "node_modules/@wdio/cli/node_modules/devtools/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "node_modules/@wdio/browserstack-service": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/browserstack-service/-/browserstack-service-9.0.5.tgz", + "integrity": "sha512-pJNb9jJwPf+FEwAEnnUc6d9s6/QlvcZbh9NtjO23a/wr3HvXdzhlRHwzUV1RWboDpGsww5PFmtGcIo7GdDQL+g==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" + "@browserstack/ai-sdk-node": "1.5.8", + "@percy/appium-app": "^2.0.1", + "@percy/selenium-webdriver": "^2.0.3", + "@types/gitconfiglocal": "^2.0.1", + "@wdio/logger": "9.0.4", + "@wdio/reporter": "9.0.4", + "@wdio/types": "9.0.4", + "browserstack-local": "^1.5.1", + "chalk": "^5.3.0", + "csv-writer": "^1.6.0", + "formdata-node": "5.0.1", + "git-repo-info": "^2.1.1", + "gitconfiglocal": "^2.1.0", + "uuid": "^10.0.0", + "webdriverio": "9.0.5", + "winston-transport": "^4.5.0", + "yauzl": "^3.0.0" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@wdio/cli": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/@wdio/cli/node_modules/edge-paths": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", - "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "node_modules/@wdio/browserstack-service/node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/which": "^2.0.1", - "which": "^2.0.2" + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" }, - "engines": { - "node": ">=14.0.0" + "bin": { + "browsers": "lib/cjs/main-cli.js" }, - "funding": { - "url": "https://github.com/sponsors/shirshak55" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/@wdio/browserstack-service/node_modules/@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=10" + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "node_modules/@wdio/browserstack-service/node_modules/@wdio/reporter": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-9.0.4.tgz", + "integrity": "sha512-g55MiqToKEZ+L5quk7Ddk3m1fKgh2U2rq3zLG8bZUYU+3KFglfRC/Ld5yTso8S1tG4EDgl5HKSN5Bl8I5gncbg==", "dev": true, "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" + "@types/node": "^20.1.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "diff": "^5.0.0", + "object-inspect": "^1.12.0" }, "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "node_modules/@wdio/browserstack-service/node_modules/@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", "dev": true, "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" + "@types/node": "^20.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "node_modules/@wdio/browserstack-service/node_modules/@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", "dev": true, - "engines": { - "node": ">=16" + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/@wdio/browserstack-service/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "debug": "^4.3.4" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 14" } }, - "node_modules/@wdio/cli/node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/@wdio/browserstack-service/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "balanced-match": "^1.0.0" } }, - "node_modules/@wdio/cli/node_modules/hosted-git-info": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", - "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "node_modules/@wdio/browserstack-service/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, - "dependencies": { - "lru-cache": "^10.0.1" - }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@wdio/cli/node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "node_modules/@wdio/browserstack-service/node_modules/deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", "dev": true, "engines": { - "node": "14 || >=16.14" + "node": ">=16.0.0" } }, - "node_modules/@wdio/cli/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/@wdio/browserstack-service/node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", "dev": true, "optional": true, - "peer": true, + "peer": true + }, + "node_modules/@wdio/browserstack-service/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/@wdio/cli/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 14" } }, - "node_modules/@wdio/cli/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "node_modules/@wdio/browserstack-service/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "optional": true, - "peer": true, "engines": { - "node": ">=16" + "node": ">=12" } }, - "node_modules/@wdio/cli/node_modules/json-parse-even-better-errors": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", - "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "node_modules/@wdio/browserstack-service/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/cli/node_modules/lighthouse-logger": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", - "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "node_modules/@wdio/browserstack-service/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/@wdio/cli/node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/@wdio/browserstack-service/node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", "dev": true, "optional": true, "peer": true, "dependencies": { - "ms": "2.0.0" + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/lines-and-columns": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", - "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "node_modules/@wdio/browserstack-service/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=10" } }, - "node_modules/@wdio/cli/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "node_modules/@wdio/browserstack-service/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dev": true, "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, - "node_modules/@wdio/cli/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "node_modules/@wdio/browserstack-service/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/@wdio/cli/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" + "node_modules/@wdio/browserstack-service/node_modules/webdriverio": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.5.tgz", + "integrity": "sha512-80zhuLBT5W5wiLNZ0maT1cVUrxmwpMuBgCprwZjI8lFe+KUhGLClrJXud/RrVT9x9rDCYa6pGCtQ4UqA+2U+sw==", + "dev": true, + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "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.2.1", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "puppeteer-core": "^22.3.0" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } } }, - "node_modules/@wdio/cli/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/@wdio/browserstack-service/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "optional": true, - "peer": true - }, - "node_modules/@wdio/cli/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "peer": true, "engines": { - "node": "4.x || >=6.0.0" + "node": ">=10.0.0" }, "peerDependencies": { - "encoding": "^0.1.0" + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { - "encoding": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { "optional": true } } }, - "node_modules/@wdio/cli/node_modules/normalize-package-data": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", - "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "node_modules/@wdio/browserstack-service/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/@wdio/cli/node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "node_modules/@wdio/cli": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.0.5.tgz", + "integrity": "sha512-D/QBlodNIdxuNpUPbuhk+mLidVLT+Vsb0Q0Fd4lh57Jy8kw5nJ56ykqiI0WE1oI0i+XtyJ7iFOPUztuCjjhX3A==", "dev": true, "dependencies": { - "path-key": "^4.0.0" + "@types/node": "^20.1.1", + "@vitest/snapshot": "^1.2.1", + "@wdio/config": "9.0.5", + "@wdio/globals": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "async-exit-hook": "^2.0.1", + "chalk": "^5.2.0", + "chokidar": "^3.5.3", + "cli-spinners": "^3.0.0", + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "execa": "^9.2.0", + "import-meta-resolve": "^4.0.0", + "inquirer": "^10.1.8", + "lodash.flattendeep": "^4.4.0", + "lodash.pickby": "^4.6.0", + "lodash.union": "^4.6.0", + "read-pkg-up": "^10.0.0", + "recursive-readdir": "^2.2.3", + "tsx": "^4.7.2", + "webdriverio": "9.0.5", + "yargs": "^17.7.2" }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "bin": { + "wdio": "bin/wdio.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "node_modules/@wdio/cli/node_modules/@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", "dev": true, "dependencies": { - "mimic-fn": "^4.0.0" + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" }, - "engines": { - "node": ">=12" + "bin": { + "browsers": "lib/cjs/main-cli.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "node_modules/@wdio/cli/node_modules/@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", "dev": true, "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "node_modules/@wdio/cli/node_modules/@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", "dev": true, "dependencies": { - "p-limit": "^4.0.0" + "@types/node": "^20.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/parse-json": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", - "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "node_modules/@wdio/cli/node_modules/@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.21.4", - "error-ex": "^1.3.2", - "json-parse-even-better-errors": "^3.0.0", - "lines-and-columns": "^2.0.3", - "type-fest": "^3.8.0" + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" }, "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/parse-json/node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "node_modules/@wdio/cli/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, - "engines": { - "node": ">=14.16" + "dependencies": { + "debug": "^4.3.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 14" } }, - "node_modules/@wdio/cli/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "node_modules/@wdio/cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/@wdio/cli/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "node_modules/@wdio/cli/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "engines": { - "node": ">=12" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@wdio/cli/node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "node_modules/@wdio/cli/node_modules/deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" - }, "engines": { - "node": ">= 14" + "node": ">=16.0.0" } }, - "node_modules/@wdio/cli/node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "node_modules/@wdio/cli/node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@wdio/cli/node_modules/execa": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.4.0.tgz", + "integrity": "sha512-yKHlle2YGxZE842MERVIplWwNH5VYmqqcPFgtnlU//K8gxuFFXu0pwd/CrfXTumFpeEiufsP7+opT/bPJa1yVw==", "dev": true, "dependencies": { - "debug": "^4.3.4" + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" }, "engines": { - "node": ">= 14" + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/@wdio/cli/node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "node_modules/@wdio/cli/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", "dev": true, "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" }, "engines": { - "node": ">= 14" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@wdio/cli/node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "node_modules/@wdio/cli/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -4722,7 +4773,19 @@ "node": ">= 14" } }, - "node_modules/@wdio/cli/node_modules/proxy-agent/node_modules/lru-cache": { + "node_modules/@wdio/cli/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@wdio/cli/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", @@ -4731,122 +4794,147 @@ "node": ">=12" } }, - "node_modules/@wdio/cli/node_modules/puppeteer-core": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.3.0.tgz", - "integrity": "sha512-264pBrIui5bO6NJeOcbJrLa0OCwmA4+WK00JMrLIKTfRiqe2gx8KWTzLsjyw/bizErp3TKS7vt/I0i5fTC+mAw==", + "node_modules/@wdio/cli/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@puppeteer/browsers": "1.3.0", - "chromium-bidi": "0.4.9", - "cross-fetch": "3.1.6", - "debug": "4.3.4", - "devtools-protocol": "0.0.1120988", - "ws": "8.13.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" + "node": ">=16 || 14 >=14.17" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/cli/node_modules/read-pkg": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", - "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", + "node_modules/@wdio/cli/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, "dependencies": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^6.0.0", - "parse-json": "^7.0.0", - "type-fest": "^4.2.0" + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@wdio/cli/node_modules/read-pkg-up": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", - "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", + "node_modules/@wdio/cli/node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", "dev": true, - "dependencies": { - "find-up": "^6.3.0", - "read-pkg": "^8.1.0", - "type-fest": "^4.2.0" + "engines": { + "node": ">=18" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@wdio/cli/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, "engines": { - "node": ">=16" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@wdio/cli/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/@wdio/cli/node_modules/pretty-ms": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", "dev": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "parse-ms": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@wdio/cli/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@wdio/cli/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" }, "engines": { - "node": ">=10" + "node": ">= 14" } }, - "node_modules/@wdio/cli/node_modules/serialize-error": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "node_modules/@wdio/cli/node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { - "type-fest": "^2.12.2" + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@wdio/cli/node_modules/serialize-error/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "node_modules/@wdio/cli/node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, "engines": { - "node": ">=12.20" + "node": ">=18" + } + }, + "node_modules/@wdio/cli/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=10" } }, "node_modules/@wdio/cli/node_modules/signal-exit": { @@ -4861,233 +4949,73 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/cli/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "node_modules/@wdio/cli/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dev": true, "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, - "node_modules/@wdio/cli/node_modules/type-fest": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.1.tgz", - "integrity": "sha512-7ZnJYTp6uc04uYRISWtiX3DSKB/fxNQT0B5o1OUeCqiQiwF+JC9+rJiZIDrPrNCLLuTqyQmh4VdQqh/ZOkv9MQ==", - "dev": true, + "node_modules/@wdio/cli/node_modules/webdriverio": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.5.tgz", + "integrity": "sha512-80zhuLBT5W5wiLNZ0maT1cVUrxmwpMuBgCprwZjI8lFe+KUhGLClrJXud/RrVT9x9rDCYa6pGCtQ4UqA+2U+sw==", + "dev": true, + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "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.2.1", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.5" + }, "engines": { - "node": ">=16" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "puppeteer-core": "^22.3.0" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } } }, - "node_modules/@wdio/cli/node_modules/ua-parser-js": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", - "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "node_modules/@wdio/cli/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "optional": true, - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/@wdio/cli/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], "optional": true, "peer": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@wdio/cli/node_modules/webdriverio": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.29.1.tgz", - "integrity": "sha512-NZK95ivXCqdPraB3FHMw6ByxnCvtgFXkjzG2l3Oq5z0IuJS2aMow3AKFIyiuG6is/deGCe+Tb8eFTCqak7UV+w==", - "dev": true, - "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/repl": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "archiver": "^6.0.0", - "aria-query": "^5.0.0", - "css-shorthand-properties": "^1.1.1", - "css-value": "^0.0.1", - "devtools-protocol": "^0.0.1249869", - "grapheme-splitter": "^1.0.2", - "import-meta-resolve": "^4.0.0", - "is-plain-obj": "^4.1.0", - "lodash.clonedeep": "^4.5.0", - "lodash.zip": "^4.2.0", - "minimatch": "^9.0.0", - "puppeteer-core": "^20.9.0", - "query-selector-shadow-dom": "^1.0.0", - "resq": "^1.9.1", - "rgb2hex": "0.2.5", - "serialize-error": "^11.0.1", - "webdriver": "8.29.1" - }, - "engines": { - "node": "^16.13 || >=18" - }, - "peerDependencies": { - "devtools": "^8.14.0" - }, - "peerDependenciesMeta": { - "devtools": { - "optional": true - } - } - }, - "node_modules/@wdio/cli/node_modules/webdriverio/node_modules/@puppeteer/browsers": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", - "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", - "dev": true, - "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.3.0", - "tar-fs": "3.0.4", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wdio/cli/node_modules/webdriverio/node_modules/chromium-bidi": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", - "integrity": "sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==", - "dev": true, - "dependencies": { - "mitt": "3.0.0" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/@wdio/cli/node_modules/webdriverio/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dev": true, - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/@wdio/cli/node_modules/webdriverio/node_modules/devtools-protocol": { - "version": "0.0.1249869", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1249869.tgz", - "integrity": "sha512-Ctp4hInA0BEavlUoRy9mhGq0i+JSo/AwVyX2EFgZmV1kYB+Zq+EMBAn52QWu6FbRr10hRb6pBl420upbp4++vg==", - "dev": true - }, - "node_modules/@wdio/cli/node_modules/webdriverio/node_modules/puppeteer-core": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.9.0.tgz", - "integrity": "sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==", - "dev": true, - "dependencies": { - "@puppeteer/browsers": "1.4.6", - "chromium-bidi": "0.4.16", - "cross-fetch": "4.0.0", - "debug": "4.3.4", - "devtools-protocol": "0.0.1147663", - "ws": "8.13.0" - }, - "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wdio/cli/node_modules/webdriverio/node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1147663", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", - "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", - "dev": true - }, - "node_modules/@wdio/cli/node_modules/webdriverio/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dev": true, - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "node_modules/@wdio/cli/node_modules/webdriverio/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@wdio/cli/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true, "engines": { "node": ">=10.0.0" }, @@ -5122,40 +5050,14 @@ "node": ">=12" } }, - "node_modules/@wdio/cli/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@wdio/cli/node_modules/zip-stream": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.1.tgz", - "integrity": "sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==", - "dev": true, - "dependencies": { - "archiver-utils": "^4.0.1", - "compress-commons": "^5.0.1", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/@wdio/concise-reporter": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/concise-reporter/-/concise-reporter-8.29.1.tgz", - "integrity": "sha512-dUhClWeq1naL1Qa1nSMDeH8aCVViOKiEzhBhQjgrMOz1Mh3l6O/woqbK2iKDVZDRhfGghtGcV0vpoEUvt8ZKOA==", + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/concise-reporter/-/concise-reporter-8.38.2.tgz", + "integrity": "sha512-wE36By4Z9iCtRzihpYrmCehsmNc8t3gHviBsUxV4tmYh/SQr+WX/dysWnojer6KWIJ2rT0rOzyQPmrwhdFKAFg==", "dev": true, "dependencies": { - "@wdio/reporter": "8.29.1", - "@wdio/types": "8.29.1", + "@wdio/reporter": "8.38.2", + "@wdio/types": "8.38.2", "chalk": "^5.0.1", "pretty-ms": "^7.0.1" }, @@ -5176,416 +5078,417 @@ } }, "node_modules/@wdio/config": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.29.1.tgz", - "integrity": "sha512-zNUac4lM429HDKAitO+fdlwUH1ACQU8lww+DNVgUyuEb86xgVdTqHeiJr/3kOMJAq9IATeE7mDtYyyn6HPm1JA==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.0.5.tgz", + "integrity": "sha512-+dxUU2SLXLkqQhVU/wauU1VgqEKIFubOyUb6B0ueAMpM1aolc62zhE9D9rrQYbjkPOM7nFsjuuGR5+9+zaoZ6g==", "dev": true, "dependencies": { - "@wdio/logger": "8.28.0", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", "decamelize": "^6.0.0", - "deepmerge-ts": "^5.0.0", + "deepmerge-ts": "^7.0.3", "glob": "^10.2.2", "import-meta-resolve": "^4.0.0" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=18" } }, - "node_modules/@wdio/config/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@wdio/config/node_modules/@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/config/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "node_modules/@wdio/config/node_modules/@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/config/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "node_modules/@wdio/config/node_modules/@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", "dev": true, "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "@types/node": "^20.1.0" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/@wdio/config/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/@wdio/config/node_modules/@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/@wdio/globals": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.29.1.tgz", - "integrity": "sha512-F+fPnX75f44/crZDfQ2FYSino/IMIdbnQGLIkaH0VnoljVJIHuxnX4y5Zqr4yRgurL9DsZaH22cLHrPXaHUhPg==", + "node_modules/@wdio/config/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, - "engines": { - "node": "^16.13 || >=18" + "dependencies": { + "debug": "^4.3.4" }, - "optionalDependencies": { - "expect-webdriverio": "^4.9.3", - "webdriverio": "8.29.1" + "engines": { + "node": ">= 14" } }, - "node_modules/@wdio/globals/node_modules/@puppeteer/browsers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", - "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", + "node_modules/@wdio/config/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "http-proxy-agent": "5.0.0", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@wdio/globals/node_modules/@types/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "node_modules/@wdio/config/node_modules/deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", "dev": true, - "optional": true, - "peer": true + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@wdio/globals/node_modules/archiver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.1.tgz", - "integrity": "sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==", + "node_modules/@wdio/config/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, - "optional": true, "dependencies": { - "archiver-utils": "^4.0.1", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^5.0.1" + "agent-base": "^7.0.2", + "debug": "4" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 14" } }, - "node_modules/@wdio/globals/node_modules/archiver-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz", - "integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==", + "node_modules/@wdio/config/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "optional": true, - "dependencies": { - "glob": "^8.0.0", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, "engines": { - "node": ">= 12.0.0" + "node": ">=12" } }, - "node_modules/@wdio/globals/node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true, - "optional": true - }, - "node_modules/@wdio/globals/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@wdio/config/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dev": true, - "optional": true, "dependencies": { - "balanced-match": "^1.0.0" + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/@wdio/globals/node_modules/chrome-launcher": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.0.tgz", - "integrity": "sha512-rJYWeEAERwWIr3c3mEVXwNiODPEdMRlRxHc47B1qHPOolHZnkj7rMv1QSUfPoG6MgatWj5AxSpnKKR4QEwEQIQ==", + "node_modules/@wdio/config/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^2.0.1" - }, "bin": { - "print-chrome-path": "bin/print-chrome-path.js" + "semver": "bin/semver.js" }, "engines": { - "node": ">=12.13.0" + "node": ">=10" } }, - "node_modules/@wdio/globals/node_modules/compress-commons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", - "integrity": "sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==", + "node_modules/@wdio/config/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dev": true, - "optional": true, "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^5.0.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" }, - "engines": { - "node": ">= 12.0.0" + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, - "node_modules/@wdio/globals/node_modules/crc32-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.0.tgz", - "integrity": "sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==", + "node_modules/@wdio/config/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, - "optional": true, "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">= 12.0.0" + "node": ">=12" } }, - "node_modules/@wdio/globals/node_modules/cross-fetch": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", - "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "node_modules/@wdio/globals": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.0.5.tgz", + "integrity": "sha512-ZkopKj1qEDNKuF1a87JTLfTKCBFgCHLUns5ob5D1oEmMFp0NwB89HHGBWgtuJpCUmxJAbf4rCKglVeKhB9rY7A==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "node-fetch": "^2.6.11" + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "expect-webdriverio": "^5.0.1", + "webdriverio": "9.0.5" } }, - "node_modules/@wdio/globals/node_modules/devtools": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/devtools/-/devtools-8.29.1.tgz", - "integrity": "sha512-fbH0Z7CPK4OZSgUw2QcAppczowxtSyvFztPUmiFyi99cUadjEOwlg0aL3pBVlIDo67olYjGb8GD1M5Z4yI/P6w==", + "node_modules/@wdio/globals/node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", "dev": true, "optional": true, - "peer": true, "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "chrome-launcher": "^1.0.0", - "edge-paths": "^3.0.5", - "import-meta-resolve": "^4.0.0", - "puppeteer-core": "20.3.0", - "query-selector-shadow-dom": "^1.0.0", - "ua-parser-js": "^1.0.1", - "uuid": "^9.0.0", - "which": "^4.0.0" + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=18" } }, - "node_modules/@wdio/globals/node_modules/devtools-protocol": { - "version": "0.0.1120988", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", - "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==", + "node_modules/@wdio/globals/node_modules/@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "optional": true, - "peer": true + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@wdio/globals/node_modules/devtools/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "node_modules/@wdio/globals/node_modules/@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", "dev": true, "optional": true, - "peer": true, "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/@wdio/globals/node_modules/edge-paths": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", - "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "node_modules/@wdio/globals/node_modules/@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", "dev": true, "optional": true, - "peer": true, "dependencies": { - "@types/which": "^2.0.1", - "which": "^2.0.2" + "@types/node": "^20.1.0" }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/shirshak55" + "node": ">=18" } }, - "node_modules/@wdio/globals/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/@wdio/globals/node_modules/@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", "dev": true, "optional": true, - "peer": true, - "engines": { - "node": ">=10" + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/globals/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/@wdio/globals/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "optional": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "debug": "^4.3.4" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 14" } }, - "node_modules/@wdio/globals/node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/@wdio/globals/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "optional": true, "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "balanced-match": "^1.0.0" } }, - "node_modules/@wdio/globals/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/@wdio/globals/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "optional": true, - "peer": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, "engines": { - "node": ">= 6" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@wdio/globals/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "node_modules/@wdio/globals/node_modules/deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", "dev": true, "optional": true, - "peer": true, "engines": { - "node": ">=16" + "node": ">=16.0.0" } }, - "node_modules/@wdio/globals/node_modules/lighthouse-logger": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", - "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "node_modules/@wdio/globals/node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@wdio/globals/node_modules/expect-webdriverio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-5.0.1.tgz", + "integrity": "sha512-tYJaXtu5qQr/oEXzDKoVzyA/g2wyeE1YsuVJQC8/bKnp1lX3I4NQ2Yb9u1rJ9vdpFha6pki8PzHlTk5PVZBQWQ==", "dev": true, "optional": true, - "peer": true, "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" + "@vitest/snapshot": "^2.0.5", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">=18 || >=20 || >=22" + }, + "peerDependencies": { + "@wdio/globals": "^9.0.0-alpha.350", + "@wdio/logger": "^9.0.0-alpha.350", + "webdriverio": "^9.0.0-alpha.350" + }, + "peerDependenciesMeta": { + "@wdio/globals": { + "optional": false + }, + "@wdio/logger": { + "optional": false + }, + "webdriverio": { + "optional": false + } } }, - "node_modules/@wdio/globals/node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/@wdio/globals/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "optional": true, - "peer": true, "dependencies": { - "ms": "2.0.0" + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, "node_modules/@wdio/globals/node_modules/lru-cache": { @@ -5599,9 +5502,9 @@ } }, "node_modules/@wdio/globals/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "optional": true, "dependencies": { @@ -5614,374 +5517,126 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/globals/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@wdio/globals/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/@wdio/globals/node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dev": true, "optional": true, "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", + "pac-proxy-agent": "^7.0.1", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.2" }, "engines": { "node": ">= 14" } }, - "node_modules/@wdio/globals/node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "node_modules/@wdio/globals/node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", "dev": true, "optional": true, + "peer": true, "dependencies": { - "debug": "^4.3.4" + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" }, "engines": { - "node": ">= 14" + "node": ">=18" } }, - "node_modules/@wdio/globals/node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "node_modules/@wdio/globals/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "optional": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">= 14" + "node": ">=10" } }, - "node_modules/@wdio/globals/node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "node_modules/@wdio/globals/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dev": true, "optional": true, "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@wdio/globals/node_modules/puppeteer-core": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.3.0.tgz", - "integrity": "sha512-264pBrIui5bO6NJeOcbJrLa0OCwmA4+WK00JMrLIKTfRiqe2gx8KWTzLsjyw/bizErp3TKS7vt/I0i5fTC+mAw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@puppeteer/browsers": "1.3.0", - "chromium-bidi": "0.4.9", - "cross-fetch": "3.1.6", - "debug": "4.3.4", - "devtools-protocol": "0.0.1120988", - "ws": "8.13.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wdio/globals/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@wdio/globals/node_modules/serialize-error": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", - "dev": true, - "optional": true, - "dependencies": { - "type-fest": "^2.12.2" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@wdio/globals/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "optional": true, - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@wdio/globals/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "optional": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@wdio/globals/node_modules/ua-parser-js": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", - "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "optional": true, - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/@wdio/globals/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "optional": true, - "peer": true, - "bin": { - "uuid": "dist/bin/uuid" + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, "node_modules/@wdio/globals/node_modules/webdriverio": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.29.1.tgz", - "integrity": "sha512-NZK95ivXCqdPraB3FHMw6ByxnCvtgFXkjzG2l3Oq5z0IuJS2aMow3AKFIyiuG6is/deGCe+Tb8eFTCqak7UV+w==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.5.tgz", + "integrity": "sha512-80zhuLBT5W5wiLNZ0maT1cVUrxmwpMuBgCprwZjI8lFe+KUhGLClrJXud/RrVT9x9rDCYa6pGCtQ4UqA+2U+sw==", "dev": true, "optional": true, "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/repl": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "archiver": "^6.0.0", - "aria-query": "^5.0.0", + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "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", - "devtools-protocol": "^0.0.1249869", - "grapheme-splitter": "^1.0.2", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.2.1", "import-meta-resolve": "^4.0.0", "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", "lodash.zip": "^4.2.0", - "minimatch": "^9.0.0", - "puppeteer-core": "^20.9.0", - "query-selector-shadow-dom": "^1.0.0", - "resq": "^1.9.1", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", "rgb2hex": "0.2.5", - "serialize-error": "^11.0.1", - "webdriver": "8.29.1" - }, - "engines": { - "node": "^16.13 || >=18" - }, - "peerDependencies": { - "devtools": "^8.14.0" - }, - "peerDependenciesMeta": { - "devtools": { - "optional": true - } - } - }, - "node_modules/@wdio/globals/node_modules/webdriverio/node_modules/@puppeteer/browsers": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", - "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", - "dev": true, - "optional": true, - "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.3.0", - "tar-fs": "3.0.4", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wdio/globals/node_modules/webdriverio/node_modules/chromium-bidi": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", - "integrity": "sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==", - "dev": true, - "optional": true, - "dependencies": { - "mitt": "3.0.0" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/@wdio/globals/node_modules/webdriverio/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dev": true, - "optional": true, - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/@wdio/globals/node_modules/webdriverio/node_modules/devtools-protocol": { - "version": "0.0.1249869", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1249869.tgz", - "integrity": "sha512-Ctp4hInA0BEavlUoRy9mhGq0i+JSo/AwVyX2EFgZmV1kYB+Zq+EMBAn52QWu6FbRr10hRb6pBl420upbp4++vg==", - "dev": true, - "optional": true - }, - "node_modules/@wdio/globals/node_modules/webdriverio/node_modules/puppeteer-core": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.9.0.tgz", - "integrity": "sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==", - "dev": true, - "optional": true, - "dependencies": { - "@puppeteer/browsers": "1.4.6", - "chromium-bidi": "0.4.16", - "cross-fetch": "4.0.0", - "debug": "4.3.4", - "devtools-protocol": "0.0.1147663", - "ws": "8.13.0" + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.5" }, "engines": { - "node": ">=16.3.0" + "node": ">=18" }, "peerDependencies": { - "typescript": ">= 4.7.4" + "puppeteer-core": "^22.3.0" }, "peerDependenciesMeta": { - "typescript": { + "puppeteer-core": { "optional": true } } }, - "node_modules/@wdio/globals/node_modules/webdriverio/node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1147663", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", - "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", - "dev": true, - "optional": true - }, - "node_modules/@wdio/globals/node_modules/webdriverio/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dev": true, - "optional": true, - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, "node_modules/@wdio/globals/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -5999,9 +5654,9 @@ } }, "node_modules/@wdio/globals/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "optional": true, "dependencies": { @@ -6017,44 +5672,29 @@ "node": ">=12" } }, - "node_modules/@wdio/globals/node_modules/zip-stream": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.1.tgz", - "integrity": "sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==", - "dev": true, - "optional": true, - "dependencies": { - "archiver-utils": "^4.0.1", - "compress-commons": "^5.0.1", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/@wdio/local-runner": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.29.1.tgz", - "integrity": "sha512-Z3QAgxe1uQ97C7NS1CdMhgmHaLu/sbb47HTbw1yuuLk+SwsBIQGhNpTSA18QVRSUXq70G3bFvjACwqyap1IEQg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.0.5.tgz", + "integrity": "sha512-BFZ/e7z1s2cYsix1evijydaDn0YffeIHjPsMoa9b+zhW8BoZfTEDGKblYvzRgjUDD4elXs+YRZpA6EhjcGJTxQ==", "dev": true, "dependencies": { "@types/node": "^20.1.0", - "@wdio/logger": "8.28.0", - "@wdio/repl": "8.24.12", - "@wdio/runner": "8.29.1", - "@wdio/types": "8.29.1", + "@wdio/logger": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/runner": "9.0.5", + "@wdio/types": "9.0.4", "async-exit-hook": "^2.0.1", "split2": "^4.1.0", "stream-buffers": "^3.0.2" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=18" } }, - "node_modules/@wdio/logger": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.28.0.tgz", - "integrity": "sha512-/s6zNCqwy1hoc+K4SJypis0Ud0dlJ+urOelJFO1x0G0rwDRWyFiUP6ijTaCcFxAm29jYEcEPWijl2xkVIHwOyA==", + "node_modules/@wdio/local-runner/node_modules/@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", "dev": true, "dependencies": { "chalk": "^5.1.2", @@ -6063,22 +5703,22 @@ "strip-ansi": "^7.1.0" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=18" } }, - "node_modules/@wdio/logger/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@wdio/local-runner/node_modules/@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "@types/node": "^20.1.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=18" } }, - "node_modules/@wdio/logger/node_modules/chalk": { + "node_modules/@wdio/local-runner/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", @@ -6090,32 +5730,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@wdio/logger/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=12" + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/logger/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@wdio/mocha-framework": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.29.1.tgz", - "integrity": "sha512-R9dKMNqWgtUvZo33ORjUQV8Z/WLX5h/pg9u/xIvZSGXuNSw1h+5DWF6UiNFscxBFblL9UvBi6V9ila2LHgE4ew==", + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.38.2.tgz", + "integrity": "sha512-qJmRL5E6/ypjCUACH4hvCAAmTdU4YUrUlp9o/IKvTIAHMnZPE0/HgUFixCeu8Mop+rdzTPVBrbqxpRDdSnraYA==", "dev": true, "dependencies": { "@types/mocha": "^10.0.0", "@types/node": "^20.1.0", - "@wdio/logger": "8.28.0", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.38.2", + "@wdio/utils": "8.38.2", "mocha": "^10.0.0" }, "engines": { @@ -6123,32 +5775,32 @@ } }, "node_modules/@wdio/protocols": { - "version": "8.24.12", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.24.12.tgz", - "integrity": "sha512-QnVj3FkapmVD3h2zoZk+ZQ8gevSj9D9MiIQIy8eOnY4FAneYZ9R9GvoW+mgNcCZO8S8++S/jZHetR8n+8Q808g==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.0.4.tgz", + "integrity": "sha512-T9v8Jkp94NxLLil5J7uJ/+YHk5/7fhOggzGcN+LvuCHS6jbJFZ/11c4SUEuXw27Yqk01fFXPBbF6TwcTTOuW/Q==", "dev": true }, "node_modules/@wdio/repl": { - "version": "8.24.12", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.24.12.tgz", - "integrity": "sha512-321F3sWafnlw93uRTSjEBVuvWCxTkWNDs7ektQS15drrroL3TMeFOynu4rDrIz0jXD9Vas0HCD2Tq/P0uxFLdw==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.4.tgz", + "integrity": "sha512-5Bc5ARjWA7t6MZNnVJ9WvO1iDsy6iOsrSDWiP7APWAdaF/SJCP3SFE2c+PdQJpJWhr2Kk0fRGuyDM+GdsmZhwg==", "dev": true, "dependencies": { "@types/node": "^20.1.0" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=18" } }, "node_modules/@wdio/reporter": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.29.1.tgz", - "integrity": "sha512-LZeYHC+HHJRYiFH9odaotDazZh0zNhu4mTuL/T/e3c/Q3oPSQjLvfQYhB3Ece1QA9PKjP1VPmr+g9CvC0lMixA==", + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.38.2.tgz", + "integrity": "sha512-R78UdAtAnkaV22NYlCCcbPPhmYweiDURiw64LYhlVIQrKNuXUQcafR2kRlWKy31rZc9thSLs5LzrZDReENUlFQ==", "dev": true, "dependencies": { "@types/node": "^20.1.0", - "@wdio/logger": "8.28.0", - "@wdio/types": "8.29.1", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.38.2", "diff": "^5.0.0", "object-inspect": "^1.12.0" }, @@ -6157,341 +5809,206 @@ } }, "node_modules/@wdio/runner": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.29.1.tgz", - "integrity": "sha512-MvYFf4RgRmzxjAzy6nxvaDG1ycBRvoz772fT06csjxuaVYm57s8mlB8X+U1UQMx/IzujAb53fSeAmNcyU3FNEA==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.0.5.tgz", + "integrity": "sha512-qZF7k3BeQaM7pQRwIvedbfaC7xBU1xRY+wFkp44U/wvYZOOrqWiwv/Synk1iCFkOdxl/b+Gqp68dDmS9BrVDmw==", "dev": true, "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/globals": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "deepmerge-ts": "^5.0.0", - "expect-webdriverio": "^4.9.3", - "gaze": "^1.1.2", - "webdriver": "8.29.1", - "webdriverio": "8.29.1" + "@types/node": "^20.11.28", + "@wdio/config": "9.0.5", + "@wdio/globals": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "deepmerge-ts": "^7.0.3", + "expect-webdriverio": "^5.0.1", + "gaze": "^1.1.3", + "webdriver": "9.0.5", + "webdriverio": "9.0.5" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=18" } }, "node_modules/@wdio/runner/node_modules/@puppeteer/browsers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", - "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "http-proxy-agent": "5.0.0", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@wdio/runner/node_modules/@types/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@wdio/runner/node_modules/archiver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.1.tgz", - "integrity": "sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==", + "node_modules/@wdio/runner/node_modules/@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "archiver-utils": "^4.0.1", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^5.0.1" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, - "engines": { - "node": ">= 12.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@wdio/runner/node_modules/archiver-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz", - "integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==", + "node_modules/@wdio/runner/node_modules/@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", "dev": true, "dependencies": { - "glob": "^8.0.0", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/@wdio/runner/node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/@wdio/runner/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "node": ">=18" } }, - "node_modules/@wdio/runner/node_modules/chrome-launcher": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.0.tgz", - "integrity": "sha512-rJYWeEAERwWIr3c3mEVXwNiODPEdMRlRxHc47B1qHPOolHZnkj7rMv1QSUfPoG6MgatWj5AxSpnKKR4QEwEQIQ==", + "node_modules/@wdio/runner/node_modules/@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^2.0.1" - }, - "bin": { - "print-chrome-path": "bin/print-chrome-path.js" + "@types/node": "^20.1.0" }, "engines": { - "node": ">=12.13.0" + "node": ">=18" } }, - "node_modules/@wdio/runner/node_modules/compress-commons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", - "integrity": "sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==", + "node_modules/@wdio/runner/node_modules/@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", "dev": true, "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^5.0.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">=18" } }, - "node_modules/@wdio/runner/node_modules/crc32-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.0.tgz", - "integrity": "sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==", + "node_modules/@wdio/runner/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "debug": "^4.3.4" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 14" } }, - "node_modules/@wdio/runner/node_modules/cross-fetch": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", - "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "node_modules/@wdio/runner/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "node-fetch": "^2.6.11" + "balanced-match": "^1.0.0" } }, - "node_modules/@wdio/runner/node_modules/devtools": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/devtools/-/devtools-8.29.1.tgz", - "integrity": "sha512-fbH0Z7CPK4OZSgUw2QcAppczowxtSyvFztPUmiFyi99cUadjEOwlg0aL3pBVlIDo67olYjGb8GD1M5Z4yI/P6w==", + "node_modules/@wdio/runner/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "chrome-launcher": "^1.0.0", - "edge-paths": "^3.0.5", - "import-meta-resolve": "^4.0.0", - "puppeteer-core": "20.3.0", - "query-selector-shadow-dom": "^1.0.0", - "ua-parser-js": "^1.0.1", - "uuid": "^9.0.0", - "which": "^4.0.0" - }, "engines": { - "node": "^16.13 || >=18" - } - }, - "node_modules/@wdio/runner/node_modules/devtools-protocol": { - "version": "0.0.1120988", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", - "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@wdio/runner/node_modules/devtools/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "engines": { - "node": "^16.13.0 || >=18.0.0" + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@wdio/runner/node_modules/edge-paths": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", - "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "node_modules/@wdio/runner/node_modules/deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@types/which": "^2.0.1", - "which": "^2.0.2" - }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/shirshak55" + "node": ">=16.0.0" } }, - "node_modules/@wdio/runner/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/@wdio/runner/node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", "dev": true, "optional": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "peer": true }, - "node_modules/@wdio/runner/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/@wdio/runner/node_modules/expect-webdriverio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-5.0.1.tgz", + "integrity": "sha512-tYJaXtu5qQr/oEXzDKoVzyA/g2wyeE1YsuVJQC8/bKnp1lX3I4NQ2Yb9u1rJ9vdpFha6pki8PzHlTk5PVZBQWQ==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "@vitest/snapshot": "^2.0.5", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" }, "engines": { - "node": ">=12" + "node": ">=18 || >=20 || >=22" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@wdio/runner/node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" + "peerDependencies": { + "@wdio/globals": "^9.0.0-alpha.350", + "@wdio/logger": "^9.0.0-alpha.350", + "webdriverio": "^9.0.0-alpha.350" }, - "engines": { - "node": ">=10" + "peerDependenciesMeta": { + "@wdio/globals": { + "optional": false + }, + "@wdio/logger": { + "optional": false + }, + "webdriverio": { + "optional": false + } } }, - "node_modules/@wdio/runner/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/@wdio/runner/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/@wdio/runner/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/@wdio/runner/node_modules/lighthouse-logger": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", - "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" - } - }, - "node_modules/@wdio/runner/node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" + "node": ">= 14" } }, "node_modules/@wdio/runner/node_modules/lru-cache": { @@ -6504,9 +6021,9 @@ } }, "node_modules/@wdio/runner/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -6518,356 +6035,146 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/runner/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@wdio/runner/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/@wdio/runner/node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dev": true, "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", + "pac-proxy-agent": "^7.0.1", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.2" }, "engines": { "node": ">= 14" } }, - "node_modules/@wdio/runner/node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "node_modules/@wdio/runner/node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { - "debug": "^4.3.4" + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" }, "engines": { - "node": ">= 14" + "node": ">=18" } }, - "node_modules/@wdio/runner/node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "node_modules/@wdio/runner/node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" }, "engines": { - "node": ">= 14" + "node": ">=18" } }, - "node_modules/@wdio/runner/node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "node_modules/@wdio/runner/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">= 14" + "node": ">=10" } }, - "node_modules/@wdio/runner/node_modules/puppeteer-core": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.3.0.tgz", - "integrity": "sha512-264pBrIui5bO6NJeOcbJrLa0OCwmA4+WK00JMrLIKTfRiqe2gx8KWTzLsjyw/bizErp3TKS7vt/I0i5fTC+mAw==", + "node_modules/@wdio/runner/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@puppeteer/browsers": "1.3.0", - "chromium-bidi": "0.4.9", - "cross-fetch": "3.1.6", - "debug": "4.3.4", - "devtools-protocol": "0.0.1120988", - "ws": "8.13.0" - }, - "engines": { - "node": ">=16.0.0" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wdio/runner/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@wdio/runner/node_modules/serialize-error": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", - "dev": true, - "dependencies": { - "type-fest": "^2.12.2" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@wdio/runner/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@wdio/runner/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@wdio/runner/node_modules/ua-parser-js": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", - "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "optional": true, - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/@wdio/runner/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "optional": true, - "peer": true, - "bin": { - "uuid": "dist/bin/uuid" + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, "node_modules/@wdio/runner/node_modules/webdriverio": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.29.1.tgz", - "integrity": "sha512-NZK95ivXCqdPraB3FHMw6ByxnCvtgFXkjzG2l3Oq5z0IuJS2aMow3AKFIyiuG6is/deGCe+Tb8eFTCqak7UV+w==", - "dev": true, - "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/repl": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "archiver": "^6.0.0", - "aria-query": "^5.0.0", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.5.tgz", + "integrity": "sha512-80zhuLBT5W5wiLNZ0maT1cVUrxmwpMuBgCprwZjI8lFe+KUhGLClrJXud/RrVT9x9rDCYa6pGCtQ4UqA+2U+sw==", + "dev": true, + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "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", - "devtools-protocol": "^0.0.1249869", - "grapheme-splitter": "^1.0.2", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.2.1", "import-meta-resolve": "^4.0.0", "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", "lodash.zip": "^4.2.0", - "minimatch": "^9.0.0", - "puppeteer-core": "^20.9.0", - "query-selector-shadow-dom": "^1.0.0", - "resq": "^1.9.1", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", "rgb2hex": "0.2.5", - "serialize-error": "^11.0.1", - "webdriver": "8.29.1" - }, - "engines": { - "node": "^16.13 || >=18" - }, - "peerDependencies": { - "devtools": "^8.14.0" - }, - "peerDependenciesMeta": { - "devtools": { - "optional": true - } - } - }, - "node_modules/@wdio/runner/node_modules/webdriverio/node_modules/@puppeteer/browsers": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", - "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", - "dev": true, - "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.3.0", - "tar-fs": "3.0.4", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wdio/runner/node_modules/webdriverio/node_modules/chromium-bidi": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", - "integrity": "sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==", - "dev": true, - "dependencies": { - "mitt": "3.0.0" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/@wdio/runner/node_modules/webdriverio/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dev": true, - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/@wdio/runner/node_modules/webdriverio/node_modules/devtools-protocol": { - "version": "0.0.1249869", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1249869.tgz", - "integrity": "sha512-Ctp4hInA0BEavlUoRy9mhGq0i+JSo/AwVyX2EFgZmV1kYB+Zq+EMBAn52QWu6FbRr10hRb6pBl420upbp4++vg==", - "dev": true - }, - "node_modules/@wdio/runner/node_modules/webdriverio/node_modules/puppeteer-core": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.9.0.tgz", - "integrity": "sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==", - "dev": true, - "dependencies": { - "@puppeteer/browsers": "1.4.6", - "chromium-bidi": "0.4.16", - "cross-fetch": "4.0.0", - "debug": "4.3.4", - "devtools-protocol": "0.0.1147663", - "ws": "8.13.0" + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.5" }, "engines": { - "node": ">=16.3.0" + "node": ">=18" }, "peerDependencies": { - "typescript": ">= 4.7.4" + "puppeteer-core": "^22.3.0" }, "peerDependenciesMeta": { - "typescript": { + "puppeteer-core": { "optional": true } } }, - "node_modules/@wdio/runner/node_modules/webdriverio/node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1147663", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", - "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", - "dev": true - }, - "node_modules/@wdio/runner/node_modules/webdriverio/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dev": true, - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, "node_modules/@wdio/runner/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -6885,9 +6192,9 @@ } }, "node_modules/@wdio/runner/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -6902,28 +6209,14 @@ "node": ">=12" } }, - "node_modules/@wdio/runner/node_modules/zip-stream": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.1.tgz", - "integrity": "sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==", - "dev": true, - "dependencies": { - "archiver-utils": "^4.0.1", - "compress-commons": "^5.0.1", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/@wdio/spec-reporter": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.29.1.tgz", - "integrity": "sha512-tuDHihrTjCxFCbSjT0jMvAarLA1MtatnCnhv0vguu3ZWXELR1uESX2KzBmpJ+chGZz3oCcKszT8HOr6Pg2a1QA==", + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.38.2.tgz", + "integrity": "sha512-Dntk+lmrp+0I3HRRWkkXED+smshvgsW5cdLKwJhEJ1liI48MdBpdNGf9IHTVckE6nfxcWDyFI4icD9qYv/5bFA==", "dev": true, "dependencies": { - "@wdio/reporter": "8.29.1", - "@wdio/types": "8.29.1", + "@wdio/reporter": "8.38.2", + "@wdio/types": "8.38.2", "chalk": "^5.1.2", "easy-table": "^1.2.0", "pretty-ms": "^7.0.0" @@ -6945,9 +6238,9 @@ } }, "node_modules/@wdio/types": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.29.1.tgz", - "integrity": "sha512-rZYzu+sK8zY1PjCEWxNu4ELJPYKDZRn7HFcYNgR122ylHygfldwkb5TioI6Pn311hQH/S+663KEeoq//Jb0f8A==", + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.38.2.tgz", + "integrity": "sha512-+wj1c1OSLdnN4WO5b44Ih4263dTl/eSwMGSI4/pCgIyXIuYQH38JQ+6WRa+c8vJEskUzboq2cSgEQumVZ39ozQ==", "dev": true, "dependencies": { "@types/node": "^20.1.0" @@ -6957,18 +6250,18 @@ } }, "node_modules/@wdio/utils": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.29.1.tgz", - "integrity": "sha512-Dm91DKL/ZKeZ2QogWT8Twv0p+slEgKyB/5x9/kcCG0Q2nNa+tZedTjOhryzrsPiWc+jTSBmjGE4katRXpJRFJg==", + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.38.2.tgz", + "integrity": "sha512-y5AnBwsGcu/XuCBGCgKmlvKdwEIFyzLA+Cr+denySxY3jbWDONtPUcGaVdFALwsIa5jcIjcATqGmZcCPGnkd7g==", "dev": true, "dependencies": { "@puppeteer/browsers": "^1.6.0", - "@wdio/logger": "8.28.0", - "@wdio/types": "8.29.1", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.38.2", "decamelize": "^6.0.0", "deepmerge-ts": "^5.1.0", - "edgedriver": "^5.3.5", - "geckodriver": "^4.2.0", + "edgedriver": "^5.5.0", + "geckodriver": "^4.3.1", "get-port": "^7.0.0", "import-meta-resolve": "^4.0.0", "locate-app": "^2.1.0", @@ -6980,169 +6273,156 @@ "node": "^16.13 || >=18" } }, - "node_modules/@wdio/utils/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, "node_modules/@xmldom/xmldom": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.8.tgz", - "integrity": "sha512-PrJx38EfpitFhwmILRl37jAdBlsww6AZ6rRVK4QS7T7RHLhX7mSs647sTmgr9GIxe3qjXdesmomEgbgaokrVFg==", - "deprecated": "this version is no longer supported, please update to at least 0.8.*", + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", "dev": true, "engines": { "node": ">=10.0.0" @@ -7160,12 +6440,35 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.48", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.48.tgz", + "integrity": "sha512-J7cliimZ2snAbr0IhLx2U8BwfA1pKucahKzTpFtYq4hEgKxwvFJcIjCIVNPwQpfVab7iVP+AKmoH1gidBlyhiQ==", + "dev": true, + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, "node_modules/abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -7183,6 +6486,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7200,9 +6504,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true, "engines": { "node": ">=0.4.0" @@ -7340,6 +6644,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -7356,9 +6661,9 @@ } }, "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -7381,70 +6686,181 @@ } }, "node_modules/archiver": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", - "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "dev": true, "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.3", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.0.0", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "dev": true, "dependencies": { - "glob": "^7.1.4", + "glob": "^10.0.0", "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", + "lodash": "^4.17.15", "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, - "node_modules/archiver/node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "dev": true - }, - "node_modules/archiver/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archiver/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -7454,12 +6870,12 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/arr-diff": { @@ -7526,6 +6942,22 @@ "node": ">=0.10.0" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-differ": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", @@ -7556,15 +6988,16 @@ "dev": true }, "node_modules/array-includes": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", - "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5", - "get-intrinsic": "^1.1.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -7640,6 +7073,15 @@ "node": ">=0.10.0" } }, + "node_modules/array-sort/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-uniq": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", @@ -7658,15 +7100,55 @@ "node": ">=0.10.0" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -7676,6 +7158,61 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -7686,15 +7223,16 @@ } }, "node_modules/assert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", - "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "dev": true, "dependencies": { - "es6-object-assign": "^1.1.0", - "is-nan": "^1.2.1", - "object-is": "^1.0.1", - "util": "^0.12.0" + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" } }, "node_modules/assert-plus": { @@ -7736,20 +7274,11 @@ } }, "node_modules/ast-types/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", @@ -7772,10 +7301,16 @@ } }, "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] }, "node_modules/async-exit-hook": { "version": "2.0.1", @@ -7786,6 +7321,15 @@ "node": ">=0.12.0" } }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", @@ -7817,10 +7361,13 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -7838,288 +7385,121 @@ } }, "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", "dev": true }, - "node_modules/babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - } - }, - "node_modules/babel-code-frame/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-code-frame/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, - "node_modules/babel-code-frame/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "node_modules/babel-code-frame/node_modules/js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", "dev": true }, - "node_modules/babel-code-frame/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "node_modules/babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" }, "engines": { - "node": ">=0.10.0" + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" } }, - "node_modules/babel-code-frame/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, "engines": { - "node": ">=0.8.0" + "node": ">=8" } }, - "node_modules/babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "dev": true, - "dependencies": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - } - }, - "node_modules/babel-core/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/babel-core/node_modules/json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/babel-core/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "dependencies": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - } - }, - "node_modules/babel-generator/node_modules/jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha512-n7pFrqQm44TCYvrCDb0MqabAF+JUBq+ijBvNMUxpkLjJaAu32faIexewMumrH5KLLJ1HDyT0PTEqRyAe/GwwuQ==", - "dev": true, - "dependencies": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "node_modules/babel-loader": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", - "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", - "dev": true, - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", - "dev": true, - "dependencies": { - "babel-runtime": "^6.22.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" + "@babel/helper-define-polyfill-provider": "^0.6.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha512-veliHlHX06wjaeY8xNITbveXSiI+ASFnOqvne/LaIJIqOWi2Ogmj91KOugEz/hoh/fwMhXNBJPCv8Xaz5CyM4A==", - "dev": true, - "dependencies": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-register/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "dev": true, - "hasInstallScript": true - }, - "node_modules/babel-register/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/babel-plugin-transform-runtime": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.15.0.tgz", + "integrity": "sha512-sS2r8bFVzJ36EHlzzOvJvlre/Sec92V+oWQVb/pMwo/EKADB/cHdVD1jVXXsOgMRvCEwNyvDcpNWSh80N3N/KA==", "dev": true, "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" + "babel-runtime": "^6.9.0" } }, "node_modules/babel-runtime": { @@ -8146,90 +7526,6 @@ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "dev": true }, - "node_modules/babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", - "dev": true, - "dependencies": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "node_modules/babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", - "dev": true, - "dependencies": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "node_modules/babel-traverse/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/babel-traverse/node_modules/globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-traverse/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", - "dev": true, - "dependencies": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "node_modules/babel-types/node_modules/to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true, - "bin": { - "babylon": "bin/babylon.js" - } - }, "node_modules/bach": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", @@ -8266,6 +7562,52 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "dev": true, + "optional": true, + "dependencies": { + "streamx": "^2.18.0" + } + }, "node_modules/base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", @@ -8344,9 +7686,9 @@ "dev": true }, "node_modules/basic-ftp": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz", - "integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, "engines": { "node": ">=10.0.0" @@ -8377,9 +7719,9 @@ } }, "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", "dev": true, "engines": { "node": ">=0.6" @@ -8408,12 +7750,15 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/binaryextensions": { @@ -8438,35 +7783,10 @@ "file-uri-to-path": "1.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "node_modules/body": { "version": "5.1.0", @@ -8481,9 +7801,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -8493,7 +7813,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -8558,12 +7878,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -8576,9 +7896,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "funding": [ { "type": "opencollective", @@ -8587,13 +7907,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" @@ -8612,9 +7936,9 @@ } }, "node_modules/browserstack-local": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.1.tgz", - "integrity": "sha512-T/wxyWDzvBHbDvl7fZKpFU7mYze6nrUkBhNy+d+8bXBqgQX10HTYvajIGO0wb49oGSLCPM0CMZTV/s7e6LF0sA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.5.tgz", + "integrity": "sha512-jKne7yosrMcptj3hqxp36TP9k0ZW2sCqhyurX24rUL4G3eT7OLgv+CSQN8iq5dtkv5IK+g+v8fWvsiC/S9KxMg==", "dev": true, "dependencies": { "agent-base": "^6.0.2", @@ -8834,56 +8158,46 @@ "node": ">=0.10.0" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", - "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "pump": "^3.0.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8914,9 +8228,9 @@ "dev": true }, "node_modules/caniuse-lite": { - "version": "1.0.30001429", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz", - "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==", + "version": "1.0.30001633", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001633.tgz", + "integrity": "sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==", "funding": [ { "type": "opencollective", @@ -8925,6 +8239,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -8945,18 +8263,18 @@ } }, "node_modules/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -8978,6 +8296,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -9024,86 +8343,120 @@ "dev": true }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">=18.17" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "node_modules/chrome-launcher": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.1.tgz", - "integrity": "sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg==", + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0" + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" }, - "bin": { - "print-chrome-path": "bin/print-chrome-path.js" + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" }, - "engines": { - "node": ">=12.13.0" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/chrome-launcher/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, "engines": { - "node": ">=10" + "node": ">= 8.10.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "engines": { "node": ">=6.0" } }, + "node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -9155,121 +8508,114 @@ "node": ">=0.10.0" } }, - "node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "deprecated": "Please upgrade to v0.1.7", + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/cli-spinners": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.1.0.tgz", + "integrity": "sha512-2MH0N34TpDAs9ROPVkZJfBWFoYfv4zfkJF14PBHY4v/qRY75SLcm4WaEKNCLScsXieosa/tY/+slJ+BDswJxHQ==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, "engines": { - "node": ">=0.10.0" + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/class-utils/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "deprecated": "Please upgrade to v0.1.5", + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 12" } }, - "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "dependencies": { - "is-buffer": "^1.1.5" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "restore-cursor": "^3.1.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=8" + "node": ">=7.0.0" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/clone": { @@ -9290,16 +8636,30 @@ "node": ">= 0.10" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, "dependencies": { - "mimic-response": "^1.0.0" + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/clone-stats": { @@ -9359,6 +8719,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -9366,7 +8727,8 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", @@ -9399,9 +8761,9 @@ } }, "node_modules/comma-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz", - "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "dev": true, "funding": { "type": "github", @@ -9415,9 +8777,9 @@ "dev": true }, "node_modules/comment-parser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", - "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, "engines": { "node": ">= 12.0.0" @@ -9430,38 +8792,89 @@ "dev": true }, "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/compress-commons": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", - "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "dev": true, "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/compress-commons/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" } }, "node_modules/concat-map": { @@ -9619,14 +9032,14 @@ "dev": true }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -9656,9 +9069,9 @@ } }, "node_modules/core-js": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", - "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -9666,11 +9079,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.0.tgz", - "integrity": "sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", "dependencies": { - "browserslist": "^4.21.4" + "browserslist": "^4.23.0" }, "funding": { "type": "opencollective", @@ -9678,9 +9091,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.0.tgz", - "integrity": "sha512-LiN6fylpVBVwT8twhhluD9TzXmZQQsr2I2eIKtWNbZI1XMfBT7CV18itaN6RA7EtQd/SDdRx/wzvAShX2HvhQA==", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.37.1.tgz", + "integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -9737,50 +9150,71 @@ } }, "node_modules/crc32-stream": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", - "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "dev": true, "dependencies": { "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, - "node_modules/crc32-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/criteo-direct-rsa-validate": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/criteo-direct-rsa-validate/-/criteo-direct-rsa-validate-1.1.0.tgz", - "integrity": "sha512-7gQ3zX+d+hS/vOxzLrZ4aRAceB7qNJ0VzaGNpcWjDCmtOpASB50USJDupTik/H2nHgiSAA3VNZ3SFuONs8LR9Q==" - }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "dependencies": { - "node-fetch": "2.6.7" + "safe-buffer": "~5.2.0" } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -9791,6 +9225,27 @@ "node": ">= 8" } }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", @@ -9869,13 +9324,16 @@ "dev": true }, "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "dev": true, "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/dashdash": { @@ -9891,12 +9349,63 @@ } }, "node_modules/data-uri-to-buffer": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz", - "integrity": "sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "dev": true, "engines": { - "node": ">= 14" + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/date-format": { @@ -9924,10 +9433,16 @@ "dev": true, "optional": true }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -9961,12 +9476,12 @@ } }, "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", "dev": true, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9994,66 +9509,45 @@ "node": ">=0.10" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "dependencies": { "type-detect": "^4.0.0" }, "engines": { - "node": ">=0.12" + "node": ">=6" } }, "node_modules/deep-equal": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", - "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "es-get-iterator": "^1.1.1", - "get-intrinsic": "^1.0.1", - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.2", - "is-regex": "^1.1.1", + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", "isarray": "^2.0.5", - "object-is": "^1.1.4", + "object-is": "^1.1.5", "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.3", - "which-boxed-primitive": "^1.0.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", - "which-typed-array": "^1.1.2" + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10065,15 +9559,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/deepmerge-ts": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", @@ -10095,6 +9580,15 @@ "node": ">=0.10.0" } }, + "node_modules/default-compare/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-resolution": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", @@ -10109,6 +9603,7 @@ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dev": true, + "optional": true, "dependencies": { "clone": "^1.0.2" }, @@ -10121,38 +9616,35 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, + "optional": true, "engines": { "node": ">=0.8" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -10287,18 +9779,6 @@ "node": ">=0.10.0" } }, - "node_modules/detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", - "dev": true, - "dependencies": { - "repeating": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/detect-newline": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", @@ -10308,313 +9788,71 @@ "node": ">=0.10.0" } }, - "node_modules/devtools": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/devtools/-/devtools-7.25.4.tgz", - "integrity": "sha512-R6/S/dCqxoX4Y6PxIGM9JFAuSRZzUeV5r+CoE/frhmno6mTe7dEEgwkJlfit3LkKRoul8n4DsL2A3QtWOvq5IA==", - "dev": true, - "dependencies": { - "@types/node": "^18.0.0", - "@types/ua-parser-js": "^0.7.33", - "@wdio/config": "7.25.4", - "@wdio/logger": "7.19.0", - "@wdio/protocols": "7.22.0", - "@wdio/types": "7.25.4", - "@wdio/utils": "7.25.4", - "chrome-launcher": "^0.15.0", - "edge-paths": "^2.1.0", - "puppeteer-core": "^13.1.3", - "query-selector-shadow-dom": "^1.0.0", - "ua-parser-js": "^1.0.1", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/devtools-protocol": { - "version": "0.0.1061995", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1061995.tgz", - "integrity": "sha512-pKZZWTjWa/IF4ENCg6GN8bu/AxSZgdhjSa26uc23wz38Blt2Tnm9icOPcSG3Cht55rMq35in1w3rWVPcZ60ArA==", - "dev": true + "version": "0.0.1260888", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1260888.tgz", + "integrity": "sha512-9rTIZ4ZjWwalCPiaY+kPiALLfOKgAz5CTi/Zb1L+qSZ8PH3zVo1T8JcgXIIqg1iM3pZ6hF+n9xO5r2jZ/SF+jg==", + "dev": true, + "optional": true, + "peer": true }, - "node_modules/devtools/node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true }, - "node_modules/devtools/node_modules/@wdio/config": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-7.25.4.tgz", - "integrity": "sha512-vb0emDtD9FbFh/yqW6oNdo2iuhQp8XKj6GX9fyy9v4wZgg3B0HPMVJxhIfcoHz7LMBWlHSo9YdvhFI5EQHRLBA==", + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, - "dependencies": { - "@wdio/logger": "7.19.0", - "@wdio/types": "7.25.4", - "@wdio/utils": "7.25.4", - "deepmerge": "^4.0.0", - "glob": "^8.0.3" - }, "engines": { - "node": ">=12.0.0" + "node": ">=0.3.1" } }, - "node_modules/devtools/node_modules/@wdio/logger": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-7.19.0.tgz", - "integrity": "sha512-xR7SN/kGei1QJD1aagzxs3KMuzNxdT/7LYYx+lt6BII49+fqL/SO+5X0FDCZD0Ds93AuQvvz9eGyzrBI2FFXmQ==", + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "loglevel": "^1.6.0", - "loglevel-plugin-prefix": "^0.8.4", - "strip-ansi": "^6.0.0" - }, "engines": { - "node": ">=12.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/devtools/node_modules/@wdio/protocols": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-7.22.0.tgz", - "integrity": "sha512-8EXRR+Ymdwousm/VGtW3H1hwxZ/1g1H99A1lF0U4GuJ5cFWHCd0IVE5H31Z52i8ZruouW8jueMkGZPSo2IIUSQ==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, - "node_modules/devtools/node_modules/@wdio/types": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.25.4.tgz", - "integrity": "sha512-muvNmq48QZCvocctnbe0URq2FjJjUPIG4iLoeMmyF0AQgdbjaUkMkw3BHYNHVTbSOU9WMsr2z8alhj/I2H6NRQ==", + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "dependencies": { - "@types/node": "^18.0.0", - "got": "^11.8.1" + "esutils": "^2.0.2" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "^4.6.2" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=6.0.0" } }, - "node_modules/devtools/node_modules/@wdio/utils": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-7.25.4.tgz", - "integrity": "sha512-8iwQDk+foUqSzKZKfhLxjlCKOkfRJPNHaezQoevNgnrTq/t0ek+ldZCATezb9B8jprAuP4mgS9xi22akc6RkzA==", + "node_modules/doctrine-temporary-fork": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine-temporary-fork/-/doctrine-temporary-fork-2.1.0.tgz", + "integrity": "sha512-nliqOv5NkE4zMON4UA6AMJE6As35afs8aYXATpU4pTUdIKiARZwrJVEP1boA3Rx1ZXHVkwxkhcq4VkqvsuRLsA==", "dev": true, "dependencies": { - "@wdio/logger": "7.19.0", - "@wdio/types": "7.25.4", - "p-iteration": "^1.1.8" + "esutils": "^2.0.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=0.10.0" } }, - "node_modules/devtools/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/devtools/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/devtools/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/devtools/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/devtools/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/devtools/node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/devtools/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/devtools/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/devtools/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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/devtools/node_modules/ua-parser-js": { - "version": "1.0.33", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", - "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "engines": { - "node": "*" - } - }, - "node_modules/devtools/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true - }, - "node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/doctrine-temporary-fork": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine-temporary-fork/-/doctrine-temporary-fork-2.1.0.tgz", - "integrity": "sha512-nliqOv5NkE4zMON4UA6AMJE6As35afs8aYXATpU4pTUdIKiARZwrJVEP1boA3Rx1ZXHVkwxkhcq4VkqvsuRLsA==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/documentation": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.1.tgz", - "integrity": "sha512-Y/brACCE3sNnDJPFiWlhXrqGY+NelLYVZShLGse5bT1KdohP4JkPf5T2KNq1YWhIEbDYl/1tebRLC0WYbPQxVw==", + "node_modules/documentation": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.3.tgz", + "integrity": "sha512-B7cAviVKN9Rw7Ofd+9grhVuxiHwly6Ieh+d/ceMw8UdBOv/irkuwnDEJP8tq0wgdLJDUVuIkovV+AX9mTrZFxg==", "dev": true, "dependencies": { "@babel/core": "^7.18.10", @@ -10682,9 +9920,9 @@ } }, "node_modules/documentation/node_modules/chalk": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz", - "integrity": "sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -10693,10 +9931,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/documentation/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/documentation/node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { @@ -10713,6 +9967,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/documentation/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/documentation/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -10725,56 +9991,232 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/documentation/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "node_modules/documentation/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/documentation/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/documentation/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "p-locate": "^6.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/documentation/node_modules/yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", + "node_modules/documentation/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yallist": "^4.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" } }, - "node_modules/dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "node_modules/documentation/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "node_modules/documentation/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", "dev": true, "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/documentation/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/documentation/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/documentation/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/documentation/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/documentation/node_modules/read-pkg": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", + "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^2.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/documentation/node_modules/read-pkg-up": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", + "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^7.1.0", + "type-fest": "^2.5.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/documentation/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/documentation/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/documentation/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/documentation/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", "entities": "^4.2.0" }, "funding": { @@ -10829,25 +10271,38 @@ } }, "node_modules/dotenv": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", - "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/dset": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", - "integrity": "sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", "engines": { "node": ">=4" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -10888,21 +10343,21 @@ "dev": true }, "node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", "dev": true, "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" + "stream-shift": "^1.0.2" } }, "node_modules/duplexify/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { "inherits": "^2.0.3", @@ -10963,71 +10418,13 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/edge-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-2.2.1.tgz", - "integrity": "sha512-AI5fC7dfDmCdKo3m5y7PkYE8m6bMqR6pvVpgtrZkkhcJXFLelUgkjrhk3kXXx8Kbw2cRaTT4LkOR7hqf39KJdw==", - "dev": true, - "dependencies": { - "@types/which": "^1.3.2", - "which": "^2.0.2" - } - }, - "node_modules/edgedriver": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.3.9.tgz", - "integrity": "sha512-G0wNgFMFRDnFfKaXG2R6HiyVHqhKwdQ3EgoxW3wPlns2wKqem7F+HgkWBcevN7Vz0nN4AXtskID7/6jsYDXcKw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@wdio/logger": "^8.16.17", - "decamelize": "^6.0.0", - "edge-paths": "^3.0.5", - "node-fetch": "^3.3.2", - "unzipper": "^0.10.14", - "which": "^4.0.0" - }, - "bin": { - "edgedriver": "bin/edgedriver.js" - } - }, - "node_modules/edgedriver/node_modules/@types/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "node_modules/ecc-jsbn/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, - "node_modules/edgedriver/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/edgedriver/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/edgedriver/node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/edgedriver/node_modules/edge-paths": { + "node_modules/edge-paths": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", @@ -11043,7 +10440,13 @@ "url": "https://github.com/sponsors/shirshak55" } }, - "node_modules/edgedriver/node_modules/edge-paths/node_modules/which": { + "node_modules/edge-paths/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/edge-paths/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", @@ -11058,64 +10461,23 @@ "node": ">= 8" } }, - "node_modules/edgedriver/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/edgedriver": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.6.1.tgz", + "integrity": "sha512-3Ve9cd5ziLByUdigw6zovVeWJjVs8QHVmqOB0sJ0WNeVPcwf4p18GnxMmVvlFmYRloUwf5suNuorea4QzwBIOA==", "dev": true, + "hasInstallScript": true, "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "@wdio/logger": "^8.38.0", + "@zip.js/zip.js": "^2.7.48", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^4.4.1", + "node-fetch": "^3.3.2", + "which": "^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/edgedriver/node_modules/unzipper": { - "version": "0.10.14", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", - "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/edgedriver/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/edgedriver/node_modules/which/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" + "bin": { + "edgedriver": "bin/edgedriver.js" } }, "node_modules/ee-first": { @@ -11124,9 +10486,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "dependencies": { "jake": "^10.8.5" @@ -11139,9 +10501,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + "version": "1.4.802", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz", + "integrity": "sha512-TnTMUATbgNdPXVSHsxvNVSG0uEd6cSZsANjm8c9HbvflZVVn1yTRcmVXYT1Ma95/ssB/Dcd30AHweH2TE+dNpA==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -11162,10 +10524,36 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, "engines": { "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11176,9 +10564,9 @@ } }, "node_modules/engine.io": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", - "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -11186,38 +10574,38 @@ "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", - "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -11227,18 +10615,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", @@ -11288,35 +10664,62 @@ } }, "node_modules/es-abstract": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", - "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "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.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "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", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -11325,49 +10728,119 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", - "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.0", - "has-symbols": "^1.0.1", - "is-arguments": "^1.1.0", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", "is-map": "^2.0.2", "is-set": "^2.0.2", - "is-string": "^1.0.5", - "isarray": "^2.0.5" + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", + "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -11377,14 +10850,15 @@ } }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -11411,12 +10885,6 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", - "dev": true - }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -11432,13 +10900,16 @@ } }, "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dev": true, "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/es6-weak-map": { @@ -11453,10 +10924,49 @@ "es6-symbol": "^3.1.1" } }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -11470,6 +10980,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -11570,84 +11081,101 @@ } }, "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", + "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.1.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.22.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/eslint-config-standard": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-10.2.1.tgz", - "integrity": "sha512-UkFojTV1o0GOe1edOEiuI5ccYLJSuNngtqSeClNzhsmG8KPJ+7mRxgtp2oYhqZAK/brlXMoCd+VgXViE0AfyKw==", + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", "dev": true, + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, "peerDependencies": { - "eslint": ">=3.19.0", - "eslint-plugin-import": ">=2.2.0", - "eslint-plugin-node": ">=4.2.2", - "eslint-plugin-promise": ">=3.5.0", - "eslint-plugin-standard": ">=3.0.0" + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-compat-utils/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "dependencies": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -11659,10 +11187,67 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.8.6.tgz", + "integrity": "sha512-d9UjvYpj/REmUoZvOtDEmayPlwyP4zOwwMBgtC6RtrpZta8u1AIVmxgZBYJIcCKKXwAcLs+DX2yn2LeMaTqKcQ==", + "dev": true, + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^1.0.2", + "stable-hash": "^0.0.4", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript/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==", + "dev": true + }, "node_modules/eslint-module-utils": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", - "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -11685,102 +11270,176 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" }, "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" + "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "eslint": ">=4.19.1" + "eslint": ">=8" } }, "node_modules/eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "@rtsao/scc": "^1.1.0", + "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.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/eslint-plugin-import-x": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.6.1.tgz", + "integrity": "sha512-wluSUifMIb7UfwWXqx7Yx0lE/SGCcGXECLx/9bCmbY2nneLwvAZ4vkd1IXDjPKFvdcdUgr1BaRnaRpx3k2+Pfw==", "dev": true, "dependencies": { - "ms": "2.0.0" + "@types/doctrine": "^0.0.9", + "@typescript-eslint/scope-manager": "^8.1.0", + "@typescript-eslint/utils": "^8.1.0", + "debug": "^4.3.4", + "doctrine": "^3.0.0", + "enhanced-resolve": "^5.17.1", + "eslint-import-resolver-node": "^0.3.9", + "get-tsconfig": "^4.7.3", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3", + "semver": "^7.6.3", + "stable-hash": "^0.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/eslint-plugin-import-x/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" + "balanced-match": "^1.0.0" } }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/eslint-plugin-jsdoc": { - "version": "38.1.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz", - "integrity": "sha512-n4s95oYlg0L43Bs8C0dkzIldxYf8pLCutC/tCbjIdF7VDiobuzPI+HZn9Q0BvgOvgPNgh5n7CSStql25HUG4Tw==", + "node_modules/eslint-plugin-import-x/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.22.1", - "comment-parser": "1.3.1", - "debug": "^4.3.4", - "escape-string-regexp": "^4.0.0", - "esquery": "^1.4.0", - "regextras": "^0.8.0", - "semver": "^7.3.5", - "spdx-expression-parse": "^3.0.1" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^12 || ^14 || ^16 || ^17" + "node": ">=16 || 14 >=14.17" }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "node_modules/eslint-plugin-import-x/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "50.6.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.6.tgz", + "integrity": "sha512-4jLo9NZqHfyNtiBxAU293eX1xi6oUIBcAxJJL/hHeeNhh26l4l/Apmu0x9SarvSQ/gWNOrnFci4DSPupN4//WA==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.49.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.6", + "escape-string-regexp": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", @@ -11793,13 +11452,10 @@ } }, "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -11807,95 +11463,197 @@ "node": ">=10" } }, - "node_modules/eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "dependencies": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/eslint-plugin-n": { + "version": "17.16.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.16.2.tgz", + "integrity": "sha512-iQM5Oj+9o0KaeLoObJC/uxNGpktZCkYiTTBo8PkRWq3HwNcRxwpvSDFjBhQ5+HLJzBTy+CLDC5+bw0Z5GyhlOQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.1", + "enhanced-resolve": "^5.17.1", + "eslint-plugin-es-x": "^7.8.0", + "get-tsconfig": "^4.8.1", + "globals": "^15.11.0", + "ignore": "^5.3.2", + "minimatch": "^9.0.5", + "semver": "^7.6.3" }, "engines": { - "node": ">=8.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" }, "peerDependencies": { - "eslint": ">=5.16.0" + "eslint": ">=8.23.0" } }, - "node_modules/eslint-plugin-node/node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" } }, - "node_modules/eslint-plugin-prebid": { - "resolved": "plugins/eslint", - "link": true + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/eslint-plugin-promise": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz", - "integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz", + "integrity": "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==", "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0" + }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" }, "peerDependencies": { - "eslint": "^7.0.0" + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/eslint-plugin-standard": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-3.1.0.tgz", - "integrity": "sha512-fVcdyuKRr0EZ4fjWl3c+gp1BANFJD1+RaWa2UPYfMZ6jCtp5RG00kSaXnK/dE5sYzt4kaWJ9qdxqUfc0d9kX0w==", + "node_modules/eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, "peerDependencies": { - "eslint": ">=3.19.0" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "esutils": "^2.0.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=0.10.0" } }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "node_modules/eslint-plugin-react/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" }, - "engines": { - "node": ">=6" + "bin": { + "resolve": "bin/resolve" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, "engines": { - "node": ">=4" + "node": ">=8.0.0" } }, "node_modules/eslint-visitor-keys": { @@ -11907,13 +11665,20 @@ "node": ">=10" } }, - "node_modules/eslint/node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "dependencies": { - "@babel/highlight": "^7.10.4" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/eslint/node_modules/ansi-styles": { @@ -11977,21 +11742,71 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "type-fest": "^0.20.2" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -12001,28 +11816,55 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" + "p-locate": "^5.0.0" }, - "bin": { - "semver": "bin/semver.js" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -12040,10 +11882,10 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "engines": { "node": ">=10" @@ -12052,27 +11894,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", "dev": true, "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=0.10" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, "engines": { - "node": ">=4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -12089,9 +11964,9 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -12193,6 +12068,15 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -12242,6 +12126,12 @@ "node": ">=4.8" } }, + "node_modules/execa/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/execa/node_modules/path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -12344,74 +12234,17 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "deprecated": "Please upgrade to v0.1.7", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "deprecated": "Please upgrade to v0.1.5", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/expand-brackets/node_modules/is-extendable": { @@ -12457,936 +12290,677 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/expect-webdriverio": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-4.9.3.tgz", - "integrity": "sha512-ASHsFc/QaK5ipF4ct3e8hd3elm8wNXk/Qa3EemtYDmfUQ4uzwqDf75m/QFQpwVNCjEpkNP7Be/6X9kz7bN0P9Q==", - "dev": true, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { - "@vitest/snapshot": "^1.2.1", - "expect": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "lodash.isequal": "^4.5.0" + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": ">=16 || >=18 || >=20" + "node": ">= 0.10.0" }, - "optionalDependencies": { - "@wdio/globals": "^8.27.0", - "@wdio/logger": "^8.24.12", - "webdriverio": "^8.27.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/expect-webdriverio/node_modules/@puppeteer/browsers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", - "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", - "dev": true, - "optional": true, - "peer": true, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "http-proxy-agent": "5.0.0", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "bin": { - "browsers": "lib/cjs/main-cli.js" + "mime": "cli.js" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=4" } }, - "node_modules/expect-webdriverio/node_modules/@types/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", - "dev": true, - "optional": true, - "peer": true + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/expect-webdriverio/node_modules/archiver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.1.tgz", - "integrity": "sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==", - "dev": true, - "optional": true, + "node_modules/express/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { - "archiver-utils": "^4.0.1", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^5.0.1" + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 0.8.0" } }, - "node_modules/expect-webdriverio/node_modules/archiver-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz", - "integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==", - "dev": true, - "optional": true, - "dependencies": { - "glob": "^8.0.0", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, + "node_modules/express/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "engines": { - "node": ">= 12.0.0" + "node": ">= 0.8" } }, - "node_modules/expect-webdriverio/node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true, - "optional": true + "node_modules/express/node_modules/send/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==" }, - "node_modules/expect-webdriverio/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, - "optional": true, "dependencies": { - "balanced-match": "^1.0.0" + "type": "^2.7.2" } }, - "node_modules/expect-webdriverio/node_modules/chrome-launcher": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.0.tgz", - "integrity": "sha512-rJYWeEAERwWIr3c3mEVXwNiODPEdMRlRxHc47B1qHPOolHZnkj7rMv1QSUfPoG6MgatWj5AxSpnKKR4QEwEQIQ==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^2.0.1" - }, - "bin": { - "print-chrome-path": "bin/print-chrome-path.js" + "kind-of": "^1.1.0" }, "engines": { - "node": ">=12.13.0" + "node": ">=0.10.0" } }, - "node_modules/expect-webdriverio/node_modules/compress-commons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", - "integrity": "sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==", + "node_modules/extend-shallow/node_modules/kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", "dev": true, - "optional": true, - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^5.0.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, "engines": { - "node": ">= 12.0.0" + "node": ">=0.10.0" } }, - "node_modules/expect-webdriverio/node_modules/crc32-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.0.tgz", - "integrity": "sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==", + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, - "optional": true, "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" }, "engines": { - "node": ">= 12.0.0" + "node": ">=4" } }, - "node_modules/expect-webdriverio/node_modules/cross-fetch": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", - "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", - "dev": true, - "optional": true, - "peer": true, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, "dependencies": { - "node-fetch": "^2.6.11" + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" } }, - "node_modules/expect-webdriverio/node_modules/devtools": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/devtools/-/devtools-8.29.1.tgz", - "integrity": "sha512-fbH0Z7CPK4OZSgUw2QcAppczowxtSyvFztPUmiFyi99cUadjEOwlg0aL3pBVlIDo67olYjGb8GD1M5Z4yI/P6w==", + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "chrome-launcher": "^1.0.0", - "edge-paths": "^3.0.5", - "import-meta-resolve": "^4.0.0", - "puppeteer-core": "20.3.0", - "query-selector-shadow-dom": "^1.0.0", - "ua-parser-js": "^1.0.1", - "uuid": "^9.0.0", - "which": "^4.0.0" + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "engines": { - "node": "^16.13 || >=18" + "node": ">=0.10.0" } }, - "node_modules/expect-webdriverio/node_modules/devtools-protocol": { - "version": "0.0.1120988", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", - "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/expect-webdriverio/node_modules/devtools/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" + "is-descriptor": "^1.0.0" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/expect-webdriverio/node_modules/edge-paths": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", - "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "@types/which": "^2.0.1", - "which": "^2.0.2" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/shirshak55" + "node": ">=0.10.0" } }, - "node_modules/expect-webdriverio/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, - "optional": true, - "peer": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/expect-webdriverio/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, - "optional": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" }, "engines": { - "node": ">=12" + "node": ">= 10.17.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "optionalDependencies": { + "@types/yauzl": "^2.9.1" } }, - "node_modules/expect-webdriverio/node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, - "optional": true, "dependencies": { - "brace-expansion": "^2.0.1" + "pump": "^3.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/expect-webdriverio/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "node": ">=8" }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/expect-webdriverio/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=16" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/expect-webdriverio/node_modules/lighthouse-logger": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", - "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "node_modules/extract-zip/node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, - "optional": true, - "peer": true, "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } }, - "node_modules/expect-webdriverio/node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } + "engines": [ + "node >=0.6.0" + ] }, - "node_modules/expect-webdriverio/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "optional": true, - "engines": { - "node": ">=12" - } + "node_modules/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==", + "dev": true }, - "node_modules/expect-webdriverio/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", "dev": true, - "optional": true, "dependencies": { - "brace-expansion": "^2.0.1" + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.10" } }, - "node_modules/expect-webdriverio/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "optional": true, - "peer": true + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true }, - "node_modules/expect-webdriverio/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "optional": true, "dependencies": { - "whatwg-url": "^5.0.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=8.6.0" } }, - "node_modules/expect-webdriverio/node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "dev": true, - "optional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" + "strnum": "^1.0.5" }, - "engines": { - "node": ">= 14" + "bin": { + "fxparser": "src/cli/cli.js" } }, - "node_modules/expect-webdriverio/node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "optional": true, "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "reusify": "^1.0.4" } }, - "node_modules/expect-webdriverio/node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "node_modules/faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==", "dev": true, - "optional": true, "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "websocket-driver": ">=0.5.1" }, "engines": { - "node": ">= 14" + "node": ">=0.4.0" } }, - "node_modules/expect-webdriverio/node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, - "optional": true, "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "pend": "~1.2.0" } }, - "node_modules/expect-webdriverio/node_modules/puppeteer-core": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.3.0.tgz", - "integrity": "sha512-264pBrIui5bO6NJeOcbJrLa0OCwmA4+WK00JMrLIKTfRiqe2gx8KWTzLsjyw/bizErp3TKS7vt/I0i5fTC+mAw==", + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "dev": true, - "optional": true, - "peer": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], "dependencies": { - "@puppeteer/browsers": "1.3.0", - "chromium-bidi": "0.4.9", - "cross-fetch": "3.1.6", - "debug": "4.3.4", - "devtools-protocol": "0.0.1120988", - "ws": "8.13.0" + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": "^12.20 || >= 14.13" } }, - "node_modules/expect-webdriverio/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "dev": true, - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, "engines": { - "node": ">= 6" + "node": ">= 8" } }, - "node_modules/expect-webdriverio/node_modules/serialize-error": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "dev": true, - "optional": true, "dependencies": { - "type-fest": "^2.12.2" + "is-unicode-supported": "^2.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/expect-webdriverio/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "optional": true, "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/expect-webdriverio/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "dev": true, - "optional": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "optional": true }, - "node_modules/expect-webdriverio/node_modules/ua-parser-js": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", - "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "optional": true, - "peer": true, - "engines": { - "node": "*" + "dependencies": { + "minimatch": "^5.0.1" } }, - "node_modules/expect-webdriverio/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "optional": true, - "peer": true, - "bin": { - "uuid": "dist/bin/uuid" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/expect-webdriverio/node_modules/webdriverio": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.29.1.tgz", - "integrity": "sha512-NZK95ivXCqdPraB3FHMw6ByxnCvtgFXkjzG2l3Oq5z0IuJS2aMow3AKFIyiuG6is/deGCe+Tb8eFTCqak7UV+w==", + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, - "optional": true, "dependencies": { - "@types/node": "^20.1.0", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/repl": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "archiver": "^6.0.0", - "aria-query": "^5.0.0", - "css-shorthand-properties": "^1.1.1", - "css-value": "^0.0.1", - "devtools-protocol": "^0.0.1249869", - "grapheme-splitter": "^1.0.2", - "import-meta-resolve": "^4.0.0", - "is-plain-obj": "^4.1.0", - "lodash.clonedeep": "^4.5.0", - "lodash.zip": "^4.2.0", - "minimatch": "^9.0.0", - "puppeteer-core": "^20.9.0", - "query-selector-shadow-dom": "^1.0.0", - "resq": "^1.9.1", - "rgb2hex": "0.2.5", - "serialize-error": "^11.0.1", - "webdriver": "8.29.1" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^16.13 || >=18" - }, - "peerDependencies": { - "devtools": "^8.14.0" - }, - "peerDependenciesMeta": { - "devtools": { - "optional": true - } + "node": ">=10" } }, - "node_modules/expect-webdriverio/node_modules/webdriverio/node_modules/@puppeteer/browsers": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", - "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "optional": true, "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.3.0", - "tar-fs": "3.0.4", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=8" } }, - "node_modules/expect-webdriverio/node_modules/webdriverio/node_modules/chromium-bidi": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", - "integrity": "sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==", - "dev": true, - "optional": true, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { - "mitt": "3.0.0" + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" }, - "peerDependencies": { - "devtools-protocol": "*" + "engines": { + "node": ">= 0.8" } }, - "node_modules/expect-webdriverio/node_modules/webdriverio/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dev": true, - "optional": true, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { - "node-fetch": "^2.6.12" + "ms": "2.0.0" } }, - "node_modules/expect-webdriverio/node_modules/webdriverio/node_modules/devtools-protocol": { - "version": "0.0.1249869", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1249869.tgz", - "integrity": "sha512-Ctp4hInA0BEavlUoRy9mhGq0i+JSo/AwVyX2EFgZmV1kYB+Zq+EMBAn52QWu6FbRr10hRb6pBl420upbp4++vg==", - "dev": true, - "optional": true + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/expect-webdriverio/node_modules/webdriverio/node_modules/puppeteer-core": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.9.0.tgz", - "integrity": "sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==", + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, - "optional": true, "dependencies": { - "@puppeteer/browsers": "1.4.6", - "chromium-bidi": "0.4.16", - "cross-fetch": "4.0.0", - "debug": "4.3.4", - "devtools-protocol": "0.0.1147663", - "ws": "8.13.0" + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" }, "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" + "node": ">=8" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, - "node_modules/expect-webdriverio/node_modules/webdriverio/node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1147663", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", - "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", - "dev": true, - "optional": true - }, - "node_modules/expect-webdriverio/node_modules/webdriverio/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "optional": true, "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "node_modules/expect-webdriverio/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true, - "optional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/expect-webdriverio/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "node_modules/findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "dev": true, - "optional": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" }, "engines": { - "node": ">=12" + "node": ">= 0.10" } }, - "node_modules/expect-webdriverio/node_modules/zip-stream": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.1.tgz", - "integrity": "sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==", + "node_modules/findup-sync/node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", "dev": true, - "optional": true, - "dependencies": { - "archiver-utils": "^4.0.1", - "compress-commons": "^5.0.1", - "readable-stream": "^3.6.0" - }, "engines": { - "node": ">= 12.0.0" + "node": ">=0.10.0" } }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "node_modules/findup-sync/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dev": true, - "dependencies": { - "type": "^2.7.2" + "node": ">=0.10.0" } }, - "node_modules/ext/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "dev": true - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { - "kind-of": "^1.1.0" + "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/extend-shallow/node_modules/kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "node_modules/findup-sync/node_modules/braces/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "node_modules/findup-sync/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "dev": true, "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "node_modules/findup-sync/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "dev": true, "dependencies": { - "is-descriptor": "^1.0.0" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/extglob/node_modules/extend-shallow": { + "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", @@ -13398,7 +12972,7 @@ "node": ">=0.10.0" } }, - "node_modules/extglob/node_modules/is-extendable": { + "node_modules/findup-sync/node_modules/fill-range/node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", @@ -13407,1279 +12981,793 @@ "node": ">=0.10.0" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "node_modules/findup-sync/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/findup-sync/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" + "kind-of": "^3.0.2" }, "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" + "node": ">=0.10.0" } }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "pump": "^3.0.0" + "is-buffer": "^1.1.5" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/faker": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", - "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==", - "dev": true - }, - "node_modules/fancy-log": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "node_modules/findup-sync/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "dependencies": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==", + "node_modules/findup-sync/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, "dependencies": { - "websocket-driver": ">=0.5.1" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "engines": { - "node": ">=0.4.0" + "node": ">=0.10.0" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", "dev": true, "dependencies": { - "pend": "~1.2.0" + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "dev": true - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "node_modules/fined/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "isobject": "^3.0.1" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=0.10.0" } }, - "node_modules/fetch-blob/node_modules/web-streams-polyfill": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", - "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", "dev": true, "engines": { - "node": ">= 8" + "node": ">= 0.10" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "flat": "cli.js" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, "dependencies": { - "minimatch": "^5.0.1" + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "is-callable": "^1.2.7" }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "for-in": "^1.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "node": ">=0.10.0" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", + "dev": true }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=8" + "node": ">=14" }, "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/find-up": { + "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, "engines": { - "node": ">= 0.10" + "node": "*" } }, - "node_modules/findup-sync/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/fork-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", + "integrity": "sha512-Pqq5NnT78ehvUnAk/We/Jr22vSvanRlFTpAmQ88xBY/M1TlHe+P0ILuEyXS595ysdGfaj22634LBkGMA2GTcpA==", + "dev": true }, - "node_modules/findup-sync/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.12" } }, - "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/formdata-node": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-5.0.1.tgz", + "integrity": "sha512-8xnIjMYGKPj+rY2BTbAmpqVpi8der/2FT4d9f7J32FlsCpO5EzZPq3C/N56zdv8KweHzVF6TGijsS1JT6r1H2g==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/braces/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 14.17" } }, - "node_modules/findup-sync/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "dev": true, "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "fetch-blob": "^3.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=12.20.0" } }, - "node_modules/findup-sync/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "map-cache": "^0.2.2" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/findup-sync/node_modules/fill-range/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/findup-sync/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", "dev": true }, - "node_modules/findup-sync/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "node_modules/fs-extra": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz", + "integrity": "sha512-5rU898vl/Z948L+kkJedbmo/iltzmiF5bn/eEk0j/SgrPpI+Ydau9xlJPicV7Av2CHYBGz5LAlwTnBU80j1zPQ==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "jsonfile": "~1.0.1", + "mkdirp": "0.3.x", + "ncp": "~0.4.2", + "rimraf": "~2.2.0" } }, - "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/fs-extra/node_modules/mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true }, - "node_modules/findup-sync/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/fs-extra/node_modules/rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, - "engines": { - "node": ">=0.10.0" + "bin": { + "rimraf": "bin.js" } }, - "node_modules/findup-sync/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "node_modules/fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", "dev": true, "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/findup-sync/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "node_modules/fs-mkdirp-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, + "node_modules/fs-readfile-promise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-3.0.1.tgz", + "integrity": "sha512-LsSxMeaJdYH27XrW7Dmq0Gx63mioULCRel63B5VeELYLavi1wF5s0XfsIdKDFdCL9hsfQ2qBvXJszQtQJ9h17A==", "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" + "graceful-fs": "^4.1.11" } }, - "node_modules/fined/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/fs.extra": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fs.extra/-/fs.extra-1.3.2.tgz", + "integrity": "sha512-Ig401VXtyrWrz23k9KxAx9OrnL8AHSLNhQ8YJH2wSYuH0ZUfxwBeY6zXkd/oOyVRFTlpEu/0n5gHeuZt7aqbkw==", "dev": true, "dependencies": { - "isobject": "^3.0.1" + "fs-extra": "~0.6.1", + "mkdirp": "~0.3.5", + "walk": "^2.3.9" }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true, - "engines": { - "node": ">= 0.10" - } + "node_modules/fs.extra/node_modules/mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=0.6" } }, - "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "node_modules/flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">=4.0" + "node": "*" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" } }, - "node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", - "dev": true, + "node_modules/fun-hooks": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/fun-hooks/-/fun-hooks-0.9.10.tgz", + "integrity": "sha512-7xBjdT+oMYOPWgwFxNiNzF4ubeUvim4zs1DnQqSSGyxu8UD7AW/6Z0iFsVRwuVSIZKUks2en2VHHotmNfj3ipw==", "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "typescript-tuple": "^2.2.1" } }, - "node_modules/foreachasync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", - "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", - "dev": true + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, - "engines": { - "node": ">=14" - }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "dev": true, + "dependencies": { + "globule": "^1.0.0" + }, "engines": { - "node": "*" + "node": ">= 4.0.0" } }, - "node_modules/fork-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", - "integrity": "sha512-Pqq5NnT78ehvUnAk/We/Jr22vSvanRlFTpAmQ88xBY/M1TlHe+P0ILuEyXS595ysdGfaj22634LBkGMA2GTcpA==", - "dev": true - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "node_modules/geckodriver": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.4.1.tgz", + "integrity": "sha512-nnAdIrwLkMcDu4BitWXF23pEMeZZ0Cj7HaWWFdSpeedBP9z6ft150JYiGO2mwzw6UiR823Znk1JeIf07RyzloA==", "dev": true, + "hasInstallScript": true, "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "@wdio/logger": "^8.28.0", + "@zip.js/zip.js": "^2.7.44", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "node-fetch": "^3.3.2", + "tar-fs": "^3.0.6", + "which": "^4.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" }, "engines": { - "node": ">= 0.12" + "node": "^16.13 || >=18 || >=20" } }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "node_modules/geckodriver/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, "engines": { - "node": ">= 14.17" + "node": ">= 14" } }, - "node_modules/formdata-node": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-5.0.1.tgz", - "integrity": "sha512-8xnIjMYGKPj+rY2BTbAmpqVpi8der/2FT4d9f7J32FlsCpO5EzZPq3C/N56zdv8KweHzVF6TGijsS1JT6r1H2g==", + "node_modules/geckodriver/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" + "agent-base": "^7.0.2", + "debug": "4" }, "engines": { - "node": ">= 14.17" + "node": ">= 14" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/geckodriver/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dev": true, "dependencies": { - "fetch-blob": "^3.1.2" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" }, - "engines": { - "node": ">=12.20.0" + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "engines": { - "node": ">= 0.6" + "node": ">=6.9.0" } }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "dependencies": { - "map-cache": "^0.2.2" - }, "engines": { - "node": ">=0.10.0" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, "engines": { - "node": ">= 0.6" + "node": "*" } }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - }, "engines": { - "node": ">= 0.10" + "node": ">=8.0.0" } }, - "node_modules/fs-mkdirp-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/fs-readfile-promise": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-3.0.1.tgz", - "integrity": "sha512-LsSxMeaJdYH27XrW7Dmq0Gx63mioULCRel63B5VeELYLavi1wF5s0XfsIdKDFdCL9hsfQ2qBvXJszQtQJ9h17A==", - "dependencies": { - "graceful-fs": "^4.1.11" + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fs.extra": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fs.extra/-/fs.extra-1.3.2.tgz", - "integrity": "sha512-Ig401VXtyrWrz23k9KxAx9OrnL8AHSLNhQ8YJH2wSYuH0ZUfxwBeY6zXkd/oOyVRFTlpEu/0n5gHeuZt7aqbkw==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dependencies": { - "fs-extra": "~0.6.1", - "mkdirp": "~0.3.5", - "walk": "^2.3.9" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "*" + "node": ">= 0.4" } }, - "node_modules/fs.extra/node_modules/fs-extra": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz", - "integrity": "sha512-5rU898vl/Z948L+kkJedbmo/iltzmiF5bn/eEk0j/SgrPpI+Ydau9xlJPicV7Av2CHYBGz5LAlwTnBU80j1zPQ==", + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, "dependencies": { - "jsonfile": "~1.0.1", - "mkdirp": "0.3.x", - "ncp": "~0.4.2", - "rimraf": "~2.2.0" - } - }, - "node_modules/fs.extra/node_modules/jsonfile": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz", - "integrity": "sha512-KbsDJNRfRPF5v49tMNf9sqyyGqGLBcz1v5kZT01kG5ns5mQSltwxCKVmUzVKtEinkUnTDtSrp6ngWpV7Xw0ZlA==", - "dev": true - }, - "node_modules/fs.extra/node_modules/mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", - "dev": true - }, - "node_modules/fs.extra/node_modules/rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "pump": "^3.0.0" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "deprecated": "This package is no longer supported.", + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" + "node": ">= 0.4" }, - "bin": { - "mkdirp": "bin/cmd.js" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "resolve-pkg-maps": "^1.0.0" }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/fun-hooks": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/fun-hooks/-/fun-hooks-0.9.10.tgz", - "integrity": "sha512-7xBjdT+oMYOPWgwFxNiNzF4ubeUvim4zs1DnQqSSGyxu8UD7AW/6Z0iFsVRwuVSIZKUks2en2VHHotmNfj3ipw==", - "dependencies": { - "typescript-tuple": "^2.2.1" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 14" } }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 14" } }, - "node_modules/gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { - "globule": "^1.0.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">= 4.0.0" + "node": ">=14.14" } }, - "node_modules/geckodriver": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.3.1.tgz", - "integrity": "sha512-ol7JLsj55o5k+z7YzeSy2mdJROXMAxIa+uzr3A1yEMr5HISqQOTslE3ZeARcxR4jpAY3fxmHM+sq32qbe/eXfA==", + "node_modules/get-uri/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@wdio/logger": "^8.24.12", - "decamelize": "^6.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "node-fetch": "^3.3.2", - "tar-fs": "^3.0.4", - "unzipper": "^0.10.14", - "which": "^4.0.0" - }, - "bin": { - "geckodriver": "bin/geckodriver.js" + "universalify": "^2.0.0" }, - "engines": { - "node": "^16.13 || >=18 || >=20" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/geckodriver/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, "engines": { - "node": ">= 14" + "node": ">=0.10.0" } }, - "node_modules/geckodriver/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, - "engines": { - "node": ">= 12" + "dependencies": { + "assert-plus": "^1.0.0" } }, - "node_modules/geckodriver/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "node_modules/git-repo-info": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/git-repo-info/-/git-repo-info-2.1.1.tgz", + "integrity": "sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==", "dev": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/geckodriver/node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/geckodriver/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/geckodriver/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/geckodriver/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/geckodriver/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dev": true, - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "node_modules/geckodriver/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/geckodriver/node_modules/unzipper": { - "version": "0.10.14", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", - "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/geckodriver/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-port": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz", - "integrity": "sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-uri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.2.tgz", - "integrity": "sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.0", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/get-uri/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/get-uri/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/git-repo-info": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/git-repo-info/-/git-repo-info-2.1.1.tgz", - "integrity": "sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==", - "dev": true, - "engines": { - "node": ">= 4.0" + "node": ">= 4.0" } }, "node_modules/git-up": { @@ -14693,9 +13781,9 @@ } }, "node_modules/git-url-parse": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", - "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.1.tgz", + "integrity": "sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==", "dev": true, "dependencies": { "git-up": "^7.0.0" @@ -14723,21 +13811,20 @@ "dev": true }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -14776,10 +13863,31 @@ "node": ">= 0.10" } }, - "node_modules/glob-stream/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "node_modules/glob-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-stream/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "dependencies": { "is-glob": "^3.1.0", @@ -15012,25 +14120,25 @@ "node": ">=0.10.0" } }, - "node_modules/glob-watcher/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/glob-watcher/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "isobject": "^3.0.1" + "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/glob-watcher/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/glob-watcher/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "dependencies": { - "is-buffer": "^1.1.5" + "isobject": "^3.0.1" }, "engines": { "node": ">=0.10.0" @@ -15085,15 +14193,6 @@ "node": ">=0.10.0" } }, - "node_modules/glob-watcher/node_modules/micromatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/glob-watcher/node_modules/readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -15121,6 +14220,30 @@ "node": ">=0.10.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", @@ -15167,6 +14290,12 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/global-prefix/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/global-prefix/node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -15180,11 +14309,15 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "dev": true, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globals-docs": { @@ -15193,6 +14326,22 @@ "integrity": "sha512-qpPnUKkWnz8NESjrCvnlGklsgiQzlq+rcCxoG5uNQ+dNA7cFMCmn231slLAwS2N/PlkzZ3COL8CcS10jXmLHqg==", "dev": true }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globule": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", @@ -15253,45 +14402,20 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.5", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", - "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "engines": { - "node": ">=10.19.0" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/grapheme-splitter": { "version": "1.0.4", @@ -15299,6 +14423,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/gulp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", @@ -15333,6 +14463,27 @@ "node": ">=0.9" } }, + "node_modules/gulp-clean/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gulp-clean/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -15664,129 +14815,178 @@ "node": ">=6" } }, - "node_modules/gulp-connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/gulp-if": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz", + "integrity": "sha512-fCUEngzNiEZEK2YuPm+sdMpO6ukb8+/qzbGfJBXyNOXz85bCG7yBI+pPSl+N90d7gnLvMsarthsAImx0qy7BAw==", "dev": true, "dependencies": { - "ms": "2.0.0" + "gulp-match": "^1.1.0", + "ternary-stream": "^3.0.0", + "through2": "^3.0.1" } }, - "node_modules/gulp-connect/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "node_modules/gulp-if/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "dev": true, - "engines": { - "node": ">= 0.6" + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" } }, - "node_modules/gulp-connect/node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", - "dev": true - }, - "node_modules/gulp-connect/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "node_modules/gulp-js-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gulp-js-escape/-/gulp-js-escape-1.0.1.tgz", + "integrity": "sha512-F+53crhLb78CTlG7ZZJFWzP0+/4q0vt2/pULXFkTMs6AGBo0Eh5cx+eWsqqHv8hrNIUsuTab3Se8rOOzP/6+EQ==", "dev": true, "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" + "through2": "^0.6.3" } }, - "node_modules/gulp-connect/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "node_modules/gulp-js-escape/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "dev": true }, - "node_modules/gulp-connect/node_modules/mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "node_modules/gulp-js-escape/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", "dev": true, - "bin": { - "mime": "cli.js" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, - "node_modules/gulp-connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/gulp-js-escape/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true }, - "node_modules/gulp-connect/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "node_modules/gulp-js-escape/node_modules/through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", "dev": true, "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" } }, - "node_modules/gulp-connect/node_modules/send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "node_modules/gulp-match": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.1.0.tgz", + "integrity": "sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==", "dev": true, "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "engines": { - "node": ">= 0.8.0" + "minimatch": "^3.0.3" } }, - "node_modules/gulp-connect/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "node_modules/gulp-connect/node_modules/statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "node_modules/gulp-rename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.0.0.tgz", + "integrity": "sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ==", "dev": true, "engines": { - "node": ">= 0.6" + "node": ">=4" } }, - "node_modules/gulp-eslint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gulp-eslint/-/gulp-eslint-6.0.0.tgz", - "integrity": "sha512-dCVPSh1sA+UVhn7JSQt7KEb4An2sQNbOdB3PA8UCfxsoPlAKjJHxYHGXdXC7eb+V1FAnilSFFqslPrq037l1ig==", + "node_modules/gulp-replace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.4.tgz", + "integrity": "sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==", "dev": true, "dependencies": { - "eslint": "^6.0.0", - "fancy-log": "^1.3.2", - "plugin-error": "^1.0.1" + "@types/node": "*", + "@types/vinyl": "^2.0.4", + "istextorbinary": "^3.0.0", + "replacestream": "^4.0.3", + "yargs-parser": ">=5.0.0-security.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gulp-run-command": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/gulp-run-command/-/gulp-run-command-0.0.10.tgz", + "integrity": "sha512-i6o4XRqoadZB2doWCqkrCe7FmFwqPZ0Fxx74FGt83/KT5wKRRaKiFh598W64HE0Br9es6Oyq+nA+/AWbCfeSYQ==", + "dev": true, + "dependencies": { + "babel-plugin-transform-runtime": "6.15.0", + "cross-spawn": "4.0.0", + "spawn-args": "0.2.0", + "timeout-as-promise": "^1.0.0" + } + }, + "node_modules/gulp-run-command/node_modules/cross-spawn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.0.tgz", + "integrity": "sha512-dAZV+Hv1PRxSUrJd9Hk9MS4gL5eEafKhrmsRlod5oHg8aP3A2FsXkga4ihfMFxlEgmMa+LS84jPKFdhk5wZwuw==", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "node_modules/gulp-run-command/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/gulp-run-command/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/gulp-run-command/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/gulp-run-command/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/gulp-shell": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/gulp-shell/-/gulp-shell-0.8.0.tgz", + "integrity": "sha512-wHNCgmqbWkk1c6Gc2dOL5SprcoeujQdeepICwfQRo91DIylTE7a794VEE+leq3cE2YDoiS5ulvRfKVIEMazcTQ==", + "dev": true, + "dependencies": { + "chalk": "^3.0.0", + "fancy-log": "^1.3.3", + "lodash.template": "^4.5.0", + "plugin-error": "^1.0.1", + "through2": "^3.0.1", + "tslib": "^1.10.0" + }, + "engines": { + "node": ">=10.0.0" } }, - "node_modules/gulp-eslint/node_modules/ansi-colors": { + "node_modules/gulp-shell/node_modules/ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", @@ -15798,16 +14998,22 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "node_modules/gulp-shell/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/gulp-eslint/node_modules/arr-diff": { + "node_modules/gulp-shell/node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", @@ -15816,7 +15022,7 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/arr-union": { + "node_modules/gulp-shell/node_modules/arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", @@ -15825,16 +15031,20 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "node_modules/gulp-shell/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/gulp-eslint/node_modules/color-convert": { + "node_modules/gulp-shell/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", @@ -15846,1289 +15056,1280 @@ "node": ">=7.0.0" } }, - "node_modules/gulp-eslint/node_modules/color-name": { + "node_modules/gulp-shell/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/gulp-eslint/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "node_modules/gulp-shell/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "dev": true, "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "engines": { - "node": ">=4.8" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/gulp-shell/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "bin": { - "semver": "bin/semver" + "engines": { + "node": ">=8" } }, - "node_modules/gulp-eslint/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/gulp-eslint/node_modules/eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "node_modules/gulp-shell/node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" }, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 0.10" } }, - "node_modules/gulp-eslint/node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "node_modules/gulp-shell/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, "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/gulp-eslint/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/gulp-shell/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "dev": true, - "engines": { - "node": ">=4" + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" } }, - "node_modules/gulp-eslint/node_modules/eslint/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "node_modules/gulp-sourcemaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", + "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", "dev": true, "dependencies": { - "ansi-regex": "^4.1.0" + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" }, "engines": { - "node": ">=6" + "node": ">= 6" } }, - "node_modules/gulp-eslint/node_modules/espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "node_modules/gulp-sourcemaps/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true, - "dependencies": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=6.0.0" + "node": ">=0.4.0" } }, - "node_modules/gulp-eslint/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "node_modules/gulp-sourcemaps/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/gulp-sourcemaps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "node_modules/gulp-sourcemaps/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, "dependencies": { - "flat-cache": "^2.0.1" - }, - "engines": { - "node": ">=4" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/gulp-eslint/node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "node_modules/gulp-terser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gulp-terser/-/gulp-terser-2.1.0.tgz", + "integrity": "sha512-lQ3+JUdHDVISAlUIUSZ/G9Dz/rBQHxOiYDQ70IVWFQeh4b33TC1MCIU+K18w07PS3rq/CVc34aQO4SUbdaNMPQ==", "dev": true, "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "plugin-error": "^1.0.1", + "terser": "^5.9.0", + "through2": "^4.0.2", + "vinyl-sourcemaps-apply": "^0.2.1" }, "engines": { - "node": ">=4" + "node": ">=10" } }, - "node_modules/gulp-eslint/node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "node_modules/gulp-eslint/node_modules/globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "node_modules/gulp-terser/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, "dependencies": { - "type-fest": "^0.8.1" + "ansi-wrap": "^0.1.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/has-flag": { + "node_modules/gulp-terser/node_modules/arr-diff": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "node_modules/gulp-terser/node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, "engines": { - "node": ">=8.0.0" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/gulp-terser/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/gulp-terser/node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/gulp-eslint/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, "engines": { - "node": ">=4" + "node": ">= 0.10" } }, - "node_modules/gulp-eslint/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "node_modules/gulp-util": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", + "integrity": "sha512-q5oWPc12lwSFS9h/4VIjG+1NuNDlJ48ywV2JKItY4Ycc/n1fXJeYPVQsfu5ZrhQi7FGSDBalwUCLar/GyHXKGw==", + "deprecated": "gulp-util is deprecated - replace it, following the guidelines at https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5", "dev": true, "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "array-differ": "^1.0.0", + "array-uniq": "^1.0.2", + "beeper": "^1.0.0", + "chalk": "^1.0.0", + "dateformat": "^2.0.0", + "fancy-log": "^1.1.0", + "gulplog": "^1.0.0", + "has-gulplog": "^0.1.0", + "lodash._reescape": "^3.0.0", + "lodash._reevaluate": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.template": "^3.0.0", + "minimist": "^1.1.0", + "multipipe": "^0.1.2", + "object-assign": "^3.0.0", + "replace-ext": "0.0.1", + "through2": "^2.0.0", + "vinyl": "^0.5.0" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/gulp-eslint/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" + "node": ">=0.10" } }, - "node_modules/gulp-eslint/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "node_modules/gulp-util/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "node_modules/gulp-util/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", "dev": true, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "node_modules/gulp-util/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "dev": true, "dependencies": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "node_modules/gulp-util/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, "engines": { - "node": ">= 0.8.0" + "node": ">=0.8" } }, - "node_modules/gulp-eslint/node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "engines": { - "node": ">=6.5.0" - } + "node_modules/gulp-util/node_modules/clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha512-dhUqc57gSMCo6TX85FLfe51eC/s+Im2MLkAgJwfaRRexR2tA4dd3eLEW4L6efzHc2iNorrRRXITifnDLlRrhaA==", + "dev": true }, - "node_modules/gulp-eslint/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/gulp-util/node_modules/lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha512-0B4Y53I0OgHUJkt+7RmlDFWKjVAI/YUpWNiL9GQz5ORDr4ttgfQGo+phBWKFLJbBdtOwgMuUkdOHOnPg45jKmQ==", "dev": true, "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "lodash._basecopy": "^3.0.0", + "lodash._basetostring": "^3.0.0", + "lodash._basevalues": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0", + "lodash.keys": "^3.0.0", + "lodash.restparam": "^3.0.0", + "lodash.templatesettings": "^3.0.0" } }, - "node_modules/gulp-eslint/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "node_modules/gulp-util/node_modules/lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha512-TcrlEr31tDYnWkHFWDCV3dHYroKEXpJZ2YJYvJdhN+y4AkWMDZ5I4I8XDtUKqSAyG81N7w+I1mFEJtcED+tGqQ==", "dev": true, "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0" } }, - "node_modules/gulp-eslint/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "node_modules/gulp-util/node_modules/object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "node_modules/gulp-util/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-regex": "^2.0.0" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/gulp-util/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", "dev": true, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.0" } }, - "node_modules/gulp-eslint/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==", + "node_modules/gulp-util/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/gulp-eslint/node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "node_modules/gulp-util/node_modules/vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha512-P5zdf3WB9uzr7IFoVQ2wZTmUwHL8cMZWJGzLBNCHNZ3NB6HTMsYABtt7z8tAGIINLXyAob9B9a1yzVGMFOYKEA==", "dev": true, "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.9" } }, - "node_modules/gulp-eslint/node_modules/table/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, + "node_modules/gulp-wrap": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/gulp-wrap/-/gulp-wrap-0.15.0.tgz", + "integrity": "sha512-f17zkGObA+hE/FThlg55gfA0nsXbdmHK1WqzjjB2Ytq1TuhLR7JiCBJ3K4AlMzCyoFaCjfowos+VkToUNE0WTQ==", "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "consolidate": "^0.15.1", + "es6-promise": "^4.2.6", + "fs-readfile-promise": "^3.0.1", + "js-yaml": "^3.13.0", + "lodash": "^4.17.11", + "node.extend": "2.0.2", + "plugin-error": "^1.0.1", + "through2": "^3.0.1", + "tryit": "^1.0.1", + "vinyl-bufferstream": "^1.0.1" }, "engines": { - "node": ">=6" + "node": ">=6.14", + "npm": ">=1.4.3" } }, - "node_modules/gulp-eslint/node_modules/table/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, + "node_modules/gulp-wrap/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dependencies": { - "ansi-regex": "^4.1.0" + "ansi-wrap": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, + "node_modules/gulp-wrap/node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, + "node_modules/gulp-wrap/node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gulp-eslint/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, + "node_modules/gulp-wrap/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "dependencies": { - "isexe": "^2.0.0" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, - "bin": { - "which": "bin/which" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-if": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz", - "integrity": "sha512-fCUEngzNiEZEK2YuPm+sdMpO6ukb8+/qzbGfJBXyNOXz85bCG7yBI+pPSl+N90d7gnLvMsarthsAImx0qy7BAw==", - "dev": true, + "node_modules/gulp-wrap/node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "dependencies": { - "gulp-match": "^1.1.0", - "ternary-stream": "^3.0.0", - "through2": "^3.0.1" + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/gulp-if/node_modules/through2": { + "node_modules/gulp-wrap/node_modules/through2": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, "dependencies": { "inherits": "^2.0.4", "readable-stream": "2 || 3" } }, - "node_modules/gulp-js-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gulp-js-escape/-/gulp-js-escape-1.0.1.tgz", - "integrity": "sha512-F+53crhLb78CTlG7ZZJFWzP0+/4q0vt2/pULXFkTMs6AGBo0Eh5cx+eWsqqHv8hrNIUsuTab3Se8rOOzP/6+EQ==", + "node_modules/gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", "dev": true, "dependencies": { - "through2": "^0.6.3" + "glogg": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/gulp-js-escape/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true - }, - "node_modules/gulp-js-escape/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gulp-js-escape/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true - }, - "node_modules/gulp-js-escape/node_modules/through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "dependencies": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "node_modules/gulp-match": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.1.0.tgz", - "integrity": "sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==", + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "dependencies": { - "minimatch": "^3.0.3" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-rename": { + "node_modules/har-schema": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.0.0.tgz", - "integrity": "sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ==", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", "dev": true, "engines": { "node": ">=4" } }, - "node_modules/gulp-replace": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.3.tgz", - "integrity": "sha512-HcPHpWY4XdF8zxYkDODHnG2+7a3nD/Y8Mfu3aBgMiCFDW3X2GiOKXllsAmILcxe3KZT2BXoN18WrpEFm48KfLQ==", + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", "dev": true, "dependencies": { - "@types/node": "^14.14.41", - "@types/vinyl": "^2.0.4", - "istextorbinary": "^3.0.0", - "replacestream": "^4.0.3", - "yargs-parser": ">=5.0.0-security.0" + "ajv": "^6.12.3", + "har-schema": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" } }, - "node_modules/gulp-replace/node_modules/@types/node": { - "version": "14.18.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.33.tgz", - "integrity": "sha512-qelS/Ra6sacc4loe/3MSjXNL1dNQ/GjxNHVzuChwMfmk7HuycRLVQN2qNY3XahK+fZc5E2szqQSKUyAF0E+2bg==", - "dev": true - }, - "node_modules/gulp-shell": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/gulp-shell/-/gulp-shell-0.8.0.tgz", - "integrity": "sha512-wHNCgmqbWkk1c6Gc2dOL5SprcoeujQdeepICwfQRo91DIylTE7a794VEE+leq3cE2YDoiS5ulvRfKVIEMazcTQ==", - "dev": true, - "dependencies": { - "chalk": "^3.0.0", - "fancy-log": "^1.3.3", - "lodash.template": "^4.5.0", - "plugin-error": "^1.0.1", - "through2": "^3.0.1", - "tslib": "^1.10.0" - }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", "engines": { - "node": ">=10.0.0" + "node": ">= 0.4.0" } }, - "node_modules/gulp-shell/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", "dev": true, "dependencies": { - "ansi-wrap": "^0.1.0" + "ansi-regex": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-shell/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/gulp-shell/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gulp-shell/node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/gulp-shell/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "node_modules/has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha512-+F4GzLjwHNNDEAJW2DC1xXfEoPkRDmUdJ7CBYw4MpqtDwOnqdImJl7GWlpqx+Wko6//J8uKTnIe4wZSv7yCqmw==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "sparkles": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">= 0.10" } }, - "node_modules/gulp-shell/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "es-define-property": "^1.0.0" }, - "engines": { - "node": ">=7.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gulp-shell/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/gulp-shell/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "dunder-proto": "^1.0.0" }, "engines": { - "node": ">=0.10.0" - } + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/gulp-shell/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gulp-shell/node_modules/plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gulp-shell/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==", + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gulp-shell/node_modules/through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", "dev": true, "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-sourcemaps": { + "node_modules/has-values/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/has-values/node_modules/is-number": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", - "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, "dependencies": { - "@gulp-sourcemaps/identity-map": "^2.0.1", - "@gulp-sourcemaps/map-sources": "^1.0.0", - "acorn": "^6.4.1", - "convert-source-map": "^1.0.0", - "css": "^3.0.0", - "debug-fabulous": "^1.0.0", - "detect-newline": "^2.0.0", - "graceful-fs": "^4.0.0", - "source-map": "^0.6.0", - "strip-bom-string": "^1.0.0", - "through2": "^2.0.0" + "kind-of": "^3.0.2" }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, - "node_modules/gulp-sourcemaps/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "is-buffer": "^1.1.5" }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/gulp-sourcemaps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-sourcemaps/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", "dev": true, "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-terser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gulp-terser/-/gulp-terser-2.1.0.tgz", - "integrity": "sha512-lQ3+JUdHDVISAlUIUSZ/G9Dz/rBQHxOiYDQ70IVWFQeh4b33TC1MCIU+K18w07PS3rq/CVc34aQO4SUbdaNMPQ==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { - "plugin-error": "^1.0.1", - "terser": "^5.9.0", - "through2": "^4.0.2", - "vinyl-sourcemaps-apply": "^0.2.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=10" + "node": ">= 0.4" } }, - "node_modules/gulp-terser/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", "dev": true, "dependencies": { - "ansi-wrap": "^0.1.0" + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/gulp-terser/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/gulp-terser/node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "node_modules/hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/gulp-terser/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "node_modules/hast-util-sanitize": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-4.1.0.tgz", + "integrity": "sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==", "dev": true, "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "@types/hast": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/gulp-terser/node_modules/plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "node_modules/hast-util-to-html": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", + "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", "dev": true, "dependencies": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^7.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" }, - "engines": { - "node": ">= 0.10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha512-q5oWPc12lwSFS9h/4VIjG+1NuNDlJ48ywV2JKItY4Ycc/n1fXJeYPVQsfu5ZrhQi7FGSDBalwUCLar/GyHXKGw==", - "deprecated": "gulp-util is deprecated - replace it, following the guidelines at https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5", + "node_modules/hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", "dev": true, "dependencies": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">=0.10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/gulp-util/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/headers-utils": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.5.tgz", + "integrity": "sha512-DAzV5P/pk3wTU/8TLZN+zFTDv4Xa1QDTU8pRvovPetcOMbmqq8CwsAvZBLPZHH6usxyy31zMp7I4aCYb6XIf6w==", + "dev": true + }, + "node_modules/highlight.js": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", + "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=12.0.0" } }, - "node_modules/gulp-util/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-util/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "lru-cache": "^10.0.1" }, "engines": { - "node": ">=0.10.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/gulp-util/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "dev": true, "engines": { - "node": ">=0.8" + "node": "14 || >=16.14" } }, - "node_modules/gulp-util/node_modules/clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha512-dhUqc57gSMCo6TX85FLfe51eC/s+Im2MLkAgJwfaRRexR2tA4dd3eLEW4L6efzHc2iNorrRRXITifnDLlRrhaA==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/gulp-util/node_modules/lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha512-0B4Y53I0OgHUJkt+7RmlDFWKjVAI/YUpWNiL9GQz5ORDr4ttgfQGo+phBWKFLJbBdtOwgMuUkdOHOnPg45jKmQ==", + "node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", "dev": true, - "dependencies": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/gulp-util/node_modules/lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha512-TcrlEr31tDYnWkHFWDCV3dHYroKEXpJZ2YJYvJdhN+y4AkWMDZ5I4I8XDtUKqSAyG81N7w+I1mFEJtcED+tGqQ==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" - } + "node_modules/htmlfy": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.2.1.tgz", + "integrity": "sha512-HoomFHQ3av1uhq+7FxJTq4Ns0clAD+tGbQNrSd0WFY3UAjjUk6G3LaWEqdgmIXYkY4pexZiyZ3ykZJhQlM0J5A==", + "dev": true }, - "node_modules/gulp-util/node_modules/object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, - "node_modules/gulp-util/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dependencies": { - "ansi-regex": "^2.0.0" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/gulp-util/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true }, - "node_modules/gulp-util/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/gulp-util/node_modules/vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha512-P5zdf3WB9uzr7IFoVQ2wZTmUwHL8cMZWJGzLBNCHNZ3NB6HTMsYABtt7z8tAGIINLXyAob9B9a1yzVGMFOYKEA==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 0.9" + "node": ">= 14" } }, - "node_modules/gulp-wrap": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/gulp-wrap/-/gulp-wrap-0.15.0.tgz", - "integrity": "sha512-f17zkGObA+hE/FThlg55gfA0nsXbdmHK1WqzjjB2Ytq1TuhLR7JiCBJ3K4AlMzCyoFaCjfowos+VkToUNE0WTQ==", + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, "dependencies": { - "consolidate": "^0.15.1", - "es6-promise": "^4.2.6", - "fs-readfile-promise": "^3.0.1", - "js-yaml": "^3.13.0", - "lodash": "^4.17.11", - "node.extend": "2.0.2", - "plugin-error": "^1.0.1", - "through2": "^3.0.1", - "tryit": "^1.0.1", - "vinyl-bufferstream": "^1.0.1" + "debug": "^4.3.4" }, "engines": { - "node": ">=6.14", - "npm": ">=1.4.3" + "node": ">= 14" } }, - "node_modules/gulp-wrap/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, "dependencies": { - "ansi-wrap": "^0.1.0" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.8", + "npm": ">=1.3.7" } }, - "node_modules/gulp-wrap/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "node_modules/gulp-wrap/node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=18.18.0" } }, - "node_modules/gulp-wrap/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-wrap/node_modules/plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "dependencies": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-wrap/node_modules/through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - } - }, - "node_modules/gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, - "dependencies": { - "glogg": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "dependencies": { - "duplexer": "^0.1.2" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" + "node": ">=4" } }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=0.8.19" } }, - "node_modules/har-schema": { + "node_modules/individual": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz", + "integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", "dev": true, "engines": { - "node": ">=4" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", + "node_modules/inquirer": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-10.1.8.tgz", + "integrity": "sha512-syxGpOzLyqVeZi1KDBjRTnCn5PiGWySGHP0BbqXbqsEK0ckkZk3egAepEWslUjZXj0rhkUapVXM/IpADWe4D6w==", "dev": true, "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" + "@inquirer/prompts": "^5.3.8", + "@inquirer/type": "^1.5.2", + "@types/mute-stream": "^0.0.4", + "ansi-escapes": "^4.3.2", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "rxjs": "^7.8.1" }, "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, "dependencies": { - "function-bind": "^1.1.1" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { - "node": ">= 0.4.0" + "node": ">= 0.4" } }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "engines": { - "node": ">=4" + "node": ">= 0.10" } }, - "node_modules/has-gulplog": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha512-+F4GzLjwHNNDEAJW2DC1xXfEoPkRDmUdJ7CBYw4MpqtDwOnqdImJl7GWlpqx+Wko6//J8uKTnIe4wZSv7yCqmw==", + "node_modules/is": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", + "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", + "engines": { + "node": "*" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", "dev": true, "dependencies": { - "sparkles": "^1.0.0" + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/has-property-descriptors": { + "node_modules/is-accessor-descriptor": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "hasown": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.10" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -17136,10 +16337,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { "node": ">= 0.4" }, @@ -17147,13 +16354,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -17162,1418 +16379,1346 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "has-bigints": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/has-values/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", "dev": true, "dependencies": { - "is-buffer": "^1.1.5" + "semver": "^7.6.3" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dependencies": { - "function-bind": "^1.1.2" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hast-util-is-element": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz", - "integrity": "sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==", + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", "dev": true, "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0" + "hasown": "^2.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.4" } }, - "node_modules/hast-util-sanitize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-4.0.0.tgz", - "integrity": "sha512-pw56+69jq+QSr/coADNvWTmBPDy+XsmwaF5KnUys4/wM1jt/fZdl7GPxhXXXYdXnz3Gj3qMkbUCH2uKjvX0MgQ==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "dependencies": { - "@types/hast": "^2.0.0" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hast-util-to-html": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz", - "integrity": "sha512-/D/E5ymdPYhHpPkuTHOUkSatxr4w1ZKrZsG0Zv/3C2SRVT0JFJG53VS45AMrBtYk0wp5A7ksEhiC8QaOZM95+A==", + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "dependencies": { - "@types/hast": "^2.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-is-element": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "html-void-elements": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.2", - "unist-util-is": "^5.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hast-util-whitespace": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", - "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==", + "node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "bin": { - "he": "bin/he" + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/headers-utils": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.5.tgz", - "integrity": "sha512-DAzV5P/pk3wTU/8TLZN+zFTDv4Xa1QDTU8pRvovPetcOMbmqq8CwsAvZBLPZHH6usxyy31zMp7I4aCYb6XIf6w==", - "dev": true - }, - "node_modules/highlight.js": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.6.0.tgz", - "integrity": "sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==", - "dev": true, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dependencies": { + "is-plain-object": "^2.0.4" + }, "engines": { - "node": ">=12.0.0" + "node": ">=0.10.0" } }, - "node_modules/home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha512-ycURW7oUxE2sNiPVw1HVEFsW+ecOpJ5zaj7eC0RlwhibhRBod20muUN8qu/gzx956YrLolVvs1MTXwKgC2rVEg==", - "dev": true, + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" + "isobject": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "dependencies": { - "parse-passwd": "^1.0.0" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" + "call-bound": "^1.0.3" }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/html-void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", - "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=8" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", "dev": true }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", - "dev": true - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=0.10.0" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, "engines": { - "node": ">= 14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", "dev": true, "dependencies": { - "debug": "^4.3.4" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" }, "engines": { - "node": ">= 14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", "dev": true, - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" + "node": ">=0.10.0" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, "engines": { - "node": ">=10.19.0" + "node": ">=0.12.0" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "dependencies": { - "agent-base": "6", - "debug": "4" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true, "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "node": ">=12" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, "engines": { - "node": ">= 4" + "node": ">=0.10.0" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=6" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/import-meta-resolve": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", - "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "node_modules/is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha512-mjJd3PujZMl7j+D395WTIO5tU5RIDBfVSRtRR4VOJou3H66E38UjbjvDGh3slJzPuolsb+yQFqwHNNdyp5jg3w==", + "dev": true + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { - "node": ">=0.8.19" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/individual": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz", - "integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==", - "dev": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", "dev": true, "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "protocols": "^2.0.1" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", - "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=0.10.0" } }, - "node_modules/inquirer": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", - "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "dependencies": { - "@ljharb/through": "^2.3.11", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^5.0.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=14.18.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "engines": { - "node": ">= 12" - } + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "unc-path-regex": "^0.1.2" }, "engines": { - "node": ">=7.0.0" + "node": ">=0.10.0" } }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/inquirer/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inquirer/node_modules/figures": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", - "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", "dev": true, - "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" - }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/inquirer/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, "engines": { - "node": ">=0.12.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, - "dependencies": { - "tslib": "^2.1.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/inquirer/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "node_modules/inquirer/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "is-docker": "^2.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "dependencies": { - "loose-envify": "^1.0.0" + "engines": { + "node": ">=16" } }, - "node_modules/invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", - "dev": true, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "engines": { "node": ">=0.10.0" } }, - "node_modules/ip": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" + "node_modules/istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha512-nMtdn4hvK0HjUlzr1DrKSUY8ychprt8dzHOgY2KXsIhHu5PuQQEOTM27gV9Xblyon7aUH/TSFIjRHEODF/FRPg==", + "deprecated": "This module is no longer maintained, try this instead:\n npm i nyc\nVisit https://istanbul.js.org/integrations for other alternatives.", + "dev": true, + "dependencies": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "istanbul": "lib/cli.js" } }, - "node_modules/is": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "deprecated": "Please upgrade to v1.0.1", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { - "kind-of": "^6.0.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=10" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/istanbul-lib-report/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, "dependencies": { - "binary-extensions": "^2.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dependencies": { - "has": "^1.0.3" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "deprecated": "Please upgrade to v1.0.1", + "node_modules/istanbul/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { - "kind-of": "^6.0.0" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/istanbul/node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "node_modules/istanbul/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "dev": true + }, + "node_modules/istanbul/node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "has-flag": "^1.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.8.0" } }, - "node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "node_modules/istanbul/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "isexe": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "bin": { + "which": "bin/which" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "node_modules/istextorbinary": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-3.3.0.tgz", + "integrity": "sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==", "dev": true, - "bin": { - "is-docker": "cli.js" + "dependencies": { + "binaryextensions": "^2.2.0", + "textextensions": "^3.2.0" }, "engines": { "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://bevry.me/fund" } }, - "node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, "dependencies": { - "is-plain-object": "^2.0.4" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/is-extendable/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "dependencies": { - "isobject": "^3.0.1" + "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "color-convert": "^2.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", - "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "node_modules/jake/node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "dev": true }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "color-name": "~1.1.4" }, "engines": { - "node": ">=0.10.0" + "node": ">=7.0.0" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "node_modules/jake/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, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=0.12.0" + "node": ">=7.0.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/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, "dependencies": { - "has-tostringtag": "^1.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "is-unc-path": "^1.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=0.10.0" + "node": ">=7.0.0" } }, - "node_modules/is-running": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", - "integrity": "sha512-mjJd3PujZMl7j+D395WTIO5tU5RIDBfVSRtRR4VOJou3H66E38UjbjvDGh3slJzPuolsb+yQFqwHNNdyp5jg3w==", + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "node_modules/jest-matcher-utils/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, "dependencies": { - "call-bind": "^1.0.2" + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/is-ssh": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", - "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { - "protocols": "^2.0.1" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/is-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", - "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=7.0.0" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true - }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "node_modules/jest-message-util/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, "dependencies": { - "call-bind": "^1.0.2" + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "is-docker": "^2.0.0" + "color-convert": "^2.0.1" }, "engines": { "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "engines": { - "node": ">= 8.0.0" }, "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "engines": { - "node": ">=0.10.0" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true - }, - "node_modules/istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha512-nMtdn4hvK0HjUlzr1DrKSUY8ychprt8dzHOgY2KXsIhHu5PuQQEOTM27gV9Xblyon7aUH/TSFIjRHEODF/FRPg==", - "deprecated": "This module is no longer maintained, try this instead:\n npm i nyc\nVisit https://istanbul.js.org/integrations for other alternatives.", + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "bin": { - "istanbul": "lib/cli.js" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "node": ">=10" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=8" + "node": ">=7.0.0" } }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-util/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", @@ -18582,7 +17727,7 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { + "node_modules/jest-util/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==", @@ -18594,534 +17739,674 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">=10" + "node": ">= 10.13.0" } }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/istanbul/node_modules/glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": "*" + "node": ">=4" } }, - "node_modules/istanbul/node_modules/has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=12.0.0" } }, - "node_modules/istanbul/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "bin": { - "mkdirp": "bin/cmd.js" + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" } }, - "node_modules/istanbul/node_modules/resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/istanbul/node_modules/supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, - "dependencies": { - "has-flag": "^1.0.0" + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": ">=0.8.0" + "node": ">=6" } }, - "node_modules/istanbul/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/jsonfile": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz", + "integrity": "sha512-KbsDJNRfRPF5v49tMNf9sqyyGqGLBcz1v5kZT01kG5ns5mQSltwxCKVmUzVKtEinkUnTDtSrp6ngWpV7Xw0ZlA==", + "dev": true + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dev": true, "dependencies": { - "isexe": "^2.0.0" + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" }, - "bin": { - "which": "bin/which" + "engines": { + "node": ">=0.6.0" } }, - "node_modules/istextorbinary": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-3.3.0.tgz", - "integrity": "sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==", + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "dependencies": { - "binaryextensions": "^2.2.0", - "textextensions": "^3.2.0" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://bevry.me/fund" + "node": ">=4.0" } }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "node_modules/jake": { - "version": "10.8.7", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", - "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "node_modules/just-debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", + "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==", + "dev": true + }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/karma": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", + "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", "dev": true, "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" }, "bin": { - "jake": "bin/cli.js" + "karma": "bin/karma" }, "engines": { - "node": ">=10" + "node": ">= 10" } }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/karma-babel-preprocessor": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/karma-babel-preprocessor/-/karma-babel-preprocessor-8.0.2.tgz", + "integrity": "sha512-6ZUnHwaK2EyhgxbgeSJW6n6WZUYSEdekHIV/qDUnPgMkVzQBHEvd07d2mTL5AQjV8uTUgH6XslhaPrp+fHWH2A==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=6" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "@babel/core": "7" } }, - "node_modules/jake/node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/karma-browserstack-launcher": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-browserstack-launcher/-/karma-browserstack-launcher-1.4.0.tgz", + "integrity": "sha512-bUQK84U+euDfOUfEjcF4IareySMOBNRLrrl9q6cttIe8f011Ir6olLITTYMOJDcGY58wiFIdhPHSPd9Pi6+NfQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" + "browserstack": "~1.5.1", + "browserstacktunnel-wrapper": "~2.0.2", + "q": "~1.5.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } + "peerDependencies": { + "karma": ">=0.9" + } }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/karma-chai": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/karma-chai/-/karma-chai-0.1.0.tgz", + "integrity": "sha512-mqKCkHwzPMhgTYca10S90aCEX9+HjVjjrBFAsw36Zj7BlQNbokXXCAe6Ji04VUMsxcY5RLP7YphpfO06XOubdg==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "peerDependencies": { + "chai": "*", + "karma": ">=0.10.9" } }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "which": "^1.2.1" } }, - "node_modules/jake/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==", + "node_modules/karma-chrome-launcher/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "isexe": "^2.0.0" }, - "engines": { - "node": ">=8" + "bin": { + "which": "bin/which" } }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", "dev": true, "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/karma-coverage-istanbul-reporter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", + "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^3.0.2", + "minimatch": "^3.0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/mattlewis92" } }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/karma-coverage-istanbul-reporter/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=10" + "node": "*" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/karma-coverage-istanbul-reporter/node_modules/istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=6" } }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/karma-coverage-istanbul-reporter/node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/jest-diff/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==", + "node_modules/karma-coverage-istanbul-reporter/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "pify": "^4.0.1", + "semver": "^5.6.0" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "node_modules/karma-coverage-istanbul-reporter/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=6" } }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "node_modules/karma-coverage-istanbul-reporter/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "glob": "^7.1.3" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "bin": { + "rimraf": "bin.js" } }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/karma-coverage-istanbul-reporter/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/karma-coverage-istanbul-reporter/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/karma-es5-shim": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/karma-es5-shim/-/karma-es5-shim-0.0.4.tgz", + "integrity": "sha512-8xU6F2/R6u6HAZ/nlyhhx3WEhj4C6hJorG7FR2REX81pgj2LSo9ADJXxCGIeXg6Qr2BGpxp4hcZcEOYGAwiumg==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "es5-shim": "^4.0.5" } }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/karma-firefox-launcher": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz", + "integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==", "dev": true, "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "is-wsl": "^2.2.0", + "which": "^3.0.0" } }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/karma-firefox-launcher/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/jest-matcher-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/karma-firefox-launcher/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, "engines": { - "node": ">=8" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/jest-matcher-utils/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==", + "node_modules/karma-ie-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-ie-launcher/-/karma-ie-launcher-1.0.0.tgz", + "integrity": "sha512-ts71ke8pHvw6qdRtq0+7VY3ANLoZuUNNkA8abRaWV13QRPNm7TtSOqyszjHUtuwOWKcsSz4tbUtrNICrQC+SXQ==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "lodash": "^4.6.1" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "karma": ">=0.9" } }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "node_modules/karma-mocha": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", + "integrity": "sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "minimist": "^1.2.3" } }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha512-Hr6nhkIp0GIJJrvzY8JFeHpQZNseuIakGac4bpw8K1+5F0tLb6l7uvXRa8mt2Z+NVwYgCct4QAfp2R2QP6o00w==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" }, + "peerDependencies": { + "karma": ">=0.13" + } + }, + "node_modules/karma-mocha-reporter/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=4" } }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/karma-mocha-reporter/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-regex": "^3.0.0" }, "engines": { - "node": ">=10" + "node": ">=4" + } + }, + "node_modules/karma-opera-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-opera-launcher/-/karma-opera-launcher-1.0.0.tgz", + "integrity": "sha512-rdty4FlVIowmUhPuG08TeXKHvaRxeDSzPxGIkWguCF3A32kE0uvXZ6dXW08PuaNjai8Ip3f5Pn9Pm2HlChaxCw==", + "dev": true, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "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, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-script-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-script-launcher/-/karma-script-launcher-1.0.0.tgz", + "integrity": "sha512-5NRc8KmTBjNPE3dNfpJP90BArnBohYV4//MsLFfUA1e6N+G1/A5WuWctaFBtMQ6MWRybs/oguSej0JwDr8gInA==", + "dev": true, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-sinon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/karma-sinon/-/karma-sinon-1.0.5.tgz", + "integrity": "sha512-wrkyAxJmJbn75Dqy17L/8aILJWFm7znd1CE8gkyxTBFnjMSOe2XTJ3P30T8SkxWZHmoHX0SCaUJTDBEoXs25Og==", + "dev": true, + "engines": { + "node": ">= 0.10.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependencies": { + "karma": ">=0.10", + "sinon": "*" } }, - "node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/karma-sourcemap-loader": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz", + "integrity": "sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==", "dev": true, "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "graceful-fs": "^4.1.2" } }, - "node_modules/jest-message-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "node_modules/karma-spec-reporter": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.32.tgz", + "integrity": "sha512-ZXsYERZJMTNRR2F3QN11OWF5kgnT/K2dzhM+oY3CDyMrDI3TjIWqYGG7c15rR9wjmy9lvdC+CCshqn3YZqnNrA==", + "dev": true, + "dependencies": { + "colors": "^1.1.2" + }, + "peerDependencies": { + "karma": ">=0.9" + } }, - "node_modules/jest-message-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/karma-webpack": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.1.tgz", + "integrity": "sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ==", "dev": true, + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^9.0.3", + "webpack-merge": "^4.1.5" + }, "engines": { - "node": ">=8" + "node": ">= 18" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, - "node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/karma-webpack/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-message-util/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==", + "node_modules/karma-webpack/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=8" + "node": "*" } }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "node_modules/karma-webpack/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-util/node_modules/ansi-styles": { + "node_modules/karma-webpack/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/karma/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", @@ -19136,23 +18421,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "node_modules/jest-util/node_modules/color-convert": { + "node_modules/karma/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", @@ -19164,9753 +18444,31705 @@ "node": ">=7.0.0" } }, - "node_modules/jest-util/node_modules/color-name": { + "node_modules/karma/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/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==", + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { - "has-flag": "^4.0.0" - }, + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, "engines": { - "node": ">= 10.13.0" + "node": ">=0.10.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/karma/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { "node": ">=8" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=10" } }, - "node_modules/js-yaml/node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, "engines": { - "node": ">=4" + "node": ">=10" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "node_modules/keycode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", + "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", "dev": true }, - "node_modules/jsdoc-type-pratt-parser": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", - "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "engines": { - "node": ">=12.0.0" + "dependencies": { + "json-buffer": "3.0.1" } }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "engines": { + "node": ">= 8" + } }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true + "node_modules/konan": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/konan/-/konan-2.1.1.tgz", + "integrity": "sha512-7ZhYV84UzJ0PR/RJnnsMZcAbn+kLasJhVNWsu8ZyVEJYRpGA5XESQ9d/7zOa08U0Ou4cmB++hMNY/3OSV9KIbg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.10.5", + "@babel/traverse": "^7.10.5" + } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "node_modules/last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", + "dev": true, + "dependencies": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + }, + "engines": { + "node": ">= 0.10" + } }, - "node_modules/json-stable-stringify-without-jsonify": { + "node_modules/lazystream": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } }, - "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "bin": { - "json5": "lib/cli.js" + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "dev": true, + "dependencies": { + "invert-kv": "^1.0.0" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", + "dev": true, + "bin": { + "lcov-parse": "bin/cli.js" + } + }, + "node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", "dev": true, "dependencies": { - "universalify": "^2.0.0" + "flush-write-stream": "^1.0.2" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">= 0.10" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=0.6.0" + "node": ">= 0.8.0" } }, - "node_modules/just-clone": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/just-clone/-/just-clone-1.0.2.tgz", - "integrity": "sha512-p93GINPwrve0w3HUzpXmpTl7MyzzWz1B5ag44KEtq/hP1mtK8lA2b9Q0VQaPlnY87352osJcE6uBmN0e8kuFMw==" + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } }, - "node_modules/just-debounce": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", - "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==", - "dev": true - }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, - "node_modules/karma": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz", - "integrity": "sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA==", + "node_modules/liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", "dev": true, "dependencies": { - "@colors/colors": "1.5.0", - "body-parser": "^1.19.0", - "braces": "^3.0.2", - "chokidar": "^3.5.1", - "connect": "^3.7.0", - "di": "^0.0.1", - "dom-serialize": "^2.2.1", - "glob": "^7.1.7", - "graceful-fs": "^4.2.6", - "http-proxy": "^1.18.1", - "isbinaryfile": "^4.0.8", - "lodash": "^4.17.21", - "log4js": "^6.4.1", - "mime": "^2.5.2", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.5", - "qjobs": "^1.2.0", - "range-parser": "^1.2.1", - "rimraf": "^3.0.2", - "socket.io": "^4.4.1", - "source-map": "^0.6.1", - "tmp": "^0.2.1", - "ua-parser-js": "^0.7.30", - "yargs": "^16.1.1" - }, - "bin": { - "karma": "bin/karma" + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" }, "engines": { - "node": ">= 10" - } - }, - "node_modules/karma-babel-preprocessor": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/karma-babel-preprocessor/-/karma-babel-preprocessor-8.0.2.tgz", - "integrity": "sha512-6ZUnHwaK2EyhgxbgeSJW6n6WZUYSEdekHIV/qDUnPgMkVzQBHEvd07d2mTL5AQjV8uTUgH6XslhaPrp+fHWH2A==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "@babel/core": "7" + "node": ">= 0.8" } }, - "node_modules/karma-browserstack-launcher": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/karma-browserstack-launcher/-/karma-browserstack-launcher-1.4.0.tgz", - "integrity": "sha512-bUQK84U+euDfOUfEjcF4IareySMOBNRLrrl9q6cttIe8f011Ir6olLITTYMOJDcGY58wiFIdhPHSPd9Pi6+NfQ==", + "node_modules/liftoff/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "dependencies": { - "browserstack": "~1.5.1", - "browserstacktunnel-wrapper": "~2.0.2", - "q": "~1.5.0" + "isobject": "^3.0.1" }, - "peerDependencies": { - "karma": ">=0.9" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/karma-chai": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/karma-chai/-/karma-chai-0.1.0.tgz", - "integrity": "sha512-mqKCkHwzPMhgTYca10S90aCEX9+HjVjjrBFAsw36Zj7BlQNbokXXCAe6Ji04VUMsxcY5RLP7YphpfO06XOubdg==", + "node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", "dev": true, - "peerDependencies": { - "chai": "*", - "karma": ">=0.10.9" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/karma-chrome-launcher": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", - "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", - "dev": true, - "dependencies": { - "which": "^1.2.1" - } + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true }, - "node_modules/karma-chrome-launcher/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "node_modules/live-connect-common": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-4.1.0.tgz", + "integrity": "sha512-sRklgbe13377aR+G0qCBiZPayQw5oZZozkuxKEoyipxscLbVzwe9gtA7CPpbmo6UcOdQxdCE6A7J1tI0wTSmqw==", + "engines": { + "node": ">=20" } }, - "node_modules/karma-coverage": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.0.tgz", - "integrity": "sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA==", - "dev": true, + "node_modules/live-connect-js": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-7.2.0.tgz", + "integrity": "sha512-oZY4KqwrG1C+CDKApcsdDdMG4j2d44lhmvbNy4ZE6sPFr+W8R3m0+V+JxXB8p6tgSePJ8X/uhzAGos0lDg/MAg==", + "license": "Apache-2.0", "dependencies": { - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.1", - "istanbul-reports": "^3.0.5", - "minimatch": "^3.0.4" + "live-connect-common": "^v4.1.0", + "tiny-hashes": "1.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=20" } }, - "node_modules/karma-coverage-istanbul-reporter": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", - "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", + "node_modules/livereload-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", + "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^3.0.2", - "minimatch": "^3.0.4" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/mattlewis92" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", "dev": true, "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" + "error-ex": "^1.2.0" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "node_modules/load-json-file/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" + "is-utf8": "^0.2.0" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, "engines": { - "node": ">=6" + "node": ">=6.11.5" } }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" }, - "bin": { - "rimraf": "bin.js" + "engines": { + "node": ">=8.9.0" } }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/locate-app": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.4.15.tgz", + "integrity": "sha512-oAGHATXPUHSQ74Om+3dXBRNYtCzU7Wzuhlj/WIZchqHb/5/TGJRzLEtHipMDOak0UZG9U365RMXyBzgV/fhOww==", "dev": true, - "bin": { - "semver": "bin/semver" + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "dependencies": { + "@promptbook/utils": "0.50.0-10", + "type-fest": "2.13.0", + "userhome": "1.0.0" } }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/locate-app/node_modules/type-fest": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", + "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/karma-es5-shim": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/karma-es5-shim/-/karma-es5-shim-0.0.4.tgz", - "integrity": "sha512-8xU6F2/R6u6HAZ/nlyhhx3WEhj4C6hJorG7FR2REX81pgj2LSo9ADJXxCGIeXg6Qr2BGpxp4hcZcEOYGAwiumg==", + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "dependencies": { - "es5-shim": "^4.0.5" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/karma-firefox-launcher": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz", - "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==", + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==", + "dev": true + }, + "node_modules/lodash._basetostring": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha512-mTzAr1aNAv/i7W43vOR/uD/aJ4ngbtsRaCubp2BfZhlGU/eORUjg/7F6X0orNMdv33JOrdgGybtvMN/po3EWrA==", + "dev": true + }, + "node_modules/lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha512-H94wl5P13uEqlCg7OcNNhMQ8KvWSIyqXzOPusRgHC9DK3o54P6P3xtbXlVbRABG4q5gSmp7EDdJ0MSuW9HX6Mg==", + "dev": true + }, + "node_modules/lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", + "dev": true + }, + "node_modules/lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==", + "dev": true + }, + "node_modules/lodash._reescape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha512-Sjlavm5y+FUVIF3vF3B75GyXrzsfYV8Dlv3L4mEpuB9leg8N6yf/7rU06iLPx9fY0Mv3khVp9p7Dx0mGV6V5OQ==", + "dev": true + }, + "node_modules/lodash._reevaluate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha512-OrPwdDc65iJiBeUe5n/LIjd7Viy99bKwDdk7Z5ljfZg0uFRFlfQaCy9tZ4YMAag9WAZmlVpe1iZrkIMMSMHD3w==", + "dev": true + }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "node_modules/lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==", + "dev": true + }, + "node_modules/lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "dev": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.escape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha512-n1PZMXgaaDWZDSvuNZ/8XOcYO2hOKDqZel5adtR30VKQAtoWs/5AOeFA0vPV8moiPzlqe7F4cP2tzpFewQyelQ==", "dev": true, "dependencies": { - "is-wsl": "^2.2.0", - "which": "^2.0.1" + "lodash._root": "^3.0.0" } }, - "node_modules/karma-ie-launcher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/karma-ie-launcher/-/karma-ie-launcher-1.0.0.tgz", - "integrity": "sha512-ts71ke8pHvw6qdRtq0+7VY3ANLoZuUNNkA8abRaWV13QRPNm7TtSOqyszjHUtuwOWKcsSz4tbUtrNICrQC+SXQ==", + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "dev": true + }, + "node_modules/lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", "dev": true, "dependencies": { - "lodash": "^4.6.1" - }, - "peerDependencies": { - "karma": ">=0.9" + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" } }, - "node_modules/karma-mocha": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", - "integrity": "sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==", + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.pickby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==", + "dev": true + }, + "node_modules/lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==", + "dev": true + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", "dev": true, "dependencies": { - "minimist": "^1.2.3" + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" } }, - "node_modules/karma-mocha-reporter": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", - "integrity": "sha512-Hr6nhkIp0GIJJrvzY8JFeHpQZNseuIakGac4bpw8K1+5F0tLb6l7uvXRa8mt2Z+NVwYgCct4QAfp2R2QP6o00w==", + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", "dev": true, "dependencies": { - "chalk": "^2.1.0", - "log-symbols": "^2.1.0", - "strip-ansi": "^4.0.0" - }, - "peerDependencies": { - "karma": ">=0.13" + "lodash._reinterpolate": "^3.0.0" } }, - "node_modules/karma-mocha-reporter/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "dev": true + }, + "node_modules/log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", "dev": true, "engines": { - "node": ">=4" + "node": ">=0.8.6" } }, - "node_modules/karma-mocha-reporter/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "dev": true, "dependencies": { - "ansi-regex": "^3.0.0" + "chalk": "^2.0.1" }, "engines": { "node": ">=4" } }, - "node_modules/karma-opera-launcher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/karma-opera-launcher/-/karma-opera-launcher-1.0.0.tgz", - "integrity": "sha512-rdty4FlVIowmUhPuG08TeXKHvaRxeDSzPxGIkWguCF3A32kE0uvXZ6dXW08PuaNjai8Ip3f5Pn9Pm2HlChaxCw==", - "dev": true, - "peerDependencies": { - "karma": ">=0.9" - } - }, - "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==", + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", "dev": true, - "peerDependencies": { - "karma": ">=0.9" + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" } }, - "node_modules/karma-script-launcher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/karma-script-launcher/-/karma-script-launcher-1.0.0.tgz", - "integrity": "sha512-5NRc8KmTBjNPE3dNfpJP90BArnBohYV4//MsLFfUA1e6N+G1/A5WuWctaFBtMQ6MWRybs/oguSej0JwDr8gInA==", + "node_modules/logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", "dev": true, - "peerDependencies": { - "karma": ">=0.9" + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" } }, - "node_modules/karma-sinon": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/karma-sinon/-/karma-sinon-1.0.5.tgz", - "integrity": "sha512-wrkyAxJmJbn75Dqy17L/8aILJWFm7znd1CE8gkyxTBFnjMSOe2XTJ3P30T8SkxWZHmoHX0SCaUJTDBEoXs25Og==", + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "dev": true, "engines": { - "node": ">= 0.10.0" + "node": ">=0.1.90" + } + }, + "node_modules/loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" }, - "peerDependencies": { - "karma": ">=0.10", - "sinon": "*" + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" } }, - "node_modules/karma-sourcemap-loader": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz", - "integrity": "sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==", + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true + }, + "node_modules/lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/karma-spec-reporter": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.32.tgz", - "integrity": "sha512-ZXsYERZJMTNRR2F3QN11OWF5kgnT/K2dzhM+oY3CDyMrDI3TjIWqYGG7c15rR9wjmy9lvdC+CCshqn3YZqnNrA==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "dependencies": { - "colors": "^1.1.2" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "peerDependencies": { - "karma": ">=0.9" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/karma-webpack": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.0.tgz", - "integrity": "sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA==", + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "dependencies": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "webpack-merge": "^4.1.5" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "webpack": "^5.0.0" + "get-func-name": "^2.0.1" } }, - "node_modules/karma/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", "dev": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "es5-ext": "~0.10.2" } }, - "node_modules/karma/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/m3u8-parser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", + "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", "dev": true, "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" } }, - "node_modules/karma/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" } }, - "node_modules/karma/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "dependencies": { - "rimraf": "^3.0.0" + "semver": "^6.0.0" }, "engines": { - "node": ">=8.17.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/karma/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", "dev": true, "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "kind-of": "^6.0.2" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/karma/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/keycode": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", - "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", + "node_modules/map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", "dev": true }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", "dev": true, "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, + "object-visit": "^1.0.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", "dev": true, - "engines": { - "node": ">=6" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/konan": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/konan/-/konan-2.1.1.tgz", - "integrity": "sha512-7ZhYV84UzJ0PR/RJnnsMZcAbn+kLasJhVNWsu8ZyVEJYRpGA5XESQ9d/7zOa08U0Ou4cmB++hMNY/3OSV9KIbg==", + "node_modules/matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", "dev": true, "dependencies": { - "@babel/parser": "^7.10.5", - "@babel/traverse": "^7.10.5" + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "engines": { + "node": ">= 0.10.0" } }, - "node_modules/ky": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", - "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", + "node_modules/matchdep/node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", "dev": true, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", + "node_modules/matchdep/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "dependencies": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "node_modules/matchdep/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { - "readable-stream": "^2.0.5" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">= 0.6.3" + "node": ">=0.10.0" } }, - "node_modules/lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "node_modules/matchdep/node_modules/braces/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, - "dependencies": { - "invert-kv": "^1.0.0" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/lcov-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", - "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", - "dev": true, - "bin": { - "lcov-parse": "bin/cli.js" - } - }, - "node_modules/lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", + "node_modules/matchdep/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "dev": true, "dependencies": { - "flush-write-stream": "^1.0.2" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/matchdep/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "dev": true, "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/liftoff": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "node_modules/matchdep/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/liftoff/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/matchdep/node_modules/fill-range/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/lighthouse-logger": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.3.0.tgz", - "integrity": "sha512-BbqAKApLb9ywUli+0a+PcV04SyJ/N1q/8qgCNe6U97KbPCS1BTksEuHFLYdvc8DltuhfxIUBqDZsC0bBGtl3lA==", - "dev": true, - "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" - } - }, - "node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/matchdep/node_modules/findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", "dev": true, "dependencies": { - "ms": "2.0.0" + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/lighthouse-logger/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "node_modules/matchdep/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "node_modules/live-connect-common": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.3.tgz", - "integrity": "sha512-ZPycT04ROBUvPiksnLTunrKC3ROhBSeO99fQ+4qMIkgKwP2CvS44L7fK+0WFV4nAi+65KbzSng7JWcSlckfw8w==", - "engines": { - "node": ">=18" - } - }, - "node_modules/live-connect-js": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.4.tgz", - "integrity": "sha512-lg2XeCaj/eEbK66QGGDEdz9IdT/K3ExZ83Qo6xGVLdP5XJ33xAUCk/gds34rRTmpIwUfAnboOpyj3UoYtS3QUQ==", + "node_modules/matchdep/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, "dependencies": { - "live-connect-common": "^v3.0.3", - "tiny-hashes": "1.0.1" + "is-extglob": "^2.1.0" }, "engines": { - "node": ">=18" + "node": ">=0.10.0" } }, - "node_modules/livereload-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", - "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", - "dev": true - }, - "node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "node_modules/matchdep/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" + "kind-of": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "node_modules/matchdep/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "error-ex": "^1.2.0" + "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/load-json-file/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "node_modules/matchdep/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "node_modules/matchdep/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, "dependencies": { - "is-utf8": "^0.2.0" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "engines": { - "node": ">=6.11.5" + "node": ">= 0.4" } }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", "dev": true, "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" }, - "engines": { - "node": ">=8.9.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/locate-app": { - "version": "2.2.13", - "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.2.13.tgz", - "integrity": "sha512-1jp6iRFrHKBj9vq6Idb0cSjly+KnCIMbxZ2BBKSEzIC4ZJosv47wnLoiJu2EgOAdjhGvNcy/P2fbDCS/WziI8g==", + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", "dev": true, "dependencies": { - "n12": "1.8.16", - "type-fest": "2.13.0", - "userhome": "1.0.0" + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/locate-app/node_modules/type-fest": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", - "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, "engines": { - "node": ">=12.20" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "dev": true, + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-inject": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz", + "integrity": "sha512-CcJ0mHa36QYumDKiZ2OIR+ClhfOM7zIzN+Wfy8tRZ1hpH9DKLCS+Mh4DyK5bCxzE9uxMWcbIpeNFWsg1zrj/2g==", + "dev": true, + "dependencies": { + "mdast-util-to-string": "^1.0.0" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", + "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-toc": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-toc/-/mdast-util-toc-6.1.1.tgz", + "integrity": "sha512-Er21728Kow8hehecK2GZtb7Ny3omcoPUVrmObiSUwmoRYVZaXLR751QROEFjR8W/vAQdHMLj49Lz20J55XaNpw==", + "dev": true, + "dependencies": { + "@types/extend": "^3.0.0", + "@types/mdast": "^3.0.0", + "extend": "^3.0.0", + "github-slugger": "^2.0.0", + "mdast-util-to-string": "^3.1.0", + "unist-util-is": "^5.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-toc/node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true + }, + "node_modules/mdast-util-toc/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "dev": true, + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "dev": true, + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "dev": true, + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "dev": true, + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "dev": true, + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "dev": true, + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "dev": true, + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dev": true, + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/mocha": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/mocha/node_modules/chalk/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mocha/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/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==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/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==", + "dev": true + }, + "node_modules/mocha/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpd-parser": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.22.1.tgz", + "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha512-7ZxrUybYv9NonoXgwoOqtStIu18D1c3eFZj27hqgf5kBrBF8Q+tE8V0MW8dKM5QLkQPh1JhhbKgHLY9kifov4Q==", + "dev": true, + "dependencies": { + "duplexer2": "0.0.2" + } + }, + "node_modules/mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/mux.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", + "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "dev": true, + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "optional": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/ncp": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz", + "integrity": "sha512-PfGU8jYWdRl4FqJfCy0IzbkGyFHntfWygZg46nFk/dJD/XRrk2cj0SsKSX9n5u5gE0E0YfEpKWrEkfjnlZSTXA==", + "dev": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/neostandard": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.12.1.tgz", + "integrity": "sha512-As/LDK+xx591BLb1rPRaPs+JfXFgyNx5BoBui1KBeF/J4s0mW8+NBohrYnMfgm1w1t7E/Y/tU34MjMiP6lns6A==", + "dev": true, + "dependencies": { + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@stylistic/eslint-plugin": "2.11.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import-x": "^4.5.0", + "eslint-plugin-n": "^17.14.0", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-react": "^7.37.2", + "find-up": "^5.0.0", + "globals": "^15.13.0", + "peowly": "^1.3.2", + "typescript-eslint": "^8.17.0" + }, + "bin": { + "neostandard": "cli.mjs" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0" + } + }, + "node_modules/neostandard/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/neostandard/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/neostandard/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/neostandard/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/neostandard/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/neostandard/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/nise": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "dev": true, + "dependencies": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/nise/node_modules/lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/node-request-interceptor": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.6.3.tgz", + "integrity": "sha512-8I2V7H2Ch0NvW7qWcjmS0/9Lhr0T6x7RD6PDirhvWEkUQvy83x8BA4haYMr09r/rig7hcgYSjYh6cd4U7G1vLA==", + "dev": true, + "dependencies": { + "@open-draft/until": "^1.0.3", + "debug": "^4.3.0", + "headers-utils": "^1.2.0", + "strict-event-emitter": "^0.1.0" + } + }, + "node_modules/node.extend": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", + "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==", + "dependencies": { + "has": "^1.0.3", + "is": "^3.2.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", + "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "dependencies": { + "once": "^1.3.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", + "dev": true, + "dependencies": { + "lcid": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-imports": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", + "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", + "dev": true, + "dependencies": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-path": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", + "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", + "dev": true, + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", + "dev": true, + "dependencies": { + "parse-path": "^7.0.0" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-type/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/peowly": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/peowly/-/peowly-1.3.2.tgz", + "integrity": "sha512-BYIrwr8JCXY49jUZscgw311w9oGEKo7ux/s+BxrhKTQbiQ0iYNdZNJ5LgagaeercQdFHwnR7Z5IxxFWVQ+BasQ==", + "dev": true, + "engines": { + "node": ">=18.6.0" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-6.1.0.tgz", + "integrity": "sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "dev": true, + "dependencies": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "optional": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", + "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/read-pkg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", + "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^6.0.0", + "parse-json": "^7.0.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.0.0.tgz", + "integrity": "sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^8.0.0", + "type-fest": "^3.12.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz", + "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remark": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.3.tgz", + "integrity": "sha512-bfmJW1dmR2LvaMJuAnE88pZP9DktIFYXazkTfOIKZzi3Knk9lT0roItIA24ydOucI3bV/g/tXBA6hzqq3FV9Ew==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "remark-parse": "^10.0.0", + "remark-stringify": "^10.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-html": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-15.0.2.tgz", + "integrity": "sha512-/CIOI7wzHJzsh48AiuIyIe1clxVkUtreul73zcCXLub0FmnevQE0UMFDQm7NUx8/3rl/4zCshlMfqBdWScQthw==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "hast-util-sanitize": "^4.0.0", + "hast-util-to-html": "^8.0.0", + "mdast-util-to-hast": "^12.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-reference-links": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/remark-reference-links/-/remark-reference-links-6.0.1.tgz", + "integrity": "sha512-34wY2C6HXSuKVTRtyJJwefkUD8zBOZOSHFZ4aSTnU2F656gr9WeuQ2dL6IJDK3NPd2F6xKF2t4XXcQY9MygAXg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.3.tgz", + "integrity": "sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-toc": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/remark-toc/-/remark-toc-8.0.1.tgz", + "integrity": "sha512-7he2VOm/cy13zilnOTZcyAoyoolV26ULlon6XyCFU+vG54Z/LWJnwphj/xKIDLOt66QmJUgTyUvLVHi2aAElyg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-toc": "^6.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-bom-buffer/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", + "dev": true, + "dependencies": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-bom-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha512-AFBWBy9EVRTa/LhEcG8QDP3FvpwZqmvN2QFDuJswFeaVhWnZMp8q3E6Zd90SR04PlIwfGdyVjNyLPyen/ek5CQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/replacestream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", + "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.3", + "object-assign": "^4.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", + "dev": true, + "dependencies": { + "value-or-function": "^3.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rust-result": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz", + "integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==", + "dev": true, + "dependencies": { + "individual": "^2.0.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safaridriver": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", + "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==", + "dev": true + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-json-parse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", + "integrity": "sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==", + "dev": true + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "deprecated": "This package has been deprecated in favour of @sinonjs/samsam", + "dev": true + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==", + "dev": true, + "dependencies": { + "sver-compat": "^1.5.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", + "dev": true + }, + "node_modules/send/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true, + "bin": { + "mime": "cli.js" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/send/node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serialize-error": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "dev": true, + "dependencies": { + "type-fest": "^2.12.2" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static/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==" + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sinon": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", + "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", + "deprecated": "16.1.1", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@sinonjs/formatio": "^2.0.0", + "diff": "^3.1.0", + "lodash.get": "^4.4.2", + "lolex": "^2.2.0", + "nise": "^1.2.0", + "supports-color": "^5.1.0", + "type-detect": "^4.0.5" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "dev": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/snapdragon/node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spacetrim": { + "version": "0.11.25", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.25.tgz", + "integrity": "sha512-SWxXDROciuJs9YEYXUBjot5k/cqNGPPbT3QmkInFne4AGc1y+76It+jqU8rfsXKt57RRiunzZn1m9+KfuuNklw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" + } + ] + }, + "node_modules/sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/spawn-args": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/spawn-args/-/spawn-args-0.2.0.tgz", + "integrity": "sha512-73BoniQDcRWgnLAf/suKH6V5H54gd1KLzwYN9FB6J/evqTV33htH9xwV/4BHek+++jzxpVlZQKKZkqstPQPmQg==", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true + }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-buffers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", + "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.1.0.tgz", + "integrity": "sha512-8hSYfU+WKLdNcHVXJ0VxRXiPESalzRe7w1l8dg9+/22Ry+iZQUoQuoJ27R30GMD1TiyYINWsIEGY05WrskhSKw==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", + "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sver-compat": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", + "integrity": "sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg==", + "dev": true, + "dependencies": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/temp-fs": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/temp-fs/-/temp-fs-0.9.9.tgz", + "integrity": "sha512-WfecDCR1xC9b0nsrzSaxPf3ZuWeWLUWblW4vlDQAa1biQaKHiImHnJfeQocQe/hXKMcolRzgkcVX/7kK4zoWbw==", + "dev": true, + "dependencies": { + "rimraf": "~2.5.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/temp-fs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/temp-fs/node_modules/rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/ternary-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-3.0.0.tgz", + "integrity": "sha512-oIzdi+UL/JdktkT+7KU5tSIQjj8pbShj3OASuvDEhm0NT5lppsm7aXWAmAq4/QMaBIyfuEcNLbAQA+HpaISobQ==", + "dev": true, + "dependencies": { + "duplexify": "^4.1.1", + "fork-stream": "^0.0.4", + "merge-stream": "^2.0.0", + "through2": "^3.0.1" + } + }, + "node_modules/ternary-stream/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/terser": { + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", + "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/textextensions": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", + "integrity": "sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/through2-filter/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/timeout-as-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/timeout-as-promise/-/timeout-as-promise-1.0.0.tgz", + "integrity": "sha512-G4so1NA+qeCiBK+IX3vi6YyumjdDr86q2Y+RjyGjcw3qrRnFFNmi3Y76Ijk8EtqZxcgDdeq/qj8JFxQcsqaEmA==", + "dev": true + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tiny-hashes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tiny-hashes/-/tiny-hashes-1.0.1.tgz", + "integrity": "sha512-knIN5zj4fl7kW4EBU5sLP20DWUvi/rVouvJezV0UAym2DkQaqm365Nyc8F3QEiOvunNDMxR8UhcXd1d5g+Wg1g==" + }, + "node_modules/tiny-lr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", + "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", + "dev": true, + "dependencies": { + "body": "^5.1.0", + "debug": "^3.1.0", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.3.0", + "object-assign": "^4.1.0", + "qs": "^6.4.0" + } + }, + "node_modules/tiny-lr/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "dev": true, + "dependencies": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-regex/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", + "dev": true, + "dependencies": { + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/to-through/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tryit": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha512-6C5h3CE+0qjGp+YKYTs74xR0k/Nw/ePtl/Lp6CCf44hqBQ66qnH1sDFR5mV/Gc48EsrHLB53lCFSffQCkka3kg==" + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsx": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz", + "integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==", + "dev": true, + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "dependencies": { + "typescript-logic": "^0.0.0" + } + }, + "node_modules/typescript-eslint": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.1.tgz", + "integrity": "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.26.1", + "@typescript-eslint/parser": "8.26.1", + "@typescript-eslint/utils": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "node_modules/typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "dependencies": { + "typescript-compare": "^0.0.2" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", + "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/uglify-js": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undertaker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", + "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "fast-levenshtein": "^1.0.0", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", + "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==", + "dev": true + }, + "node_modules/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "dev": true, + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "node_modules/unist-builder": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.1.tgz", + "integrity": "sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/unzipper": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.9.15.tgz", + "integrity": "sha512-2aaUvO4RAeHDvOCuEtth7jrHFaCKTSXPqUkXwADaLBzGbgZGzUDccoEdJ5lW+3RmfpOZYNx0Rw6F6PUzM6caIA==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true + }, + "node_modules/unzipper/node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==", + "dev": true + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/userhome": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.0.tgz", + "integrity": "sha512-ayFKY3H+Pwfy4W98yPdtH1VqH4psDeyW8lYYFzfecR9d6hqLpqhecktvYR3SEEXt7vG0S1JEpciI3g94pMErig==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-reporter": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-7.0.5.tgz", + "integrity": "sha512-NdWWXkv6gcd7AZMvDomlQbK3MqFWL1RlGzMn++/O2TI+68+nqxCPTvLugdOtfSzXmjh+xUyhp07HhlrbJjT+mw==", + "dev": true, + "dependencies": { + "@types/supports-color": "^8.0.0", + "string-width": "^5.0.0", + "supports-color": "^9.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile": "^5.0.0", + "vfile-message": "^3.0.0", + "vfile-sort": "^3.0.0", + "vfile-statistics": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-reporter/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/vfile-reporter/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vfile-reporter/node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/vfile-sort": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-3.0.1.tgz", + "integrity": "sha512-1os1733XY6y0D5x0ugqSeaVJm9lYgj0j5qdcZQFyxlZOSy1jYarL77lLyb5gK4Wqr1d5OxmuyflSO3zKyFnTFw==", + "dev": true, + "dependencies": { + "vfile": "^5.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-statistics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-2.0.1.tgz", + "integrity": "sha512-W6dkECZmP32EG/l+dp2jCLdYzmnDBIw6jwiLZSER81oR5AHRcVqL+k3Z+pfH1R73le6ayDkJRMk0sutj1bMVeg==", + "dev": true, + "dependencies": { + "vfile": "^5.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/video.js": { + "version": "7.21.6", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.6.tgz", + "integrity": "sha512-m41TbODrUCToVfK1aljVd296CwDQnCRewpIm5tTXMuV87YYSGw1H+VDOaV45HlpcWSsTWWLF++InDgGJfthfUw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "2.16.3", + "@videojs/vhs-utils": "^3.0.4", + "@videojs/xhr": "2.6.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.5" + } + }, + "node_modules/video.js/node_modules/safe-json-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz", + "integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==", + "dev": true, + "dependencies": { + "rust-result": "^1.0.0" + } + }, + "node_modules/videojs-contrib-ads": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-ads/-/videojs-contrib-ads-6.9.0.tgz", + "integrity": "sha512-nzKz+jhCGMTYffSNVYrmp9p70s05v6jUMOY3Z7DpVk3iFrWK4Zi/BIkokDWrMoHpKjdmCdKzfJVBT+CrUj6Spw==", + "dev": true, + "dependencies": { + "global": "^4.3.2", + "video.js": "^6 || ^7" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/videojs-font": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", + "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==", + "dev": true + }, + "node_modules/videojs-ima": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/videojs-ima/-/videojs-ima-2.3.0.tgz", + "integrity": "sha512-8r0BZGT+WCTO6PePyKZHikV79Ojqh4yLMx4+DmPyXeRcKUVsQ7Va0R7Ok8GRcA8Zy3l1PM6jzLrD/W1rwKhZ8g==", + "dev": true, + "dependencies": { + "@hapi/cryptiles": "^5.1.0", + "can-autoplay": "^3.0.2", + "extend": ">=3.0.2", + "videojs-contrib-ads": "^6.9.0" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "video.js": "^5.19.2 || ^6 || ^7 || ^8" + } + }, + "node_modules/videojs-playlist": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-5.1.2.tgz", + "integrity": "sha512-8YgNq/iL17RLTXpfWAkuhM0Sq4w/x5YPVaNbUycjfqqGL/bp3Nrmc2W0qkPfh0ryB7r4cHfJbtHYP7zlW3ZkdQ==", + "dev": true, + "dependencies": { + "global": "^4.3.2", + "video.js": "^6 || ^7 || ^8" + }, + "engines": { + "node": ">=4.4.0" + } + }, + "node_modules/videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "dev": true, + "dependencies": { + "global": "^4.3.1" + } + }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-bufferstream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz", + "integrity": "sha512-yCCIoTf26Q9SQ0L9cDSavSL7Nt6wgQw8TU1B/bb9b9Z4A3XTypXCGdc5BvXl4ObQvVY8JrDkFnWa/UqBqwM2IA==", + "dependencies": { + "bufferstreams": "1.0.1" + } + }, + "node_modules/vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-fs/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "dev": true, + "dependencies": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-sourcemap/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/vinyl-sourcemap/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", + "dev": true, + "dependencies": { + "source-map": "^0.5.1" + } + }, + "node_modules/vinyl/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "optional": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wait-port/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wait-port/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/wait-port/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wait-port/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wait-port/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/wait-port/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wait-port/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "dev": true, + "dependencies": { + "foreachasync": "^3.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "optional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webdriver": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.0.5.tgz", + "integrity": "sha512-+xkdfbmG1IZrXxiPwab450Xuh9QClOcxTJ6tnde0rzxlPxdUqZqzwuMtM+VXZybxF4yCLrJWbeT0BpwJFAz1nA==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "deepmerge-ts": "^7.0.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webdriver/node_modules/@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "dev": true, + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webdriver/node_modules/@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", + "dev": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webdriver/node_modules/@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webdriver/node_modules/@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webdriver/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webdriver/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webdriver/node_modules/deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/webdriver/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webdriver/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webdriver/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webdriver/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/webdriver/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/webdriver/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/webdriverio": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.9.tgz", + "integrity": "sha512-IwvKzhcJ9NjOL55xwj27uTTKkfxsg77dmAfqoKFSP5dQ70JzU+NgxiALEjjWQDybtt1yGIkHk7wjjxjboMU1uw==", + "dev": true, + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.8", + "@wdio/logger": "9.0.8", + "@wdio/protocols": "9.0.8", + "@wdio/repl": "9.0.8", + "@wdio/types": "9.0.8", + "@wdio/utils": "9.0.8", + "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.2.1", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.8" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": "^22.3.0" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@puppeteer/browsers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", + "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", + "dev": true, + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webdriverio/node_modules/@wdio/config": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.0.8.tgz", + "integrity": "sha512-37L+hd+A1Nyehd/pgfTrLC6w+Ngbu0CIoFh9Vv6v8Cgu5Hih0TLofvlg+J1BNbcTd5eQ2tFKZBDeFMhQaIiTpg==", + "dev": true, + "dependencies": { + "@wdio/logger": "9.0.8", + "@wdio/types": "9.0.8", + "@wdio/utils": "9.0.8", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/@wdio/logger": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.8.tgz", + "integrity": "sha512-uIyYIDBwLczmsp9JE5hN3ME8Xg+9WNBfSNXD69ICHrY9WPTzFf94UeTuavK7kwSKF3ro2eJbmNZItYOfnoovnw==", + "dev": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/@wdio/protocols": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.0.8.tgz", + "integrity": "sha512-xRH54byFf623/w/KW62xkf/C2mGyigSfMm+UT3tNEAd5ZA9X2VAWQWQBPzdcrsck7Fxk4zlQX8Kb34RSs7Cy4Q==", + "dev": true + }, + "node_modules/webdriverio/node_modules/@wdio/repl": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.8.tgz", + "integrity": "sha512-3iubjl4JX5zD21aFxZwQghqC3lgu+mSs8c3NaiYYNCC+IT5cI/8QuKlgh9s59bu+N3gG988jqMJeCYlKuUv/iw==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/@wdio/types": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.8.tgz", + "integrity": "sha512-pmz2iRWddTanrv8JC7v3wUGm17KRv2WyyJhQfklMSANn9V1ep6pw1RJG2WJnKq4NojMvH1nVv1sMZxXrYPhpYw==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/@wdio/utils": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.8.tgz", + "integrity": "sha512-p3EgOdkhCvMxJFd3WTtSChqYFQu2mz69/5tOsljDaL+4QYwnRR7O8M9wFsL3/9XMVcHdnC4Ija2VRxQ/lb+hHQ==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.8", + "@wdio/types": "9.0.8", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webdriverio/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/webdriverio/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/webdriverio/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webdriverio/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webdriverio/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webdriverio/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webdriverio/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/webdriverio/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/webdriverio/node_modules/webdriver": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.0.8.tgz", + "integrity": "sha512-UnV0ANriSTUgypGk0pz8lApeQuHt+72WEDQG5hFwkkSvggtKLyWdT7+PQkNoXvDajTmiLIqUOq8XPI/Pm71rtw==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.0.8", + "@wdio/logger": "9.0.8", + "@wdio/protocols": "9.0.8", + "@wdio/types": "9.0.8", + "@wdio/utils": "9.0.8", + "deepmerge-ts": "^7.0.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-manifest-plugin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-5.0.0.tgz", + "integrity": "sha512-8RQfMAdc5Uw3QbCQ/CBV/AXqOR8mt03B6GJmRbhWopE8GzRfEpn+k0ZuWywxW+5QZsffhmFDY1J6ohqJo+eMuw==", + "dev": true, + "dependencies": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "webpack": "^5.47.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-7.0.0.tgz", + "integrity": "sha512-XoAQTHyCaYMo6TS7Atv1HYhtmBgKiVLONJbzLBl2V3eibXQ2IT/MCRM841RW/r3vToKD5ivrTJFWgd/ghoxoRg==", + "dev": true, + "dependencies": { + "fancy-log": "^1.3.3", + "lodash.clone": "^4.3.2", + "lodash.some": "^4.2.2", + "memory-fs": "^0.5.0", + "plugin-error": "^1.0.1", + "supports-color": "^8.1.1", + "through": "^2.3.8", + "vinyl": "^2.2.1" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "webpack": "^5.21.2" + } + }, + "node_modules/webpack-stream/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-stream/node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-stream/node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-stream/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-stream/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-stream/node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack-stream/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", + "dev": true + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/winston-transport": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "dev": true, + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yargs": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.3.tgz", + "integrity": "sha512-7OGt4xXoWJQh5ulgZ78rKaqY7dNWbjfK+UKxGcIlaM2j7C4fqGchyv8CPvEWdRPrHp6Ula/YU8yGRpYGOHrI+g==", + "dev": true + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "plugins/eslint": { + "name": "eslint-plugin-prebid", + "version": "1.0.0", + "extraneous": true, + "license": "Apache-2.0" + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==" + }, + "@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/eslint-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", + "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", + "dev": true, + "requires": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "requires": { + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "requires": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "requires": { + "@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.14.2" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "requires": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==" + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + } + }, + "@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" + }, + "@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + }, + "@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==" + }, + "@babel/helper-wrap-function": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "requires": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helpers": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "requires": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" + } + }, + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "requires": { + "@babel/types": "^7.26.10" + } + }, + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + } + }, + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "requires": {} + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "requires": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + } + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "requires": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "requires": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "requires": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "requires": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "requires": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "requires": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", + "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", + "requires": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/preset-env": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", + "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "requires": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + } + }, + "@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/register": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.24.6.tgz", + "integrity": "sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "dependencies": { + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + }, + "@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "requires": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + } + }, + "@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + } + } + }, + "@babel/types": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "requires": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + } + }, + "@browserstack/ai-sdk-node": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@browserstack/ai-sdk-node/-/ai-sdk-node-1.5.8.tgz", + "integrity": "sha512-34snogSnvskHxUZYOX61ga1/oTlyXwneRtd7Epu2bEdSsRR1rMm8xXhO6DVrLsHPwPHz+ljAlwVwhcE2uKysxw==", + "dev": true, + "requires": { + "axios": "^1.6.2", + "uuid": "9.0.1" + } + }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@es-joy/jsdoccomment": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", + "integrity": "sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==", + "dev": true, + "requires": { + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.1.0" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.4.3" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + } + } + }, + "@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true + }, + "@eslint/compat": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.7.tgz", + "integrity": "sha512-xvv7hJE32yhegJ8xNAnb62ggiAwTYHBpUCWhRxEj/ksvgDJuSXfoDkBcRYaYNFiJ+jH0IE3K16hd+xXzhBgNbg==", + "dev": true, + "requires": {} + }, + "@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + } + }, + "@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "dev": true + }, + "@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.15" + } + }, + "@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", + "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "dev": true + }, + "@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true + }, + "@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "requires": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + } + }, + "@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", + "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", + "dev": true, + "requires": { + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", + "dev": true, + "requires": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "dev": true, + "requires": { + "@hapi/hoek": "9.x.x" + } + }, + "@hapi/cryptiles": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-5.1.0.tgz", + "integrity": "sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==", + "dev": true, + "requires": { + "@hapi/boom": "9.x.x" + } + }, + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true + }, + "@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "requires": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "dependencies": { + "@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true + } + } + }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true + }, + "@inquirer/checkbox": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.4.7.tgz", + "integrity": "sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/confirm": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", + "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + } + }, + "@inquirer/core": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.10.tgz", + "integrity": "sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==", + "dev": true, + "requires": { + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.2", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.1.0", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "dependencies": { + "@types/node": { + "version": "22.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", + "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==", + "dev": true, + "requires": { + "undici-types": "~6.19.2" + } + }, + "cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + } + } + }, + "@inquirer/editor": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.1.22.tgz", + "integrity": "sha512-K1QwTu7GCK+nKOVRBp5HY9jt3DXOfPGPr6WRDrPImkcJRelG9UTx2cAtK1liXmibRrzJlTWOwqgWT3k2XnS62w==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2", + "external-editor": "^3.1.0" + } + }, + "@inquirer/expand": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.1.22.tgz", + "integrity": "sha512-wTZOBkzH+ItPuZ3ZPa9lynBsdMp6kQ9zbjVPYEtSBG7UulGjg2kQiAnUjgyG4SlntpTce5bOmXAPvE4sguXjpA==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/figures": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", + "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", + "dev": true + }, + "@inquirer/input": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.2.9.tgz", + "integrity": "sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + } + }, + "@inquirer/number": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.0.10.tgz", + "integrity": "sha512-kWTxRF8zHjQOn2TJs+XttLioBih6bdc5CcosXIzZsrTY383PXI35DuhIllZKu7CdXFi2rz2BWPN9l0dPsvrQOA==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + } + }, + "@inquirer/password": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.1.22.tgz", + "integrity": "sha512-5Fxt1L9vh3rAKqjYwqsjU4DZsEvY/2Gll+QkqR4yEpy6wvzLxdSgFhUcxfDAOtO4BEoTreWoznC0phagwLU5Kw==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2", + "ansi-escapes": "^4.3.2" + } + }, + "@inquirer/prompts": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", + "dev": true, + "requires": { + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" + } + }, + "@inquirer/rawlist": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.2.4.tgz", + "integrity": "sha512-pb6w9pWrm7EfnYDgQObOurh2d2YH07+eDo3xQBsNAM2GRhliz6wFXGi1thKQ4bN6B0xDd6C3tBsjdr3obsCl3Q==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/search": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.0.7.tgz", + "integrity": "sha512-p1wpV+3gd1eST/o5N3yQpYEdFNCzSP0Klrl+5bfD3cTTz8BGG6nf4Z07aBW0xjlKIj1Rp0y3x/X4cZYi6TfcLw==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/select": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.4.7.tgz", + "integrity": "sha512-JH7XqPEkBpNWp3gPCqWqY8ECbyMoFcCZANlL6pV9hf59qK6dGmkOlx1ydyhY+KZ0c5X74+W6Mtp+nm2QX0/MAQ==", + "dev": true, + "requires": { + "@inquirer/core": "^9.0.10", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/type": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.2.tgz", + "integrity": "sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==", + "dev": true, + "requires": { + "mute-stream": "^1.0.0" + } + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "requires": { + "eslint-scope": "5.1.1" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true + }, + "@open-draft/until": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz", + "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==", + "dev": true + }, + "@percy/appium-app": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@percy/appium-app/-/appium-app-2.0.6.tgz", + "integrity": "sha512-0NT8xgaq4UOhcqVc4H3D440M7H5Zko8mDpY5j30TRpjOQ3ctLPJalmUVKOCFv4rSzjd2LmyE2F9KXTPA3zqQsw==", + "dev": true, + "requires": { + "@percy/sdk-utils": "^1.28.2", + "tmp": "^0.2.1" + } + }, + "@percy/sdk-utils": { + "version": "1.28.7", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.28.7.tgz", + "integrity": "sha512-LIhfHnkcS0fyIdo3gvKn7rwodZjbEtyLkgiDRSRulcBOatI2mhn2Bh269sXXiiFTyAW2BDQjyE3DWc4hkGbsbQ==", + "dev": true + }, + "@percy/selenium-webdriver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@percy/selenium-webdriver/-/selenium-webdriver-2.0.5.tgz", + "integrity": "sha512-bNj52xQm02dY872loFa+8OwyuGcdYHYvCKflmSEsF9EDRiSDj0Wr+XP+DDIgDAl9xXschA7OOdXCLTWV4zEQWA==", + "dev": true, + "requires": { + "@percy/sdk-utils": "^1.28.0", + "node-request-interceptor": "^0.6.3" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true + }, + "@polka/url": { + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "dev": true + }, + "@promptbook/utils": { + "version": "0.50.0-10", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.50.0-10.tgz", + "integrity": "sha512-Z94YoY/wcZb5m1QoXgmIC1rVeDguGK5bWmUTYdWCqh/LHVifRdJ1C+tBzS0h+HMOD0XzMjZhBQ/mBgTZ/QNW/g==", + "dev": true, + "requires": { + "moment": "2.30.1", + "prettier": "2.8.1", + "spacetrim": "0.11.25" + } + }, + "@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "requires": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.1", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "dev": true, + "requires": { + "samsam": "1.3.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true + }, + "@stylistic/eslint-plugin": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.11.0.tgz", + "integrity": "sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "^8.13.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true + } + } + }, + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true + }, + "@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", + "dev": true + }, + "@types/extend": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.4.tgz", + "integrity": "sha512-ArMouDUTJEz1SQRpFsT2rIw7DeqICFv5aaVzLSIYMYQSLcwcGOfT3VyglQs/p7K3F7fT4zxr0NWxYZIdifD6dA==", + "dev": true + }, + "@types/gitconfiglocal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/gitconfiglocal/-/gitconfiglocal-2.0.3.tgz", + "integrity": "sha512-W6hyZux6TrtKfF2I9XNLVcsFr4xRr0T+S6hrJ9nDkhA2vzsFPIEAbnY4vgb6v2yKXQ9MJVcbLsARNlMfg4EVtQ==", + "dev": true + }, + "@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dev": true, + "requires": { + "@types/unist": "^2" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "requires": { + "@types/unist": "^2" + } + }, + "@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, + "@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "dev": true + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/supports-color": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", + "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "dev": true + }, + "@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true + }, + "@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "dev": true + }, + "@types/vinyl": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", + "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", + "dev": true, + "requires": { + "@types/expect": "^1.20.4", + "@types/node": "*" + } + }, + "@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true + }, + "@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", + "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/type-utils": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "dependencies": { + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", + "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1" + } + }, + "@typescript-eslint/type-utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", + "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + } + }, + "@typescript-eslint/types": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", + "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", + "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true + } + } + }, + "@typescript-eslint/utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", + "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", + "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.26.1", + "eslint-visitor-keys": "^4.2.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true + } + } + }, + "@videojs/http-streaming": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.16.3.tgz", + "integrity": "sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "3.0.5", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "^0.22.1", + "mux.js": "6.0.1", + "video.js": "^6 || ^7" + } + }, + "@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + } + }, + "@videojs/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, + "@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "requires": { + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/snapshot": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "dev": true, + "requires": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + } + }, + "@vue/compiler-core": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", + "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", + "dev": true, + "optional": true, + "requires": { + "@babel/parser": "^7.24.4", + "@vue/shared": "3.4.27", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "@vue/compiler-dom": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", + "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", + "dev": true, + "optional": true, + "requires": { + "@vue/compiler-core": "3.4.27", + "@vue/shared": "3.4.27" + } + }, + "@vue/compiler-sfc": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", + "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", + "dev": true, + "optional": true, + "requires": { + "@babel/parser": "^7.24.4", + "@vue/compiler-core": "3.4.27", + "@vue/compiler-dom": "3.4.27", + "@vue/compiler-ssr": "3.4.27", + "@vue/shared": "3.4.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" + } + }, + "@vue/compiler-ssr": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", + "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", + "dev": true, + "optional": true, + "requires": { + "@vue/compiler-dom": "3.4.27", + "@vue/shared": "3.4.27" + } + }, + "@vue/shared": { + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", + "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "dev": true, + "optional": true + }, + "@wdio/browserstack-service": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/browserstack-service/-/browserstack-service-9.0.5.tgz", + "integrity": "sha512-pJNb9jJwPf+FEwAEnnUc6d9s6/QlvcZbh9NtjO23a/wr3HvXdzhlRHwzUV1RWboDpGsww5PFmtGcIo7GdDQL+g==", + "dev": true, + "requires": { + "@browserstack/ai-sdk-node": "1.5.8", + "@percy/appium-app": "^2.0.1", + "@percy/selenium-webdriver": "^2.0.3", + "@types/gitconfiglocal": "^2.0.1", + "@wdio/logger": "9.0.4", + "@wdio/reporter": "9.0.4", + "@wdio/types": "9.0.4", + "browserstack-local": "^1.5.1", + "chalk": "^5.3.0", + "csv-writer": "^1.6.0", + "formdata-node": "5.0.1", + "git-repo-info": "^2.1.1", + "gitconfiglocal": "^2.1.0", + "uuid": "^10.0.0", + "webdriverio": "9.0.5", + "winston-transport": "^4.5.0", + "yauzl": "^3.0.0" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "requires": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + }, + "@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/reporter": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-9.0.4.tgz", + "integrity": "sha512-g55MiqToKEZ+L5quk7Ddk3m1fKgh2U2rq3zLG8bZUYU+3KFglfRC/Ld5yTso8S1tG4EDgl5HKSN5Bl8I5gncbg==", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "diff": "^5.0.0", + "object-inspect": "^1.12.0" + } + }, + "@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + }, + "puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true + }, + "webdriverio": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.5.tgz", + "integrity": "sha512-80zhuLBT5W5wiLNZ0maT1cVUrxmwpMuBgCprwZjI8lFe+KUhGLClrJXud/RrVT9x9rDCYa6pGCtQ4UqA+2U+sw==", + "dev": true, + "requires": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "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.2.1", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.5" + } + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "optional": true, + "peer": true, + "requires": {} + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "@wdio/cli": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.0.5.tgz", + "integrity": "sha512-D/QBlodNIdxuNpUPbuhk+mLidVLT+Vsb0Q0Fd4lh57Jy8kw5nJ56ykqiI0WE1oI0i+XtyJ7iFOPUztuCjjhX3A==", + "dev": true, + "requires": { + "@types/node": "^20.1.1", + "@vitest/snapshot": "^1.2.1", + "@wdio/config": "9.0.5", + "@wdio/globals": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "async-exit-hook": "^2.0.1", + "chalk": "^5.2.0", + "chokidar": "^3.5.3", + "cli-spinners": "^3.0.0", + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "execa": "^9.2.0", + "import-meta-resolve": "^4.0.0", + "inquirer": "^10.1.8", + "lodash.flattendeep": "^4.4.0", + "lodash.pickby": "^4.6.0", + "lodash.union": "^4.6.0", + "read-pkg-up": "^10.0.0", + "recursive-readdir": "^2.2.3", + "tsx": "^4.7.2", + "webdriverio": "9.0.5", + "yargs": "^17.7.2" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "dev": true, + "requires": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + }, + "@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "execa": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.4.0.tgz", + "integrity": "sha512-yKHlle2YGxZE842MERVIplWwNH5VYmqqcPFgtnlU//K8gxuFFXu0pwd/CrfXTumFpeEiufsP7+opT/bPJa1yVw==", + "dev": true, + "requires": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + } + }, + "get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "requires": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "requires": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + } + }, + "parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "pretty-ms": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", + "dev": true, + "requires": { + "parse-ms": "^4.0.0" + } + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + }, + "puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + } + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "webdriverio": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.5.tgz", + "integrity": "sha512-80zhuLBT5W5wiLNZ0maT1cVUrxmwpMuBgCprwZjI8lFe+KUhGLClrJXud/RrVT9x9rDCYa6pGCtQ4UqA+2U+sw==", + "dev": true, + "requires": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "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.2.1", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.5" + } + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "optional": true, + "peer": true, + "requires": {} + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "@wdio/concise-reporter": { + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/concise-reporter/-/concise-reporter-8.38.2.tgz", + "integrity": "sha512-wE36By4Z9iCtRzihpYrmCehsmNc8t3gHviBsUxV4tmYh/SQr+WX/dysWnojer6KWIJ2rT0rOzyQPmrwhdFKAFg==", + "dev": true, + "requires": { + "@wdio/reporter": "8.38.2", + "@wdio/types": "8.38.2", + "chalk": "^5.0.1", + "pretty-ms": "^7.0.1" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + } + } + }, + "@wdio/config": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.0.5.tgz", + "integrity": "sha512-+dxUU2SLXLkqQhVU/wauU1VgqEKIFubOyUb6B0ueAMpM1aolc62zhE9D9rrQYbjkPOM7nFsjuuGR5+9+zaoZ6g==", + "dev": true, + "requires": { + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "dev": true, + "requires": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + }, + "@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "@wdio/globals": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.0.5.tgz", + "integrity": "sha512-ZkopKj1qEDNKuF1a87JTLfTKCBFgCHLUns5ob5D1oEmMFp0NwB89HHGBWgtuJpCUmxJAbf4rCKglVeKhB9rY7A==", + "dev": true, + "requires": { + "expect-webdriverio": "^5.0.1", + "webdriverio": "9.0.5" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "optional": true, + "requires": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + }, + "@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "dev": true, + "optional": true, + "requires": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + } + }, + "@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", + "dev": true, + "optional": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "optional": true, + "requires": { + "debug": "^4.3.4" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "optional": true + }, + "deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true, + "optional": true + }, + "devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "expect-webdriverio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-5.0.1.tgz", + "integrity": "sha512-tYJaXtu5qQr/oEXzDKoVzyA/g2wyeE1YsuVJQC8/bKnp1lX3I4NQ2Yb9u1rJ9vdpFha6pki8PzHlTk5PVZBQWQ==", + "dev": true, + "optional": true, + "requires": { + "@vitest/snapshot": "^2.0.5", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "optional": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "optional": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "optional": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + }, + "puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "optional": true + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "optional": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "webdriverio": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.5.tgz", + "integrity": "sha512-80zhuLBT5W5wiLNZ0maT1cVUrxmwpMuBgCprwZjI8lFe+KUhGLClrJXud/RrVT9x9rDCYa6pGCtQ4UqA+2U+sw==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "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.2.1", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.5" + } + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "optional": true, + "peer": true, + "requires": {} + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "optional": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "@wdio/local-runner": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.0.5.tgz", + "integrity": "sha512-BFZ/e7z1s2cYsix1evijydaDn0YffeIHjPsMoa9b+zhW8BoZfTEDGKblYvzRgjUDD4elXs+YRZpA6EhjcGJTxQ==", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/runner": "9.0.5", + "@wdio/types": "9.0.4", + "async-exit-hook": "^2.0.1", + "split2": "^4.1.0", + "stream-buffers": "^3.0.2" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + } + } + }, + "@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + } + } + }, + "@wdio/mocha-framework": { + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.38.2.tgz", + "integrity": "sha512-qJmRL5E6/ypjCUACH4hvCAAmTdU4YUrUlp9o/IKvTIAHMnZPE0/HgUFixCeu8Mop+rdzTPVBrbqxpRDdSnraYA==", + "dev": true, + "requires": { + "@types/mocha": "^10.0.0", + "@types/node": "^20.1.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.38.2", + "@wdio/utils": "8.38.2", + "mocha": "^10.0.0" + } + }, + "@wdio/protocols": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.0.4.tgz", + "integrity": "sha512-T9v8Jkp94NxLLil5J7uJ/+YHk5/7fhOggzGcN+LvuCHS6jbJFZ/11c4SUEuXw27Yqk01fFXPBbF6TwcTTOuW/Q==", + "dev": true + }, + "@wdio/repl": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.4.tgz", + "integrity": "sha512-5Bc5ARjWA7t6MZNnVJ9WvO1iDsy6iOsrSDWiP7APWAdaF/SJCP3SFE2c+PdQJpJWhr2Kk0fRGuyDM+GdsmZhwg==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/reporter": { + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.38.2.tgz", + "integrity": "sha512-R78UdAtAnkaV22NYlCCcbPPhmYweiDURiw64LYhlVIQrKNuXUQcafR2kRlWKy31rZc9thSLs5LzrZDReENUlFQ==", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.38.2", + "diff": "^5.0.0", + "object-inspect": "^1.12.0" + } + }, + "@wdio/runner": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.0.5.tgz", + "integrity": "sha512-qZF7k3BeQaM7pQRwIvedbfaC7xBU1xRY+wFkp44U/wvYZOOrqWiwv/Synk1iCFkOdxl/b+Gqp68dDmS9BrVDmw==", + "dev": true, + "requires": { + "@types/node": "^20.11.28", + "@wdio/config": "9.0.5", + "@wdio/globals": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "deepmerge-ts": "^7.0.3", + "expect-webdriverio": "^5.0.1", + "gaze": "^1.1.3", + "webdriver": "9.0.5", + "webdriverio": "9.0.5" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "dev": true, + "requires": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + }, + "@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + } + }, + "@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "expect-webdriverio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-5.0.1.tgz", + "integrity": "sha512-tYJaXtu5qQr/oEXzDKoVzyA/g2wyeE1YsuVJQC8/bKnp1lX3I4NQ2Yb9u1rJ9vdpFha6pki8PzHlTk5PVZBQWQ==", + "dev": true, + "requires": { + "@vitest/snapshot": "^2.0.5", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + }, + "puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + } + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "webdriverio": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.5.tgz", + "integrity": "sha512-80zhuLBT5W5wiLNZ0maT1cVUrxmwpMuBgCprwZjI8lFe+KUhGLClrJXud/RrVT9x9rDCYa6pGCtQ4UqA+2U+sw==", + "dev": true, + "requires": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/repl": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "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.2.1", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.5" + } + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "optional": true, + "peer": true, + "requires": {} + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "@wdio/spec-reporter": { + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.38.2.tgz", + "integrity": "sha512-Dntk+lmrp+0I3HRRWkkXED+smshvgsW5cdLKwJhEJ1liI48MdBpdNGf9IHTVckE6nfxcWDyFI4icD9qYv/5bFA==", + "dev": true, + "requires": { + "@wdio/reporter": "8.38.2", + "@wdio/types": "8.38.2", + "chalk": "^5.1.2", + "easy-table": "^1.2.0", + "pretty-ms": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + } + } + }, + "@wdio/types": { + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.38.2.tgz", + "integrity": "sha512-+wj1c1OSLdnN4WO5b44Ih4263dTl/eSwMGSI4/pCgIyXIuYQH38JQ+6WRa+c8vJEskUzboq2cSgEQumVZ39ozQ==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "8.38.2", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.38.2.tgz", + "integrity": "sha512-y5AnBwsGcu/XuCBGCgKmlvKdwEIFyzLA+Cr+denySxY3jbWDONtPUcGaVdFALwsIa5jcIjcATqGmZcCPGnkd7g==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.38.2", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.5.0", + "geckodriver": "^4.3.1", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.1.0", + "safaridriver": "^0.1.0", + "split2": "^4.2.0", + "wait-port": "^1.0.4" + } + }, + "@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "@zip.js/zip.js": { + "version": "2.7.48", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.48.tgz", + "integrity": "sha512-J7cliimZ2snAbr0IhLx2U8BwfA1pKucahKzTpFtYq4hEgKxwvFJcIjCIVNPwQpfVab7iVP+AKmoH1gidBlyhiQ==", + "dev": true + }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "peer": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true + }, + "aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "ajv": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "dev": true, + "optional": true + }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==" + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", + "dev": true, + "requires": { + "buffer-equal": "^1.0.0" + } + }, + "archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "requires": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "dependencies": { + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "requires": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, + "are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "requires": { + "dequal": "^2.0.3" + } + }, + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + }, + "dependencies": { + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", + "dev": true + } + } + }, + "arr-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA==", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw==", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + } + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==", + "dev": true + }, + "array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + } + }, + "array-initial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", + "integrity": "sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==", + "dev": true, + "requires": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "array-last": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", + "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "array-sort": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", + "dev": true, + "requires": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true + }, + "array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + } + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==" + }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + } + } + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true + }, + "async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + } + }, + "async-each": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "dev": true + }, + "async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true + }, + "async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true + }, + "async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==", + "dev": true, + "requires": { + "async-done": "^1.2.2" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true + }, + "aws4": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", + "dev": true + }, + "axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "dev": true, + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, + "babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.15.0.tgz", + "integrity": "sha512-sS2r8bFVzJ36EHlzzOvJvlre/Sec92V+oWQVb/pMwo/EKADB/cHdVD1jVXXsOgMRvCEwNyvDcpNWSh80N3N/KA==", + "dev": true, + "requires": { + "babel-runtime": "^6.9.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + } + } + }, + "bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==", + "dev": true, + "requires": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + } + }, + "bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "optional": true + }, + "bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "dev": true, + "optional": true, + "requires": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "bare-os": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "dev": true, + "optional": true + }, + "bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "requires": { + "bare-os": "^2.1.0" + } + }, + "bare-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "dev": true, + "optional": true, + "requires": { + "streamx": "^2.18.0" + } + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha512-3vqtKL1N45I5dV0RdssXZG7X6pCqQrWPNOlBPZPrd+QkE2HEhR57Z04m0KtpbsZH73j+a3F8UD1TQnn+ExTvIA==", + "dev": true + }, + "big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, + "binaryextensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz", + "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "body": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", + "integrity": "sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==", + "dev": true, + "requires": { + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" + }, + "dependencies": { + "bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==", + "dev": true + }, + "raw-body": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", + "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==", + "dev": true, + "requires": { + "bytes": "1", + "string_decoder": "0.10" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + } + } + }, + "body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "requires": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + } + }, + "browserstack": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.3.tgz", + "integrity": "sha512-AO+mECXsW4QcqC9bxwM29O7qWa7bJT94uBFzeb5brylIQwawuEziwq20dPYbins95GlWzOawgyDNdjYAo32EKg==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + } + } + }, + "browserstack-local": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.5.tgz", + "integrity": "sha512-jKne7yosrMcptj3hqxp36TP9k0ZW2sCqhyurX24rUL4G3eT7OLgv+CSQN8iq5dtkv5IK+g+v8fWvsiC/S9KxMg==", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "https-proxy-agent": "^5.0.1", + "is-running": "^2.1.0", + "ps-tree": "=1.2.0", + "temp-fs": "^0.9.9" + } + }, + "browserstacktunnel-wrapper": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/browserstacktunnel-wrapper/-/browserstacktunnel-wrapper-2.0.5.tgz", + "integrity": "sha512-oociT3nl+FhQnyJbAb1RM4oQ5pN7aKeXEURkTkiEVm/Rji2r0agl3Wbw5V23VFn9lCU5/fGyDejRZPtGYsEcFw==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1", + "unzipper": "^0.9.3" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + } + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, - "node_modules/lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==", + "buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", "dev": true }, - "node_modules/lodash._basetostring": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha512-mTzAr1aNAv/i7W43vOR/uD/aJ4ngbtsRaCubp2BfZhlGU/eORUjg/7F6X0orNMdv33JOrdgGybtvMN/po3EWrA==", + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", "dev": true }, - "node_modules/lodash._basevalues": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha512-H94wl5P13uEqlCg7OcNNhMQ8KvWSIyqXzOPusRgHC9DK3o54P6P3xtbXlVbRABG4q5gSmp7EDdJ0MSuW9HX6Mg==", + "bufferstreams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-1.0.1.tgz", + "integrity": "sha512-LZmiIfQprMLS6/k42w/PTc7awhU8AdNNcUerxTgr01WlP9agR2SgMv0wjlYYFD6eDOi8WvofrTX8RayjR/AeUQ==", + "requires": { + "readable-stream": "^1.0.33" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + } + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, - "node_modules/lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "node_modules/lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==", + "can-autoplay": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/can-autoplay/-/can-autoplay-3.0.2.tgz", + "integrity": "sha512-Ih6wc7yJB4TylS/mLyAW0Dj5Nh3Gftq/g966TcxgvpNCOzlbqTs85srAq7mwIspo4w8gnLCVVGroyCHfh6l9aA==", "dev": true }, - "node_modules/lodash._reescape": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha512-Sjlavm5y+FUVIF3vF3B75GyXrzsfYV8Dlv3L4mEpuB9leg8N6yf/7rU06iLPx9fY0Mv3khVp9p7Dx0mGV6V5OQ==", + "caniuse-lite": { + "version": "1.0.30001633", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001633.tgz", + "integrity": "sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, - "node_modules/lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha512-OrPwdDc65iJiBeUe5n/LIjd7Viy99bKwDdk7Z5ljfZg0uFRFlfQaCy9tZ4YMAag9WAZmlVpe1iZrkIMMSMHD3w==", + "ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "dev": true }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "dev": true }, - "node_modules/lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==", + "character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "dev": true }, - "node_modules/lodash.clone": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", - "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "dev": true }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + } + }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true }, - "node_modules/lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha512-n1PZMXgaaDWZDSvuNZ/8XOcYO2hOKDqZel5adtR30VKQAtoWs/5AOeFA0vPV8moiPzlqe7F4cP2tzpFewQyelQ==", + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, "dependencies": { - "lodash._root": "^3.0.0" + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + } } }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "cli-spinners": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.1.0.tgz", + "integrity": "sha512-2MH0N34TpDAs9ROPVkZJfBWFoYfv4zfkJF14PBHY4v/qRY75SLcm4WaEKNCLScsXieosa/tY/+slJ+BDswJxHQ==", "dev": true }, - "node_modules/lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "dev": true }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", "dev": true }, - "node_modules/lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", "dev": true }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true + "cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } }, - "node_modules/lodash.isobject": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", - "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "collection-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA==", + "dev": true, + "requires": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } }, - "node_modules/lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", "dev": true, - "dependencies": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } }, - "node_modules/lodash.pickby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", - "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==", + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "node_modules/lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==", + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true }, - "node_modules/lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" + "requires": { + "delayed-stream": "~1.0.0" } }, - "node_modules/lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0" - } + "comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true }, - "node_modules/lodash.zip": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", - "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, - "node_modules/log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true + }, + "compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "dev": true, - "engines": { - "node": ">=0.8.6" + "requires": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, - "node_modules/log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, - "dependencies": { - "chalk": "^2.0.1" - }, - "engines": { - "node": ">=4" + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, - "node_modules/log4js": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.7.0.tgz", - "integrity": "sha512-KA0W9ffgNBLDj6fZCq/lRbgR6ABAodRIDHrZnS48vOtfKa4PzWImb0Md1lmGCdO3n3sbCm/n1/WmrNlZ8kCI3Q==", + "concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", "dev": true, - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.3" + "requires": { + "source-map": "^0.6.1" }, - "engines": { - "node": ">=8.0" + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, - "node_modules/logform": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", - "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" }, - "engines": { - "node": ">= 12.0.0" + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } } }, - "node_modules/logform/node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "dev": true, - "engines": { - "node": ">=0.1.90" + "connect-livereload": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/connect-livereload/-/connect-livereload-0.6.1.tgz", + "integrity": "sha512-3R0kMOdL7CjJpU66fzAkCe6HNtd3AavCS4m+uW4KtJjrdGPT0SQEZieAYd+cm+lJoBznNQ4lqipYWkhBMgk00g==", + "dev": true + }, + "consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "requires": { + "bluebird": "^3.1.1" } }, - "node_modules/loglevel": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", - "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" } }, - "node_modules/loglevel-plugin-prefix": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", - "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "continuable-cache": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", + "integrity": "sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==", "dev": true }, - "node_modules/lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", "dev": true }, - "node_modules/longest-streak": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz", - "integrity": "sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==", + "copy-props": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", + "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "requires": { + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" + "core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==" + }, + "core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "requires": { + "browserslist": "^4.23.0" } }, - "node_modules/loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "deprecated": "Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5", + "core-js-pure": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.37.1.tgz", + "integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.0" + "requires": { + "object-assign": "^4", + "vary": "^1" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "coveralls": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.1.1.tgz", + "integrity": "sha512-+dxnG2NHncSD1NrqbSM3dn/lE57O6Qf/koe9+I7c+wzkqRmEvcp0kgJdxKInzYzkICKkFMZsX3Vct3++tsF9ww==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "js-yaml": "^3.13.1", + "lcov-parse": "^1.0.0", + "log-driver": "^1.2.7", + "minimist": "^1.2.5", + "request": "^2.88.2" } }, - "node_modules/lru-cache": { + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true + }, + "crc32-stream": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "dependencies": { - "es5-ext": "~0.10.2" + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, - "node_modules/m3u8-parser": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.1.tgz", - "integrity": "sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.5", - "global": "^4.4.0" - } + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", "dev": true, - "optional": true, + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + }, "dependencies": { - "sourcemap-codec": "^1.4.8" + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" } }, - "node_modules/make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } + "css-shorthand-properties": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz", + "integrity": "sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==", + "dev": true }, - "node_modules/make-iterator/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "dev": true }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true }, - "node_modules/map-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", - "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "csv-writer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", + "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==", "dev": true }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "dev": true, - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" } }, - "node_modules/markdown-table": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.2.tgz", - "integrity": "sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==", + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "requires": { + "assert-plus": "^1.0.0" } }, - "node_modules/marky": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", - "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "dev": true }, - "node_modules/matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", + "data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, - "dependencies": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - }, - "engines": { - "node": ">= 0.10.0" + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" } }, - "node_modules/matchdep/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" } }, - "node_modules/matchdep/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" } }, - "node_modules/matchdep/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true }, - "node_modules/matchdep/node_modules/braces/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==", + "dev": true }, - "node_modules/matchdep/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "optional": true + }, + "debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "requires": { + "ms": "2.1.2" } }, - "node_modules/matchdep/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "requires": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "node_modules/matchdep/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "decamelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "dev": true + }, + "decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "character-entities": "^2.0.0" } }, - "node_modules/matchdep/node_modules/fill-range/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true + }, + "deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "type-detect": "^4.0.0" } }, - "node_modules/matchdep/node_modules/findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", + "deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" } }, - "node_modules/matchdep/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/matchdep/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true }, - "node_modules/matchdep/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" + "requires": { + "kind-of": "^5.0.2" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } } }, - "node_modules/matchdep/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ==", + "dev": true + }, + "defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" + "optional": true, + "requires": { + "clone": "^1.0.2" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "optional": true + } } }, - "node_modules/matchdep/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, - "node_modules/matchdep/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" } }, - "node_modules/matchdep/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" } }, - "node_modules/mdast-util-definitions": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz", - "integrity": "sha512-rQ+Gv7mHttxHOBx2dkF4HWTg+EE+UR78ptQWDylzPKaQuVGdG4HIoY3SrS/pCp80nZ04greFvXbVFHT+uf0JVQ==", + "degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.1.tgz", - "integrity": "sha512-SobxkQXFAdd4b5WmEakmkVoh18icjQRxGy5OWTCzgsLRm1Fu/KCtwD1HIQSsmq5ZRjVH0Ehwg6/Fn3xIUk+nKw==", - "dev": true, "dependencies": { - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } } }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1260888", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1260888.tgz", + "integrity": "sha512-9rTIZ4ZjWwalCPiaY+kPiALLfOKgAz5CTi/Zb1L+qSZ8PH3zVo1T8JcgXIIqg1iM3pZ6hF+n9xO5r2jZ/SF+jg==", "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "optional": true, + "peer": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true }, - "node_modules/mdast-util-from-markdown": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz", - "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==", + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "esutils": "^2.0.2" } }, - "node_modules/mdast-util-from-markdown/node_modules/mdast-util-to-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", - "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==", + "doctrine-temporary-fork": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine-temporary-fork/-/doctrine-temporary-fork-2.1.0.tgz", + "integrity": "sha512-nliqOv5NkE4zMON4UA6AMJE6As35afs8aYXATpU4pTUdIKiARZwrJVEP1boA3Rx1ZXHVkwxkhcq4VkqvsuRLsA==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "esutils": "^2.0.2" } }, - "node_modules/mdast-util-gfm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.1.tgz", - "integrity": "sha512-42yHBbfWIFisaAfV1eixlabbsa6q7vHeSPY+cg+BBjX51M8xhgMacqH9g6TftB/9+YkcI0ooV4ncfrJslzm/RQ==", + "documentation": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.3.tgz", + "integrity": "sha512-B7cAviVKN9Rw7Ofd+9grhVuxiHwly6Ieh+d/ceMw8UdBOv/irkuwnDEJP8tq0wgdLJDUVuIkovV+AX9mTrZFxg==", "dev": true, - "dependencies": { - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-gfm-autolink-literal": "^1.0.0", - "mdast-util-gfm-footnote": "^1.0.0", - "mdast-util-gfm-strikethrough": "^1.0.0", - "mdast-util-gfm-table": "^1.0.0", - "mdast-util-gfm-task-list-item": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" + "requires": { + "@babel/core": "^7.18.10", + "@babel/generator": "^7.18.10", + "@babel/parser": "^7.18.11", + "@babel/traverse": "^7.18.11", + "@babel/types": "^7.18.10", + "@vue/compiler-sfc": "^3.2.37", + "chalk": "^5.0.1", + "chokidar": "^3.5.3", + "diff": "^5.1.0", + "doctrine-temporary-fork": "2.1.0", + "git-url-parse": "^13.1.0", + "github-slugger": "1.4.0", + "glob": "^8.0.3", + "globals-docs": "^2.4.1", + "highlight.js": "^11.6.0", + "ini": "^3.0.0", + "js-yaml": "^4.1.0", + "konan": "^2.1.1", + "lodash": "^4.17.21", + "mdast-util-find-and-replace": "^2.2.1", + "mdast-util-inject": "^1.1.0", + "micromark-util-character": "^1.1.0", + "parse-filepath": "^1.0.2", + "pify": "^6.0.0", + "read-pkg-up": "^9.1.0", + "remark": "^14.0.2", + "remark-gfm": "^3.0.1", + "remark-html": "^15.0.1", + "remark-reference-links": "^6.0.1", + "remark-toc": "^8.0.1", + "resolve": "^1.22.1", + "strip-json-comments": "^5.0.0", + "unist-builder": "^3.0.0", + "unist-util-visit": "^4.1.0", + "vfile": "^5.3.4", + "vfile-reporter": "^7.0.4", + "vfile-sort": "^3.0.0", + "vue-template-compiler": "^2.7.8", + "yargs": "^17.5.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "requires": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "requires": { + "p-locate": "^6.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "requires": { + "p-limit": "^4.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true + }, + "read-pkg": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", + "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", + "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", + "dev": true, + "requires": { + "find-up": "^6.3.0", + "read-pkg": "^7.1.0", + "type-fest": "^2.5.0" + } + }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + }, + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } } }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.2.tgz", - "integrity": "sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==", + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "ccount": "^2.0.0", - "mdast-util-find-and-replace": "^2.0.0", - "micromark-util-character": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" } }, - "node_modules/mdast-util-gfm-footnote": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.1.tgz", - "integrity": "sha512-p+PrYlkw9DeCRkTVw1duWqPRHX6Ywh2BNKJQcZbCwAuP/59B0Lk9kakuAd7KbQprVO4GzdW8eS5++A9PUSqIyw==", + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0", - "micromark-util-normalize-identifier": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" } }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.1.tgz", - "integrity": "sha512-zKJbEPe+JP6EUv0mZ0tQUyLQOC+FADt0bARldONot/nefuISkaZFlmVK4tU6JgfyZGrky02m/I6PmehgAgZgqg==", + "dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "domelementtype": "^2.3.0" } }, - "node_modules/mdast-util-gfm-table": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.6.tgz", - "integrity": "sha512-uHR+fqFq3IvB3Rd4+kzXW8dmpxUhvgCQZep6KdjsLK4O6meK5dYZEayLtIxNus1XO3gfjfcIFe8a7L0HZRGgag==", + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" } }, - "node_modules/mdast-util-gfm-task-list-item": { + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true + }, + "dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==" + }, + "dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.1.tgz", - "integrity": "sha512-KZ4KLmPdABXOsfnM6JHUIjxEvcx2ulk656Z/4Balw071/5qgnhz+H1uGtf2zIGnrnvDC8xR4Fj9uKbjAFGNIeA==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" } }, - "node_modules/mdast-util-inject": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz", - "integrity": "sha512-CcJ0mHa36QYumDKiZ2OIR+ClhfOM7zIzN+Wfy8tRZ1hpH9DKLCS+Mh4DyK5bCxzE9uxMWcbIpeNFWsg1zrj/2g==", + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==", "dev": true, + "requires": { + "readable-stream": "~1.1.9" + }, "dependencies": { - "mdast-util-to-string": "^1.0.0" + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + } } }, - "node_modules/mdast-util-to-hast": { - "version": "12.2.4", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.2.4.tgz", - "integrity": "sha512-a21xoxSef1l8VhHxS1Dnyioz6grrJkoaCUgGzMD/7dWHvboYX3VW53esRUfB5tgTyz4Yos1n25SPcj35dJqmAg==", + "duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", "dev": true, - "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", - "trim-lines": "^3.0.0", - "unist-builder": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, - "node_modules/mdast-util-to-markdown": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz", - "integrity": "sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==", + "each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" + "requires": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } } }, - "node_modules/mdast-util-to-markdown/node_modules/mdast-util-to-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", - "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==", + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "easy-table": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.2.0.tgz", + "integrity": "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "ansi-regex": "^5.0.1", + "wcwidth": "^1.0.1" } }, - "node_modules/mdast-util-to-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", - "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + } } }, - "node_modules/mdast-util-toc": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-toc/-/mdast-util-toc-6.1.0.tgz", - "integrity": "sha512-0PuqZELXZl4ms1sF7Lqigrqik4Ll3UhbI+jdTrfw7pZ9QPawgl7LD4GQ8MkU7bT/EwiVqChNTbifa2jLLKo76A==", + "edge-paths": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", + "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", "dev": true, - "dependencies": { - "@types/extend": "^3.0.0", - "@types/github-slugger": "^1.0.0", - "@types/mdast": "^3.0.0", - "extend": "^3.0.0", - "github-slugger": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "unist-util-is": "^5.0.0", - "unist-util-visit": "^3.0.0" + "requires": { + "@types/which": "^2.0.1", + "which": "^2.0.2" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, - "node_modules/mdast-util-toc/node_modules/mdast-util-to-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", - "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==", + "edgedriver": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.6.1.tgz", + "integrity": "sha512-3Ve9cd5ziLByUdigw6zovVeWJjVs8QHVmqOB0sJ0WNeVPcwf4p18GnxMmVvlFmYRloUwf5suNuorea4QzwBIOA==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "@wdio/logger": "^8.38.0", + "@zip.js/zip.js": "^2.7.48", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^4.4.1", + "node-fetch": "^3.3.2", + "which": "^4.0.0" } }, - "node_modules/mdast-util-toc/node_modules/unist-util-visit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz", - "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==", + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "jake": "^10.8.5" } }, - "node_modules/mdast-util-toc/node_modules/unist-util-visit-parents": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz", - "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "electron-to-chromium": { + "version": "1.4.802", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz", + "integrity": "sha512-TnTMUATbgNdPXVSHsxvNVSG0uEd6cSZsANjm8c9HbvflZVVn1yTRcmVXYT1Ma95/ssB/Dcd30AHweH2TE+dNpA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dev": true, + "requires": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } } }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" } }, - "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dev": true, + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true + } } }, - "node_modules/memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" } }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" } }, - "node_modules/micromark": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.1.0.tgz", - "integrity": "sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==", + "error": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", + "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "requires": { + "string-template": "~0.2.1" } }, - "node_modules/micromark-core-commonmark": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", - "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "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.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "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.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + } + }, + "es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + } + }, + "es-module-lexer": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", + "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", + "dev": true + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" } }, - "node_modules/micromark-extension-gfm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.1.tgz", - "integrity": "sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==", + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^1.0.0", - "micromark-extension-gfm-footnote": "^1.0.0", - "micromark-extension-gfm-strikethrough": "^1.0.0", - "micromark-extension-gfm-table": "^1.0.0", - "micromark-extension-gfm-tagfilter": "^1.0.0", - "micromark-extension-gfm-task-list-item": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" } }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.3.tgz", - "integrity": "sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==", + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "hasown": "^2.0.0" } }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.0.4.tgz", - "integrity": "sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg==", + "es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, - "dependencies": { - "micromark-core-commonmark": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" } }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.4.tgz", - "integrity": "sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ==", + "es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" } }, - "node_modules/micromark-extension-gfm-table": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.5.tgz", - "integrity": "sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==", + "es5-shim": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/es5-shim/-/es5-shim-4.6.7.tgz", + "integrity": "sha512-jg21/dmlrNQI7JyyA2w7n+yifSxBng0ZralnSfVZjoCawgNTCnS+yBCyVM9DL5itm7SUnDGgv7hcq2XCZX4iRQ==", + "dev": true + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "dev": true, - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" } }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.1.tgz", - "integrity": "sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA==", + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", "dev": true, - "dependencies": { - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "es6-promise": "^4.0.3" } }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.3.tgz", - "integrity": "sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q==", + "es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dev": true, - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "d": "^1.0.2", + "ext": "^1.7.0" } }, - "node_modules/micromark-factory-destination": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", - "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + }, + "dependencies": { + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", + "dev": true }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-factory-label": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", - "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "eslint": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", + "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.1.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.22.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" } }, - "node_modules/micromark-factory-space": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", - "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "requires": { + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true } - ], + } + }, + "eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + }, "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "node_modules/micromark-factory-title": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", - "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "eslint-import-resolver-typescript": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.8.6.tgz", + "integrity": "sha512-d9UjvYpj/REmUoZvOtDEmayPlwyP4zOwwMBgtC6RtrpZta8u1AIVmxgZBYJIcCKKXwAcLs+DX2yn2LeMaTqKcQ==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^1.0.2", + "stable-hash": "^0.0.4", + "tinyglobby": "^0.2.12" + }, + "dependencies": { + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true } - ], - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" } }, - "node_modules/micromark-factory-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", - "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "requires": { + "debug": "^3.2.7" + }, "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "node_modules/micromark-util-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", - "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + } + }, + "eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "requires": { + "@rtsao/scc": "^1.1.0", + "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.0", + "hasown": "^2.0.2", + "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.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-util-chunked": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", - "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "eslint-plugin-import-x": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.6.1.tgz", + "integrity": "sha512-wluSUifMIb7UfwWXqx7Yx0lE/SGCcGXECLx/9bCmbY2nneLwvAZ4vkd1IXDjPKFvdcdUgr1BaRnaRpx3k2+Pfw==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "@types/doctrine": "^0.0.9", + "@typescript-eslint/scope-manager": "^8.1.0", + "@typescript-eslint/utils": "^8.1.0", + "debug": "^4.3.4", + "doctrine": "^3.0.0", + "enhanced-resolve": "^5.17.1", + "eslint-import-resolver-node": "^0.3.9", + "get-tsconfig": "^4.7.3", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3", + "semver": "^7.6.3", + "stable-hash": "^0.0.4", + "tslib": "^2.6.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" } }, - "node_modules/micromark-util-classify-character": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", - "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "eslint-plugin-jsdoc": { + "version": "50.6.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.6.tgz", + "integrity": "sha512-4jLo9NZqHfyNtiBxAU293eX1xi6oUIBcAxJJL/hHeeNhh26l4l/Apmu0x9SarvSQ/gWNOrnFci4DSPupN4//WA==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "@es-joy/jsdoccomment": "~0.49.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.6", + "escape-string-regexp": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", - "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "eslint-plugin-n": { + "version": "17.16.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.16.2.tgz", + "integrity": "sha512-iQM5Oj+9o0KaeLoObJC/uxNGpktZCkYiTTBo8PkRWq3HwNcRxwpvSDFjBhQ5+HLJzBTy+CLDC5+bw0Z5GyhlOQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.1", + "enhanced-resolve": "^5.17.1", + "eslint-plugin-es-x": "^7.8.0", + "get-tsconfig": "^4.8.1", + "globals": "^15.11.0", + "ignore": "^5.3.2", + "minimatch": "^9.0.5", + "semver": "^7.6.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true } - ], - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", - "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "eslint-plugin-promise": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz", + "integrity": "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "@eslint-community/eslint-utils": "^4.4.0" + } + }, + "eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" } }, - "node_modules/micromark-util-decode-string": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", - "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + } + }, + "espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "requires": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "dependencies": { + "acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true } - ], + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", + "dev": true + }, + "esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } } }, - "node_modules/micromark-util-encode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", - "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==", + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true } - ] + } }, - "node_modules/micromark-util-html-tag-name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz", - "integrity": "sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==", + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "optional": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + }, + "dependencies": { + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true } - ] + } }, - "node_modules/micromark-util-normalize-identifier": { + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "execa": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", - "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" } }, - "node_modules/micromark-util-resolve-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", - "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true } - ], - "dependencies": { - "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-util-sanitize-uri": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz", - "integrity": "sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==", + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "requires": { + "homedir-polyfill": "^1.0.1" } }, - "node_modules/micromark-util-subtokenize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", - "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" } }, - "node_modules/micromark-util-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", - "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", - "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } } - ] + } }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" + "requires": { + "type": "^2.7.2" } }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", "dev": true, - "bin": { - "mime": "cli.js" + "requires": { + "kind-of": "^1.1.0" }, - "engines": { - "node": ">=4.0.0" + "dependencies": { + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "dev": true + } } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, - "engines": { - "node": ">= 0.6" + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, - "engines": { - "node": ">=6" + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true + }, + "faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==", + "dev": true + }, + "fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", "dev": true, - "engines": { - "node": ">=4" + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" } }, - "node_modules/min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "dependencies": { - "dom-walk": "^0.1.0" + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "requires": { + "strnum": "^1.0.5" } }, - "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "reusify": "^1.0.4" } }, - "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==", "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" + "requires": { + "websocket-driver": ">=0.5.1" } }, - "node_modules/mitt": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", - "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", - "dev": true - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "pend": "~1.2.0" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "dev": true }, - "node_modules/mocha": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", - "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + "dependencies": { + "web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true + } } }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "dev": true, - "engines": { - "node": ">=6" + "requires": { + "is-unicode-supported": "^2.0.0" } }, - "node_modules/mocha/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "requires": { + "flat-cache": "^4.0.0" } }, - "node_modules/mocha/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true }, - "node_modules/mocha/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" + "requires": { + "minimatch": "^5.0.1" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, - "node_modules/mocha/node_modules/chalk/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==", + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "requires": { + "to-regex-range": "^5.0.1" } }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } } }, - "node_modules/mocha/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" } }, - "node_modules/mocha/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "engines": { - "node": ">=0.3.1" + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, - "node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "dev": true, - "engines": { - "node": ">=10" + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, - "node_modules/mocha/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } } }, - "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" } }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, - "node_modules/mocha/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true + }, + "for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "is-callable": "^1.2.7" } }, - "node_modules/mocha/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==", + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "requires": { + "for-in": "^1.0.1" } }, - "node_modules/mocha/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", + "dev": true + }, + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } } }, - "node_modules/mocha/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true + }, + "fork-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", + "integrity": "sha512-Pqq5NnT78ehvUnAk/We/Jr22vSvanRlFTpAmQ88xBY/M1TlHe+P0ILuEyXS595ysdGfaj22634LBkGMA2GTcpA==", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" } }, - "node_modules/mocha/node_modules/minimatch": { + "formdata-node": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-5.0.1.tgz", + "integrity": "sha512-8xnIjMYGKPj+rY2BTbAmpqVpi8der/2FT4d9f7J32FlsCpO5EzZPq3C/N56zdv8KweHzVF6TGijsS1JT6r1H2g==", "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "requires": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "requires": { + "fetch-blob": "^3.1.2" } }, - "node_modules/mocha/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==", + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", "dev": true }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "fs-extra": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz", + "integrity": "sha512-5rU898vl/Z948L+kkJedbmo/iltzmiF5bn/eEk0j/SgrPpI+Ydau9xlJPicV7Av2CHYBGz5LAlwTnBU80j1zPQ==", "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" + "requires": { + "jsonfile": "~1.0.1", + "mkdirp": "0.3.x", + "ncp": "~0.4.2", + "rimraf": "~2.2.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==", + "dev": true + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", + "dev": true + } } }, - "node_modules/mocha/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" + "requires": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, - "node_modules/mocha/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "fs-readfile-promise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-3.0.1.tgz", + "integrity": "sha512-LsSxMeaJdYH27XrW7Dmq0Gx63mioULCRel63B5VeELYLavi1wF5s0XfsIdKDFdCL9hsfQ2qBvXJszQtQJ9h17A==", + "requires": { + "graceful-fs": "^4.1.11" + } + }, + "fs.extra": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fs.extra/-/fs.extra-1.3.2.tgz", + "integrity": "sha512-Ig401VXtyrWrz23k9KxAx9OrnL8AHSLNhQ8YJH2wSYuH0ZUfxwBeY6zXkd/oOyVRFTlpEu/0n5gHeuZt7aqbkw==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "fs-extra": "~0.6.1", + "mkdirp": "~0.3.5", + "walk": "^2.3.9" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==", + "dev": true + } } }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "fun-hooks": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/fun-hooks/-/fun-hooks-0.9.10.tgz", + "integrity": "sha512-7xBjdT+oMYOPWgwFxNiNzF4ubeUvim4zs1DnQqSSGyxu8UD7AW/6Z0iFsVRwuVSIZKUks2en2VHHotmNfj3ipw==", + "requires": { + "typescript-tuple": "^2.2.1" + } + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" } }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "dev": true, - "engines": { - "node": ">=10" + "requires": { + "globule": "^1.0.0" } }, - "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "geckodriver": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.4.1.tgz", + "integrity": "sha512-nnAdIrwLkMcDu4BitWXF23pEMeZZ0Cj7HaWWFdSpeedBP9z6ft150JYiGO2mwzw6UiR823Znk1JeIf07RyzloA==", "dev": true, - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" + "requires": { + "@wdio/logger": "^8.28.0", + "@zip.js/zip.js": "^2.7.44", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "node-fetch": "^3.3.2", + "tar-fs": "^3.0.6", + "which": "^4.0.0" }, - "engines": { - "node": ">= 0.8.0" + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + } } }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" + "get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" } }, - "node_modules/mpd-parser": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz", - "integrity": "sha512-BxlSXWbKE1n7eyEPBnTEkrzhS3PdmkkKdM1pgKbPnPOH0WFZIc0sPOWi7m0Uo3Wd2a4Or8Qf4ZbS7+ASqQ49fw==", + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.5", - "@xmldom/xmldom": "^0.7.2", - "global": "^4.4.0" - }, - "bin": { - "mpd-to-m3u8-json": "bin/parse.js" + "requires": { + "pump": "^3.0.0" } }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, - "engines": { - "node": ">=4" + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" } }, - "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, - "engines": { - "node": ">=10" + "requires": { + "resolve-pkg-maps": "^1.0.0" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha512-7ZxrUybYv9NonoXgwoOqtStIu18D1c3eFZj27hqgf5kBrBF8Q+tE8V0MW8dKM5QLkQPh1JhhbKgHLY9kifov4Q==", + "get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", "dev": true, + "requires": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, "dependencies": { - "duplexer2": "0.0.2" + "data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true + }, + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + } } }, - "node_modules/mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, - "engines": { - "node": ">= 0.10" + "requires": { + "assert-plus": "^1.0.0" } }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "git-repo-info": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/git-repo-info/-/git-repo-info-2.1.1.tgz", + "integrity": "sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==", "dev": true }, - "node_modules/mux.js": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", - "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", + "git-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", + "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", "dev": true, - "dependencies": { - "@babel/runtime": "^7.11.2", - "global": "^4.4.0" - }, - "bin": { - "muxjs-transmux": "bin/transmux.js" + "requires": { + "is-ssh": "^1.4.0", + "parse-url": "^8.1.0" + } + }, + "git-url-parse": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.1.tgz", + "integrity": "sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==", + "dev": true, + "requires": { + "git-up": "^7.0.0" + } + }, + "gitconfiglocal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz", + "integrity": "sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==", + "dev": true, + "requires": { + "ini": "^1.3.2" }, - "engines": { - "node": ">=8", - "npm": ">=5" + "dependencies": { + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + } } }, - "node_modules/n12": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/n12/-/n12-1.8.16.tgz", - "integrity": "sha512-CZqHAqbzS0UsaUGkMsL+lMaYLyFr1+/ea+pD8dMziqSjkcuWVWDtgWx9phyfT7C3llqQ2+LwnStSb5afggBMfA==", + "github-slugger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.4.0.tgz", + "integrity": "sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==", "dev": true }, - "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, - "optional": true + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "requires": { + "is-glob": "^4.0.1" } }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } } }, - "node_modules/nanomatch/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "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 + }, + "glob-watcher": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", + "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "normalize-path": "^3.0.0", + "object.defaults": "^1.1.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, - "node_modules/nanomatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" } }, - "node_modules/nanomatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/ncp": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz", - "integrity": "sha512-PfGU8jYWdRl4FqJfCy0IzbkGyFHntfWygZg46nFk/dJD/XRrk2cj0SsKSX9n5u5gE0E0YfEpKWrEkfjnlZSTXA==", + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, - "bin": { - "ncp": "bin/ncp" + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } + "globals": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "dev": true }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "globals-docs": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/globals-docs/-/globals-docs-2.4.1.tgz", + "integrity": "sha512-qpPnUKkWnz8NESjrCvnlGklsgiQzlq+rcCxoG5uNQ+dNA7cFMCmn231slLAwS2N/PlkzZ3COL8CcS10jXmLHqg==", "dev": true }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, - "engines": { - "node": ">= 0.4.0" + "requires": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" } }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/nise": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", - "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "globule": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", + "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "^4.17.21", + "minimatch": "~3.0.2" + }, "dependencies": { - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^5.0.1", - "path-to-regexp": "^1.7.0" + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, - "node_modules/nise/node_modules/@sinonjs/formatio": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", - "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", "dev": true, - "dependencies": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" + "requires": { + "sparkles": "^1.0.0" } }, - "node_modules/nise/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, - "node_modules/nise/node_modules/lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", "dev": true, - "dependencies": { - "isarray": "0.0.1" + "requires": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "gulp-clean": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/gulp-clean/-/gulp-clean-0.4.0.tgz", + "integrity": "sha512-DARK8rNMo4lHOFLGTiHEJdf19GuoBDHqGUaypz+fOhrvOs3iFO7ntdYtdpNxv+AzSJBx/JfypF0yEj9ks1IStQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" + "requires": { + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2", + "rimraf": "^2.6.2", + "through2": "^2.0.3", + "vinyl": "^2.1.0" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } } - ], - "engines": { - "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "gulp-cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "requires": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-html-parser": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.6.tgz", - "integrity": "sha512-C/MGDQ2NjdjzUq41bW9kW00MPZecAe/oo89vZEFLDfWoQVDk/DdML1yuxVVKLDMFIFax2VTq6Vpfzyn7z5yYgQ==", - "dev": true, "dependencies": { - "css-select": "^5.1.0", - "he": "1.2.0" + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true + }, + "yargs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.1" + } + }, + "yargs-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" + } + } } }, - "node_modules/node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" - }, - "node_modules/node-request-interceptor": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.6.3.tgz", - "integrity": "sha512-8I2V7H2Ch0NvW7qWcjmS0/9Lhr0T6x7RD6PDirhvWEkUQvy83x8BA4haYMr09r/rig7hcgYSjYh6cd4U7G1vLA==", + "gulp-concat": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", + "integrity": "sha512-a2scActrQrDBpBbR3WUZGyGS1JEPLg5PZJdIa7/Bi3GuKAmPYDK6SFhy/NZq5R8KsKKFvtfR0fakbUCcKGCCjg==", "dev": true, - "dependencies": { - "@open-draft/until": "^1.0.3", - "debug": "^4.3.0", - "headers-utils": "^1.2.0", - "strict-event-emitter": "^0.1.0" - } - }, - "node_modules/node.extend": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", - "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==", - "dependencies": { - "has": "^1.0.3", - "is": "^3.2.1" + "requires": { + "concat-with-sourcemaps": "^1.0.0", + "through2": "^2.0.0", + "vinyl": "^2.0.0" }, - "engines": { - "node": ">=0.4.0" + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, - "node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "gulp-connect": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/gulp-connect/-/gulp-connect-5.7.0.tgz", + "integrity": "sha512-8tRcC6wgXMLakpPw9M7GRJIhxkYdgZsXwn7n56BA2bQYGLR9NOPhMzx7js+qYDy6vhNkbApGKURjAw1FjY4pNA==", "dev": true, - "dependencies": { - "abbrev": "1" + "requires": { + "ansi-colors": "^2.0.5", + "connect": "^3.6.6", + "connect-livereload": "^0.6.0", + "fancy-log": "^1.3.2", + "map-stream": "^0.0.7", + "send": "^0.16.2", + "serve-index": "^1.9.1", + "serve-static": "^1.13.2", + "tiny-lr": "^1.1.1" }, - "bin": { - "nopt": "bin/nopt.js" + "dependencies": { + "ansi-colors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-2.0.5.tgz", + "integrity": "sha512-yAdfUZ+c2wetVNIFsNRn44THW+Lty6S5TwMpUfLA/UaGhiXbBv/F8E60/1hMLd0cnF/CDoWH8vzVaI5bAcHCjw==", + "dev": true + } } }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "gulp-if": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz", + "integrity": "sha512-fCUEngzNiEZEK2YuPm+sdMpO6ukb8+/qzbGfJBXyNOXz85bCG7yBI+pPSl+N90d7gnLvMsarthsAImx0qy7BAw==", "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" + "requires": { + "gulp-match": "^1.1.0", + "ternary-stream": "^3.0.0", + "through2": "^3.0.1" }, - "engines": { - "node": ">=10" + "dependencies": { + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "gulp-js-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gulp-js-escape/-/gulp-js-escape-1.0.1.tgz", + "integrity": "sha512-F+53crhLb78CTlG7ZZJFWzP0+/4q0vt2/pULXFkTMs6AGBo0Eh5cx+eWsqqHv8hrNIUsuTab3Se8rOOzP/6+EQ==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "requires": { + "through2": "^0.6.3" }, - "engines": { - "node": ">=10" + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "gulp-match": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.1.0.tgz", + "integrity": "sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "minimatch": "^3.0.3" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "gulp-rename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.0.0.tgz", + "integrity": "sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ==", + "dev": true + }, + "gulp-replace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.4.tgz", + "integrity": "sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "@types/node": "*", + "@types/vinyl": "^2.0.4", + "istextorbinary": "^3.0.0", + "replacestream": "^4.0.3", + "yargs-parser": ">=5.0.0-security.0" } }, - "node_modules/now-and-later": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", - "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "gulp-run-command": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/gulp-run-command/-/gulp-run-command-0.0.10.tgz", + "integrity": "sha512-i6o4XRqoadZB2doWCqkrCe7FmFwqPZ0Fxx74FGt83/KT5wKRRaKiFh598W64HE0Br9es6Oyq+nA+/AWbCfeSYQ==", + "dev": true, + "requires": { + "babel-plugin-transform-runtime": "6.15.0", + "cross-spawn": "4.0.0", + "spawn-args": "0.2.0", + "timeout-as-promise": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.0.tgz", + "integrity": "sha512-dAZV+Hv1PRxSUrJd9Hk9MS4gL5eEafKhrmsRlod5oHg8aP3A2FsXkga4ihfMFxlEgmMa+LS84jPKFdhk5wZwuw==", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } + }, + "gulp-shell": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/gulp-shell/-/gulp-shell-0.8.0.tgz", + "integrity": "sha512-wHNCgmqbWkk1c6Gc2dOL5SprcoeujQdeepICwfQRo91DIylTE7a794VEE+leq3cE2YDoiS5ulvRfKVIEMazcTQ==", "dev": true, - "dependencies": { - "once": "^1.3.2" + "requires": { + "chalk": "^3.0.0", + "fancy-log": "^1.3.3", + "lodash.template": "^4.5.0", + "plugin-error": "^1.0.1", + "through2": "^3.0.1", + "tslib": "^1.10.0" }, - "engines": { - "node": ">= 0.10" + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } } }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "gulp-sourcemaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", + "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", "dev": true, - "dependencies": { - "path-key": "^2.0.0" + "requires": { + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" }, - "engines": { - "node": ">=4" + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "gulp-terser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gulp-terser/-/gulp-terser-2.1.0.tgz", + "integrity": "sha512-lQ3+JUdHDVISAlUIUSZ/G9Dz/rBQHxOiYDQ70IVWFQeh4b33TC1MCIU+K18w07PS3rq/CVc34aQO4SUbdaNMPQ==", "dev": true, - "engines": { - "node": ">=4" + "requires": { + "plugin-error": "^1.0.1", + "terser": "^5.9.0", + "through2": "^4.0.2", + "vinyl-sourcemaps-apply": "^0.2.1" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + } } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "gulp-util": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", + "integrity": "sha512-q5oWPc12lwSFS9h/4VIjG+1NuNDlJ48ywV2JKItY4Ycc/n1fXJeYPVQsfu5ZrhQi7FGSDBalwUCLar/GyHXKGw==", "dev": true, - "dependencies": { - "boolbase": "^1.0.0" + "requires": { + "array-differ": "^1.0.0", + "array-uniq": "^1.0.2", + "beeper": "^1.0.0", + "chalk": "^1.0.0", + "dateformat": "^2.0.0", + "fancy-log": "^1.1.0", + "gulplog": "^1.0.0", + "has-gulplog": "^0.1.0", + "lodash._reescape": "^3.0.0", + "lodash._reevaluate": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.template": "^3.0.0", + "minimist": "^1.1.0", + "multipipe": "^0.1.2", + "object-assign": "^3.0.0", + "replace-ext": "0.0.1", + "through2": "^2.0.0", + "vinyl": "^0.5.0" }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha512-dhUqc57gSMCo6TX85FLfe51eC/s+Im2MLkAgJwfaRRexR2tA4dd3eLEW4L6efzHc2iNorrRRXITifnDLlRrhaA==", + "dev": true + }, + "lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha512-0B4Y53I0OgHUJkt+7RmlDFWKjVAI/YUpWNiL9GQz5ORDr4ttgfQGo+phBWKFLJbBdtOwgMuUkdOHOnPg45jKmQ==", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash._basetostring": "^3.0.0", + "lodash._basevalues": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0", + "lodash.keys": "^3.0.0", + "lodash.restparam": "^3.0.0", + "lodash.templatesettings": "^3.0.0" + } + }, + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha512-TcrlEr31tDYnWkHFWDCV3dHYroKEXpJZ2YJYvJdhN+y4AkWMDZ5I4I8XDtUKqSAyG81N7w+I1mFEJtcED+tGqQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0" + } + }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha512-P5zdf3WB9uzr7IFoVQ2wZTmUwHL8cMZWJGzLBNCHNZ3NB6HTMsYABtt7z8tAGIINLXyAob9B9a1yzVGMFOYKEA==", + "dev": true, + "requires": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + } + } } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "gulp-wrap": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/gulp-wrap/-/gulp-wrap-0.15.0.tgz", + "integrity": "sha512-f17zkGObA+hE/FThlg55gfA0nsXbdmHK1WqzjjB2Ytq1TuhLR7JiCBJ3K4AlMzCyoFaCjfowos+VkToUNE0WTQ==", + "requires": { + "consolidate": "^0.15.1", + "es6-promise": "^4.2.6", + "fs-readfile-promise": "^3.0.1", + "js-yaml": "^3.13.0", + "lodash": "^4.17.11", + "node.extend": "2.0.2", + "plugin-error": "^1.0.1", + "through2": "^3.0.1", + "tryit": "^1.0.1", + "vinyl-bufferstream": "^1.0.1" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", "dev": true, - "engines": { - "node": "*" + "requires": { + "glogg": "^1.0.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "duplexer": "^0.1.2" } }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" } }, - "node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "deprecated": "Please upgrade to v0.1.7", + "has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" + "requires": { + "ansi-regex": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + } } }, - "node_modules/object-copy/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true }, - "node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "deprecated": "Please upgrade to v0.1.5", + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha512-+F4GzLjwHNNDEAJW2DC1xXfEoPkRDmUdJ7CBYw4MpqtDwOnqdImJl7GWlpqx+Wko6//J8uKTnIe4wZSv7yCqmw==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "sparkles": "^1.0.0" } }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "es-define-property": "^1.0.0" } }, - "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "dunder-proto": "^1.0.0" } }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "has-symbols": "^1.0.3" } }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" } }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", "dev": true, - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" } }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "@types/hast": "^2.0.0" } }, - "node_modules/object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", "dev": true, - "dependencies": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" } }, - "node_modules/object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "hast-util-sanitize": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-4.1.0.tgz", + "integrity": "sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==", "dev": true, - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/hast": "^2.0.0" } }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "hast-util-to-html": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", + "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^7.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" } }, - "node_modules/object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==", + "hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", "dev": true, - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" } }, - "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "dev": true + }, + "hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "headers-utils": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.5.tgz", + "integrity": "sha512-DAzV5P/pk3wTU/8TLZN+zFTDv4Xa1QDTU8pRvovPetcOMbmqq8CwsAvZBLPZHH6usxyy31zMp7I4aCYb6XIf6w==", + "dev": true + }, + "highlight.js": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", + "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, - "engines": { - "node": ">= 0.8" + "requires": { + "parse-passwd": "^1.0.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, + "requires": { + "lru-cache": "^10.0.1" + }, "dependencies": { - "wrappy": "1" + "lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true + } } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "dev": true + }, + "htmlfy": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.2.1.tgz", + "integrity": "sha512-HoomFHQ3av1uhq+7FxJTq4Ns0clAD+tGbQNrSd0WFY3UAjjUk6G3LaWEqdgmIXYkY4pexZiyZ3ykZJhQlM0J5A==", + "dev": true + }, + "htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, - "bin": { - "opener": "bin/opener-bin.js" + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" } }, - "node_modules/opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, - "dependencies": { - "is-wsl": "^1.1.0" + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, - "engines": { - "node": ">=4" + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + } } }, - "node_modules/opn/node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "dev": true, - "engines": { - "node": ">=4" + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } } }, - "node_modules/ora/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "individual": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz", + "integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "requires": { + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true + }, + "inquirer": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-10.1.8.tgz", + "integrity": "sha512-syxGpOzLyqVeZi1KDBjRTnCn5PiGWySGHP0BbqXbqsEK0ckkZk3egAepEWslUjZXj0rhkUapVXM/IpADWe4D6w==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "requires": { + "@inquirer/prompts": "^5.3.8", + "@inquirer/type": "^1.5.2", + "@types/mute-stream": "^0.0.4", + "ansi-escapes": "^4.3.2", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "rxjs": "^7.8.1" } }, - "node_modules/ora/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" } }, - "node_modules/ora/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, - "node_modules/ora/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", + "dev": true }, - "node_modules/ora/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + } } }, - "node_modules/ora/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==", + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", + "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" } }, - "node_modules/ordered-read-streams": { + "is-accessor-descriptor": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", "dev": true, - "dependencies": { - "readable-stream": "^2.0.1" + "requires": { + "hasown": "^2.0.0" } }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, - "node_modules/os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", + "is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, - "dependencies": { - "lcid": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true }, - "node_modules/p-cancelable": { + "is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, - "engines": { - "node": ">=4" + "requires": { + "has-bigints": "^1.0.2" } }, - "node_modules/p-iteration": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/p-iteration/-/p-iteration-1.1.8.tgz", - "integrity": "sha512-IMFBSDIYcPNnW7uWYGrBqmvTiq7W0uB0fJn6shQZs7dlF3OvrHOre+JT9ikSZ7gZS3vWqclVgoQSvToJrns7uQ==", + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, - "engines": { - "node": ">=8.0.0" + "requires": { + "binary-extensions": "^2.0.0" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true + }, + "is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", "dev": true, - "dependencies": { - "p-limit": "^2.2.0" + "requires": { + "semver": "^7.6.3" }, - "engines": { - "node": ">=8" + "dependencies": { + "semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true + } } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "requires": { + "hasown": "^2.0.2" } }, - "node_modules/pac-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", - "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.2" - }, - "engines": { - "node": ">= 14" + "requires": { + "hasown": "^2.0.0" } }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "requires": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" } }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" } }, - "node_modules/pac-resolver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz", - "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==", + "is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "ip": "^1.1.8", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" } }, - "node_modules/parent-module": { + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extendable": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" }, - "engines": { - "node": ">=6" + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + } } }, - "node_modules/parse-filepath": { + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-function": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" + "requires": { + "has-tostringtag": "^1.0.0" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "is-extglob": "^2.1.1" } }, - "node_modules/parse-ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", - "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true + }, + "is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", "dev": true, - "engines": { - "node": ">=6" + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" } }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, + "is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, - "engines": { - "node": ">= 0.10" + "requires": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" } }, - "node_modules/parse-passwd": { + "is-relative": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "is-unc-path": "^1.0.0" } }, - "node_modules/parse-path": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", - "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", - "dev": true, - "dependencies": { - "protocols": "^2.0.0" - } + "is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha512-mjJd3PujZMl7j+D395WTIO5tU5RIDBfVSRtRR4VOJou3H66E38UjbjvDGh3slJzPuolsb+yQFqwHNNdyp5jg3w==", + "dev": true }, - "node_modules/parse-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", - "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", - "dev": true, - "dependencies": { - "parse-path": "^7.0.0" - } + "is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" + "is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" } }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "protocols": "^2.0.1" } }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "which-typed-array": "^1.1.16" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dev": true, - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "unc-path-regex": "^0.1.2" } }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, - "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true }, - "node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bound": "^1.0.3" } }, - "node_modules/path-type/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" } }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, - "engines": { - "node": "*" + "requires": { + "is-docker": "^2.0.0" } }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "dependencies": { - "through": "~2.3" - } + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha512-nMtdn4hvK0HjUlzr1DrKSUY8ychprt8dzHOgY2KXsIhHu5PuQQEOTM27gV9Xblyon7aUH/TSFIjRHEODF/FRPg==", "dev": true, - "engines": { - "node": ">=8.6" + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, - "node_modules/pify": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-6.1.0.tgz", - "integrity": "sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==", + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, - "dependencies": { - "pinkie": "^2.0.0" + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, - "node_modules/pkcs7": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", - "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, - "dependencies": { - "@babel/runtime": "^7.5.5" - }, - "bin": { - "pkcs7": "bin/cli.js" + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "istextorbinary": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-3.3.0.tgz", + "integrity": "sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==", "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" + "requires": { + "binaryextensions": "^2.2.0", + "textextensions": "^3.2.0" } }, - "node_modules/plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, - "dependencies": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } } - ], - "optional": true, - "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, - "optional": true, - "bin": { - "nanoid": "bin/nanoid.cjs" + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true }, - "node_modules/pretty-format": { + "jest-matcher-utils": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, - "engines": { - "node": ">=10" + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "node_modules/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, - "engines": { - "node": ">= 0.8" + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "node_modules/pretty-ms": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", - "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, - "dependencies": { - "parse-ms": "^2.1.0" + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, - "engines": { - "node": ">=10" + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } } }, - "node_modules/private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } + "jsdoc-type-pratt-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", + "dev": true }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, - "node_modules/property-information": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", - "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true }, - "node_modules/protocols": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", - "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, - "node_modules/proxy-agent": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", - "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" - }, - "engines": { - "node": ">= 14" - } + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "jsonfile": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz", + "integrity": "sha512-KbsDJNRfRPF5v49tMNf9sqyyGqGLBcz1v5kZT01kG5ns5mQSltwxCKVmUzVKtEinkUnTDtSrp6ngWpV7Xw0ZlA==", + "dev": true + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" } }, - "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" } }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, - "engines": { - "node": ">=12" + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "node_modules/proxy-from-env": { + "just-debounce": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", + "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==", "dev": true }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, - "node_modules/ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "karma": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", + "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", "dev": true, - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" + "requires": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" }, - "engines": { - "node": ">= 0.10" + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "karma-babel-preprocessor": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/karma-babel-preprocessor/-/karma-babel-preprocessor-8.0.2.tgz", + "integrity": "sha512-6ZUnHwaK2EyhgxbgeSJW6n6WZUYSEdekHIV/qDUnPgMkVzQBHEvd07d2mTL5AQjV8uTUgH6XslhaPrp+fHWH2A==", + "dev": true, + "requires": {} }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "karma-browserstack-launcher": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-browserstack-launcher/-/karma-browserstack-launcher-1.4.0.tgz", + "integrity": "sha512-bUQK84U+euDfOUfEjcF4IareySMOBNRLrrl9q6cttIe8f011Ir6olLITTYMOJDcGY58wiFIdhPHSPd9Pi6+NfQ==", + "dev": true, + "requires": { + "browserstack": "~1.5.1", + "browserstacktunnel-wrapper": "~2.0.2", + "q": "~1.5.0" + } + }, + "karma-chai": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/karma-chai/-/karma-chai-0.1.0.tgz", + "integrity": "sha512-mqKCkHwzPMhgTYca10S90aCEX9+HjVjjrBFAsw36Zj7BlQNbokXXCAe6Ji04VUMsxcY5RLP7YphpfO06XOubdg==", + "dev": true, + "requires": {} + }, + "karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", "dev": true, + "requires": { + "which": "^1.2.1" + }, "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, - "node_modules/pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + } + }, + "karma-coverage-istanbul-reporter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", + "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^3.0.2", + "minimatch": "^3.0.4" + }, "dependencies": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + } + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, - "node_modules/pumpify/node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "karma-es5-shim": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/karma-es5-shim/-/karma-es5-shim-0.0.4.tgz", + "integrity": "sha512-8xU6F2/R6u6HAZ/nlyhhx3WEhj4C6hJorG7FR2REX81pgj2LSo9ADJXxCGIeXg6Qr2BGpxp4hcZcEOYGAwiumg==", "dev": true, - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" + "requires": { + "es5-shim": "^4.0.5" } }, - "node_modules/pumpify/node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "karma-firefox-launcher": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz", + "integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==", "dev": true, + "requires": { + "is-wsl": "^2.2.0", + "which": "^3.0.0" + }, "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "karma-ie-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-ie-launcher/-/karma-ie-launcher-1.0.0.tgz", + "integrity": "sha512-ts71ke8pHvw6qdRtq0+7VY3ANLoZuUNNkA8abRaWV13QRPNm7TtSOqyszjHUtuwOWKcsSz4tbUtrNICrQC+SXQ==", "dev": true, - "engines": { - "node": ">=6" + "requires": { + "lodash": "^4.6.1" } }, - "node_modules/puppeteer-core": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-13.7.0.tgz", - "integrity": "sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q==", + "karma-mocha": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", + "integrity": "sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==", "dev": true, - "dependencies": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.981744", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.5.0" - }, - "engines": { - "node": ">=10.18.1" + "requires": { + "minimist": "^1.2.3" } }, - "node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.981744", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.981744.tgz", - "integrity": "sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==", - "dev": true - }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha512-Hr6nhkIp0GIJJrvzY8JFeHpQZNseuIakGac4bpw8K1+5F0tLb6l7uvXRa8mt2Z+NVwYgCct4QAfp2R2QP6o00w==", "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "requires": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true }, - "utf-8-validate": { - "optional": true + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } } } }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "karma-opera-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-opera-launcher/-/karma-opera-launcher-1.0.0.tgz", + "integrity": "sha512-rdty4FlVIowmUhPuG08TeXKHvaRxeDSzPxGIkWguCF3A32kE0uvXZ6dXW08PuaNjai8Ip3f5Pn9Pm2HlChaxCw==", "dev": true, - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" + "requires": {} + }, + "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, + "requires": {} + }, + "karma-script-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-script-launcher/-/karma-script-launcher-1.0.0.tgz", + "integrity": "sha512-5NRc8KmTBjNPE3dNfpJP90BArnBohYV4//MsLFfUA1e6N+G1/A5WuWctaFBtMQ6MWRybs/oguSej0JwDr8gInA==", + "dev": true, + "requires": {} + }, + "karma-sinon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/karma-sinon/-/karma-sinon-1.0.5.tgz", + "integrity": "sha512-wrkyAxJmJbn75Dqy17L/8aILJWFm7znd1CE8gkyxTBFnjMSOe2XTJ3P30T8SkxWZHmoHX0SCaUJTDBEoXs25Og==", + "dev": true, + "requires": {} + }, + "karma-sourcemap-loader": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz", + "integrity": "sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2" } }, - "node_modules/qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "karma-spec-reporter": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.32.tgz", + "integrity": "sha512-ZXsYERZJMTNRR2F3QN11OWF5kgnT/K2dzhM+oY3CDyMrDI3TjIWqYGG7c15rR9wjmy9lvdC+CCshqn3YZqnNrA==", "dev": true, - "engines": { - "node": ">=0.9" + "requires": { + "colors": "^1.1.2" } }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" + "karma-webpack": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.1.tgz", + "integrity": "sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^9.0.3", + "webpack-merge": "^4.1.5" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + } } }, - "node_modules/query-selector-shadow-dom": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.0.tgz", - "integrity": "sha512-bK0/0cCI+R8ZmOF1QjT7HupDUYCxbf/9TJgAmSXQxZpftXmTAeil9DRoCnTDkWbvOyZzhcMBwKpptWcdkGFIMg==", + "keycode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", + "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", "dev": true }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "engines": { - "node": ">=0.4.x" + "requires": { + "json-buffer": "3.0.1" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==" + }, + "konan": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/konan/-/konan-2.1.1.tgz", + "integrity": "sha512-7ZhYV84UzJ0PR/RJnnsMZcAbn+kLasJhVNWsu8ZyVEJYRpGA5XESQ9d/7zOa08U0Ou4cmB++hMNY/3OSV9KIbg==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "@babel/parser": "^7.10.5", + "@babel/traverse": "^7.10.5" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" + "requires": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" } }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", "dev": true }, - "node_modules/read-pkg": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", - "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^2.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "flush-write-stream": "^1.0.2" } }, - "node_modules/read-pkg-up": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", - "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "dependencies": { - "find-up": "^6.3.0", - "read-pkg": "^7.1.0", - "type-fest": "^2.5.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" } }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "requires": { + "immediate": "~3.0.5" + } + }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } } }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.1.1.tgz", - "integrity": "sha512-vJXaRMJgRVD3+cUZs3Mncj2mxpt5mP0EmNOsxRSZRMlbqjvxzDEOIUWXGmavo0ZC9+tNZCBLQ66reA11nbpHZg==", + "lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true + }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true + }, + "live-connect-common": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-4.1.0.tgz", + "integrity": "sha512-sRklgbe13377aR+G0qCBiZPayQw5oZZozkuxKEoyipxscLbVzwe9gtA7CPpbmo6UcOdQxdCE6A7J1tI0wTSmqw==" + }, + "live-connect-js": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-7.2.0.tgz", + "integrity": "sha512-oZY4KqwrG1C+CDKApcsdDdMG4j2d44lhmvbNy4ZE6sPFr+W8R3m0+V+JxXB8p6tgSePJ8X/uhzAGos0lDg/MAg==", + "requires": { + "live-connect-common": "^v4.1.0", + "tiny-hashes": "1.0.1" + } + }, + "livereload-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", + "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } } }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" } }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "locate-app": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.4.15.tgz", + "integrity": "sha512-oAGHATXPUHSQ74Om+3dXBRNYtCzU7Wzuhlj/WIZchqHb/5/TGJRzLEtHipMDOak0UZG9U365RMXyBzgV/fhOww==", "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "requires": { + "@promptbook/utils": "0.50.0-10", + "type-fest": "2.13.0", + "userhome": "1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "type-fest": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", + "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", + "dev": true + } } }, - "node_modules/read-pkg-up/node_modules/path-exists": { + "locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "requires": { + "p-locate": "^4.1.0" } }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/read-pkg-up/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==", + "dev": true + }, + "lodash._basetostring": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha512-mTzAr1aNAv/i7W43vOR/uD/aJ4ngbtsRaCubp2BfZhlGU/eORUjg/7F6X0orNMdv33JOrdgGybtvMN/po3EWrA==", + "dev": true + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha512-H94wl5P13uEqlCg7OcNNhMQ8KvWSIyqXzOPusRgHC9DK3o54P6P3xtbXlVbRABG4q5gSmp7EDdJ0MSuW9HX6Mg==", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==", + "dev": true + }, + "lodash._reescape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha512-Sjlavm5y+FUVIF3vF3B75GyXrzsfYV8Dlv3L4mEpuB9leg8N6yf/7rU06iLPx9fY0Mv3khVp9p7Dx0mGV6V5OQ==", + "dev": true + }, + "lodash._reevaluate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha512-OrPwdDc65iJiBeUe5n/LIjd7Viy99bKwDdk7Z5ljfZg0uFRFlfQaCy9tZ4YMAag9WAZmlVpe1iZrkIMMSMHD3w==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==", + "dev": true + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "lodash.escape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha512-n1PZMXgaaDWZDSvuNZ/8XOcYO2hOKDqZel5adtR30VKQAtoWs/5AOeFA0vPV8moiPzlqe7F4cP2tzpFewQyelQ==", "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "lodash._root": "^3.0.0" } }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" } }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "lodash.pickby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==", + "dev": true }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==", + "dev": true + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true }, - "node_modules/readdir-glob": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.2.tgz", - "integrity": "sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA==", + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", "dev": true, - "dependencies": { - "minimatch": "^5.1.0" + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" } }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "requires": { + "lodash._reinterpolate": "^3.0.0" } }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "dev": true + }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" + "requires": { + "chalk": "^2.0.1" } }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" } }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", "dev": true, - "dependencies": { - "minimatch": "^3.0.5" + "requires": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" + "@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true + } } }, - "node_modules/regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + "loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "dev": true }, - "node_modules/regenerator-transform": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", - "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", - "dependencies": { - "@babel/runtime": "^7.8.4" - } + "loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true }, - "node_modules/regex-not/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } + "longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" + "requires": { + "get-func-name": "^2.0.1" } }, - "node_modules/regexpu-core": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.1.tgz", - "integrity": "sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" } }, - "node_modules/regextras": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", - "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", "dev": true, - "engines": { - "node": ">=0.1.14" + "requires": { + "es5-ext": "~0.10.2" } }, - "node_modules/regjsgen": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", - "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" + "m3u8-parser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", + "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "bin": { - "jsesc": "bin/jsesc" + "magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" } }, - "node_modules/remark": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.2.tgz", - "integrity": "sha512-A3ARm2V4BgiRXaUo5K0dRvJ1lbogrbXnhkJRmD0yw092/Yl0kOCZt1k9ZeElEwkZsWGsMumz6qL5MfNJH9nOBA==", + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "remark-parse": "^10.0.0", - "remark-stringify": "^10.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "semver": "^6.0.0" } }, - "node_modules/remark-gfm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", - "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-gfm": "^2.0.0", - "micromark-extension-gfm": "^2.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "kind-of": "^6.0.2" } }, - "node_modules/remark-html": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-15.0.1.tgz", - "integrity": "sha512-7ta5UPRqj8nP0GhGMYUAghZ/DRno7dgq7alcW90A7+9pgJsXzGJlFgwF8HOP1b1tMgT3WwbeANN+CaTimMfyNQ==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "hast-util-sanitize": "^4.0.0", - "hast-util-to-html": "^8.0.0", - "mdast-util-to-hast": "^12.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true + }, + "map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "requires": { + "object-visit": "^1.0.0" } }, - "node_modules/remark-parse": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", - "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==", + "markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "dev": true + }, + "matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" + "requires": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, - "node_modules/remark-reference-links": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/remark-reference-links/-/remark-reference-links-6.0.1.tgz", - "integrity": "sha512-34wY2C6HXSuKVTRtyJJwefkUD8zBOZOSHFZ4aSTnU2F656gr9WeuQ2dL6IJDK3NPd2F6xKF2t4XXcQY9MygAXg==", + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", "dev": true, - "dependencies": { + "requires": { "@types/mdast": "^3.0.0", - "unified": "^10.0.0", + "@types/unist": "^2.0.0", "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" } }, - "node_modules/remark-stringify": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.2.tgz", - "integrity": "sha512-6wV3pvbPvHkbNnWB0wdDvVFHOe1hBRAx1Q/5g/EpH4RppAII6J8Gnwe7VbHuXaoKIF6LAg6ExTel/+kNqSQ7lw==", + "mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", "dev": true, - "dependencies": { + "requires": { "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.0.0", - "unified": "^10.0.0" + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true + } } }, - "node_modules/remark-toc": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/remark-toc/-/remark-toc-8.0.1.tgz", - "integrity": "sha512-7he2VOm/cy13zilnOTZcyAoyoolV26ULlon6XyCFU+vG54Z/LWJnwphj/xKIDLOt66QmJUgTyUvLVHi2aAElyg==", + "mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", "dev": true, - "dependencies": { + "requires": { "@types/mdast": "^3.0.0", - "mdast-util-toc": "^6.0.0", - "unified": "^10.0.0" + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0" + } + } } }, - "node_modules/remove-bom-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" } }, - "node_modules/remove-bom-buffer/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/remove-bom-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", + "mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", "dev": true, - "dependencies": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" } }, - "node_modules/remove-bom-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" } }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", "dev": true, - "engines": { - "node": ">=0.10" + "requires": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" } }, - "node_modules/repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", "dev": true, - "dependencies": { - "is-finite": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" } }, - "node_modules/replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha512-AFBWBy9EVRTa/LhEcG8QDP3FvpwZqmvN2QFDuJswFeaVhWnZMp8q3E6Zd90SR04PlIwfGdyVjNyLPyen/ek5CQ==", + "mdast-util-inject": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz", + "integrity": "sha512-CcJ0mHa36QYumDKiZ2OIR+ClhfOM7zIzN+Wfy8tRZ1hpH9DKLCS+Mh4DyK5bCxzE9uxMWcbIpeNFWsg1zrj/2g==", "dev": true, - "engines": { - "node": ">= 0.4" + "requires": { + "mdast-util-to-string": "^1.0.0" } }, - "node_modules/replace-homedir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==", + "mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" } }, - "node_modules/replacestream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", - "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", + "mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.3", - "object-assign": "^4.0.1", - "readable-stream": "^2.0.2" + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" } }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", "dev": true, - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">= 6" + "dependencies": { + "mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0" + } + } } }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "mdast-util-to-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", + "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", + "dev": true + }, + "mdast-util-toc": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-toc/-/mdast-util-toc-6.1.1.tgz", + "integrity": "sha512-Er21728Kow8hehecK2GZtb7Ny3omcoPUVrmObiSUwmoRYVZaXLR751QROEFjR8W/vAQdHMLj49Lz20J55XaNpw==", "dev": true, - "engines": { - "node": ">=0.6" + "requires": { + "@types/extend": "^3.0.0", + "@types/mdast": "^3.0.0", + "extend": "^3.0.0", + "github-slugger": "^2.0.0", + "mdast-util-to-string": "^3.1.0", + "unist-util-is": "^5.0.0", + "unist-util-visit": "^4.0.0" + }, + "dependencies": { + "github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true + }, + "mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0" + } + } } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" } }, - "node_modules/require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true + "micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "dev": true, + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", "dev": true, - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", + "micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", "dev": true, - "dependencies": { - "value-or-function": "^3.0.0" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true + "micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "dev": true, + "requires": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", "dev": true, - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/resq": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/resq/-/resq-1.10.2.tgz", - "integrity": "sha512-HmgVS3j+FLrEDBTDYysPdPVF9/hioDMJ/otOiQDKqk77YfZeeLOj0qi34yObumcud1gBpk+wpBTEg4kMicD++A==", + "micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", "dev": true, - "dependencies": { - "fast-deep-equal": "^2.0.1" + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/resq/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "dev": true + "micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "dev": true, + "requires": { + "micromark-util-types": "^1.0.0" + } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", "dev": true, - "engines": { - "node": ">=0.12" + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true + "micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } }, - "node_modules/rgb2hex": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", - "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", - "dev": true + "micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", "dev": true, - "engines": { - "node": ">=0.12.0" + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/rust-result": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz", - "integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==", + "micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", "dev": true, - "dependencies": { - "individual": "^2.0.0" + "requires": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "requires": { + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", "dev": true, - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/safaridriver": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", - "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==", - "dev": true - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==", - "dev": true - }, - "node_modules/safe-regex": { + "micromark-util-combine-extensions": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", "dev": true, - "dependencies": { - "ret": "~0.1.10" + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", "dev": true, - "engines": { - "node": ">=10" + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "dev": true }, - "node_modules/samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", - "deprecated": "This package has been deprecated in favour of @sinonjs/samsam", + "micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", "dev": true }, - "node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "requires": { + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "requires": { + "micromark-util-types": "^1.0.0" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/semver-greatest-satisfied-range": { + "micromark-util-subtokenize": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", "dev": true, - "dependencies": { - "sver-compat": "^1.5.0" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } + "micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "dev": true }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, - "node_modules/send/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==" + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } }, - "node_modules/serialize-error": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", - "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "dom-walk": "^0.1.0" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "brace-expansion": "^1.1.7" } }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "optional": true, + "peer": true + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, - "dependencies": { - "randombytes": "^2.1.0" + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" } }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" + "requires": { + "minimist": "^1.2.6" } }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "mocha": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", "dev": true, - "dependencies": { - "ms": "2.0.0" + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "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, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } } }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "dev": true + }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", "dev": true, - "engines": { - "node": ">= 0.6" + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + } } }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "mpd-parser": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.22.1.tgz", + "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==", "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true }, - "node_modules/serve-index/node_modules/ms": { + "mrmime": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "dev": true }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha512-7ZxrUybYv9NonoXgwoOqtStIu18D1c3eFZj27hqgf5kBrBF8Q+tE8V0MW8dKM5QLkQPh1JhhbKgHLY9kifov4Q==", "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" + "requires": { + "duplexer2": "0.0.2" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", "dev": true }, - "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", - "dependencies": { - "define-data-property": "^1.1.1", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } + "mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "mux.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", + "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" } }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "optional": true }, - "node_modules/set-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, - "engines": { - "node": ">=0.10.0" - } + "optional": true }, - "node_modules/set-value/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "dev": true, - "dependencies": { - "isobject": "^3.0.1" + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + } } }, - "node_modules/setimmediate": { + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "ncp": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz", + "integrity": "sha512-PfGU8jYWdRl4FqJfCy0IzbkGyFHntfWygZg46nFk/dJD/XRrk2cj0SsKSX9n5u5gE0E0YfEpKWrEkfjnlZSTXA==", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "neostandard": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.12.1.tgz", + "integrity": "sha512-As/LDK+xx591BLb1rPRaPs+JfXFgyNx5BoBui1KBeF/J4s0mW8+NBohrYnMfgm1w1t7E/Y/tU34MjMiP6lns6A==", + "dev": true, + "requires": { + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@stylistic/eslint-plugin": "2.11.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import-x": "^4.5.0", + "eslint-plugin-n": "^17.14.0", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-react": "^7.37.2", + "find-up": "^5.0.0", + "globals": "^15.13.0", + "peowly": "^1.3.2", + "typescript-eslint": "^8.17.0" + }, + "dependencies": { + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } + }, + "netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true + }, + "next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, + "nice-try": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "nise": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" }, - "engines": { - "node": ">=8" + "dependencies": { + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true + }, + "node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" } }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "requires": { + "css-select": "^5.1.0", + "he": "1.2.0" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, - "node_modules/sinon": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", - "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", - "deprecated": "16.1.1", + "node-request-interceptor": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.6.3.tgz", + "integrity": "sha512-8I2V7H2Ch0NvW7qWcjmS0/9Lhr0T6x7RD6PDirhvWEkUQvy83x8BA4haYMr09r/rig7hcgYSjYh6cd4U7G1vLA==", "dev": true, - "hasInstallScript": true, - "dependencies": { - "@sinonjs/formatio": "^2.0.0", - "diff": "^3.1.0", - "lodash.get": "^4.4.2", - "lolex": "^2.2.0", - "nise": "^1.2.0", - "supports-color": "^5.1.0", - "type-detect": "^4.0.5" + "requires": { + "@open-draft/until": "^1.0.3", + "debug": "^4.3.0", + "headers-utils": "^1.2.0", + "strict-event-emitter": "^0.1.0" } }, - "node_modules/sinon/node_modules/diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "node.extend": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", + "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==", + "requires": { + "has": "^1.0.3", + "is": "^3.2.1" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", "dev": true, - "engines": { - "node": ">=0.3.1" + "requires": { + "abbrev": "1" } }, - "node_modules/sirv": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", - "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "normalize-package-data": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", + "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", "dev": true, - "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" + "requires": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" }, - "engines": { - "node": ">= 10" + "dependencies": { + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + } } }, - "node_modules/slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "once": "^1.3.2" } }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" + "requires": { + "path-key": "^2.0.0" }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "dependencies": { + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + } } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "requires": { + "boolbase": "^1.0.0" } }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" }, - "engines": { - "node": ">=7.0.0" + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } } }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" } }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", "dev": true, - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "isobject": "^3.0.0" } }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" } }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" } }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, - "node_modules/snapdragon-util/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" } }, - "node_modules/snapdragon/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", "dev": true, - "dependencies": { - "ms": "2.0.0" + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" } }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "isobject": "^3.0.1" } }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==", "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" } }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "deprecated": "Please upgrade to v0.1.7", + "object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "wrappy": "1" } }, - "node_modules/snapdragon/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true }, - "node_modules/snapdragon/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "deprecated": "Please upgrade to v0.1.5", + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" + "requires": { + "is-wsl": "^1.1.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true + } } }, - "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" } }, - "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "readable-stream": "^2.0.1" } }, - "node_modules/snapdragon/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "lcid": "^1.0.0" } }, - "node_modules/snapdragon/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true }, - "node_modules/snapdragon/node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" + "requires": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" } }, - "node_modules/socket.io": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", - "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.2", - "engine.io": "~6.4.1", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" - }, - "engines": { - "node": ">=10.0.0" + "requires": { + "p-try": "^2.0.0" } }, - "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "dependencies": { - "ws": "~8.11.0" + "requires": { + "p-limit": "^2.2.0" } }, - "node_modules/socket.io-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", - "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "requires": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" }, - "engines": { - "node": ">=10.0.0" + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + } } }, - "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" + "requires": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" } }, - "node_modules/socks-proxy-agent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", - "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "socks": "^2.7.1" - }, - "engines": { - "node": ">= 14" + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-imports": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", + "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", + "dev": true, + "requires": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" } }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", "dev": true, - "dependencies": { - "debug": "^4.3.4" + "requires": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" }, - "engines": { - "node": ">= 14" + "dependencies": { + "type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true + } } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", "dev": true }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true + }, + "parse-path": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", + "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "protocols": "^2.0.0" } }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "parse-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "parse-path": "^7.0.0" } }, - "node_modules/source-map-resolve": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", "dev": true, + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0" + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + } } }, - "node_modules/source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, + "requires": { + "parse5": "^7.0.0" + }, "dependencies": { - "source-map": "^0.5.6" + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + } } }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", "dev": true }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true, - "optional": true + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true }, - "node_modules/space-separated-tokens": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz", - "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true }, - "node_modules/sparkles": { + "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true }, - "node_modules/spdx-correct": { + "path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "requires": { + "path-root-regex": "^0.1.0" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true }, - "node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "dependencies": { - "through": "2" + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, - "engines": { - "node": "*" - } - }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } } }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, - "engines": { - "node": ">= 10.x" - } + "pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", "dev": true, - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "through": "~2.3" } }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "peowly": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/peowly/-/peowly-1.3.2.tgz", + "integrity": "sha512-BYIrwr8JCXY49jUZscgw311w9oGEKo7ux/s+BxrhKTQbiQ0iYNdZNJ5LgagaeercQdFHwnR7Z5IxxFWVQ+BasQ==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-6.1.0.tgz", + "integrity": "sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, - "engines": { - "node": "*" + "requires": { + "pinkie": "^2.0.0" } }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" + "requires": { + "@babel/runtime": "^7.5.5" } }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "find-up": "^4.0.0" } }, - "node_modules/static-extend": { + "plugin-error": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", "dev": true, - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" } }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true }, - "node_modules/static-extend/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "deprecated": "Please upgrade to v0.1.7", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true }, - "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "optional": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" } }, - "node_modules/static-extend/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "node_modules/static-extend/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "deprecated": "Please upgrade to v0.1.5", + "prettier": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", + "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } } }, - "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true + }, + "pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "parse-ms": "^2.1.0" } }, - "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } } }, - "node_modules/statuses": { + "property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true + }, + "protocols": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "dev": true }, - "node_modules/stream-buffers": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", - "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", - "dev": true, - "engines": { - "node": ">= 0.10.0" + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" } }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, "dependencies": { - "duplexer": "~0.1.1" + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + } } }, - "node_modules/stream-exhaust": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", - "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, - "node_modules/stream-shift": { + "prr": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true }, - "node_modules/streamroller": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.3.tgz", - "integrity": "sha512-CphIJyFx2SALGHeINanjFRKQ4l7x2c+rXYJ4BMq0gd+ZK0gi4VT8b+eHe2wi58x4UayBAKx4xtHpXT/ea1cz8w==", + "ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", "dev": true, - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">=8.0" + "requires": { + "event-stream": "=3.3.4" } }, - "node_modules/streamroller/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true }, - "node_modules/streamroller/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true }, - "node_modules/streamroller/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, - "engines": { - "node": ">= 4.0.0" + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/streamx": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", - "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==", + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } } }, - "node_modules/strict-event-emitter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.1.0.tgz", - "integrity": "sha512-8hSYfU+WKLdNcHVXJ0VxRXiPESalzRe7w1l8dg9+/22Ry+iZQUoQuoJ27R30GMD1TiyYINWsIEGY05WrskhSKw==", + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "dev": true }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true }, - "node_modules/string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==", + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true }, - "node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "safe-buffer": "^5.1.0" } }, - "node_modules/stringify-entities": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", - "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", - "dev": true, - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "read-pkg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", + "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^6.0.0", + "parse-json": "^7.0.0", + "type-fest": "^4.2.0" + }, + "dependencies": { + "type-fest": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz", + "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==", + "dev": true + } } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "read-pkg-up": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.0.0.tgz", + "integrity": "sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "find-up": "^6.3.0", + "read-pkg": "^8.0.0", + "type-fest": "^3.12.0" + }, + "dependencies": { + "find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "requires": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + } + }, + "locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "requires": { + "p-locate": "^6.0.0" + } + }, + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "requires": { + "p-limit": "^4.0.0" + } + }, + "path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true + }, + "type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true + } } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } } }, - "node_modules/strip-json-comments": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.0.tgz", - "integrity": "sha512-V1LGY4UUo0jgwC+ELQ2BNWfPa17TIuwBLg+j1AA/9RPzKINl1lhxVEu2r+ZTTO8aetIsUzE5Qj6LMSBkoGYKKw==", + "readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, - "engines": { - "node": ">=14.16" + "requires": { + "minimatch": "^5.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" } }, - "node_modules/sver-compat": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg==", + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "dev": true, - "dependencies": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" + "requires": { + "resolve": "^1.1.6" } }, - "node_modules/table": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", - "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", + "recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" + "requires": { + "minimatch": "^3.0.5" } }, - "node_modules/table/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" } }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" + "regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "requires": { + "regenerate": "^1.4.2" } }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" + "regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "requires": { + "@babel/runtime": "^7.8.4" } }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" }, - "engines": { - "node": ">= 6" + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + } } }, - "node_modules/temp-fs": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/temp-fs/-/temp-fs-0.9.9.tgz", - "integrity": "sha512-WfecDCR1xC9b0nsrzSaxPf3ZuWeWLUWblW4vlDQAa1biQaKHiImHnJfeQocQe/hXKMcolRzgkcVX/7kK4zoWbw==", + "regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, - "dependencies": { - "rimraf": "~2.5.2" - }, - "engines": { - "node": ">=0.8.0" + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" } }, - "node_modules/temp-fs/node_modules/rimraf": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", - "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.0.5" - }, - "bin": { - "rimraf": "bin.js" + "regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" } }, - "node_modules/ternary-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-3.0.0.tgz", - "integrity": "sha512-oIzdi+UL/JdktkT+7KU5tSIQjj8pbShj3OASuvDEhm0NT5lppsm7aXWAmAq4/QMaBIyfuEcNLbAQA+HpaISobQ==", - "dev": true, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "requires": { + "jsesc": "~0.5.0" + }, "dependencies": { - "duplexify": "^4.1.1", - "fork-stream": "^0.0.4", - "merge-stream": "^2.0.0", - "through2": "^3.0.1" + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" + } } }, - "node_modules/ternary-stream/node_modules/through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "remark": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.3.tgz", + "integrity": "sha512-bfmJW1dmR2LvaMJuAnE88pZP9DktIFYXazkTfOIKZzi3Knk9lT0roItIA24ydOucI3bV/g/tXBA6hzqq3FV9Ew==", "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" + "requires": { + "@types/mdast": "^3.0.0", + "remark-parse": "^10.0.0", + "remark-stringify": "^10.0.0", + "unified": "^10.0.0" } }, - "node_modules/terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", - "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "remark-html": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-15.0.2.tgz", + "integrity": "sha512-/CIOI7wzHJzsh48AiuIyIe1clxVkUtreul73zcCXLub0FmnevQE0UMFDQm7NUx8/3rl/4zCshlMfqBdWScQthw==", "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "requires": { + "@types/mdast": "^3.0.0", + "hast-util-sanitize": "^4.0.0", + "hast-util-to-html": "^8.0.0", + "mdast-util-to-hast": "^12.0.0", + "unified": "^10.0.0" } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "remark-reference-links": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/remark-reference-links/-/remark-reference-links-6.0.1.tgz", + "integrity": "sha512-34wY2C6HXSuKVTRtyJJwefkUD8zBOZOSHFZ4aSTnU2F656gr9WeuQ2dL6IJDK3NPd2F6xKF2t4XXcQY9MygAXg==", "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "requires": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" } }, - "node_modules/terser/node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "remark-stringify": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.3.tgz", + "integrity": "sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==", "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.0.0", + "unified": "^10.0.0" } }, - "node_modules/terser/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "remark-toc": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/remark-toc/-/remark-toc-8.0.1.tgz", + "integrity": "sha512-7he2VOm/cy13zilnOTZcyAoyoolV26ULlon6XyCFU+vG54Z/LWJnwphj/xKIDLOt66QmJUgTyUvLVHi2aAElyg==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-toc": "^6.0.0", + "unified": "^10.0.0" } }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", "dev": true, + "requires": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + }, "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + } } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "requires": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" }, - "engines": { - "node": ">=8" + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", "dev": true }, - "node_modules/textextensions": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", - "integrity": "sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==", + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha512-AFBWBy9EVRTa/LhEcG8QDP3FvpwZqmvN2QFDuJswFeaVhWnZMp8q3E6Zd90SR04PlIwfGdyVjNyLPyen/ek5CQ==", + "dev": true + }, + "replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + } + }, + "replacestream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", + "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.3", + "object-assign": "^4.0.1", + "readable-stream": "^2.0.2" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" }, - "funding": { - "url": "https://bevry.me/fund" + "dependencies": { + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "dependencies": { - "readable-stream": "3" - } + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true }, - "node_modules/through2-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", - "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", - "dev": true, - "dependencies": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true }, - "node_modules/through2-filter/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, - "node_modules/through2/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" } }, - "node_modules/time-stamp": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-options": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "value-or-function": "^3.0.0" } }, - "node_modules/timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", - "dev": true, - "dependencies": { - "es5-ext": "~0.10.46", - "next-tick": "1" - } + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true }, - "node_modules/tiny-hashes": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tiny-hashes/-/tiny-hashes-1.0.1.tgz", - "integrity": "sha512-knIN5zj4fl7kW4EBU5sLP20DWUvi/rVouvJezV0UAym2DkQaqm365Nyc8F3QEiOvunNDMxR8UhcXd1d5g+Wg1g==" + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "dev": true }, - "node_modules/tiny-lr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", - "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", + "resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1" + }, "dependencies": { - "body": "^5.1.0", - "debug": "^3.1.0", - "faye-websocket": "~0.10.0", - "livereload-js": "^2.3.0", - "object-assign": "^4.1.0", - "qs": "^6.4.0" + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true + } } }, - "node_modules/tiny-lr/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } + "reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true }, - "node_modules/to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", - "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } + "rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "dev": true }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" + "requires": { + "glob": "^7.1.3" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, - "node_modules/to-object-path/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", "dev": true }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "queue-microtask": "^1.2.2" } }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "rust-result": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz", + "integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==", "dev": true, - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "individual": "^2.0.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "dependencies": { - "is-number": "^7.0.0" + "requires": { + "tslib": "^2.1.0" }, - "engines": { - "node": ">=8.0" + "dependencies": { + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + } } }, - "node_modules/to-regex/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "mri": "^1.1.0" } }, - "node_modules/to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", - "dev": true, - "dependencies": { - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" + "safaridriver": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", + "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==", + "dev": true + }, + "safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" } }, - "node_modules/to-through/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-json-parse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", + "integrity": "sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==", + "dev": true + }, + "safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "requires": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "requires": { + "ret": "~0.1.10" } }, - "node_modules/totalist": { + "safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, - "engines": { - "node": ">=6" + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", "dev": true, - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" }, - "engines": { - "node": ">=0.8" + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==", "dev": true, - "engines": { - "node": "*" + "requires": { + "sver-compat": "^1.5.0" } }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + } } }, - "node_modules/trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "serialize-error": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "type-fest": "^2.12.2" + }, + "dependencies": { + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true + } } }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, - "engines": { - "node": ">= 14.0.0" + "requires": { + "randombytes": "^2.1.0" } }, - "node_modules/trough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", - "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } } }, - "node_modules/tryit": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", - "integrity": "sha512-6C5h3CE+0qjGp+YKYTs74xR0k/Nw/ePtl/Lp6CCf44hqBQ66qnH1sDFR5mV/Gc48EsrHLB53lCFSffQCkka3kg==" + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + } + } + } + } }, - "node_modules/tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" } }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" }, - "engines": { - "node": "*" + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } } }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, - "node_modules/type": { + "setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" + "requires": { + "kind-of": "^6.0.2" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "engines": { - "node": ">=4" + "requires": { + "shebang-regex": "^3.0.0" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/typescript-compare": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", - "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "sinon": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", + "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^2.0.0", + "diff": "^3.1.0", + "lodash.get": "^4.4.2", + "lolex": "^2.2.0", + "nise": "^1.2.0", + "supports-color": "^5.1.0", + "type-detect": "^4.0.5" + }, "dependencies": { - "typescript-logic": "^0.0.0" + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + } } }, - "node_modules/typescript-logic": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" - }, - "node_modules/typescript-tuple": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", - "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", - "dependencies": { - "typescript-compare": "^0.0.2" + "sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" } }, - "node_modules/ua-parser-js": { - "version": "0.7.33", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", - "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", + "slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } } - ], - "engines": { - "node": "*" } }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" }, - "engines": { - "node": ">=0.8.0" + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } } }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "requires": { + "kind-of": "^3.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } } }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "dev": true, - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" } }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "debug": "~4.3.4", + "ws": "~8.17.1" } }, - "node_modules/undertaker": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", - "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, - "dependencies": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "fast-levenshtein": "^1.0.0", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" } }, - "node_modules/undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", + "socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, - "engines": { - "node": ">= 0.10" + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" } }, - "node_modules/undertaker/node_modules/fast-levenshtein": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", - "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==", + "socks-proxy-agent": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "dev": true, + "requires": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", "dev": true }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "engines": { - "node": ">=4" - } + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "optional": true }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" + "source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" } }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", - "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", - "engines": { - "node": ">=4" - } + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "engines": { - "node": ">=4" - } + "space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true }, - "node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "spacetrim": { + "version": "0.11.25", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.25.tgz", + "integrity": "sha512-SWxXDROciuJs9YEYXUBjot5k/cqNGPPbT3QmkInFne4AGc1y+76It+jqU8rfsXKt57RRiunzZn1m9+KfuuNklw==", + "dev": true + }, + "sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } + "spawn-args": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/spawn-args/-/spawn-args-0.2.0.tgz", + "integrity": "sha512-73BoniQDcRWgnLAf/suKH6V5H54gd1KLzwYN9FB6J/evqTV33htH9xwV/4BHek+++jzxpVlZQKKZkqstPQPmQg==", + "dev": true }, - "node_modules/union-value/node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" } }, - "node_modules/union-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "node_modules/unique-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", - "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", "dev": true, - "dependencies": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" + "requires": { + "through": "2" } }, - "node_modules/unist-builder": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz", - "integrity": "sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==", + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "dev": true, - "dependencies": { - "@types/unist": "^2.0.0" + "requires": { + "extend-shallow": "^3.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + } } }, - "node_modules/unist-util-generated": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz", - "integrity": "sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true }, - "node_modules/unist-util-is": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz", - "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, - "node_modules/unist-util-position": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.3.tgz", - "integrity": "sha512-p/5EMGIa1qwbXjA+QgcBXaPWjSnZfQ2Sc3yBEEfgPwsEmJd8Qh+DSk3LGnmOM4S1bY2C0AjmMnB8RuEYxpPwXQ==", + "sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, - "dependencies": { - "@types/unist": "^2.0.0" + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + } } }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz", - "integrity": "sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==", + "stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, - "dependencies": { - "@types/unist": "^2.0.0" + "requires": { + "escape-string-regexp": "^2.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } } }, - "node_modules/unist-util-visit": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.1.tgz", - "integrity": "sha512-n9KN3WV9k4h1DxYR1LoajgN93wpEi/7ZplVe02IoB4gH5ctI1AaF2670BLHQYbwj+pY83gFtyeySFiyMHJklrg==", + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + } } }, - "node_modules/unist-util-visit-parents": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.1.tgz", - "integrity": "sha512-gks4baapT/kNRaWxuGkl5BIhoanZo7sC/cUT/JToSRNL1dYoXRFl75d++NkjYk4TAu2uv2Px+l8guMajogeuiw==", + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "internal-slot": "^1.0.4" } }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "stream-buffers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", + "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", + "dev": true + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", "dev": true, - "engines": { - "node": ">= 10.0.0" + "requires": { + "duplexer": "~0.1.1" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } + "stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true + }, + "streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", "dev": true, - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } } }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", "dev": true, - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" } }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "dependencies": { - "isarray": "1.0.0" + "strict-event-emitter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.1.0.tgz", + "integrity": "sha512-8hSYfU+WKLdNcHVXJ0VxRXiPESalzRe7w1l8dg9+/22Ry+iZQUoQuoJ27R30GMD1TiyYINWsIEGY05WrskhSKw==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } } }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, - "node_modules/unset-value/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/unzipper": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.9.15.tgz", - "integrity": "sha512-2aaUvO4RAeHDvOCuEtth7jrHFaCKTSXPqUkXwADaLBzGbgZGzUDccoEdJ5lW+3RmfpOZYNx0Rw6F6PUzM6caIA==", + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, - "node_modules/unzipper/node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, - "engines": { - "node": ">=4", - "yarn": "*" + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist-lint": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, - "dependencies": { - "punycode": "^2.1.0" + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true + "stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "requires": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + } }, - "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + }, "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + } } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" + "requires": { + "ansi-regex": "^5.0.1" } }, - "node_modules/url-toolkit": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", - "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==", + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", "dev": true }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true + }, + "strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true + }, + "strip-json-comments": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", + "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", + "dev": true + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "has-flag": "^3.0.0" } }, - "node_modules/userhome": { + "supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.0.tgz", - "integrity": "sha512-ayFKY3H+Pwfy4W98yPdtH1VqH4psDeyW8lYYFzfecR9d6hqLpqhecktvYR3SEEXt7vG0S1JEpciI3g94pMErig==", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "sver-compat": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", + "integrity": "sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg==", "dev": true, - "engines": { - "node": ">= 0.8.0" + "requires": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" } }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, + "requires": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + } } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" + "tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "requires": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" } }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, - "bin": { - "uuid": "bin/uuid" + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "temp-fs": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/temp-fs/-/temp-fs-0.9.9.tgz", + "integrity": "sha512-WfecDCR1xC9b0nsrzSaxPf3ZuWeWLUWblW4vlDQAa1biQaKHiImHnJfeQocQe/hXKMcolRzgkcVX/7kK4zoWbw==", "dev": true, - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" + "requires": { + "rimraf": "~2.5.2" }, - "engines": { - "node": ">=8" + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + } } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "ternary-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-3.0.0.tgz", + "integrity": "sha512-oIzdi+UL/JdktkT+7KU5tSIQjj8pbShj3OASuvDEhm0NT5lppsm7aXWAmAq4/QMaBIyfuEcNLbAQA+HpaISobQ==", "dev": true, + "requires": { + "duplexify": "^4.1.1", + "fork-stream": "^0.0.4", + "merge-stream": "^2.0.0", + "through2": "^3.0.1" + }, "dependencies": { - "homedir-polyfill": "^1.0.1" + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "terser": { + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", + "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" }, - "engines": { - "node": ">= 0.10" + "dependencies": { + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, - "node_modules/value-or-function": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", + "text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", "dev": true, - "engines": { - "node": ">= 0.10" + "requires": { + "b4a": "^1.6.4" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } + "textextensions": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", + "integrity": "sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==", + "dev": true }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, - "engines": [ - "node >=0.6.0" - ], + "requires": { + "readable-stream": "3" + }, "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - }, - "node_modules/vfile": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.5.tgz", - "integrity": "sha512-U1ho2ga33eZ8y8pkbQLH54uKqGhFJ6GYIHnnG5AhRpAh3OWjkrRHKa/KogbmQn8We+c0KVV3rTOgR9V/WowbXQ==", + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, - "node_modules/vfile-message": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.2.tgz", - "integrity": "sha512-QjSNP6Yxzyycd4SVOtmKKyTsSvClqBPJcd00Z0zuPj3hOIjg0rUPG6DbFGPvUKRgYyaIWLPKpuEclcuvb3H8qA==", + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==", + "dev": true + }, + "timeout-as-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/timeout-as-promise/-/timeout-as-promise-1.0.0.tgz", + "integrity": "sha512-G4so1NA+qeCiBK+IX3vi6YyumjdDr86q2Y+RjyGjcw3qrRnFFNmi3Y76Ijk8EtqZxcgDdeq/qj8JFxQcsqaEmA==", + "dev": true + }, + "timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" } }, - "node_modules/vfile-reporter": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-7.0.4.tgz", - "integrity": "sha512-4cWalUnLrEnbeUQ+hARG5YZtaHieVK3Jp4iG5HslttkVl+MHunSGNAIrODOTLbtjWsNZJRMCkL66AhvZAYuJ9A==", + "tiny-hashes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tiny-hashes/-/tiny-hashes-1.0.1.tgz", + "integrity": "sha512-knIN5zj4fl7kW4EBU5sLP20DWUvi/rVouvJezV0UAym2DkQaqm365Nyc8F3QEiOvunNDMxR8UhcXd1d5g+Wg1g==" + }, + "tiny-lr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", + "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", "dev": true, - "dependencies": { - "@types/supports-color": "^8.0.0", - "string-width": "^5.0.0", - "supports-color": "^9.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-sort": "^3.0.0", - "vfile-statistics": "^2.0.0" + "requires": { + "body": "^5.1.0", + "debug": "^3.1.0", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.3.0", + "object-assign": "^4.1.0", + "qs": "^6.4.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "node_modules/vfile-reporter/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", "dev": true, - "engines": { - "node": ">=12" + "requires": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "dependencies": { + "fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true + } } }, - "node_modules/vfile-reporter/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true }, - "node_modules/vfile-reporter/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" } }, - "node_modules/vfile-reporter/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" + "requires": { + "kind-of": "^3.0.2" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } } }, - "node_modules/vfile-reporter/node_modules/supports-color": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.3.tgz", - "integrity": "sha512-aszYUX/DVK/ed5rFLb/dDinVJrQjG/vmU433wtqVSD800rYsJNWxh2R3USV90aLSU+UsyQkbNeffVLzc6B6foA==", + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "dev": true, - "engines": { - "node": ">=12" + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + } } }, - "node_modules/vfile-sort": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-3.0.0.tgz", - "integrity": "sha512-fJNctnuMi3l4ikTVcKpxTbzHeCgvDhnI44amA3NVDvA6rTC6oKCFpCVyT5n2fFMr3ebfr+WVQZedOCd73rzSxg==", + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "dependencies": { - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "requires": { + "is-number": "^7.0.0" } }, - "node_modules/vfile-statistics": { + "to-through": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-2.0.0.tgz", - "integrity": "sha512-foOWtcnJhKN9M2+20AOTlWi2dxNfAoeNIoxD5GXcO182UJyId4QrXa41fWrgcfV3FWTjdEDy3I4cpLVcQscIMA==", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", "dev": true, - "dependencies": { - "vfile-message": "^3.0.0" + "requires": { + "through2": "^2.0.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/video.js": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.20.3.tgz", - "integrity": "sha512-JMspxaK74LdfWcv69XWhX4rILywz/eInOVPdKefpQiZJSMD5O8xXYueqACP2Q5yqKstycgmmEKlJzZ+kVmDciw==", - "dev": true, "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "2.14.3", - "@videojs/vhs-utils": "^3.0.4", - "@videojs/xhr": "2.6.0", - "aes-decrypter": "3.1.3", - "global": "^4.4.0", - "keycode": "^2.2.0", - "m3u8-parser": "4.7.1", - "mpd-parser": "0.21.1", - "mux.js": "6.0.1", - "safe-json-parse": "4.0.0", - "videojs-font": "3.2.0", - "videojs-vtt.js": "^0.15.4" + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, - "node_modules/video.js/node_modules/safe-json-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz", - "integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==", - "dev": true, - "dependencies": { - "rust-result": "^1.0.0" - } + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, - "node_modules/videojs-contrib-ads": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/videojs-contrib-ads/-/videojs-contrib-ads-6.9.0.tgz", - "integrity": "sha512-nzKz+jhCGMTYffSNVYrmp9p70s05v6jUMOY3Z7DpVk3iFrWK4Zi/BIkokDWrMoHpKjdmCdKzfJVBT+CrUj6Spw==", + "totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, - "dependencies": { - "global": "^4.3.2", - "video.js": "^6 || ^7" - }, - "engines": { - "node": ">=8", - "npm": ">=5" + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" } }, - "node_modules/videojs-font": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", - "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==", + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true + }, + "trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true + }, + "triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true + }, + "trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "dev": true }, - "node_modules/videojs-ima": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/videojs-ima/-/videojs-ima-1.11.0.tgz", - "integrity": "sha512-ZRoWuGyJ75zamwZgpr0i/gZ6q7Evda/Q6R46gpW88WN7u0ORU7apw/lM1MSG4c3YDXW8LDENgzMAvMZUdifWhg==", + "tryit": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha512-6C5h3CE+0qjGp+YKYTs74xR0k/Nw/ePtl/Lp6CCf44hqBQ66qnH1sDFR5mV/Gc48EsrHLB53lCFSffQCkka3kg==" + }, + "ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, - "dependencies": { - "@hapi/cryptiles": "^5.1.0", - "can-autoplay": "^3.0.0", - "extend": ">=3.0.2", - "lodash": ">=4.17.19", - "lodash.template": ">=4.5.0", - "videojs-contrib-ads": "^6.6.5" - }, - "engines": { - "node": ">=0.8.0" - }, - "peerDependencies": { - "video.js": "^5.19.2 || ^6 || ^7" - } + "requires": {} }, - "node_modules/videojs-playlist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-5.0.0.tgz", - "integrity": "sha512-TM9bytwKqkE05wdWPEKDpkwMGhS/VgMCIsEuNxmX1J1JO9zaTIl4Wm3egf5j1dhIw19oWrqGUV/nK0YNIelCpA==", + "tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "dependencies": { - "global": "^4.3.2", - "video.js": "^6 || ^7" + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, - "engines": { - "node": ">=4.4.0" + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } } }, - "node_modules/videojs-vtt.js": { - "version": "0.15.4", - "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz", - "integrity": "sha512-r6IhM325fcLb1D6pgsMkTQT1PpFdUdYZa1iqk7wJEu+QlibBwATPfPc9Bg8Jiym0GE5yP1AG2rMLu+QMVWkYtA==", + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsx": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz", + "integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==", "dev": true, - "dependencies": { - "global": "^4.3.1" + "requires": { + "esbuild": "~0.23.0", + "fsevents": "~2.3.3", + "get-tsconfig": "^4.7.5" } }, - "node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "safe-buffer": "^5.0.1" } }, - "node_modules/vinyl-bufferstream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz", - "integrity": "sha512-yCCIoTf26Q9SQ0L9cDSavSL7Nt6wgQw8TU1B/bb9b9Z4A3XTypXCGdc5BvXl4ObQvVY8JrDkFnWa/UqBqwM2IA==", - "dependencies": { - "bufferstreams": "1.0.1" - } + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true }, - "node_modules/vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "dependencies": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "prelude-ls": "^1.2.1" } }, - "node_modules/vinyl-fs/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" } }, - "node_modules/vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "dependencies": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" } }, - "node_modules/vinyl-sourcemap/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" } }, - "node_modules/vinyl-sourcemaps-apply": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", + "typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, - "dependencies": { - "source-map": "^0.5.1" + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" } }, - "node_modules/vinyl/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, - "engines": { - "node": ">= 0.10" + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" } }, - "node_modules/void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, + "typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "peer": true + }, + "typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "requires": { + "typescript-logic": "^0.0.0" } }, - "node_modules/vue-template-compiler": { - "version": "2.7.13", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.13.tgz", - "integrity": "sha512-jYM6TClwDS9YqP48gYrtAtaOhRKkbYmbzE+Q51gX5YDr777n7tNI/IZk4QV4l/PjQPNh/FVa/E92sh/RqKMrog==", + "typescript-eslint": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.1.tgz", + "integrity": "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==", "dev": true, - "optional": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" + "requires": { + "@typescript-eslint/eslint-plugin": "8.26.1", + "@typescript-eslint/parser": "8.26.1", + "@typescript-eslint/utils": "8.26.1" } }, - "node_modules/wait-port": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", - "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", - "dev": true, - "dependencies": { - "chalk": "^4.1.2", - "commander": "^9.3.0", - "debug": "^4.3.4" - }, - "bin": { - "wait-port": "bin/wait-port.js" - }, - "engines": { - "node": ">=10" + "typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "requires": { + "typescript-compare": "^0.0.2" } }, - "node_modules/wait-port/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "ua-parser-js": { + "version": "0.7.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", + "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", + "dev": true + }, + "uglify-js": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "optional": true }, - "node_modules/wait-port/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "requires": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" } }, - "node_modules/wait-port/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" } }, - "node_modules/wait-port/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", "dev": true }, - "node_modules/wait-port/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "undertaker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", + "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", "dev": true, - "engines": { - "node": "^12.20.0 || >=14" + "requires": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "fast-levenshtein": "^1.0.0", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + }, + "dependencies": { + "fast-levenshtein": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", + "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==", + "dev": true + } } }, - "node_modules/wait-port/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" + "undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", + "dev": true + }, + "undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" } }, - "node_modules/wait-port/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==", + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" + }, + "unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true + }, + "unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "requires": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" } }, - "node_modules/walk": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", - "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, "dependencies": { - "foreachasync": "^3.0.0" + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "unist-builder": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.1.tgz", + "integrity": "sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==", "dev": true, - "dependencies": { - "defaults": "^1.0.3" + "requires": { + "@types/unist": "^2.0.0" } }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "dev": true + }, + "unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", "dev": true, - "engines": { - "node": ">= 14" + "requires": { + "@types/unist": "^2.0.0" } }, - "node_modules/webdriver": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.29.1.tgz", - "integrity": "sha512-D3gkbDUxFKBJhNHRfMriWclooLbNavVQC1MRvmENAgPNKaHnFn+M+WtP9K2sEr0XczLGNlbOzT7CKR9K5UXKXA==", + "unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", "dev": true, - "dependencies": { - "@types/node": "^20.1.0", - "@types/ws": "^8.5.3", - "@wdio/config": "8.29.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.24.12", - "@wdio/types": "8.29.1", - "@wdio/utils": "8.29.1", - "deepmerge-ts": "^5.1.0", - "got": "^12.6.1", - "ky": "^0.33.0", - "ws": "^8.8.0" - }, - "engines": { - "node": "^16.13 || >=18" + "requires": { + "@types/unist": "^2.0.0" } }, - "node_modules/webdriver/node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "requires": { + "@types/unist": "^2.0.0" } }, - "node_modules/webdriver/node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" } }, - "node_modules/webdriver/node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", "dev": true, - "engines": { - "node": ">=14.16" + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" } }, - "node_modules/webdriver/node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", "dev": true, - "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" }, - "engines": { - "node": ">=14.16" + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + } } }, - "node_modules/webdriver/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "unzipper": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.9.15.tgz", + "integrity": "sha512-2aaUvO4RAeHDvOCuEtth7jrHFaCKTSXPqUkXwADaLBzGbgZGzUDccoEdJ5lW+3RmfpOZYNx0Rw6F6PUzM6caIA==", "dev": true, - "engines": { - "node": ">=10" + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + } } }, - "node_modules/webdriver/node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "requires": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" } }, - "node_modules/webdriver/node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" + "requires": { + "punycode": "^2.1.0" } }, - "node_modules/webdriver/node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "dev": true + }, + "url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "requires": { + "punycode": "^1.4.1", + "qs": "^6.11.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + } } }, - "node_modules/webdriver/node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" } }, - "node_modules/webdriver/node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==", + "dev": true + }, + "urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "userhome": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.0.tgz", + "integrity": "sha512-ayFKY3H+Pwfy4W98yPdtH1VqH4psDeyW8lYYFzfecR9d6hqLpqhecktvYR3SEEXt7vG0S1JEpciI3g94pMErig==", + "dev": true + }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" } }, - "node_modules/webdriver/node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true + }, + "uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", "dev": true, - "engines": { - "node": ">=12.20" + "requires": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" } }, - "node_modules/webdriver/node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", "dev": true, - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "homedir-polyfill": "^1.0.1" } }, - "node_modules/webdriverio": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-7.25.4.tgz", - "integrity": "sha512-agkgwn2SIk5cAJ03uue1GnGZcUZUDN3W4fUMY9/VfO8bVJrPEgWg31bPguEWPu+YhEB/aBJD8ECxJ3OEKdy4qQ==", - "dev": true, - "dependencies": { - "@types/aria-query": "^5.0.0", - "@types/node": "^18.0.0", - "@wdio/config": "7.25.4", - "@wdio/logger": "7.19.0", - "@wdio/protocols": "7.22.0", - "@wdio/repl": "7.25.4", - "@wdio/types": "7.25.4", - "@wdio/utils": "7.25.4", - "archiver": "^5.0.0", - "aria-query": "^5.0.0", - "css-shorthand-properties": "^1.1.1", - "css-value": "^0.0.1", - "devtools": "7.25.4", - "devtools-protocol": "^0.0.1061995", - "fs-extra": "^10.0.0", - "grapheme-splitter": "^1.0.2", - "lodash.clonedeep": "^4.5.0", - "lodash.isobject": "^3.0.2", - "lodash.isplainobject": "^4.0.6", - "lodash.zip": "^4.2.0", - "minimatch": "^5.0.0", - "puppeteer-core": "^13.1.3", - "query-selector-shadow-dom": "^1.0.0", - "resq": "^1.9.1", - "rgb2hex": "0.2.5", - "serialize-error": "^8.0.0", - "webdriver": "7.25.4" - }, - "engines": { - "node": ">=12.0.0" + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, - "node_modules/webdriverio/node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", "dev": true }, - "node_modules/webdriverio/node_modules/@wdio/config": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-7.25.4.tgz", - "integrity": "sha512-vb0emDtD9FbFh/yqW6oNdo2iuhQp8XKj6GX9fyy9v4wZgg3B0HPMVJxhIfcoHz7LMBWlHSo9YdvhFI5EQHRLBA==", - "dev": true, - "dependencies": { - "@wdio/logger": "7.19.0", - "@wdio/types": "7.25.4", - "@wdio/utils": "7.25.4", - "deepmerge": "^4.0.0", - "glob": "^8.0.3" - }, - "engines": { - "node": ">=12.0.0" - } + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, - "node_modules/webdriverio/node_modules/@wdio/logger": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-7.19.0.tgz", - "integrity": "sha512-xR7SN/kGei1QJD1aagzxs3KMuzNxdT/7LYYx+lt6BII49+fqL/SO+5X0FDCZD0Ds93AuQvvz9eGyzrBI2FFXmQ==", + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "loglevel": "^1.6.0", - "loglevel-plugin-prefix": "^0.8.4", - "strip-ansi": "^6.0.0" + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" }, - "engines": { - "node": ">=12.0.0" + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + } } }, - "node_modules/webdriverio/node_modules/@wdio/protocols": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-7.22.0.tgz", - "integrity": "sha512-8EXRR+Ymdwousm/VGtW3H1hwxZ/1g1H99A1lF0U4GuJ5cFWHCd0IVE5H31Z52i8ZruouW8jueMkGZPSo2IIUSQ==", + "vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", "dev": true, - "engines": { - "node": ">=12.0.0" + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" } }, - "node_modules/webdriverio/node_modules/@wdio/repl": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-7.25.4.tgz", - "integrity": "sha512-kYhj9gLsUk4HmlXLqkVre+gwbfvw9CcnrHjqIjrmMS4mR9D8zvBb5CItb3ZExfPf9jpFzIFREbCAYoE9x/kMwg==", + "vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", "dev": true, - "dependencies": { - "@wdio/utils": "7.25.4" - }, - "engines": { - "node": ">=12.0.0" + "requires": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" } }, - "node_modules/webdriverio/node_modules/@wdio/types": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.25.4.tgz", - "integrity": "sha512-muvNmq48QZCvocctnbe0URq2FjJjUPIG4iLoeMmyF0AQgdbjaUkMkw3BHYNHVTbSOU9WMsr2z8alhj/I2H6NRQ==", + "vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", "dev": true, - "dependencies": { - "@types/node": "^18.0.0", - "got": "^11.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "^4.6.2" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" } }, - "node_modules/webdriverio/node_modules/@wdio/utils": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-7.25.4.tgz", - "integrity": "sha512-8iwQDk+foUqSzKZKfhLxjlCKOkfRJPNHaezQoevNgnrTq/t0ek+ldZCATezb9B8jprAuP4mgS9xi22akc6RkzA==", + "vfile-reporter": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-7.0.5.tgz", + "integrity": "sha512-NdWWXkv6gcd7AZMvDomlQbK3MqFWL1RlGzMn++/O2TI+68+nqxCPTvLugdOtfSzXmjh+xUyhp07HhlrbJjT+mw==", "dev": true, - "dependencies": { - "@wdio/logger": "7.19.0", - "@wdio/types": "7.25.4", - "p-iteration": "^1.1.8" + "requires": { + "@types/supports-color": "^8.0.0", + "string-width": "^5.0.0", + "supports-color": "^9.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile": "^5.0.0", + "vfile-message": "^3.0.0", + "vfile-sort": "^3.0.0", + "vfile-statistics": "^2.0.0" }, - "engines": { - "node": ">=12.0.0" + "dependencies": { + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true + } } }, - "node_modules/webdriverio/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "vfile-sort": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-3.0.1.tgz", + "integrity": "sha512-1os1733XY6y0D5x0ugqSeaVJm9lYgj0j5qdcZQFyxlZOSy1jYarL77lLyb5gK4Wqr1d5OxmuyflSO3zKyFnTFw==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "requires": { + "vfile": "^5.0.0", + "vfile-message": "^3.0.0" } }, - "node_modules/webdriverio/node_modules/brace-expansion": { + "vfile-statistics": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-2.0.1.tgz", + "integrity": "sha512-W6dkECZmP32EG/l+dp2jCLdYzmnDBIw6jwiLZSER81oR5AHRcVqL+k3Z+pfH1R73le6ayDkJRMk0sutj1bMVeg==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "requires": { + "vfile": "^5.0.0", + "vfile-message": "^3.0.0" } }, - "node_modules/webdriverio/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "video.js": { + "version": "7.21.6", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.6.tgz", + "integrity": "sha512-m41TbODrUCToVfK1aljVd296CwDQnCRewpIm5tTXMuV87YYSGw1H+VDOaV45HlpcWSsTWWLF++InDgGJfthfUw==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "2.16.3", + "@videojs/vhs-utils": "^3.0.4", + "@videojs/xhr": "2.6.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.5" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "dependencies": { + "safe-json-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz", + "integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==", + "dev": true, + "requires": { + "rust-result": "^1.0.0" + } + } } }, - "node_modules/webdriverio/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "videojs-contrib-ads": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-ads/-/videojs-contrib-ads-6.9.0.tgz", + "integrity": "sha512-nzKz+jhCGMTYffSNVYrmp9p70s05v6jUMOY3Z7DpVk3iFrWK4Zi/BIkokDWrMoHpKjdmCdKzfJVBT+CrUj6Spw==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "requires": { + "global": "^4.3.2", + "video.js": "^6 || ^7" } }, - "node_modules/webdriverio/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "videojs-font": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", + "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==", "dev": true }, - "node_modules/webdriverio/node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "videojs-ima": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/videojs-ima/-/videojs-ima-2.3.0.tgz", + "integrity": "sha512-8r0BZGT+WCTO6PePyKZHikV79Ojqh4yLMx4+DmPyXeRcKUVsQ7Va0R7Ok8GRcA8Zy3l1PM6jzLrD/W1rwKhZ8g==", "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "requires": { + "@hapi/cryptiles": "^5.1.0", + "can-autoplay": "^3.0.2", + "extend": ">=3.0.2", + "videojs-contrib-ads": "^6.9.0" } }, - "node_modules/webdriverio/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "videojs-playlist": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-5.1.2.tgz", + "integrity": "sha512-8YgNq/iL17RLTXpfWAkuhM0Sq4w/x5YPVaNbUycjfqqGL/bp3Nrmc2W0qkPfh0ryB7r4cHfJbtHYP7zlW3ZkdQ==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "global": "^4.3.2", + "video.js": "^6 || ^7 || ^8" } }, - "node_modules/webdriverio/node_modules/ky": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/ky/-/ky-0.30.0.tgz", - "integrity": "sha512-X/u76z4JtDVq10u1JA5UQfatPxgPaVDMYTrgHyiTpGN2z4TMEJkIHsoSBBSg9SWZEIXTKsi9kHgiQ9o3Y/4yog==", + "videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", "dev": true, - "engines": { - "node": ">=12" + "requires": { + "global": "^4.3.1" + } + }, + "vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" }, - "funding": { - "url": "https://github.com/sindresorhus/ky?sponsor=1" + "dependencies": { + "replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true + } } }, - "node_modules/webdriverio/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "vinyl-bufferstream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz", + "integrity": "sha512-yCCIoTf26Q9SQ0L9cDSavSL7Nt6wgQw8TU1B/bb9b9Z4A3XTypXCGdc5BvXl4ObQvVY8JrDkFnWa/UqBqwM2IA==", + "requires": { + "bufferstreams": "1.0.1" + } + }, + "vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" + "requires": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" }, - "engines": { - "node": ">=10" + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } } }, - "node_modules/webdriverio/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==", + "vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" + "requires": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" }, - "engines": { - "node": ">=8" + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } } }, - "node_modules/webdriverio/node_modules/webdriver": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-7.25.4.tgz", - "integrity": "sha512-6nVDwenh0bxbtUkHASz9B8T9mB531Fn1PcQjUGj2t5dolLPn6zuK1D7XYVX40hpn6r3SlYzcZnEBs4X0az5Txg==", + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", "dev": true, - "dependencies": { - "@types/node": "^18.0.0", - "@wdio/config": "7.25.4", - "@wdio/logger": "7.19.0", - "@wdio/protocols": "7.22.0", - "@wdio/types": "7.25.4", - "@wdio/utils": "7.25.4", - "got": "^11.0.2", - "ky": "0.30.0", - "lodash.merge": "^4.6.1" - }, - "engines": { - "node": ">=12.0.0" + "requires": { + "source-map": "^0.5.1" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", "dev": true }, - "node_modules/webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", + "vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } + "optional": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.2.0" } }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", - "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==", + "wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", "dev": true, - "dependencies": { - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", - "commander": "^7.2.0", - "gzip-size": "^6.0.0", - "lodash": "^4.17.20", - "opener": "^1.5.2", - "sirv": "^1.0.7", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" + "requires": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" }, - "engines": { - "node": ">= 10.13.0" + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "node_modules/webpack-bundle-analyzer/node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "requires": { + "foreachasync": "^3.0.0" } }, - "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" } }, - "node_modules/webpack-bundle-analyzer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "optional": true, + "requires": { + "defaults": "^1.0.3" } }, - "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { + "web-namespaces": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "dev": true }, - "node_modules/webpack-bundle-analyzer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", "dev": true }, - "node_modules/webpack-bundle-analyzer/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "webdriver": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.0.5.tgz", + "integrity": "sha512-+xkdfbmG1IZrXxiPwab450Xuh9QClOcxTJ6tnde0rzxlPxdUqZqzwuMtM+VXZybxF4yCLrJWbeT0BpwJFAz1nA==", "dev": true, - "engines": { - "node": ">= 10" + "requires": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.0.5", + "@wdio/logger": "9.0.4", + "@wdio/protocols": "9.0.4", + "@wdio/types": "9.0.4", + "@wdio/utils": "9.0.5", + "deepmerge-ts": "^7.0.3", + "ws": "^8.8.0" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "dev": true, + "requires": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + }, + "@wdio/logger": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.4.tgz", + "integrity": "sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.4.tgz", + "integrity": "sha512-MN7O4Uk3zPWvkN8d6SNdIjd7qHUlTxS7j0QfRPu6TdlYbHu6BJJ8Rr84y7GcUzCnTAJ1nOIpvUyR8MY3hOaVKg==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.5.tgz", + "integrity": "sha512-FOA+t2ixLZ9a7eEH4IZXDsR/ABPTFOTslVzRvIDIkXcxGys3Cn3LQP2tpcIV1NxI+7OOAD0fIZ9Ig3gPBoVZRQ==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.4", + "@wdio/types": "9.0.4", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } } }, - "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" + "webdriverio": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.0.9.tgz", + "integrity": "sha512-IwvKzhcJ9NjOL55xwj27uTTKkfxsg77dmAfqoKFSP5dQ70JzU+NgxiALEjjWQDybtt1yGIkHk7wjjxjboMU1uw==", + "dev": true, + "requires": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.0.8", + "@wdio/logger": "9.0.8", + "@wdio/protocols": "9.0.8", + "@wdio/repl": "9.0.8", + "@wdio/types": "9.0.8", + "@wdio/utils": "9.0.8", + "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.2.1", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.0.8" + }, + "dependencies": { + "@puppeteer/browsers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", + "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", + "dev": true, + "requires": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + }, + "@wdio/config": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.0.8.tgz", + "integrity": "sha512-37L+hd+A1Nyehd/pgfTrLC6w+Ngbu0CIoFh9Vv6v8Cgu5Hih0TLofvlg+J1BNbcTd5eQ2tFKZBDeFMhQaIiTpg==", + "dev": true, + "requires": { + "@wdio/logger": "9.0.8", + "@wdio/types": "9.0.8", + "@wdio/utils": "9.0.8", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + } + }, + "@wdio/logger": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.0.8.tgz", + "integrity": "sha512-uIyYIDBwLczmsp9JE5hN3ME8Xg+9WNBfSNXD69ICHrY9WPTzFf94UeTuavK7kwSKF3ro2eJbmNZItYOfnoovnw==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/protocols": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.0.8.tgz", + "integrity": "sha512-xRH54byFf623/w/KW62xkf/C2mGyigSfMm+UT3tNEAd5ZA9X2VAWQWQBPzdcrsck7Fxk4zlQX8Kb34RSs7Cy4Q==", + "dev": true + }, + "@wdio/repl": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.8.tgz", + "integrity": "sha512-3iubjl4JX5zD21aFxZwQghqC3lgu+mSs8c3NaiYYNCC+IT5cI/8QuKlgh9s59bu+N3gG988jqMJeCYlKuUv/iw==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/types": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.0.8.tgz", + "integrity": "sha512-pmz2iRWddTanrv8JC7v3wUGm17KRv2WyyJhQfklMSANn9V1ep6pw1RJG2WJnKq4NojMvH1nVv1sMZxXrYPhpYw==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.0.8.tgz", + "integrity": "sha512-p3EgOdkhCvMxJFd3WTtSChqYFQu2mz69/5tOsljDaL+4QYwnRR7O8M9wFsL3/9XMVcHdnC4Ija2VRxQ/lb+hHQ==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.0.8", + "@wdio/types": "9.0.8", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "deepmerge-ts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.0.tgz", + "integrity": "sha512-q6bNsfNBtgr8ZOQqmZbl94MmYWm+QcDNIkqCxVWiw1vKvf+y/N2dZQKdnDXn4c5Ygt/y63tDof6OCN+2YwWVEg==", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "webdriver": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.0.8.tgz", + "integrity": "sha512-UnV0ANriSTUgypGk0pz8lApeQuHt+72WEDQG5hFwkkSvggtKLyWdT7+PQkNoXvDajTmiLIqUOq8XPI/Pm71rtw==", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.0.8", + "@wdio/logger": "9.0.8", + "@wdio/protocols": "9.0.8", + "@wdio/types": "9.0.8", + "@wdio/utils": "9.0.8", + "deepmerge-ts": "^7.0.3", + "ws": "^8.8.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } } }, - "node_modules/webpack-bundle-analyzer/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==", + "webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" + "requires": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" }, - "engines": { - "node": ">=8" + "dependencies": { + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + }, + "acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } } }, - "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "requires": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true + "dependencies": { + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true }, - "utf-8-validate": { - "optional": true + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "requires": {} } } }, - "node_modules/webpack-manifest-plugin": { + "webpack-manifest-plugin": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-5.0.0.tgz", "integrity": "sha512-8RQfMAdc5Uw3QbCQ/CBV/AXqOR8mt03B6GJmRbhWopE8GzRfEpn+k0ZuWywxW+5QZsffhmFDY1J6ohqJo+eMuw==", "dev": true, - "dependencies": { + "requires": { "tapable": "^2.0.0", "webpack-sources": "^2.2.0" }, - "engines": { - "node": ">=12.22.0" - }, - "peerDependencies": { - "webpack": "^5.47.0" - } - }, - "node_modules/webpack-manifest-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "dev": true, "dependencies": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10.13.0" + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + } } }, - "node_modules/webpack-merge": { + "webpack-merge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", "dev": true, - "dependencies": { + "requires": { "lodash": "^4.17.15" } }, - "node_modules/webpack-sources": { + "webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } + "dev": true }, - "node_modules/webpack-stream": { + "webpack-stream": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-7.0.0.tgz", "integrity": "sha512-XoAQTHyCaYMo6TS7Atv1HYhtmBgKiVLONJbzLBl2V3eibXQ2IT/MCRM841RW/r3vToKD5ivrTJFWgd/ghoxoRg==", "dev": true, - "dependencies": { + "requires": { "fancy-log": "^1.3.3", "lodash.clone": "^4.3.2", "lodash.some": "^4.2.2", @@ -28920,546 +50152,480 @@ "through": "^2.3.8", "vinyl": "^2.2.1" }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "webpack": "^5.21.2" - } - }, - "node_modules/webpack-stream/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "dependencies": { - "ansi-wrap": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-stream/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-stream/node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-stream/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-stream/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-stream/node_modules/plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "dev": true, - "dependencies": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/webpack-stream/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/webpack/node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/webpack/node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "deprecated": "package has been renamed to acorn-import-attributes", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "node_modules/websocket-driver": { + "websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, - "dependencies": { + "requires": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" } }, - "node_modules/websocket-extensions": { + "websocket-extensions": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } + "dev": true }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, + "requires": { + "iconv-lite": "0.6.3" + }, "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true + }, + "which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" + "requires": { + "isexe": "^3.1.1" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" } }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + } + }, + "which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" } }, - "node_modules/which-module": { + "which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", "dev": true }, - "node_modules/which-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", - "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" } }, - "node_modules/winston-transport": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz", - "integrity": "sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==", + "winston-transport": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", "dev": true, - "dependencies": { + "requires": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, - "node_modules/word-wrap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", - "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true }, - "node_modules/wordwrap": { + "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, - "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, - "dependencies": { + "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, - "engines": { - "node": ">=7.0.0" + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/wrappy": { + "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/write/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } + "requires": {} }, - "node_modules/xtend": { + "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } + "dev": true }, - "node_modules/y18n": { + "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/yargs": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "yargs": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.3.tgz", "integrity": "sha512-7OGt4xXoWJQh5ulgZ78rKaqY7dNWbjfK+UKxGcIlaM2j7C4fqGchyv8CPvEWdRPrHp6Ula/YU8yGRpYGOHrI+g==", "dev": true }, - "node_modules/yargs-parser": { + "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } + "dev": true }, - "node_modules/yargs-unparser": { + "yargs-unparser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, - "dependencies": { + "requires": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", "flat": "^5.0.2", "is-plain-obj": "^2.1.0" }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + } } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", "dev": true, - "dependencies": { + "requires": { "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "pend": "~1.2.0" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true }, - "node_modules/zip-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", - "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", - "dev": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "compress-commons": "^4.1.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } + "yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true }, - "node_modules/zip-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } + "yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true }, - "node_modules/zwitch": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", - "integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "requires": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, - "plugins/eslint": { - "name": "eslint-plugin-prebid", - "version": "1.0.0", + "zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "dev": true, - "license": "Apache-2.0" + "optional": true, + "peer": true + }, + "zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true } } } diff --git a/package.json b/package.json index 6e5022bcece..a35665bd355 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,17 @@ { "name": "prebid.js", - "version": "8.47.0", + "version": "9.40.0", "description": "Header Bidding Management Library", - "main": "src/prebid.js", + "main": "src/prebid.public.js", + "exports": { + ".": "./src/prebid.public.js", + "./prebid.js": "./src/prebid.public.js", + "./prebid": "./src/prebid.public.js", + "./.babelrc.js": "./.babelrc.js", + "./babelConfig.js": "./babelConfig.js", + "./modules/*": "./modules/*", + "./modules/*.js": "./modules/*.js" + }, "scripts": { "serve": "gulp serve", "test": "gulp test", @@ -30,50 +39,47 @@ ], "globalVarName": "pbjs", "defineGlobal": true, - "author": "the prebid.js contributors", + "author": "The prebid.js contributors", "license": "Apache-2.0", "engines": { - "node": ">=12.0.0" + "node": ">=20.0.0" }, "devDependencies": { "@babel/eslint-parser": "^7.16.5", - "@wdio/browserstack-service": "^8.29.0", - "@wdio/cli": "^8.29.0", + "@babel/register": "^7.24.6", + "@eslint/compat": "^1.2.7", + "@wdio/browserstack-service": "^9.0.5", + "@wdio/cli": "^9.0.5", "@wdio/concise-reporter": "^8.29.0", - "@wdio/local-runner": "^8.29.0", + "@wdio/local-runner": "^9.0.5", "@wdio/mocha-framework": "^8.29.0", "@wdio/spec-reporter": "^8.29.0", "ajv": "6.12.3", "assert": "^2.0.0", "babel-loader": "^8.0.5", "babel-plugin-istanbul": "^6.1.1", - "babel-register": "^6.26.0", "body-parser": "^1.19.0", "chai": "^4.2.0", "coveralls": "^3.1.0", "deep-equal": "^2.0.3", "documentation": "^14.0.0", "es5-shim": "^4.5.14", - "eslint": "^7.27.0", - "eslint-config-standard": "^10.2.1", - "eslint-plugin-import": "^2.20.2", - "eslint-plugin-jsdoc": "^38.1.6", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prebid": "file:./plugins/eslint", - "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-standard": "^3.0.1", + "eslint": "^9.22.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.6", "execa": "^1.0.0", "faker": "^5.5.3", "fs.extra": "^1.3.2", - "gulp": "^4.0.0", + "globals": "^16.0.0", + "gulp": "^4.0.2", "gulp-clean": "^0.4.0", "gulp-concat": "^2.6.0", "gulp-connect": "^5.7.0", - "gulp-eslint": "^6.0.0", "gulp-if": "^3.0.0", "gulp-js-escape": "^1.0.1", "gulp-rename": "^2.0.0", "gulp-replace": "^1.0.0", + "gulp-run-command": "^0.0.10", "gulp-shell": "^0.8.0", "gulp-sourcemaps": "^3.0.0", "gulp-terser": "^2.0.1", @@ -100,20 +106,21 @@ "karma-spec-reporter": "^0.0.32", "karma-webpack": "^5.0.0", "lodash": "^4.17.21", - "mocha": "^10.0.0", + "mocha": "^10.7.3", "morgan": "^1.10.0", + "neostandard": "^0.12.1", "node-html-parser": "^6.1.5", "opn": "^5.4.0", "resolve-from": "^5.0.0", - "sinon": "^4.1.3", + "sinon": "^4.5.0", "through2": "^4.0.2", "url": "^0.11.0", "url-parse": "^1.0.5", "video.js": "^7.17.0", "videojs-contrib-ads": "^6.9.0", - "videojs-ima": "^1.11.0", + "videojs-ima": "^2.3.0", "videojs-playlist": "^5.0.0", - "webdriverio": "^7.6.1", + "webdriverio": "^9.0.9", "webpack": "^5.70.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-manifest-plugin": "^5.0.0", @@ -121,21 +128,20 @@ "yargs": "^1.3.1" }, "dependencies": { - "@babel/core": "^7.16.7", + "@babel/core": "^7.25.2", "@babel/plugin-transform-runtime": "^7.18.9", "@babel/preset-env": "^7.16.8", "@babel/runtime": "^7.18.9", "core-js": "^3.13.0", "core-js-pure": "^3.13.0", - "criteo-direct-rsa-validate": "^1.1.0", "crypto-js": "^4.2.0", "dlv": "1.1.3", - "dset": "3.1.2", + "dset": "3.1.4", "express": "^4.15.4", "fun-hooks": "^0.9.9", "gulp-wrap": "^0.15.0", - "just-clone": "^1.0.2", - "live-connect-js": "^6.3.4" + "klona": "^2.0.6", + "live-connect-js": "^7.2.0" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/plugins/eslint/index.js b/plugins/eslint/index.js new file mode 100644 index 00000000000..36b0a57fd14 --- /dev/null +++ b/plugins/eslint/index.js @@ -0,0 +1,35 @@ +const _ = require('lodash'); +const { flagErrors } = require('./validateImports.js'); + +module.exports = { + rules: { + 'validate-imports': { + meta: { + docs: { + description: 'validates module imports can be found without custom webpack resolvers, are in module whitelist, and not module entry points' + }, + schema: { + type: 'array' + } + }, + create: function (context) { + return { + "CallExpression[callee.name='require']"(node) { + let importPath = _.get(node, ['arguments', 0, 'value']); + if (importPath) { + flagErrors(context, node, importPath); + } + }, + ImportDeclaration(node) { + let importPath = node.source.value.trim(); + flagErrors(context, node, importPath); + }, + 'ExportNamedDeclaration[source]'(node) { + let importPath = node.source.value.trim(); + flagErrors(context, node, importPath); + } + }; + } + }, + } +}; diff --git a/plugins/eslint/package.json b/plugins/eslint/package.json deleted file mode 100644 index fa18ad83718..00000000000 --- a/plugins/eslint/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "eslint-plugin-prebid", - "version": "1.0.0", - "description": "validates module imports can be found without custom webpack resolvers, are in module whitelist, and not module entry points", - "main": "validateImports.js", - "author": "the prebid.js contributors", - "license": "Apache-2.0" -} diff --git a/plugins/eslint/validateImports.js b/plugins/eslint/validateImports.js index b936f44aee7..876e9d8c3c5 100644 --- a/plugins/eslint/validateImports.js +++ b/plugins/eslint/validateImports.js @@ -38,7 +38,7 @@ function flagErrors(context, node, importPath) { } // do not allow imports outside `creative` - if (isInDirectory(absFileDir, CREATIVE_PATH) && !isInDirectory(absImportPath, CREATIVE_PATH) && absImportPath !== CREATIVE_PATH) { + if ((isInDirectory(absFileDir, CREATIVE_PATH) || absFileDir == CREATIVE_PATH) && !isInDirectory(absImportPath, CREATIVE_PATH) && absImportPath !== CREATIVE_PATH) { context.report(node, `import "${importPath}": importing from outside creative is not allowed`); } @@ -53,31 +53,5 @@ function flagErrors(context, node, importPath) { } module.exports = { - rules: { - 'validate-imports': { - meta: { - docs: { - description: 'validates module imports can be found without custom webpack resolvers, are in module whitelist, and not module entry points' - } - }, - create: function(context) { - return { - "CallExpression[callee.name='require']"(node) { - let importPath = _.get(node, ['arguments', 0, 'value']); - if (importPath) { - flagErrors(context, node, importPath); - } - }, - ImportDeclaration(node) { - let importPath = node.source.value.trim(); - flagErrors(context, node, importPath); - }, - 'ExportNamedDeclaration[source]'(node) { - let importPath = node.source.value.trim(); - flagErrors(context, node, importPath); - } - } - } - } - } -}; + flagErrors +} diff --git a/src/Renderer.js b/src/Renderer.js index 2f9b2e025cb..d3dd23b50af 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -1,9 +1,10 @@ import { loadExternalScript } from './adloader.js'; import { - logError, logWarn, logMessage, deepAccess + logError, logWarn, logMessage } from './utils.js'; import {find} from './polyfill.js'; import {getGlobal} from './prebidGlobal.js'; +import { MODULE_TYPE_PREBID } from './activities/modules.js'; const pbjsInstance = getGlobal(); const moduleCode = 'outstream'; @@ -23,6 +24,7 @@ export function Renderer(options) { this.handlers = {}; this.id = id; this.renderNow = renderNow; + this.adUnitCode = adUnitCode; // a renderer may push to the command queue to delay rendering until the // render function is loaded by loadExternalScript, at which point the the command @@ -62,7 +64,7 @@ export function Renderer(options) { } else { // we expect to load a renderer url once only so cache the request to load script this.cmd.unshift(runRender) // should render run first ? - loadExternalScript(url, moduleCode, this.callback, this.documentContext); + loadExternalScript(url, MODULE_TYPE_PREBID, moduleCode, this.callback, this.documentContext); } }.bind(this); // bind the function to this object to avoid 'this' errors } @@ -100,7 +102,7 @@ Renderer.prototype.process = function() { try { this.cmd.shift().call(); } catch (error) { - logError('Error processing Renderer command: ', error); + logError(`Error processing Renderer command on ad unit '${this.adUnitCode}':`, error); } } }; @@ -143,11 +145,11 @@ function isRendererPreferredFromAdUnit(adUnitCode) { } // renderer defined at adUnit level - const adUnitRenderer = deepAccess(adUnit, 'renderer'); + const adUnitRenderer = adUnit?.renderer; const hasValidAdUnitRenderer = !!(adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render); // renderer defined at adUnit.mediaTypes level - const mediaTypeRenderer = deepAccess(adUnit, 'mediaTypes.video.renderer'); + const mediaTypeRenderer = adUnit?.mediaTypes?.video?.renderer; const hasValidMediaTypeRenderer = !!(mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render) return !!( diff --git a/src/activities/activities.js b/src/activities/activities.js index 0a17750b0b0..40f43fb9114 100644 --- a/src/activities/activities.js +++ b/src/activities/activities.js @@ -50,3 +50,8 @@ export const ACTIVITY_TRANSMIT_PRECISE_GEO = 'transmitPreciseGeo'; * transmit TID: some component wants access ot (and send along) transaction IDs */ export const ACTIVITY_TRANSMIT_TID = 'transmitTid'; + +/** + * loadExternalScript: adLoader.js is allowed to load external script + */ +export const LOAD_EXTERNAL_SCRIPT = 'loadExternalScript'; diff --git a/src/activities/params.js b/src/activities/params.js index 036a6657cf8..82d81e85ee9 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -39,9 +39,11 @@ export const ACTIVITY_PARAM_SYNC_TYPE = 'syncType' */ export const ACTIVITY_PARAM_SYNC_URL = 'syncUrl'; /** + * Configuration options for analytics adapter - the argument passed to `enableAnalytics`. + * Relevant for: reportAnalytics * @private - * configuration options for analytics adapter - the argument passed to `enableAnalytics`. - * relevant for: reportAnalytics + * @constant + * @type {string} */ export const ACTIVITY_PARAM_ANL_CONFIG = '_config'; diff --git a/src/activities/redactor.js b/src/activities/redactor.js index 694a96b2b14..65d14722ce5 100644 --- a/src/activities/redactor.js +++ b/src/activities/redactor.js @@ -7,6 +7,7 @@ import { ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD } from './activities.js'; +import { scrubIPv4, scrubIPv6 } from '../utils/ipUtils.js'; export const ORTB_UFPD_PATHS = [ 'data', @@ -21,6 +22,8 @@ export const ORTB_UFPD_PATHS = [ ].map(f => `user.${f}`).concat('device.ext.cdep'); export const ORTB_EIDS_PATHS = ['user.eids', 'user.ext.eids']; export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', 'device.geo.lon']; +export const ORTB_IPV4_PATHS = ['device.ip'] +export const ORTB_IPV6_PATHS = ['device.ipv6'] /** * @typedef TransformationRuleDef @@ -157,6 +160,22 @@ export function ortb2TransmitRules(isAllowed = isActivityAllowed) { return Math.round((val + Number.EPSILON) * 100) / 100; } }, + { + name: ACTIVITY_TRANSMIT_PRECISE_GEO, + paths: ORTB_IPV4_PATHS, + applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_PRECISE_GEO, isAllowed), + get(val) { + return scrubIPv4(val); + } + }, + { + name: ACTIVITY_TRANSMIT_PRECISE_GEO, + paths: ORTB_IPV6_PATHS, + applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_PRECISE_GEO, isAllowed), + get(val) { + return scrubIPv6(val); + } + }, { name: ACTIVITY_TRANSMIT_TID, paths: ['source.tid'], diff --git a/src/activities/rules.js b/src/activities/rules.js index f84f1080843..7b4f4634f07 100644 --- a/src/activities/rules.js +++ b/src/activities/rules.js @@ -40,19 +40,19 @@ export function ruleRegistry(logger = prefixLog('Activity control:')) { /** * Register an activity control rule. * - * @param {string} activity activity name - set is defined in `activities.js` - * @param {string} ruleName a name for this rule; used for logging. - * @param {function({}): {allow: boolean, reason?: string}} rule definition function. Takes in activity + * @param {string} activity - Activity name, as defined in `activities.js`. + * @param {string} ruleName - A name for this rule, used for logging. + * @param {function(Object): {allow: boolean, reason?: string}} rule - Rule definition function. Takes in activity * parameters as a single map; MAY return an object {allow, reason}, where allow is true/false, * and reason is an optional message used for logging. * - * {allow: true} will allow this activity AS LONG AS no other rules with same or higher priority return {allow: false}; + * {allow: true} will allow this activity AS LONG AS no other rules with the same or higher priority return {allow: false}; * {allow: false} will deny this activity AS LONG AS no other rules with higher priority return {allow: true}; - * returning null/undefined has no effect - the decision is left to other rules. + * Returning null/undefined has no effect - the decision is left to other rules. * If no rule returns an allow value, the default is to allow the activity. * - * @param {number} priority rule priority; lower number means higher priority - * @returns {function(void): void} a function that unregisters the rule when called. + * @param {number} [priority=10] - Rule priority; lower number means higher priority. + * @returns {function(): void} - A function that unregisters the rule when called. */ function registerActivityControl(activity, ruleName, rule, priority = 10) { const rules = getRules(activity); diff --git a/src/adRendering.js b/src/adRendering.js index 7d306adc9cc..45a6d64b664 100644 --- a/src/adRendering.js +++ b/src/adRendering.js @@ -1,6 +1,14 @@ -import {createIframe, deepAccess, inIframe, insertElement, logError, logWarn, replaceMacros} from './utils.js'; +import { + createIframe, + createInvisibleIframe, + inIframe, + insertElement, + logError, + logWarn, + replaceMacros, triggerPixel +} from './utils.js'; import * as events from './events.js'; -import { AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS, MESSAGES } from './constants.js'; +import {AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS, MESSAGES, PB_LOCATOR} from './constants.js'; import {config} from './config.js'; import {executeRenderer, isRendererRequired} from './Renderer.js'; import {VIDEO} from './mediaTypes.js'; @@ -8,10 +16,28 @@ import {auctionManager} from './auctionManager.js'; import {getCreativeRenderer} from './creativeRenderers.js'; import {hook} from './hook.js'; import {fireNativeTrackers} from './native.js'; +import {PbPromise} from './utils/promise.js'; +import adapterManager from './adapterManager.js'; +import {useMetrics} from './utils/perfMetrics.js'; +import {filters} from './targeting.js'; +import {EVENT_TYPE_WIN, parseEventTrackers, TRACKER_METHOD_IMG} from './eventTrackers.js'; -const { AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON } = EVENTS; +const { AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON, EXPIRED_RENDER } = EVENTS; const { EXCEPTION } = AD_RENDER_FAILED_REASON; +export const getBidToRender = hook('sync', function (adId, forRender = true, override = PbPromise.resolve()) { + return override + .then(bid => bid ?? auctionManager.findBidByAdId(adId)) + .catch(() => {}) +}) + +export const markWinningBid = hook('sync', function (bid) { + (parseEventTrackers(bid.eventtrackers)[EVENT_TYPE_WIN]?.[TRACKER_METHOD_IMG] || []) + .forEach(url => triggerPixel(url)); + events.emit(BID_WON, bid); + auctionManager.addWinningBid(bid); +}) + /** * Emit the AD_RENDER_FAILED event. * @@ -47,6 +73,8 @@ export function emitAdRenderSucceeded({ doc, bid, id }) { if (bid) data.bid = bid; if (id) data.adId = id; + adapterManager.callAdRenderSucceededBidder(bid.adapterCode || bid.bidder, bid); + events.emit(AD_RENDER_SUCCEEDED, data); } @@ -99,7 +127,7 @@ function creativeMessageHandler(deps) { } export const getRenderingData = hook('sync', function (bidResponse, options) { - const {ad, adUrl, cpm, originalCpm, width, height} = bidResponse + const {ad, adUrl, cpm, originalCpm, width, height, instl} = bidResponse const repl = { AUCTION_PRICE: originalCpm || cpm, CLICKTHROUGH: options?.clickUrl || '' @@ -108,15 +136,17 @@ export const getRenderingData = hook('sync', function (bidResponse, options) { ad: replaceMacros(ad, repl), adUrl: replaceMacros(adUrl, repl), width, - height + height, + instl }; }) -export const doRender = hook('sync', function({renderFn, resizeFn, bidResponse, options}) { - if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) { +export const doRender = hook('sync', function({renderFn, resizeFn, bidResponse, options, doc, isMainDocument = doc === document && !inIframe()}) { + const videoBid = (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) + if (isMainDocument || videoBid) { emitAdRenderFail({ reason: AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT, - message: 'Cannot render video ad', + message: videoBid ? 'Cannot render video ad without a renderer' : `renderAd was prevented from writing to the main document.`, bid: bidResponse, id: bidResponse.adId }); @@ -143,33 +173,82 @@ doRender.before(function (next, args) { }, 100) export function handleRender({renderFn, resizeFn, adId, options, bidResponse, doc}) { + deferRendering(bidResponse, () => { + if (bidResponse == null) { + emitAdRenderFail({ + reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + message: `Cannot find ad '${adId}'`, + id: adId + }); + return; + } + if (bidResponse.status === BID_STATUS.RENDERED) { + logWarn(`Ad id ${adId} has been rendered before`); + events.emit(STALE_RENDER, bidResponse); + if (config.getConfig('auctionOptions')?.suppressStaleRender) { + return; + } + } + if (!filters.isBidNotExpired(bidResponse)) { + logWarn(`Ad id ${adId} has been expired`); + events.emit(EXPIRED_RENDER, bidResponse); + if (config.getConfig('auctionOptions')?.suppressExpiredRender) { + return; + } + } + + try { + doRender({renderFn, resizeFn, bidResponse, options, doc}); + } catch (e) { + emitAdRenderFail({ + reason: AD_RENDER_FAILED_REASON.EXCEPTION, + message: e.message, + id: adId, + bid: bidResponse + }); + } + }) +} + +export function markBidAsRendered(bidResponse) { + const metrics = useMetrics(bidResponse.metrics); + metrics.checkpoint('bidRender'); + metrics.timeBetween('bidWon', 'bidRender', 'render.deferred'); + metrics.timeBetween('auctionEnd', 'bidRender', 'render.pending'); + metrics.timeBetween('requestBids', 'bidRender', 'render.e2e'); + bidResponse.status = BID_STATUS.RENDERED; +} + +const DEFERRED_RENDER = new WeakMap(); +const WINNERS = new WeakSet(); + +export function deferRendering(bidResponse, renderFn) { if (bidResponse == null) { - emitAdRenderFail({ - reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, - message: `Cannot find ad '${adId}'`, - id: adId - }); + // if the bid is missing, let renderFn deal with it now + renderFn(); return; } - if (bidResponse.status === BID_STATUS.RENDERED) { - logWarn(`Ad id ${adId} has been rendered before`); - events.emit(STALE_RENDER, bidResponse); - if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { - return; - } + DEFERRED_RENDER.set(bidResponse, renderFn); + if (!bidResponse.deferRendering) { + renderIfDeferred(bidResponse); } - try { - doRender({renderFn, resizeFn, bidResponse, options, doc}); - } catch (e) { - emitAdRenderFail({ - reason: AD_RENDER_FAILED_REASON.EXCEPTION, - message: e.message, - id: adId, - bid: bidResponse - }); + markWinner(bidResponse); +} + +export function markWinner(bidResponse) { + if (!WINNERS.has(bidResponse)) { + WINNERS.add(bidResponse); + markWinningBid(bidResponse); + } +} + +export function renderIfDeferred(bidResponse) { + const renderFn = DEFERRED_RENDER.get(bidResponse); + if (renderFn) { + renderFn(); + markBidAsRendered(bidResponse); + DEFERRED_RENDER.delete(bidResponse); } - auctionManager.addWinningBid(bidResponse); - events.emit(BID_WON, bidResponse); } export function renderAdDirect(doc, adId, options) { @@ -178,9 +257,16 @@ export function renderAdDirect(doc, adId, options) { emitAdRenderFail(Object.assign({id: adId, bid}, {reason, message})); } function resizeFn(width, height) { - if (doc.defaultView && doc.defaultView.frameElement) { - width && (doc.defaultView.frameElement.width = width); - height && (doc.defaultView.frameElement.height = height); + const frame = doc.defaultView?.frameElement; + if (frame) { + if (width) { + frame.width = width; + frame.style.width && (frame.style.width = `${width}px`); + } + if (height) { + frame.height = height; + frame.style.height && (frame.style.height = `${height}px`); + } } } const messageHandler = creativeMessageHandler({resizeFn}); @@ -188,7 +274,7 @@ export function renderAdDirect(doc, adId, options) { if (adData.ad) { doc.write(adData.ad); doc.close(); - emitAdRenderSucceeded({doc, bid, adId: bid.adId}); + emitAdRenderSucceeded({doc, bid, id: bid.adId}); } else { getCreativeRenderer(bid) .then(render => render(adData, { @@ -196,7 +282,7 @@ export function renderAdDirect(doc, adId, options) { mkFrame: createIframe, }, doc.defaultView)) .then( - () => emitAdRenderSucceeded({doc, bid, adId: bid.adId}), + () => emitAdRenderSucceeded({doc, bid, id: bid.adId}), (e) => { fail(e?.reason || AD_RENDER_FAILED_REASON.EXCEPTION, e?.message) e?.stack && logError(e); @@ -211,15 +297,30 @@ export function renderAdDirect(doc, adId, options) { if (!adId || !doc) { fail(AD_RENDER_FAILED_REASON.MISSING_DOC_OR_ADID, `missing ${adId ? 'doc' : 'adId'}`); } else { - bid = auctionManager.findBidByAdId(adId); - - if ((doc === document && !inIframe())) { - fail(AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT, `renderAd was prevented from writing to the main document.`); - } else { - handleRender({renderFn, resizeFn, adId, options: {clickUrl: options?.clickThrough}, bidResponse: bid, doc}); - } + getBidToRender(adId).then(bidResponse => { + bid = bidResponse; + handleRender({renderFn, resizeFn, adId, options: {clickUrl: options?.clickThrough}, bidResponse, doc}); + }); } } catch (e) { fail(EXCEPTION, e.message); } } + +/** + * Insert an invisible, named iframe that can be used by creatives to locate the window Prebid is running in + * (by looking for one that has `.frames[PB_LOCATOR]` defined). + * This is necessary because in some situations creatives may be rendered inside nested iframes - Prebid is not necessarily + * in the immediate parent window. + */ +export function insertLocatorFrame() { + if (!window.frames[PB_LOCATOR]) { + if (!document.body) { + window.requestAnimationFrame(insertLocatorFrame); + } else { + const frame = createInvisibleIframe(); + frame.name = PB_LOCATOR; + document.body.appendChild(frame); + } + } +} diff --git a/src/adUnits.js b/src/adUnits.js index b0c728fd945..5c07718bbea 100644 --- a/src/adUnits.js +++ b/src/adUnits.js @@ -1,5 +1,3 @@ -import { deepAccess } from './utils.js'; - let adUnits = {}; export function reset() { adUnits = {} @@ -48,13 +46,22 @@ export function incrementBidderWinsCounter(adunit, bidderCode) { return incrementAdUnitCount(adunit, 'winsCounter', bidderCode); } +/** + * Increments and returns current Adunit auctions counter + * @param {string} adunit id + * @returns {number} current adunit auctions count + */ +export function incrementAuctionsCounter(adunit) { + return incrementAdUnitCount(adunit, 'auctionsCounter'); +} + /** * Returns current Adunit counter * @param {string} adunit id * @returns {number} current adunit count */ export function getRequestsCounter(adunit) { - return deepAccess(adUnits, `${adunit}.requestsCounter`) || 0; + return adUnits?.[adunit]?.requestsCounter || 0; } /** @@ -64,7 +71,7 @@ export function getRequestsCounter(adunit) { * @returns {number} current adunit bidder requests count */ export function getBidderRequestsCounter(adunit, bidder) { - return deepAccess(adUnits, `${adunit}.bidders.${bidder}.requestsCounter`) || 0; + return adUnits?.[adunit]?.bidders?.[bidder]?.requestsCounter || 0; } /** @@ -74,5 +81,14 @@ export function getBidderRequestsCounter(adunit, bidder) { * @returns {number} current adunit bidder requests count */ export function getBidderWinsCounter(adunit, bidder) { - return deepAccess(adUnits, `${adunit}.bidders.${bidder}.winsCounter`) || 0; + return adUnits?.[adunit]?.bidders?.[bidder]?.winsCounter || 0; +} + +/** + * Returns current Adunit auctions counter + * @param {string} adunit id + * @returns {number} current adunit auctions count + */ +export function getAuctionsCounter(adunit) { + return adUnits?.[adunit]?.auctionsCounter || 0; } diff --git a/src/adapterManager.js b/src/adapterManager.js index 557f4c1eee4..ef712af907f 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -1,7 +1,6 @@ /** @module adaptermanger */ import { - deepAccess, deepClone, flatten, generateUUID, @@ -10,6 +9,7 @@ import { getUniqueIdentifierStr, getUserConfiguredParams, groupBy, + internal, isArray, isPlainObject, isValidMediaTypes, @@ -20,6 +20,7 @@ import { mergeDeep, shuffle, timestamp, + uniques, } from './utils.js'; import {decorateAdUnitsWithNativeParams, nativeAdapters} from './native.js'; import {newBidder} from './adapters/bidderFactory.js'; @@ -28,15 +29,19 @@ import {config, RANDOM} from './config.js'; import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import { + getAuctionsCounter, getBidderRequestsCounter, getBidderWinsCounter, - getRequestsCounter, incrementBidderRequestsCounter, - incrementBidderWinsCounter, incrementRequestsCounter + getRequestsCounter, + incrementAuctionsCounter, + incrementBidderRequestsCounter, + incrementBidderWinsCounter, + incrementRequestsCounter } from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; import {GDPR_GVLIDS, gdprDataHandler, gppDataHandler, uspDataHandler, } from './consentHandler.js'; import * as events from './events.js'; -import { EVENTS, S2S } from './constants.js'; +import {EVENTS, S2S} from './constants.js'; import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, MODULE_TYPE_PREBID} from './activities/modules.js'; @@ -44,6 +49,7 @@ import {isActivityAllowed} from './activities/rules.js'; import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activities.js'; import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParamsBuilder} from './activities/params.js'; import {redactor} from './activities/redactor.js'; +import {EVENT_TYPE_IMPRESSION, parseEventTrackers, TRACKER_METHOD_IMG} from './eventTrackers.js'; export {gdprDataHandler, gppDataHandler, uspDataHandler, coppaDataHandler} from './consentHandler.js'; @@ -125,15 +131,17 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, src, metrics} adUnitCode: adUnit.code, transactionId: adUnit.transactionId, adUnitId: adUnit.adUnitId, - sizes: deepAccess(mediaTypes, 'banner.sizes') || deepAccess(mediaTypes, 'video.playerSize') || [], + sizes: mediaTypes?.banner?.sizes || mediaTypes?.video?.playerSize || [], bidId: bid.bid_id || getUniqueIdentifierStr(), bidderRequestId, auctionId, src, metrics, + auctionsCount: getAuctionsCounter(adUnit.code), bidRequestsCount: getRequestsCounter(adUnit.code), bidderRequestsCount: getBidderRequestsCounter(adUnit.code, bid.bidder), bidderWinsCount: getBidderWinsCounter(adUnit.code, bid.bidder), + deferBilling: !!adUnit.deferBilling })); return bids; }, []) @@ -230,9 +238,11 @@ export function getS2SBidderSet(s2sConfigs) { } /** - * @returns {{[PARTITIONS.CLIENT]: Array, [PARTITIONS.SERVER]: Array}} - * All the bidder codes in the given `adUnits`, divided in two arrays - - * those that should be routed to client, and server adapters (according to the configuration in `s2sConfigs`). + * @param {Array} adUnits - The ad units to be processed. + * @param {Object} s2sConfigs - The server-to-server configurations. + * @returns {Object} - An object containing arrays of bidder codes for client and server. + * @returns {Object} return.client - Array of bidder codes that should be routed to client adapters. + * @returns {Object} return.server - Array of bidder codes that should be routed to server adapters. */ export function _partitionBidders (adUnits, s2sConfigs, {getS2SBidders = getS2SBidderSet} = {}) { const serverBidders = getS2SBidders(s2sConfigs); @@ -255,6 +265,10 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a if (FEATURES.NATIVE) { decorateAdUnitsWithNativeParams(adUnits); } + adUnits + .map(adUnit => adUnit.code) + .filter(uniques) + .forEach(incrementAuctionsCounter); adUnits.forEach(au => { if (!isPlainObject(au.mediaTypes)) { @@ -420,7 +434,7 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request let uniqueServerRequests = serverBidderRequests.filter(serverBidRequest => serverBidRequest.uniquePbsTid === uniquePbsTid); if (s2sAdapter) { - let s2sBidRequest = {'ad_units': adUnitsS2SCopy, s2sConfig, ortb2Fragments}; + let s2sBidRequest = {'ad_units': adUnitsS2SCopy, s2sConfig, ortb2Fragments, requestBidsTimeout}; if (s2sBidRequest.ad_units.length) { let doneCbs = uniqueServerRequests.map(bidRequest => { bidRequest.start = timestamp(); @@ -551,11 +565,13 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { newAdapter = new bidAdapter.constructor(); newAdapter.setBidderCode(alias); } else { + const { useBaseGvlid = false } = options || {}; let spec = bidAdapter.getSpec(); - let gvlid = options && options.gvlid; - if (spec.gvlid != null && gvlid == null) { + const gvlid = useBaseGvlid ? spec.gvlid : options?.gvlid; + if (gvlid == null && spec.gvlid != null) { logWarn(`Alias '${alias}' will NOT re-use the GVL ID of the original adapter ('${spec.code}', gvlid: ${spec.gvlid}). Functionality that requires TCF consent may not work as expected.`) } + let skipPbsAliasing = options && options.skipPbsAliasing; newAdapter = newBidder(Object.assign({}, spec, { code: alias, gvlid, skipPbsAliasing })); _aliasRegistry[alias] = bidderCode; @@ -640,7 +656,7 @@ function invokeBidderMethod(bidder, method, spec, fn, ...params) { } function tryCallBidderMethod(bidder, method, param) { - if (param?.src !== S2S.SRC) { + if (param?.source !== S2S.SRC) { const target = getBidderMethod(bidder, method); if (target != null) { invokeBidderMethod(bidder, method, ...target, param); @@ -669,9 +685,17 @@ adapterManager.callBidWonBidder = function(bidder, bid, adUnits) { tryCallBidderMethod(bidder, 'onBidWon', bid); }; -adapterManager.callBidBillableBidder = function(bid) { - tryCallBidderMethod(bid.bidder, 'onBidBillable', bid); -}; +adapterManager.triggerBilling = (() => { + const BILLED = new WeakSet(); + return (bid) => { + if (!BILLED.has(bid)) { + BILLED.add(bid); + (parseEventTrackers(bid.eventtrackers)[EVENT_TYPE_IMPRESSION]?.[TRACKER_METHOD_IMG] || []) + .forEach((url) => internal.triggerPixel(url)); + tryCallBidderMethod(bid.bidder, 'onBidBillable', bid); + } + } +})(); adapterManager.callSetTargetingBidder = function(bidder, bid) { tryCallBidderMethod(bidder, 'onSetTargeting', bid); @@ -686,6 +710,10 @@ adapterManager.callBidderError = function(bidder, error, bidderRequest) { tryCallBidderMethod(bidder, 'onBidderError', param); }; +adapterManager.callAdRenderSucceededBidder = function (bidder, bid) { + tryCallBidderMethod(bidder, 'onAdRenderSucceeded', bid); +} + function resolveAlias(alias) { const seen = new Set(); while (_aliasRegistry.hasOwnProperty(alias) && !seen.has(alias)) { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 1d10c3161e5..ae188ea08e5 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -5,7 +5,7 @@ import {createBid} from '../bidfactory.js'; import {userSync} from '../userSync.js'; import {nativeBidIsValid} from '../native.js'; import {isValidVideoBid} from '../video.js'; -import { EVENTS, STATUS, REJECTION_REASON } from '../constants.js'; +import {EVENTS, REJECTION_REASON, STATUS} from '../constants.js'; import * as events from '../events.js'; import {includes} from '../polyfill.js'; import { @@ -13,9 +13,11 @@ import { isArray, isPlainObject, logError, - logWarn, memoize, + logWarn, + memoize, parseQueryStringParameters, - parseSizesInput, pick, + parseSizesInput, + pick, uniques } from '../utils.js'; import {hook} from '../hook.js'; @@ -190,7 +192,7 @@ export function registerBidder(spec) { } } -export function guardTids(bidderCode) { +export const guardTids = memoize(({bidderCode}) => { if (isActivityAllowed(ACTIVITY_TRANSMIT_TID, activityParams(MODULE_TYPE_BIDDER, bidderCode))) { return { bidRequest: (br) => br, @@ -228,7 +230,7 @@ export function guardTids(bidderCode) { } }) } -} +}); /** * Make a new bidder from the given spec. This is exported mainly for testing. @@ -246,7 +248,7 @@ export function newBidder(spec) { if (!Array.isArray(bidderRequest.bids)) { return; } - const tidGuard = guardTids(bidderRequest.bidderCode); + const tidGuard = guardTids(bidderRequest); const adUnitCodesHandled = {}; function addBidWithCode(adUnitCode, bid) { @@ -287,7 +289,7 @@ export function newBidder(spec) { } }); - processBidderRequests(spec, validBidRequests.map(tidGuard.bidRequest), tidGuard.bidderRequest(bidderRequest), ajax, configEnabledCallback, { + processBidderRequests(spec, validBidRequests, bidderRequest, ajax, configEnabledCallback, { onRequest: requestObject => events.emit(EVENTS.BEFORE_BIDDER_HTTP, bidderRequest, requestObject), onResponse: (resp) => { onTimelyResponse(spec.code); @@ -296,7 +298,7 @@ export function newBidder(spec) { onPaapi: (paapiConfig) => { const bidRequest = bidRequestMap[paapiConfig.bidId]; if (bidRequest) { - addComponentAuction(bidRequest, paapiConfig.config); + addPaapiConfig(bidRequest, paapiConfig); } else { logWarn('Received fledge auction configuration for an unknown bidId', paapiConfig); } @@ -308,7 +310,7 @@ export function newBidder(spec) { } adapterManager.callBidderError(spec.code, error, bidderRequest) events.emit(EVENTS.BIDDER_ERROR, { error, bidderRequest }); - logError(`Server call for ${spec.code} failed: ${errorMessage} ${error.status}. Continuing without bids.`); + logError(`Server call for ${spec.code} failed: ${errorMessage} ${error.status}. Continuing without bids.`, {bidRequests: validBidRequests}); }, onBid: (bid) => { const bidRequest = bidRequestMap[bid.requestId]; @@ -323,6 +325,8 @@ export function newBidder(spec) { bid.originalCpm = bid.cpm; bid.originalCurrency = bid.currency; bid.meta = bid.meta || Object.assign({}, bid[bidRequest.bidder]); + bid.deferBilling = bidRequest.deferBilling; + bid.deferRendering = bid.deferBilling && (bid.deferRendering ?? typeof spec.onBidBillable !== 'function'); const prebidBid = Object.assign(createBid(STATUS.GOOD, bidRequest), bid, pick(bidRequest, TIDS)); addBidWithCode(bidRequest.adUnitCode, prebidBid); } else { @@ -361,17 +365,7 @@ export function newBidder(spec) { } } -// Transition from 'fledge' to 'paapi' -// TODO: remove this in prebid 9 -const PAAPI_RESPONSE_PROPS = ['paapiAuctionConfigs', 'fledgeAuctionConfigs']; -const RESPONSE_PROPS = ['bids'].concat(PAAPI_RESPONSE_PROPS); -function getPaapiConfigs(adapterResponse) { - const [paapi, fledge] = PAAPI_RESPONSE_PROPS.map(prop => adapterResponse[prop]); - if (paapi != null && fledge != null) { - throw new Error(`Adapter response should use ${PAAPI_RESPONSE_PROPS[0]} over ${PAAPI_RESPONSE_PROPS[1]}, not both`); - } - return paapi ?? fledge; -} +const RESPONSE_PROPS = ['bids', 'paapi'] /** * Run a set of bid requests - that entails converting them to HTTP requests, sending @@ -388,11 +382,11 @@ function getPaapiConfigs(adapterResponse) { * @param onBid {function({})} invoked once for each bid in the response - with the bid as returned by interpretResponse * @param onCompletion {function()} invoked once when all bid requests have been processed */ -export const processBidderRequests = hook('sync', function (spec, bids, bidderRequest, ajax, wrapCallback, {onRequest, onResponse, onPaapi, onError, onBid, onCompletion}) { +export const processBidderRequests = hook('async', function (spec, bids, bidderRequest, ajax, wrapCallback, {onRequest, onResponse, onPaapi, onError, onBid, onCompletion}) { const metrics = adapterMetrics(bidderRequest); onCompletion = metrics.startTiming('total').stopBefore(onCompletion); - - let requests = metrics.measureTime('buildRequests', () => spec.buildRequests(bids, bidderRequest)); + const tidGuard = guardTids(bidderRequest); + let requests = metrics.measureTime('buildRequests', () => spec.buildRequests(bids.map(tidGuard.bidRequest), tidGuard.bidderRequest(bidderRequest))); if (!requests || requests.length === 0) { onCompletion(); @@ -437,12 +431,12 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe // adapters can reply with: // a single bid // an array of bids - // a BidderAuctionResponse object ({bids: [*], paapiAuctionConfigs: [*]}) + // a BidderAuctionResponse object let bids, paapiConfigs; if (response && !Object.keys(response).some(key => !RESPONSE_PROPS.includes(key))) { bids = response.bids; - paapiConfigs = getPaapiConfigs(response); + paapiConfigs = response.paapi; } else { bids = response; } @@ -531,10 +525,9 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe export const registerSyncInner = hook('async', function(spec, responses, gdprConsent, uspConsent, gppConsent) { const aliasSyncEnabled = config.getConfig('userSync.aliasSyncEnabled'); if (spec.getUserSyncs && (aliasSyncEnabled || !adapterManager.aliasRegistry[spec.code])) { - let filterConfig = config.getConfig('userSync.filterSettings'); let syncs = spec.getUserSyncs({ - iframeEnabled: !!(filterConfig && (filterConfig.iframe || filterConfig.all)), - pixelEnabled: !!(filterConfig && (filterConfig.image || filterConfig.all)), + iframeEnabled: userSync.canBidderRegisterSync('iframe', spec.code), + pixelEnabled: userSync.canBidderRegisterSync('image', spec.code), }, responses, gdprConsent, uspConsent, gppConsent); if (syncs) { if (!Array.isArray(syncs)) { @@ -548,8 +541,8 @@ export const registerSyncInner = hook('async', function(spec, responses, gdprCon } }, 'registerSyncs') -export const addComponentAuction = hook('sync', (request, fledgeAuctionConfig) => { -}, 'addComponentAuction'); +export const addPaapiConfig = hook('sync', (request, paapiConfig) => { +}, 'addPaapiConfig'); // check that the bid has a width and height set function validBidSize(adUnitCode, bid, {index = auctionManager.index} = {}) { @@ -559,6 +552,12 @@ function validBidSize(adUnitCode, bid, {index = auctionManager.index} = {}) { return true; } + if (bid.wratio != null && bid.hratio != null) { + bid.wratio = parseInt(bid.wratio, 10); + bid.hratio = parseInt(bid.hratio, 10); + return true; + } + const bidRequest = index.getBidRequest(bid); const mediaTypes = index.getMediaTypes(bid); @@ -619,6 +618,6 @@ export function isValid(adUnitCode, bid, {index = auctionManager.index} = {}) { return true; } -function adapterMetrics(bidderRequest) { +export function adapterMetrics(bidderRequest) { return useMetrics(bidderRequest.metrics).renameWith(n => [`adapter.client.${n}`, `adapters.client.${bidderRequest.bidderCode}.${n}`]) } diff --git a/src/adloader.js b/src/adloader.js index c2da2646320..2ea59d67440 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -1,51 +1,64 @@ -import {includes} from './polyfill.js'; -import { logError, logWarn, insertElement, setScriptAttributes } from './utils.js'; +import { LOAD_EXTERNAL_SCRIPT } from './activities/activities.js'; +import { activityParams } from './activities/activityParams.js'; +import { isActivityAllowed } from './activities/rules.js'; +import { includes } from './polyfill.js'; +import { insertElement, logError, logWarn, setScriptAttributes } from './utils.js'; const _requestCache = new WeakMap(); // The below list contains modules or vendors whom Prebid allows to load external JS. const _approvedLoadExternalJSList = [ + // Prebid maintained modules: 'debugging', - 'adloox', - 'criteo', 'outstream', + // RTD modules: + 'aaxBlockmeter', 'adagio', - 'spotx', + 'adloox', + 'akamaidap', + 'arcspan', + 'airgrid', 'browsi', 'brandmetrics', - 'justtag', - 'tncId', - 'akamaidap', - 'ftrackId', - 'inskin', + 'clean.io', + 'humansecurity', + 'confiant', + 'contxtful', 'hadron', + 'mediafilter', 'medianet', - 'improvedigital', 'azerionedge', - 'aaxBlockmeter', - 'confiant', - 'arcspan', - 'airgrid', - 'clean.io', 'a1Media', 'geoedge', - 'mediafilter', 'qortex', 'dynamicAdBoost', - 'contxtful', + '51Degrees', + 'symitridap', + 'wurfl', + 'nodalsAi', + 'anonymised', + 'optable', + // UserId Submodules + 'justtag', + 'tncId', + 'ftrackId', 'id5', - 'lucead', ]; /** * Loads external javascript. Can only be used if external JS is approved by Prebid. See https://github.com/prebid/prebid-js-external-js-template#policy * Each unique URL will be loaded at most 1 time. * @param {string} url the url to load + * @param {string} moduleType moduleType of the module requesting this resource * @param {string} moduleCode bidderCode or module code of the module requesting this resource * @param {function} [callback] callback function to be called after the script is loaded * @param {Document} [doc] the context document, in which the script will be loaded, defaults to loaded document * @param {object} attributes an object of attributes to be added to the script with setAttribute by [key] and [value]; Only the attributes passed in the first request of a url will be added. */ -export function loadExternalScript(url, moduleCode, callback, doc, attributes) { +export function loadExternalScript(url, moduleType, moduleCode, callback, doc, attributes) { + if (!isActivityAllowed(LOAD_EXTERNAL_SCRIPT, activityParams(moduleType, moduleCode))) { + return; + } + if (!moduleCode || !url) { logError('cannot load external script without url and moduleCode'); return; diff --git a/src/ajax.js b/src/ajax.js index ef4c2e4bcb4..913e753e2e3 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -47,10 +47,17 @@ export function toFetchRequest(url, data, options = {}) { if (options.withCredentials) { rqOpts.credentials = 'include'; } - if (options.browsingTopics && isSecureContext) { - // the Request constructor will throw an exception if the browser supports topics - // but we're not in a secure context - rqOpts.browsingTopics = true; + if (isSecureContext) { + ['browsingTopics', 'adAuctionHeaders'].forEach(opt => { + // the Request constructor will throw an exception if the browser supports topics/fledge + // but we're not in a secure context + if (options[opt]) { + rqOpts[opt] = true; + } + }) + } + if (options.keepalive) { + rqOpts.keepalive = true; } return dep.makeRequest(url, rqOpts); } @@ -101,6 +108,7 @@ function toXHR({status, statusText = '', headers, url}, responseText) { return xml; } return { + // eslint-disable-next-line no-restricted-globals readyState: XMLHttpRequest.DONE, status, statusText, @@ -127,7 +135,7 @@ export function attachCallbacks(fetchPm, callback) { success: typeof callback === 'function' ? callback : () => null, error: (e, x) => logError('Network error', e, x) }; - fetchPm.then(response => response.text().then((responseText) => [response, responseText])) + return fetchPm.then(response => response.text().then((responseText) => [response, responseText])) .then(([response, responseText]) => { const xhr = toXHR(response, responseText); response.ok || response.status === 304 ? success(responseText, xhr) : error(response.statusText, xhr); @@ -144,5 +152,19 @@ export function ajaxBuilder(timeout = 3000, {request, done} = {}) { }; } +/** + * simple wrapper around sendBeacon such that invocations of navigator.sendBeacon can be centrally maintained. + * verifies that the navigator and sendBeacon are defined for maximum compatibility + * @param {string} url The URL that will receive the data. Can be relative or absolute. + * @param {*} data An ArrayBuffer, a TypedArray, a DataView, a Blob, a string literal or object, a FormData or a URLSearchParams object containing the data to send. + * @returns {boolean} true if the user agent successfully queued the data for transfer. Otherwise, it returns false. + */ +export function sendBeacon(url, data) { + if (!window.navigator || !window.navigator.sendBeacon) { + return false; + } + return window.navigator.sendBeacon(url, data); +} + export const ajax = ajaxBuilder(); export const fetch = fetcherFactory(); diff --git a/src/auction.js b/src/auction.js index 26845936797..52847206500 100644 --- a/src/auction.js +++ b/src/auction.js @@ -66,10 +66,7 @@ */ import { - callBurl, - deepAccess, generateUUID, - getValue, isEmpty, isEmptyStr, isFn, @@ -82,11 +79,11 @@ import { } from './utils.js'; import {getPriceBucketString} from './cpmBucketManager.js'; import {getNativeTargeting, isNativeResponse, setNativeResponseProperties} from './native.js'; -import {getCacheUrl, store} from './videoCache.js'; +import {batchAndStore, storeLocally} from './videoCache.js'; import {Renderer} from './Renderer.js'; import {config} from './config.js'; import {userSync} from './userSync.js'; -import {hook} from './hook.js'; +import {hook, ignoreCallbackArg} from './hook.js'; import {find, includes} from './polyfill.js'; import {OUTSTREAM} from './video.js'; import {VIDEO} from './mediaTypes.js'; @@ -94,11 +91,13 @@ import {auctionManager} from './auctionManager.js'; import {bidderSettings} from './bidderSettings.js'; import * as events from './events.js'; import adapterManager from './adapterManager.js'; -import { EVENTS, GRANULARITY_OPTIONS, JSON_MAPPING, S2S, TARGETING_KEYS } from './constants.js'; -import {defer, GreedyPromise} from './utils/promise.js'; +import {EVENTS, GRANULARITY_OPTIONS, JSON_MAPPING, REJECTION_REASON, S2S, TARGETING_KEYS} from './constants.js'; +import {defer, PbPromise} from './utils/promise.js'; import {useMetrics} from './utils/perfMetrics.js'; import {adjustCpm} from './utils/cpm.js'; import {getGlobal} from './prebidGlobal.js'; +import {ttlCollection} from './utils/ttlCollection.js'; +import {getMinBidCacheTTL, onMinBidCacheTTLChange} from './bidTTL.js'; const { syncUsers } = userSync; @@ -150,10 +149,14 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a const _timeout = cbTimeout; const _timelyRequests = new Set(); const done = defer(); + const requestsDone = defer(); let _bidsRejected = []; let _callback = callback; let _bidderRequests = []; - let _bidsReceived = []; + let _bidsReceived = ttlCollection({ + startTime: (bid) => bid.responseTimestamp, + ttl: (bid) => getMinBidCacheTTL() == null ? null : Math.max(getMinBidCacheTTL(), bid.ttl) * 1000 + }); let _noBids = []; let _winningBids = []; let _auctionStart; @@ -162,8 +165,10 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a let _auctionStatus; let _nonBids = []; + onMinBidCacheTTLChange(() => _bidsReceived.refresh()); + function addBidRequests(bidderRequests) { _bidderRequests = _bidderRequests.concat(bidderRequests); } - function addBidReceived(bidsReceived) { _bidsReceived = _bidsReceived.concat(bidsReceived); } + function addBidReceived(bid) { _bidsReceived.add(bid); } function addBidRejected(bidsRejected) { _bidsRejected = _bidsRejected.concat(bidsRejected); } function addNoBid(noBid) { _noBids = _noBids.concat(noBid); } function addNonBids(seatnonbids) { _nonBids = _nonBids.concat(seatnonbids); } @@ -179,7 +184,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a labels: _labels, bidderRequests: _bidderRequests, noBids: _noBids, - bidsReceived: _bidsReceived, + bidsReceived: _bidsReceived.toArray(), bidsRejected: _bidsRejected, winningBids: _winningBids, timeout: _timeout, @@ -219,7 +224,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a bidsBackCallback(_adUnits, function () { try { if (_callback != null) { - const bids = _bidsReceived + const bids = _bidsReceived.toArray() .filter(bid => _adUnitCodes.includes(bid.adUnitCode)) .reduce(groupByPlacement, {}); _callback.apply(pbjsInstance, [bids, timedOut, _auctionId]); @@ -246,7 +251,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a function auctionDone() { config.resetBidder(); // when all bidders have called done callback atleast once it means auction is complete - logInfo(`Bids Received for Auction with id: ${_auctionId}`, _bidsReceived); + logInfo(`Bids Received for Auction with id: ${_auctionId}`, _bidsReceived.toArray()); _auctionStatus = AUCTION_COMPLETED; executeCallback(false); } @@ -320,6 +325,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } } }, _timeout, onTimelyResponse, ortb2Fragments); + requestsDone.resolve(); } }; @@ -370,11 +376,11 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } function addWinningBid(winningBid) { - const winningAd = adUnits.find(adUnit => adUnit.adUnitId === winningBid.adUnitId); _winningBids = _winningBids.concat(winningBid); - callBurl(winningBid); adapterManager.callBidWonBidder(winningBid.adapterCode || winningBid.bidder, winningBid, adUnits); - if (winningAd && !winningAd.deferBilling) adapterManager.callBidBillableBidder(winningBid); + if (!winningBid.deferBilling) { + adapterManager.triggerBilling(winningBid) + } } function setBidTargeting(bid) { @@ -403,12 +409,14 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a getAdUnits: () => _adUnits, getAdUnitCodes: () => _adUnitCodes, getBidRequests: () => _bidderRequests, - getBidsReceived: () => _bidsReceived, + getBidsReceived: () => _bidsReceived.toArray(), getNoBids: () => _noBids, getNonBids: () => _nonBids, getFPD: () => ortb2Fragments, getMetrics: () => metrics, - end: done.promise + end: done.promise, + requestsDone: requestsDone.promise, + getProperties }; } @@ -420,9 +428,13 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a * @param bid * @param {function(String): void} reject a function that, when called, rejects `bid` with the given reason. */ -export const addBidResponse = hook('sync', function(adUnitCode, bid, reject) { - this.dispatch.call(null, adUnitCode, bid); -}, 'addBidResponse'); +export const addBidResponse = ignoreCallbackArg(hook('async', function(adUnitCode, bid, reject) { + if (!isValidPrice(bid)) { + reject(REJECTION_REASON.PRICE_TOO_HIGH) + } else { + this.dispatch.call(null, adUnitCode, bid); + } +}, 'addBidResponse')); /** * Delay hook for adapter responses. @@ -535,7 +547,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM return addBid; })(), adapterDone: function () { - responsesReady(GreedyPromise.resolve()).finally(() => adapterDone.call(this)); + responsesReady(PbPromise.resolve()).finally(() => adapterDone.call(this)); } } } @@ -553,16 +565,23 @@ export function addBidToAuction(auctionInstance, bidResponse) { function tryAddVideoBid(auctionInstance, bidResponse, afterBidAdded, {index = auctionManager.index} = {}) { let addBid = true; - const videoMediaType = deepAccess( - index.getMediaTypes({ - requestId: bidResponse.originalRequestId || bidResponse.requestId, - adUnitId: bidResponse.adUnitId - }), 'video'); - const context = videoMediaType && deepAccess(videoMediaType, 'context'); - const useCacheKey = videoMediaType && deepAccess(videoMediaType, 'useCacheKey'); - - if (config.getConfig('cache.url') && (useCacheKey || context !== OUTSTREAM)) { - if (!bidResponse.videoCacheKey || config.getConfig('cache.ignoreBidderCacheKey')) { + const videoMediaType = index.getMediaTypes({ + requestId: bidResponse.originalRequestId || bidResponse.requestId, + adUnitId: bidResponse.adUnitId + })?.video; + const context = videoMediaType && videoMediaType?.context; + const useCacheKey = videoMediaType && videoMediaType?.useCacheKey; + const { + useLocal, + url: cacheUrl, + ignoreBidderCacheKey + } = config.getConfig('cache') || {}; + + if (useLocal) { + // stores video bid vast as local blob in the browser + storeLocally(bidResponse); + } else if (cacheUrl && (useCacheKey || context !== OUTSTREAM)) { + if (!bidResponse.videoCacheKey || ignoreBidderCacheKey) { addBid = false; callPrebidCache(auctionInstance, bidResponse, afterBidAdded, videoMediaType); } else if (!bidResponse.vastUrl) { @@ -570,74 +589,17 @@ function tryAddVideoBid(auctionInstance, bidResponse, afterBidAdded, {index = au addBid = false; } } + if (addBid) { addBidToAuction(auctionInstance, bidResponse); afterBidAdded(); } } -const _storeInCache = (batch) => { - store(batch.map(entry => entry.bidResponse), function (error, cacheIds) { - cacheIds.forEach((cacheId, i) => { - const { auctionInstance, bidResponse, afterBidAdded } = batch[i]; - if (error) { - logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); - } else { - if (cacheId.uuid === '') { - logWarn(`Supplied video cache key was already in use by Prebid Cache; caching attempt was rejected. Video bid must be discarded.`); - } else { - bidResponse.videoCacheKey = cacheId.uuid; - if (!bidResponse.vastUrl) { - bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); - } - addBidToAuction(auctionInstance, bidResponse); - afterBidAdded(); - } - } - }); - }); -}; - -const storeInCache = FEATURES.VIDEO ? _storeInCache : () => {}; - -let batchSize, batchTimeout; -config.getConfig('cache', (cacheConfig) => { - batchSize = typeof cacheConfig.cache.batchSize === 'number' && cacheConfig.cache.batchSize > 0 - ? cacheConfig.cache.batchSize - : 1; - batchTimeout = typeof cacheConfig.cache.batchTimeout === 'number' && cacheConfig.cache.batchTimeout > 0 - ? cacheConfig.cache.batchTimeout - : 0; -}); - -export const batchingCache = (timeout = setTimeout, cache = storeInCache) => { - let batches = [[]]; - let debouncing = false; - const noTimeout = cb => cb(); - - return function(auctionInstance, bidResponse, afterBidAdded) { - const batchFunc = batchTimeout > 0 ? timeout : noTimeout; - if (batches[batches.length - 1].length >= batchSize) { - batches.push([]); - } - - batches[batches.length - 1].push({auctionInstance, bidResponse, afterBidAdded}); - - if (!debouncing) { - debouncing = true; - batchFunc(() => { - batches.forEach(cache); - batches = [[]]; - debouncing = false; - }, batchTimeout); - } - } -}; - -const batchAndStore = batchingCache(); - export const callPrebidCache = hook('async', function(auctionInstance, bidResponse, afterBidAdded, videoMediaType) { - batchAndStore(auctionInstance, bidResponse, afterBidAdded); + if (FEATURES.VIDEO) { + batchAndStore(auctionInstance, bidResponse, afterBidAdded); + } }, 'callPrebidCache'); /** @@ -674,8 +636,11 @@ function getPreparedBidForAuction(bid, {index = auctionManager.index} = {}) { // but others to not be set yet (like priceStrings). See #1372 and #1389. events.emit(EVENTS.BID_ADJUSTMENT, bid); + const adUnit = index.getAdUnit(bid); + bid.instl = adUnit?.ortb2Imp?.instl === 1; + // a publisher-defined renderer can be used to render bids - const bidRenderer = index.getBidRequest(bid)?.renderer || index.getAdUnit(bid).renderer; + const bidRenderer = index.getBidRequest(bid)?.renderer || adUnit.renderer; // a publisher can also define a renderer for a mediaType const bidObjectMediaType = bid.mediaType; @@ -687,15 +652,15 @@ function getPreparedBidForAuction(bid, {index = auctionManager.index} = {}) { var renderer = null; // the renderer for the mediaType takes precendence - if (mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render && !(mediaTypeRenderer.backupOnly === true && bid.renderer)) { + if (mediaTypeRenderer && mediaTypeRenderer.render && !(mediaTypeRenderer.backupOnly === true && bid.renderer)) { renderer = mediaTypeRenderer; - } else if (bidRenderer && bidRenderer.url && bidRenderer.render && !(bidRenderer.backupOnly === true && bid.renderer)) { + } else if (bidRenderer && bidRenderer.render && !(bidRenderer.backupOnly === true && bid.renderer)) { renderer = bidRenderer; } if (renderer) { // be aware, an adapter could already have installed the bidder, in which case this overwrite's the existing adapter - bid.renderer = Renderer.install({ url: renderer.url, config: renderer.options });// rename options to config, to make it consistent? + bid.renderer = Renderer.install({ url: renderer.url, config: renderer.options, renderNow: renderer.url == null });// rename options to config, to make it consistent? bid.renderer.setRender(renderer.render); } @@ -736,7 +701,7 @@ function setupBidTargeting(bidObject) { export function getMediaTypeGranularity(mediaType, mediaTypes, mediaTypePriceGranularity) { if (mediaType && mediaTypePriceGranularity) { if (FEATURES.VIDEO && mediaType === VIDEO) { - const context = deepAccess(mediaTypes, `${VIDEO}.context`, 'instream'); + const context = mediaTypes?.[VIDEO]?.context ?? 'instream'; if (mediaTypePriceGranularity[`${VIDEO}-${context}`]) { return mediaTypePriceGranularity[`${VIDEO}-${context}`]; } @@ -809,7 +774,7 @@ export const getAdvertiserDomain = () => { */ export const getDSP = () => { return (bid) => { - return (bid.meta && (bid.meta.networkId || bid.meta.networkName)) ? deepAccess(bid, 'meta.networkName') || deepAccess(bid, 'meta.networkId') : ''; + return (bid.meta && (bid.meta.networkId || bid.meta.networkName)) ? bid?.meta?.networkName || bid?.meta?.networkId : ''; } } @@ -832,7 +797,7 @@ function createKeyVal(key, value) { return value(bidResponse, bidReq); } : function (bidResponse) { - return getValue(bidResponse, value); + return bidResponse[value]; } }; } @@ -881,8 +846,7 @@ export function getStandardBidderSettings(mediaType, bidderCode) { if (typeof find(adserverTargeting, targetingKeyVal => targetingKeyVal.key === TARGETING_KEYS.CACHE_HOST) === 'undefined') { adserverTargeting.push(createKeyVal(TARGETING_KEYS.CACHE_HOST, function(bidResponse) { - return deepAccess(bidResponse, `adserverTargeting.${TARGETING_KEYS.CACHE_HOST}`) - ? bidResponse.adserverTargeting[TARGETING_KEYS.CACHE_HOST] : urlInfo.hostname; + return bidResponse?.adserverTargeting?.[TARGETING_KEYS.CACHE_HOST] || urlInfo.hostname; })); } } @@ -974,3 +938,17 @@ function groupByPlacement(bidsByPlacement, bid) { bidsByPlacement[bid.adUnitCode].bids.push(bid); return bidsByPlacement; } + +/** + * isValidPrice is price validation function + * which checks if price from bid response + * is not higher than top limit set in config + * @type {Function} + * @param bid + * @returns {boolean} + */ +function isValidPrice(bid) { + const maxBidValue = config.getConfig('maxBid'); + if (!maxBidValue || !bid.cpm) return true; + return maxBidValue >= Number(bid.cpm); +} diff --git a/src/auctionIndex.js b/src/auctionIndex.js index afae2089518..d0b8355352a 100644 --- a/src/auctionIndex.js +++ b/src/auctionIndex.js @@ -65,6 +65,9 @@ export function AuctionIndex(getAuctions) { .flatMap(ber => ber.bids) .find(br => br && br.bidId === requestId); } + }, + getOrtb2(bid) { + return this.getBidderRequest(bid)?.ortb2 || this.getAuction(bid)?.getFPD()?.global?.ortb2 } }); } diff --git a/src/auctionManager.js b/src/auctionManager.js index a1ab1a859da..ab2947b96b4 100644 --- a/src/auctionManager.js +++ b/src/auctionManager.js @@ -26,10 +26,7 @@ import {AuctionIndex} from './auctionIndex.js'; import { BID_STATUS, JSON_MAPPING } from './constants.js'; import {useMetrics} from './utils/perfMetrics.js'; import {ttlCollection} from './utils/ttlCollection.js'; -import {getTTL, onTTLBufferChange} from './bidTTL.js'; -import {config} from './config.js'; - -const CACHE_TTL_SETTING = 'minBidCacheTTL'; +import {getMinBidCacheTTL, onMinBidCacheTTLChange} from './bidTTL.js'; /** * Creates new instance of auctionManager. There will only be one instance of auctionManager but @@ -38,27 +35,14 @@ const CACHE_TTL_SETTING = 'minBidCacheTTL'; * @returns {AuctionManager} auctionManagerInstance */ export function newAuctionManager() { - let minCacheTTL = null; - const _auctions = ttlCollection({ startTime: (au) => au.end.then(() => au.getAuctionEnd()), - ttl: (au) => minCacheTTL == null ? null : au.end.then(() => { - return Math.max(minCacheTTL, ...au.getBidsReceived().map(getTTL)) * 1000 + ttl: (au) => getMinBidCacheTTL() == null ? null : au.end.then(() => { + return Math.max(getMinBidCacheTTL(), ...au.getBidsReceived().map(bid => bid.ttl)) * 1000 }), }); - onTTLBufferChange(() => { - if (minCacheTTL != null) _auctions.refresh(); - }) - - config.getConfig(CACHE_TTL_SETTING, (cfg) => { - const prev = minCacheTTL; - minCacheTTL = cfg?.[CACHE_TTL_SETTING]; - minCacheTTL = typeof minCacheTTL === 'number' ? minCacheTTL : null; - if (prev !== minCacheTTL) { - _auctions.refresh(); - } - }) + onMinBidCacheTTLChange(() => _auctions.refresh()); const auctionManager = { onExpiry: _auctions.onExpiry @@ -73,11 +57,10 @@ export function newAuctionManager() { auctionManager.addWinningBid = function(bid) { const metrics = useMetrics(bid.metrics); metrics.checkpoint('bidWon'); - metrics.timeBetween('auctionEnd', 'bidWon', 'render.pending'); - metrics.timeBetween('requestBids', 'bidWon', 'render.e2e'); + metrics.timeBetween('auctionEnd', 'bidWon', 'adserver.pending'); + metrics.timeBetween('requestBids', 'bidWon', 'adserver.e2e'); const auction = getAuction(bid.auctionId); if (auction) { - bid.status = BID_STATUS.RENDERED; auction.addWinningBid(bid); } else { logWarn(`Auction not found when adding winning bid`); diff --git a/src/banner.js b/src/banner.js new file mode 100644 index 00000000000..25da06b6669 --- /dev/null +++ b/src/banner.js @@ -0,0 +1,21 @@ +import { isArrayOfNums, isInteger, isStr } from './utils.js'; + +/** + * List of OpenRTB 2.x banner object properties with simple validators. + * Not included: `ext` + * reference: https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md + */ +export const ORTB_BANNER_PARAMS = new Map([ + [ 'format', value => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'object') ], + [ 'w', isInteger ], + [ 'h', isInteger ], + [ 'btype', isArrayOfNums ], + [ 'battr', isArrayOfNums ], + [ 'pos', isInteger ], + [ 'mimes', value => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string') ], + [ 'topframe', value => [1, 0].includes(value) ], + [ 'expdir', isArrayOfNums ], + [ 'api', isArrayOfNums ], + [ 'id', isStr ], + [ 'vcm', value => [1, 0].includes(value) ] +]); diff --git a/src/bidTTL.js b/src/bidTTL.js index 55ba0c026b0..d685c4aa4f0 100644 --- a/src/bidTTL.js +++ b/src/bidTTL.js @@ -1,25 +1,35 @@ import {config} from './config.js'; import {logError} from './utils.js'; +const CACHE_TTL_SETTING = 'minBidCacheTTL'; let TTL_BUFFER = 1; - +let minCacheTTL = null; const listeners = []; config.getConfig('ttlBuffer', (cfg) => { if (typeof cfg.ttlBuffer === 'number') { - const prev = TTL_BUFFER; TTL_BUFFER = cfg.ttlBuffer; - if (prev !== TTL_BUFFER) { - listeners.forEach(l => l(TTL_BUFFER)) - } } else { logError('Invalid value for ttlBuffer', cfg.ttlBuffer); } }) -export function getTTL(bid) { +export function getBufferedTTL(bid) { return bid.ttl - (bid.hasOwnProperty('ttlBuffer') ? bid.ttlBuffer : TTL_BUFFER); } -export function onTTLBufferChange(listener) { +export function getMinBidCacheTTL() { + return minCacheTTL; +} + +config.getConfig(CACHE_TTL_SETTING, (cfg) => { + const prev = minCacheTTL; + minCacheTTL = cfg?.[CACHE_TTL_SETTING]; + minCacheTTL = typeof minCacheTTL === 'number' ? minCacheTTL : null; + if (prev !== minCacheTTL) { + listeners.forEach(l => l(minCacheTTL)) + } +}) + +export function onMinBidCacheTTLChange(listener) { listeners.push(listener); } diff --git a/src/config.js b/src/config.js index f4dd0de9612..b263e77c233 100644 --- a/src/config.js +++ b/src/config.js @@ -36,8 +36,9 @@ const DEFAULT_DISABLE_AJAX_TIMEOUT = false; const DEFAULT_BID_CACHE = false; const DEFAULT_DEVICE_ACCESS = true; const DEFAULT_MAX_NESTED_IFRAMES = 10; +const DEFAULT_MAXBID_VALUE = 5000 -const DEFAULT_TIMEOUTBUFFER = 400; +const DEFAULT_IFRAMES_CONFIG = {}; export const RANDOM = 'random'; const FIXED = 'fixed'; @@ -59,89 +60,159 @@ const GRANULARITY_OPTIONS = { const ALL_TOPICS = '*'; -export function newConfig() { - let listeners = []; - let defaults; - let config; - let bidderConfig; - let currBidder = null; - - function resetConfig() { - defaults = {}; - - function getProp(name) { - return props[name].val; - } +function attachProperties(config, useDefaultValues = true) { + const values = useDefaultValues ? { + priceGranularity: GRANULARITY_OPTIONS.MEDIUM, + customPriceBucket: {}, + mediaTypePriceGranularity: {}, + bidderSequence: DEFAULT_BIDDER_SEQUENCE, + auctionOptions: {} + } : {} + + function getProp(name) { + return values[name]; + } - function setProp(name, val) { - props[name].val = val; + function setProp(name, val) { + if (!values.hasOwnProperty(name)) { + Object.defineProperty(config, name, {enumerable: true}); } + values[name] = val; + } - const props = { - publisherDomain: { - set(val) { - if (val != null) { - logWarn('publisherDomain is deprecated and has no effect since v7 - use pageUrl instead') + const props = { + publisherDomain: { + set(val) { + if (val != null) { + logWarn('publisherDomain is deprecated and has no effect since v7 - use pageUrl instead') + } + setProp('publisherDomain', val); + } + }, + priceGranularity: { + set(val) { + if (validatePriceGranularity(val)) { + if (typeof val === 'string') { + setProp('priceGranularity', (hasGranularity(val)) ? val : GRANULARITY_OPTIONS.MEDIUM); + } else if (isPlainObject(val)) { + setProp('customPriceBucket', val); + setProp('priceGranularity', GRANULARITY_OPTIONS.CUSTOM) + logMessage('Using custom price granularity'); } - setProp('publisherDomain', val); } - }, - priceGranularity: { - val: GRANULARITY_OPTIONS.MEDIUM, - set(val) { - if (validatePriceGranularity(val)) { + } + }, + customPriceBucket: {}, + mediaTypePriceGranularity: { + set(val) { + val != null && setProp('mediaTypePriceGranularity', Object.keys(val).reduce((aggregate, item) => { + if (validatePriceGranularity(val[item])) { if (typeof val === 'string') { - setProp('priceGranularity', (hasGranularity(val)) ? val : GRANULARITY_OPTIONS.MEDIUM); + aggregate[item] = (hasGranularity(val[item])) ? val[item] : getProp('priceGranularity'); } else if (isPlainObject(val)) { - setProp('customPriceBucket', val); - setProp('priceGranularity', GRANULARITY_OPTIONS.CUSTOM) - logMessage('Using custom price granularity'); + aggregate[item] = val[item]; + logMessage(`Using custom price granularity for ${item}`); } + } else { + logWarn(`Invalid price granularity for media type: ${item}`); } + return aggregate; + }, {})); + } + }, + bidderSequence: { + set(val) { + if (VALID_ORDERS[val]) { + setProp('bidderSequence', val); + } else { + logWarn(`Invalid order: ${val}. Bidder Sequence was not set.`); } - }, - customPriceBucket: { - val: {}, - set() {} - }, - mediaTypePriceGranularity: { - val: {}, - set(val) { - val != null && setProp('mediaTypePriceGranularity', Object.keys(val).reduce((aggregate, item) => { - if (validatePriceGranularity(val[item])) { - if (typeof val === 'string') { - aggregate[item] = (hasGranularity(val[item])) ? val[item] : getProp('priceGranularity'); - } else if (isPlainObject(val)) { - aggregate[item] = val[item]; - logMessage(`Using custom price granularity for ${item}`); - } - } else { - logWarn(`Invalid price granularity for media type: ${item}`); - } - return aggregate; - }, {})); + } + }, + auctionOptions: { + set(val) { + if (validateauctionOptions(val)) { + setProp('auctionOptions', val); } - }, - bidderSequence: { - val: DEFAULT_BIDDER_SEQUENCE, - set(val) { - if (VALID_ORDERS[val]) { - setProp('bidderSequence', val); - } else { - logWarn(`Invalid order: ${val}. Bidder Sequence was not set.`); - } + } + } + } + + Object.defineProperties(config, Object.fromEntries( + Object.entries(props) + .map(([k, def]) => [k, Object.assign({ + get: getProp.bind(null, k), + set: setProp.bind(null, k), + enumerable: values.hasOwnProperty(k), + configurable: !values.hasOwnProperty(k) + }, def)]) + )); + + return config; + + function hasGranularity(val) { + return find(Object.keys(GRANULARITY_OPTIONS), option => val === GRANULARITY_OPTIONS[option]); + } + + function validatePriceGranularity(val) { + if (!val) { + logError('Prebid Error: no value passed to `setPriceGranularity()`'); + return false; + } + if (typeof val === 'string') { + if (!hasGranularity(val)) { + logWarn('Prebid Warning: setPriceGranularity was called with invalid setting, using `medium` as default.'); + } + } else if (isPlainObject(val)) { + if (!isValidPriceConfig(val)) { + logError('Invalid custom price value passed to `setPriceGranularity()`'); + return false; + } + } + return true; + } + + function validateauctionOptions(val) { + if (!isPlainObject(val)) { + logWarn('Auction Options must be an object') + return false + } + + for (let k of Object.keys(val)) { + if (k !== 'secondaryBidders' && k !== 'suppressStaleRender' && k !== 'suppressExpiredRender') { + logWarn(`Auction Options given an incorrect param: ${k}`) + return false + } + if (k === 'secondaryBidders') { + if (!isArray(val[k])) { + logWarn(`Auction Options ${k} must be of type Array`); + return false + } else if (!val[k].every(isStr)) { + logWarn(`Auction Options ${k} must be only string`); + return false } - }, - auctionOptions: { - val: {}, - set(val) { - if (validateauctionOptions(val)) { - setProp('auctionOptions', val); - } + } else if (k === 'suppressStaleRender' || k === 'suppressExpiredRender') { + if (!isBoolean(val[k])) { + logWarn(`Auction Options ${k} must be of type boolean`); + return false; } } } - let newConfig = { + return true; + } +} + +export function newConfig() { + let listeners = []; + let defaults; + let config; + let bidderConfig; + let currBidder = null; + + function resetConfig() { + defaults = {}; + + let newConfig = attachProperties({ // `debug` is equivalent to legacy `pbjs.logging` property debug: DEFAULT_DEBUG, bidderTimeout: DEFAULT_BIDDER_TIMEOUT, @@ -154,22 +225,17 @@ export function newConfig() { */ deviceAccess: DEFAULT_DEVICE_ACCESS, - // timeout buffer to adjust for bidder CDN latency - timeoutBuffer: DEFAULT_TIMEOUTBUFFER, disableAjaxTimeout: DEFAULT_DISABLE_AJAX_TIMEOUT, // default max nested iframes for referer detection maxNestedIframes: DEFAULT_MAX_NESTED_IFRAMES, - }; - Object.defineProperties(newConfig, - Object.fromEntries(Object.entries(props) - .map(([k, def]) => [k, Object.assign({ - get: getProp.bind(null, k), - set: setProp.bind(null, k), - enumerable: true, - }, def)])) - ); + // default max bid + maxBid: DEFAULT_MAXBID_VALUE, + userSync: { + topics: DEFAULT_IFRAMES_CONFIG + } + }); if (config) { callSubscribers( @@ -185,57 +251,6 @@ export function newConfig() { config = newConfig; bidderConfig = {}; - - function hasGranularity(val) { - return find(Object.keys(GRANULARITY_OPTIONS), option => val === GRANULARITY_OPTIONS[option]); - } - - function validatePriceGranularity(val) { - if (!val) { - logError('Prebid Error: no value passed to `setPriceGranularity()`'); - return false; - } - if (typeof val === 'string') { - if (!hasGranularity(val)) { - logWarn('Prebid Warning: setPriceGranularity was called with invalid setting, using `medium` as default.'); - } - } else if (isPlainObject(val)) { - if (!isValidPriceConfig(val)) { - logError('Invalid custom price value passed to `setPriceGranularity()`'); - return false; - } - } - return true; - } - - function validateauctionOptions(val) { - if (!isPlainObject(val)) { - logWarn('Auction Options must be an object') - return false - } - - for (let k of Object.keys(val)) { - if (k !== 'secondaryBidders' && k !== 'suppressStaleRender') { - logWarn(`Auction Options given an incorrect param: ${k}`) - return false - } - if (k === 'secondaryBidders') { - if (!isArray(val[k])) { - logWarn(`Auction Options ${k} must be of type Array`); - return false - } else if (!val[k].every(isStr)) { - logWarn(`Auction Options ${k} must be only string`); - return false - } - } else if (k === 'suppressStaleRender') { - if (!isBoolean(val[k])) { - logWarn(`Auction Options ${k} must be of type boolean`); - return false; - } - } - } - return true; - } } /** @@ -411,7 +426,6 @@ export function newConfig() { if (topic === ALL_TOPICS) { callback(getConfig()); } else { - // eslint-disable-next-line standard/no-callback-literal callback({[topic]: getConfig(topic)}); } } @@ -446,14 +460,14 @@ export function newConfig() { check(config); config.bidders.forEach(bidder => { if (!bidderConfig[bidder]) { - bidderConfig[bidder] = {}; + bidderConfig[bidder] = attachProperties({}, false); } Object.keys(config.config).forEach(topic => { let option = config.config[topic]; - - if (isPlainObject(option)) { + const currentConfig = bidderConfig[bidder][topic]; + if (isPlainObject(option) && (currentConfig == null || isPlainObject(currentConfig))) { const func = mergeFlag ? mergeDeep : Object.assign; - bidderConfig[bidder][topic] = func({}, bidderConfig[bidder][topic] || {}, option); + bidderConfig[bidder][topic] = func({}, currentConfig || {}, option); } else { bidderConfig[bidder][topic] = option; } diff --git a/src/consentHandler.js b/src/consentHandler.js index 5b5d8b805cd..69abc660f93 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -1,5 +1,5 @@ import {cyrb53Hash, isStr, timestamp} from './utils.js'; -import {defer, GreedyPromise} from './utils/promise.js'; +import {defer, PbPromise} from './utils/promise.js'; import {config} from './config.js'; /** @@ -10,14 +10,6 @@ import {config} from './config.js'; */ export const VENDORLESS_GVLID = Object.freeze({}); -/** - * Placeholder gvlid for when device.ext.cdep is present (Privacy Sandbox cookie deprecation label). When this value is used as gvlid, the gdpr - * enforcement module will look to see that publisher consent was given. - * - * see https://github.com/prebid/Prebid.js/issues/10516 - */ -export const FIRST_PARTY_GVLID = Object.freeze({}); - export class ConsentHandler { #enabled; #data; @@ -76,7 +68,7 @@ export class ConsentHandler { */ get promise() { if (this.#ready) { - return GreedyPromise.resolve(this.#data); + return PbPromise.resolve(this.#data); } if (!this.#enabled) { this.#resolve(null); @@ -108,7 +100,6 @@ class UspConsentHandler extends ConsentHandler { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { return { - usp: consentData, generatedAt: this.generatedTime }; } @@ -163,14 +154,19 @@ export function gvlidRegistry() { } } }, + /** + * @typedef {Object} GvlIdResult + * @property {Object.} modules - A map from module type to that module's GVL ID. + * @property {number} [gvlid] - The single GVL ID for this family of modules (only defined if all modules with this name declared the same ID). + */ + /** * Get a module's GVL ID(s). * - * @param {string} moduleName - * @return {{modules: {[moduleType]: number}, gvlid?: number}} an object where: + * @param {string} moduleName - The name of the module. + * @return {GvlIdResult} An object where: * `modules` is a map from module type to that module's GVL ID; - * `gvlid` is the single GVL ID for this family of modules (only defined - * if all modules with this name declared the same ID). + * `gvlid` is the single GVL ID for this family of modules (only defined if all modules with this name declare the same ID). */ get(moduleName) { const result = {modules: registry[moduleName] || {}}; @@ -195,7 +191,7 @@ export const coppaDataHandler = (() => { getConsentMeta: getCoppa, reset() {}, get promise() { - return GreedyPromise.resolve(getCoppa()) + return PbPromise.resolve(getCoppa()) }, get hash() { return getCoppa() ? '1' : '0' @@ -222,7 +218,7 @@ export function multiHandler(handlers = ALL_HANDLERS) { return Object.assign( { get promise() { - return GreedyPromise.all(handlers.map(([name, handler]) => handler.promise.then(val => [name, val]))) + return PbPromise.all(handlers.map(([name, handler]) => handler.promise.then(val => [name, val]))) .then(entries => Object.fromEntries(entries)); }, get hash() { diff --git a/src/constants.js b/src/constants.js index b40b7ddb9b0..07a806e125f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -40,8 +40,17 @@ export const EVENTS = { AUCTION_DEBUG: 'auctionDebug', BID_VIEWABLE: 'bidViewable', STALE_RENDER: 'staleRender', + EXPIRED_RENDER: 'expiredRender', BILLABLE_EVENT: 'billableEvent', - BID_ACCEPTED: 'bidAccepted' + BID_ACCEPTED: 'bidAccepted', + RUN_PAAPI_AUCTION: 'paapiRunAuction', + PBS_ANALYTICS: 'pbsAnalytics', + PAAPI_BID: 'paapiBid', + PAAPI_NO_BID: 'paapiNoBid', + PAAPI_ERROR: 'paapiError', + BEFORE_PBS_HTTP: 'beforePBSHttp', + BROWSI_INIT: 'browsiInit', + BROWSI_DATA: 'browsiData', }; export const AD_RENDER_FAILED_REASON = { @@ -135,7 +144,8 @@ export const REJECTION_REASON = { FLOOR_NOT_MET: 'Bid does not meet price floor', CANNOT_CONVERT_CURRENCY: 'Unable to convert currency', DSA_REQUIRED: 'Bid does not provide required DSA transparency info', - DSA_MISMATCH: 'Bid indicates inappropriate DSA rendering method' + DSA_MISMATCH: 'Bid indicates inappropriate DSA rendering method', + PRICE_TOO_HIGH: 'Bid price exceeds maximum value' }; export const PREBID_NATIVE_DATA_KEYS_TO_ORTB = { @@ -188,3 +198,5 @@ export const MESSAGES = { NATIVE: 'Prebid Native', EVENT: 'Prebid Event' }; + +export const PB_LOCATOR = '__pb_locator__'; diff --git a/src/creativeRenderers.js b/src/creativeRenderers.js index 8331c23c8de..bde9ccb29b0 100644 --- a/src/creativeRenderers.js +++ b/src/creativeRenderers.js @@ -1,8 +1,11 @@ -import {GreedyPromise} from './utils/promise.js'; +import {PbPromise} from './utils/promise.js'; import {createInvisibleIframe} from './utils.js'; import {RENDERER} from '../libraries/creative-renderer-display/renderer.js'; import {hook} from './hook.js'; +// the minimum rendererVersion that will be used by PUC +export const PUC_MIN_VERSION = 3; + export const getCreativeRendererSource = hook('sync', function (bidResponse) { return RENDERER; }) @@ -12,7 +15,7 @@ export const getCreativeRenderer = (function() { return function (bidResponse) { const src = getCreativeRendererSource(bidResponse); if (!renderers.hasOwnProperty(src)) { - renderers[src] = new GreedyPromise((resolve) => { + renderers[src] = new PbPromise((resolve) => { const iframe = createInvisibleIframe(); iframe.srcdoc = ``; iframe.onload = () => resolve(iframe.contentWindow.render); diff --git a/src/debugging.js b/src/debugging.js index f5d13d1a134..12b7e41c8ad 100644 --- a/src/debugging.js +++ b/src/debugging.js @@ -4,7 +4,8 @@ import {getGlobal} from './prebidGlobal.js'; import {logMessage, prefixLog} from './utils.js'; import {createBid} from './bidfactory.js'; import {loadExternalScript} from './adloader.js'; -import {GreedyPromise} from './utils/promise.js'; +import {PbPromise} from './utils/promise.js'; +import { MODULE_TYPE_PREBID } from './activities/modules.js'; export const DEBUG_KEY = '__$$PREBID_GLOBAL$$_debugging__'; @@ -13,8 +14,8 @@ function isDebuggingInstalled() { } function loadScript(url) { - return new GreedyPromise((resolve) => { - loadExternalScript(url, 'debugging', resolve); + return new PbPromise((resolve) => { + loadExternalScript(url, MODULE_TYPE_PREBID, 'debugging', resolve); }); } @@ -22,7 +23,7 @@ export function debuggingModuleLoader({alreadyInstalled = isDebuggingInstalled, let loading = null; return function () { if (loading == null) { - loading = new GreedyPromise((resolve, reject) => { + loading = new PbPromise((resolve, reject) => { // run this in a 0-delay timeout to give installedModules time to be populated setTimeout(() => { if (alreadyInstalled()) { @@ -46,7 +47,7 @@ export function debuggingControls({load = debuggingModuleLoader(), hook = getHoo let promise = null; let enabled = false; function waitForDebugging(next, ...args) { - return (promise || GreedyPromise.resolve()).then(() => next.apply(this, args)) + return (promise || PbPromise.resolve()).then(() => next.apply(this, args)) } function enable() { if (!enabled) { @@ -73,6 +74,7 @@ export const reset = ctl.reset; export function loadSession() { let storage = null; try { + // eslint-disable-next-line no-restricted-properties storage = window.sessionStorage; } catch (e) {} diff --git a/src/eventTrackers.js b/src/eventTrackers.js new file mode 100644 index 00000000000..b0c06cf0f1b --- /dev/null +++ b/src/eventTrackers.js @@ -0,0 +1,22 @@ +export const TRACKER_METHOD_IMG = 1; +export const TRACKER_METHOD_JS = 2; +export const EVENT_TYPE_IMPRESSION = 1; +export const EVENT_TYPE_WIN = 500; + +/** + * Returns a map from event type (EVENT_TYPE_*) + * to a map from tracker method (TRACKER_METHOD_*) + * to an array of tracking URLs + * + * @param {{}[]} eventTrackers an array of "Event Tracker Response Object" as defined + * in the ORTB native 1.2 spec (https://www.iab.com/wp-content/uploads/2018/03/OpenRTB-Native-Ads-Specification-Final-1.2.pdf, section 5.8) + * @returns {{[type: string]: {[method: string]: string[]}}} + */ +export function parseEventTrackers(eventTrackers) { + return (eventTrackers ?? []).reduce((tally, {event, method, url}) => { + const trackersForType = tally[event] = tally[event] ?? {}; + const trackersForMethod = trackersForType[method] = trackersForType[method] ?? []; + trackersForMethod.push(url); + return tally; + }, {}) +} diff --git a/src/fpd/enrichment.js b/src/fpd/enrichment.js index 49e2f7d7cad..0b361bb3733 100644 --- a/src/fpd/enrichment.js +++ b/src/fpd/enrichment.js @@ -1,15 +1,16 @@ import {hook} from '../hook.js'; import {getRefererInfo, parseDomain} from '../refererDetection.js'; import {findRootDomain} from './rootDomain.js'; -import {deepSetValue, getDefinedParams, getDNT, getWindowSelf, getWindowTop, mergeDeep} from '../utils.js'; +import {deepSetValue, getDefinedParams, getDNT, getWinDimensions, getDocument, getWindowSelf, getWindowTop, mergeDeep} from '../utils.js'; import {config} from '../config.js'; import {getHighEntropySUA, getLowEntropySUA} from './sua.js'; -import {GreedyPromise} from '../utils/promise.js'; +import {PbPromise} from '../utils/promise.js'; import {CLIENT_SECTIONS, clientSectionChecker, hasSection} from './oneClient.js'; import {isActivityAllowed} from '../activities/rules.js'; import {activityParams} from '../activities/activityParams.js'; import {ACTIVITY_ACCESS_DEVICE} from '../activities/activities.js'; import {MODULE_TYPE_PREBID} from '../activities/modules.js'; +import { getViewportSize } from '../../libraries/viewport/viewport.js'; export const dep = { getRefererInfo, @@ -18,22 +19,22 @@ export const dep = { getWindowSelf, getHighEntropySUA, getLowEntropySUA, + getDocument }; const oneClient = clientSectionChecker('FPD') /** - * Enrich an ortb2 object with first party data. - * @param {Promise[{}]} fpd: a promise to an ortb2 object. - * @returns: {Promise[{}]}: a promise to an enriched ortb2 object. + * Enrich an ortb2 object with first-party data. + * @param {Promise} fpd - A promise that resolves to an ortb2 object. + * @returns {Promise} - A promise that resolves to an enriched ortb2 object. */ export const enrichFPD = hook('sync', (fpd) => { const promArr = [fpd, getSUA().catch(() => null), tryToGetCdepLabel().catch(() => null)]; - return GreedyPromise.all(promArr) + return PbPromise.all(promArr) .then(([ortb2, sua, cdep]) => { const ri = dep.getRefererInfo(); - mergeLegacySetConfigs(ortb2); Object.entries(ENRICHMENTS).forEach(([section, getEnrichments]) => { const data = getEnrichments(ortb2, ri); if (data && Object.keys(data).length > 0) { @@ -52,6 +53,11 @@ export const enrichFPD = hook('sync', (fpd) => { deepSetValue(ortb2, 'device.ext', Object.assign({}, ext, ortb2.device.ext)); } + const documentLang = dep.getDocument().documentElement.lang; + if (documentLang) { + deepSetValue(ortb2, 'site.ext.data.documentLang', documentLang); + } + ortb2 = oneClient(ortb2); for (let section of CLIENT_SECTIONS) { if (hasSection(ortb2, section)) { @@ -64,17 +70,6 @@ export const enrichFPD = hook('sync', (fpd) => { }); }); -function mergeLegacySetConfigs(ortb2) { - // merge in values from "legacy" setConfig({app, site, device}) - // TODO: deprecate these eventually - ['app', 'site', 'device'].forEach(prop => { - const cfg = config.getConfig(prop); - if (cfg != null) { - ortb2[prop] = mergeDeep({}, cfg, ortb2[prop]); - } - }) -} - function winFallback(fn) { try { return fn(dep.getWindowTop()); @@ -86,7 +81,7 @@ function winFallback(fn) { function getSUA() { const hints = config.getConfig('firstPartyData.uaHints'); return !Array.isArray(hints) || hints.length === 0 - ? GreedyPromise.resolve(dep.getLowEntropySUA()) + ? PbPromise.resolve(dep.getLowEntropySUA()) : dep.getHighEntropySUA(hints); } @@ -95,7 +90,7 @@ function removeUndef(obj) { } function tryToGetCdepLabel() { - return GreedyPromise.resolve('cookieDeprecationLabel' in navigator && isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')) && navigator.cookieDeprecationLabel.getValue()); + return PbPromise.resolve('cookieDeprecationLabel' in navigator && isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')) && navigator.cookieDeprecationLabel.getValue()); } const ENRICHMENTS = { @@ -111,8 +106,12 @@ const ENRICHMENTS = { }, device() { return winFallback((win) => { - const w = win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth; - const h = win.innerHeight || win.document.documentElement.clientHeight || win.document.body.clientHeight; + // screen.width and screen.height are the physical dimensions of the screen + const w = getWinDimensions().screen.width; + const h = getWinDimensions().screen.height; + + // vpw and vph are the viewport dimensions of the browser window + const {width: vpw, height: vph} = getViewportSize(); const device = { w, @@ -120,6 +119,10 @@ const ENRICHMENTS = { dnt: getDNT() ? 1 : 0, ua: win.navigator.userAgent, language: win.navigator.language.split('-').shift(), + ext: { + vpw, + vph, + }, }; if (win.navigator?.webdriver) { @@ -132,7 +135,7 @@ const ENRICHMENTS = { regs() { const regs = {}; if (winFallback((win) => win.navigator.globalPrivacyControl)) { - deepSetValue(regs, 'ext.gpc', 1); + deepSetValue(regs, 'ext.gpc', '1'); } const coppa = config.getConfig('coppa'); if (typeof coppa === 'boolean') { diff --git a/src/fpd/sua.js b/src/fpd/sua.js index 565c3e1fd52..ac34951e347 100644 --- a/src/fpd/sua.js +++ b/src/fpd/sua.js @@ -1,5 +1,5 @@ import {isEmptyStr, isStr, isEmpty} from '../utils.js'; -import {GreedyPromise} from '../utils/promise.js'; +import {PbPromise} from '../utils/promise.js'; export const SUA_SOURCE_UNKNOWN = 0; export const SUA_SOURCE_LOW_ENTROPY = 1; @@ -60,7 +60,7 @@ export function highEntropySUAAccessor(uaData = window.navigator?.userAgentData) return isEmpty(result) ? null : Object.freeze(uaDataToSUA(SUA_SOURCE_HIGH_ENTROPY, result)) }).catch(() => null); } catch (e) { - cache[key] = GreedyPromise.resolve(null); + cache[key] = PbPromise.resolve(null); } } return cache[key]; diff --git a/src/hook.js b/src/hook.js index 3f01114935d..9a6604d656f 100644 --- a/src/hook.js +++ b/src/hook.js @@ -1,6 +1,10 @@ import funHooks from 'fun-hooks/no-eval/index.js'; import {defer} from './utils/promise.js'; +/** + * NOTE: you must not call `next` asynchronously from 'sync' hooks + * see https://github.com/snapwich/fun-hooks/issues/42 + */ export let hook = funHooks({ ready: funHooks.SYNC | funHooks.ASYNC | funHooks.QUEUE }); @@ -59,3 +63,18 @@ export function wrapHook(hook, wrapper) { ); return wrapper; } + +/** + * 'async' hooks expect the last argument to be a callback, and have special treatment for it if it's a function; + * which prevents it from being used as a normal argument in 'before' hooks - and presents a modified version of it + * to the hooked function. + * + * This returns a wrapper around a given 'async' hook that works around this, for when the last argument + * should be treated as a normal argument. + */ +export function ignoreCallbackArg(hook) { + return wrapHook(hook, function (...args) { + args.push(function () {}) + return hook.apply(this, args); + }) +} diff --git a/src/mediaTypes.js b/src/mediaTypes.js index 2afa2aefaf9..9b5318e0657 100644 --- a/src/mediaTypes.js +++ b/src/mediaTypes.js @@ -18,3 +18,5 @@ export const VIDEO = 'video'; export const BANNER = 'banner'; /** @type {VideoContext} */ export const ADPOD = 'adpod'; + +export const ALL_MEDIATYPES = [NATIVE, VIDEO, BANNER]; diff --git a/src/native.js b/src/native.js index f001200000d..d5d3e42ad80 100644 --- a/src/native.js +++ b/src/native.js @@ -1,5 +1,4 @@ import { - deepAccess, deepClone, getDefinedParams, insertHtmlIntoIframe, isArray, @@ -15,6 +14,9 @@ import {includes} from './polyfill.js'; import {auctionManager} from './auctionManager.js'; import {NATIVE_ASSET_TYPES, NATIVE_IMAGE_TYPES, PREBID_NATIVE_DATA_KEYS_TO_ORTB, NATIVE_KEYS_THAT_ARE_NOT_ASSETS, NATIVE_KEYS} from './constants.js'; import {NATIVE} from './mediaTypes.js'; +import {getRenderingData} from './adRendering.js'; +import {getCreativeRendererSource, PUC_MIN_VERSION} from './creativeRenderers.js'; +import {EVENT_TYPE_IMPRESSION, parseEventTrackers, TRACKER_METHOD_IMG, TRACKER_METHOD_JS} from './eventTrackers.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -88,20 +90,6 @@ const SUPPORTED_TYPES = { const PREBID_NATIVE_DATA_KEYS_TO_ORTB_INVERSE = inverse(PREBID_NATIVE_DATA_KEYS_TO_ORTB); const NATIVE_ASSET_TYPES_INVERSE = inverse(NATIVE_ASSET_TYPES); -const TRACKER_METHODS = { - img: 1, - js: 2, - 1: 'img', - 2: 'js' -} - -const TRACKER_EVENTS = { - impression: 1, - 'viewable-mrc50': 2, - 'viewable-mrc100': 3, - 'viewable-video50': 4, -} - export function isNativeResponse(bidResponse) { // check for native data and not mediaType; it's possible // to treat banner responses as native @@ -127,7 +115,7 @@ export function processNativeAdUnitParams(params) { export function decorateAdUnitsWithNativeParams(adUnits) { adUnits.forEach(adUnit => { const nativeParams = - adUnit.nativeParams || deepAccess(adUnit, 'mediaTypes.native'); + adUnit.nativeParams || adUnit?.mediaTypes?.native; if (nativeParams) { adUnit.nativeParams = processNativeAdUnitParams(nativeParams); } @@ -211,7 +199,7 @@ function typeIsSupported(type) { */ export const nativeAdUnit = adUnit => { const mediaType = adUnit.mediaType === 'native'; - const mediaTypes = deepAccess(adUnit, 'mediaTypes.native'); + const mediaTypes = adUnit?.mediaTypes?.native; return mediaType || mediaTypes; } export const nativeBidder = bid => includes(nativeAdapters, bid.bidder); @@ -234,7 +222,7 @@ export function nativeBidIsValid(bid, {index = auctionManager.index} = {}) { } export function isNativeOpenRTBBidValid(bidORTB, bidRequestORTB) { - if (!deepAccess(bidORTB, 'link.url')) { + if (!bidORTB?.link?.url) { logError(`native response doesn't have 'link' property. Ortb response: `, bidORTB); return false; } @@ -288,15 +276,9 @@ export function fireNativeTrackers(message, bidResponse) { } export function fireImpressionTrackers(nativeResponse, {runMarkup = (mkup) => insertHtmlIntoIframe(mkup), fetchURL = triggerPixel} = {}) { - const impTrackers = (nativeResponse.eventtrackers || []) - .filter(tracker => tracker.event === TRACKER_EVENTS.impression); - - let {img, js} = impTrackers.reduce((tally, tracker) => { - if (TRACKER_METHODS.hasOwnProperty(tracker.method)) { - tally[TRACKER_METHODS[tracker.method]].push(tracker.url) - } - return tally; - }, {img: [], js: []}); + let {[TRACKER_METHOD_IMG]: img = [], [TRACKER_METHOD_JS]: js = []} = parseEventTrackers( + nativeResponse.eventtrackers || [] + )[EVENT_TYPE_IMPRESSION] || {}; if (nativeResponse.imptrackers) { img = img.concat(nativeResponse.imptrackers); @@ -362,10 +344,7 @@ export function getNativeTargeting(bid, {index = auctionManager.index} = {}) { let keyValues = {}; const adUnit = index.getAdUnit(bid); - const globalSendTargetingKeys = deepAccess( - adUnit, - `nativeParams.sendTargetingKeys` - ) !== false; + const globalSendTargetingKeys = adUnit?.nativeParams?.ortb == null && adUnit?.nativeParams?.sendTargetingKeys !== false; const nativeKeys = getNativeKeys(adUnit); @@ -374,15 +353,15 @@ export function getNativeTargeting(bid, {index = auctionManager.index} = {}) { Object.keys(flatBidNativeKeys).forEach(asset => { const key = nativeKeys[asset]; - let value = getAssetValue(bid.native[asset]) || getAssetValue(deepAccess(bid, `native.ext.${asset}`)); + let value = getAssetValue(bid.native[asset]) || getAssetValue(bid?.native?.ext?.[asset]); if (asset === 'adTemplate' || !key || !value) { return; } - let sendPlaceholder = deepAccess(adUnit, `nativeParams.${asset}.sendId`); + let sendPlaceholder = adUnit?.nativeParams?.[asset]?.sendId; if (typeof sendPlaceholder !== 'boolean') { - sendPlaceholder = deepAccess(adUnit, `nativeParams.ext.${asset}.sendId`); + sendPlaceholder = adUnit?.nativeParams?.ext?.[asset]?.sendId; } if (sendPlaceholder) { @@ -390,9 +369,9 @@ export function getNativeTargeting(bid, {index = auctionManager.index} = {}) { value = placeholder; } - let assetSendTargetingKeys = deepAccess(adUnit, `nativeParams.${asset}.sendTargetingKeys`); + let assetSendTargetingKeys = adUnit?.nativeParams?.[asset]?.sendTargetingKeys; if (typeof assetSendTargetingKeys !== 'boolean') { - assetSendTargetingKeys = deepAccess(adUnit, `nativeParams.ext.${asset}.sendTargetingKeys`); + assetSendTargetingKeys = adUnit?.nativeParams?.ext?.[asset]?.sendTargetingKeys; } const sendTargeting = typeof assetSendTargetingKeys === 'boolean' ? assetSendTargetingKeys : globalSendTargetingKeys; @@ -434,11 +413,25 @@ export function getNativeRenderingData(bid, adUnit, keys) { } function assetsMessage(data, adObject, keys, {index = auctionManager.index} = {}) { - return { + const msg = { message: 'assetResponse', adId: data.adId, - ...getNativeRenderingData(adObject, index.getAdUnit(adObject), keys) }; + let renderData = getRenderingData(adObject).native; + if (renderData) { + // if we have native rendering data (set up by the nativeRendering module) + // include it in full ("all assets") together with the renderer. + // this is to allow PUC to use dynamic renderers without requiring changes in creative setup + msg.native = Object.assign({}, renderData); + msg.renderer = getCreativeRendererSource(adObject); + msg.rendererVersion = PUC_MIN_VERSION; + if (keys != null) { + renderData.assets = renderData.assets.filter(({key}) => keys.includes(key)) + } + } else { + renderData = getNativeRenderingData(adObject, index.getAdUnit(adObject), keys); + } + return Object.assign(msg, renderData); } const NATIVE_KEYS_INVERTED = Object.fromEntries(Object.entries(NATIVE_KEYS).map(([k, v]) => [v, k])); @@ -467,7 +460,7 @@ function getAssetValue(value) { function getNativeKeys(adUnit) { const extraNativeKeys = {} - if (deepAccess(adUnit, 'nativeParams.ext')) { + if (adUnit?.nativeParams?.ext) { Object.keys(adUnit.nativeParams.ext).forEach(extKey => { extraNativeKeys[extKey] = `hb_native_${extKey}`; }) @@ -712,8 +705,8 @@ export function legacyPropertiesToOrtbNative(legacyNative) { case 'impressionTrackers': (Array.isArray(value) ? value : [value]).forEach(url => { response.eventtrackers.push({ - event: TRACKER_EVENTS.impression, - method: TRACKER_METHODS.img, + event: EVENT_TYPE_IMPRESSION, + method: TRACKER_METHOD_IMG, url }); }); @@ -791,20 +784,20 @@ export function toOrtbNativeResponse(legacyResponse, ortbRequest) { export function toLegacyResponse(ortbResponse, ortbRequest) { const legacyResponse = {}; const requestAssets = ortbRequest?.assets || []; - legacyResponse.clickUrl = ortbResponse.link.url; + legacyResponse.clickUrl = ortbResponse.link?.url; legacyResponse.privacyLink = ortbResponse.privacy; for (const asset of ortbResponse?.assets || []) { const requestAsset = requestAssets.find(reqAsset => asset.id === reqAsset.id); if (asset.title) { legacyResponse.title = asset.title.text; } else if (asset.img) { - legacyResponse[requestAsset.img.type === NATIVE_IMAGE_TYPES.MAIN ? 'image' : 'icon'] = { + legacyResponse[requestAsset?.img?.type === NATIVE_IMAGE_TYPES.MAIN ? 'image' : 'icon'] = { url: asset.img.url, width: asset.img.w, height: asset.img.h }; } else if (asset.data) { - legacyResponse[PREBID_NATIVE_DATA_KEYS_TO_ORTB_INVERSE[NATIVE_ASSET_TYPES_INVERSE[requestAsset.data.type]]] = asset.data.value; + legacyResponse[PREBID_NATIVE_DATA_KEYS_TO_ORTB_INVERSE[NATIVE_ASSET_TYPES_INVERSE[requestAsset?.data?.type]]] = asset.data.value; } } @@ -816,10 +809,10 @@ export function toLegacyResponse(ortbResponse, ortbRequest) { legacyResponse.impressionTrackers.push(...ortbResponse.imptrackers); } for (const eventTracker of ortbResponse?.eventtrackers || []) { - if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.img) { + if (eventTracker.event === EVENT_TYPE_IMPRESSION && eventTracker.method === TRACKER_METHOD_IMG) { legacyResponse.impressionTrackers.push(eventTracker.url); } - if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.js) { + if (eventTracker.event === EVENT_TYPE_IMPRESSION && eventTracker.method === TRACKER_METHOD_JS) { jsTrackers.push(eventTracker.url); } } diff --git a/src/prebid.js b/src/prebid.js index 7f2d8798e2a..e798269fe2c 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -20,7 +20,8 @@ import { mergeDeep, transformAdServerTargetingObj, uniques, - unsupportedBidderMessage + unsupportedBidderMessage, + deepEqual } from './utils.js'; import {listenMessagesFromCreative} from './secureCreatives.js'; import {userSync} from './userSync.js'; @@ -36,12 +37,22 @@ import {default as adapterManager, getS2SBidderSet} from './adapterManager.js'; import { BID_STATUS, EVENTS, NATIVE_KEYS } from './constants.js'; import * as events from './events.js'; import {newMetrics, useMetrics} from './utils/perfMetrics.js'; -import {defer, GreedyPromise} from './utils/promise.js'; +import {defer, PbPromise} from './utils/promise.js'; import {enrichFPD} from './fpd/enrichment.js'; import {allConsent} from './consentHandler.js'; -import {renderAdDirect} from './adRendering.js'; +import { + insertLocatorFrame, + markBidAsRendered, + markWinningBid, + renderAdDirect, + renderIfDeferred +} from './adRendering.js'; import {getHighestCpm} from './utils/reducers.js'; -import {fillVideoDefaults} from './video.js'; +import {ORTB_VIDEO_PARAMS, fillVideoDefaults, validateOrtbVideoFields} from './video.js'; +import { ORTB_BANNER_PARAMS } from './banner.js'; +import { BANNER, VIDEO } from './mediaTypes.js'; +import {delayIfPrerendering} from './utils/prerendering.js'; +import { newBidder } from './adapters/bidderFactory.js'; const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; @@ -100,18 +111,83 @@ function validateSizes(sizes, targLength) { return cleanSizes; } +// synchronize fields between mediaTypes[mediaType] and ortb2Imp[mediaType] +export function syncOrtb2(adUnit, mediaType) { + const ortb2Imp = deepAccess(adUnit, `ortb2Imp.${mediaType}`); + const mediaTypes = deepAccess(adUnit, `mediaTypes.${mediaType}`); + + if (!ortb2Imp && !mediaTypes) { + // omitting sync due to not present mediaType + return; + } + + const fields = { + [VIDEO]: FEATURES.VIDEO && ORTB_VIDEO_PARAMS, + [BANNER]: ORTB_BANNER_PARAMS + }[mediaType]; + + if (!fields) { + return; + } + + [...fields].forEach(([key, validator]) => { + const mediaTypesFieldValue = deepAccess(adUnit, `mediaTypes.${mediaType}.${key}`); + const ortbFieldValue = deepAccess(adUnit, `ortb2Imp.${mediaType}.${key}`); + + if (mediaTypesFieldValue == undefined && ortbFieldValue == undefined) { + // omitting the params if it's not defined on either of sides + } else if (mediaTypesFieldValue == undefined) { + deepSetValue(adUnit, `mediaTypes.${mediaType}.${key}`, ortbFieldValue); + } else if (ortbFieldValue == undefined) { + deepSetValue(adUnit, `ortb2Imp.${mediaType}.${key}`, mediaTypesFieldValue); + } else { + 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); + } + }); +} + function validateBannerMediaType(adUnit) { const validatedAdUnit = deepClone(adUnit); const banner = validatedAdUnit.mediaTypes.banner; - const bannerSizes = validateSizes(banner.sizes); - if (bannerSizes.length > 0) { - banner.sizes = bannerSizes; + const bannerSizes = banner.sizes == null ? null : validateSizes(banner.sizes); + const format = adUnit.ortb2Imp?.banner?.format ?? banner?.format; + let formatSizes; + if (format != null) { + deepSetValue(validatedAdUnit, 'ortb2Imp.banner.format', format); + banner.format = format; + try { + formatSizes = format + .filter(({w, h, wratio, hratio}) => { + if ((w ?? h) != null && (wratio ?? hratio) != null) { + logWarn(`Ad unit banner.format specifies both w/h and wratio/hratio`, adUnit); + return false; + } + return (w != null && h != null) || (wratio != null && hratio != null); + }) + .map(({w, h, wratio, hratio}) => [w ?? wratio, h ?? hratio]); + } catch (e) { + logError(`Invalid format definition on ad unit ${adUnit.code}`, format); + } + if (formatSizes != null && bannerSizes != null && !deepEqual(bannerSizes, formatSizes)) { + logWarn(`Ad unit ${adUnit.code} has conflicting sizes and format definitions`, adUnit); + } + } + const sizes = formatSizes ?? bannerSizes ?? []; + const expdir = adUnit.ortb2Imp?.banner?.expdir ?? banner.expdir; + if (expdir != null) { + banner.expdir = expdir; + deepSetValue(validatedAdUnit, 'ortb2Imp.banner.expdir', expdir); + } + if (sizes.length > 0) { + banner.sizes = sizes; // Deprecation Warning: This property will be deprecated in next release in favor of adUnit.mediaTypes.banner.sizes - validatedAdUnit.sizes = bannerSizes; + validatedAdUnit.sizes = sizes; } else { logError('Detected a mediaTypes.banner object without a proper sizes field. Please ensure the sizes are listed like: [[300, 250], ...]. Removing invalid mediaTypes.banner object from request.'); delete validatedAdUnit.mediaTypes.banner } + syncOrtb2(validatedAdUnit, 'banner') return validatedAdUnit; } @@ -134,14 +210,35 @@ function validateVideoMediaType(adUnit) { delete validatedAdUnit.mediaTypes.video.playerSize; } } + validateOrtbVideoFields(validatedAdUnit); + syncOrtb2(validatedAdUnit, 'video'); return validatedAdUnit; } function validateNativeMediaType(adUnit) { + function err(msg) { + logError(`Error in adUnit "${adUnit.code}": ${msg}. Removing native request from ad unit`, adUnit); + delete validatedAdUnit.mediaTypes.native; + return validatedAdUnit; + } + function checkDeprecated(onDeprecated) { + for (const key of ['sendTargetingKeys', 'types']) { + if (native.hasOwnProperty(key)) { + const res = onDeprecated(key); + if (res) return res; + } + } + } const validatedAdUnit = deepClone(adUnit); const native = validatedAdUnit.mediaTypes.native; // if native assets are specified in OpenRTB format, remove legacy assets and print a warn. if (native.ortb) { + if (native.ortb.assets?.some(asset => !isNumber(asset.id) || asset.id < 0 || asset.id % 1 !== 0)) { + return err('native asset ID must be a nonnegative integer'); + } + if (checkDeprecated(key => err(`ORTB native requests cannot specify "${key}"`))) { + return validatedAdUnit; + } const legacyNativeKeys = Object.keys(NATIVE_KEYS).filter(key => NATIVE_KEYS[key].includes('hb_native_')); const nativeKeys = Object.keys(native); const intersection = nativeKeys.filter(nativeKey => legacyNativeKeys.includes(nativeKey)); @@ -149,6 +246,8 @@ function validateNativeMediaType(adUnit) { logError(`when using native OpenRTB format, you cannot use legacy native properties. Deleting ${intersection} keys from request.`); intersection.forEach(legacyKey => delete validatedAdUnit.mediaTypes.native[legacyKey]); } + } else { + checkDeprecated(key => `mediaTypes.native.${key} is deprecated, consider using native ORTB instead`, adUnit); } if (native.image && native.image.sizes && !Array.isArray(native.image.sizes)) { logError('Please use an array of sizes for native.image.sizes field. Removing invalid mediaTypes.native.image.sizes property from request.'); @@ -166,13 +265,12 @@ function validateNativeMediaType(adUnit) { } function validateAdUnitPos(adUnit, mediaType) { - let pos = deepAccess(adUnit, `mediaTypes.${mediaType}.pos`); + let pos = adUnit?.mediaTypes?.[mediaType]?.pos; if (!isNumber(pos) || isNaN(pos) || !isFinite(pos)) { let warning = `Value of property 'pos' on ad unit ${adUnit.code} should be of type: Number`; logWarn(warning); - events.emit(EVENTS.AUCTION_DEBUG, { type: 'WARNING', arguments: warning }); delete adUnit.mediaTypes[mediaType].pos; } @@ -283,7 +381,7 @@ pbjsInstance.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { /** * This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. - * @param adUnitCode {string} adUnitCode to get the bid responses for + * @param adunitCode {string} adUnitCode to get the bid responses for * @alias module:pbjs.getHighestUnusedBidResponseForAdUnitCode * @returns {Object} returnObj return bid */ @@ -393,7 +491,7 @@ pbjsInstance.getBidResponsesForAdUnitCode = function (adUnitCode) { /** * Set query string targeting on one or more GPT ad units. * @param {(string|string[])} adUnit a single `adUnit.code` or multiple. - * @param {function(object)} customSlotMatching gets a GoogleTag slot and returns a filter function for adUnitCode, so you can decide to match on either eg. return slot => { return adUnitCode => { return slot.getSlotElementId() === 'myFavoriteDivId'; } }; + * @param {function(object): function(string): boolean} customSlotMatching gets a GoogleTag slot and returns a filter function for adUnitCode, so you can decide to match on either eg. return slot => { return adUnitCode => { return slot.getSlotElementId() === 'myFavoriteDivId'; } }; * @alias module:pbjs.setTargetingForGPTAsync */ pbjsInstance.setTargetingForGPTAsync = function (adUnit, customSlotMatching) { @@ -402,31 +500,12 @@ pbjsInstance.setTargetingForGPTAsync = function (adUnit, customSlotMatching) { logError('window.googletag is not defined on the page'); return; } - - // get our ad unit codes - let targetingSet = targeting.getAllTargeting(adUnit); - - // first reset any old targeting - targeting.resetPresetTargeting(adUnit, customSlotMatching); - - // now set new targeting keys - targeting.setTargetingForGPT(targetingSet, customSlotMatching); - - Object.keys(targetingSet).forEach((adUnitCode) => { - Object.keys(targetingSet[adUnitCode]).forEach((targetingKey) => { - if (targetingKey === 'hb_adid') { - auctionManager.setStatusForBids(targetingSet[adUnitCode][targetingKey], BID_STATUS.BID_TARGETING_SET); - } - }); - }); - - // emit event - events.emit(SET_TARGETING, targetingSet); + targeting.setTargetingForGPT(adUnit, customSlotMatching); }; /** * Set query string targeting on all AST (AppNexus Seller Tag) ad units. Note that this function has to be called after all ad units on page are defined. For working example code, see [Using Prebid.js with AppNexus Publisher Ad Server](http://prebid.org/dev-docs/examples/use-prebid-with-appnexus-ad-server.html). - * @param {(string|string[])} adUnitCode adUnitCode or array of adUnitCodes + * @param {(string|string[])} adUnitCodes adUnitCode or array of adUnitCodes * @alias module:pbjs.setTargetingForAst */ pbjsInstance.setTargetingForAst = function (adUnitCodes) { @@ -500,6 +579,9 @@ pbjsInstance.requestBids = (function() { events.emit(REQUEST_BIDS); const cbTimeout = timeout || config.getConfig('bidderTimeout'); logInfo('Invoking $$PREBID_GLOBAL$$.requestBids', arguments); + if (adUnitCodes != null && !Array.isArray(adUnitCodes)) { + adUnitCodes = [adUnitCodes]; + } if (adUnitCodes && adUnitCodes.length) { // if specific adUnitCodes supplied filter adUnits for those codes adUnits = adUnits.filter(unit => includes(adUnitCodes, unit.code)); @@ -507,23 +589,25 @@ pbjsInstance.requestBids = (function() { // otherwise derive adUnitCodes from adUnits adUnitCodes = adUnits && adUnits.map(unit => unit.code); } + adUnitCodes = adUnitCodes.filter(uniques); const ortb2Fragments = { global: mergeDeep({}, config.getAnyConfig('ortb2') || {}, ortb2 || {}), - bidder: Object.fromEntries(Object.entries(config.getBidderConfig()).map(([bidder, cfg]) => [bidder, cfg.ortb2]).filter(([_, ortb2]) => ortb2 != null)) + bidder: Object.fromEntries(Object.entries(config.getBidderConfig()).map(([bidder, cfg]) => [bidder, deepClone(cfg.ortb2)]).filter(([_, ortb2]) => ortb2 != null)) } - return enrichFPD(GreedyPromise.resolve(ortb2Fragments.global)).then(global => { + return enrichFPD(PbPromise.resolve(ortb2Fragments.global)).then(global => { ortb2Fragments.global = global; return startAuction({bidsBackHandler, timeout: cbTimeout, adUnits, adUnitCodes, labels, auctionId, ttlBuffer, ortb2Fragments, metrics, defer}); }) }, 'requestBids'); - return wrapHook(delegate, function requestBids(req = {}) { + return wrapHook(delegate, delayIfPrerendering(() => !config.getConfig('allowPrerendering'), function requestBids(req = {}) { // unlike the main body of `delegate`, this runs before any other hook has a chance to; // it's also not restricted in its return value in the way `async` hooks are. // if the request does not specify adUnits, clone the global adUnit array; // otherwise, if the caller goes on to use addAdUnits/removeAdUnits, any asynchronous logic // in any hook might see their effects. + let adUnits = req.adUnits || pbjsInstance.adUnits; req.adUnits = (isArray(adUnits) ? adUnits.slice() : [adUnits]); @@ -532,7 +616,7 @@ pbjsInstance.requestBids = (function() { req.defer = defer({promiseFactory: (r) => new Promise(r)}) delegate.call(this, req); return req.defer.promise; - }); + })); })(); export const startAuction = hook('async', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, ttlBuffer, adUnitCodes, labels, auctionId, ortb2Fragments, metrics, defer } = {}) { @@ -641,7 +725,7 @@ export function executeCallbacks(fn, reqBidsConfigObj) { } } -// This hook will execute all storage callbacks which were registered before gdpr enforcement hook was added. Some bidders, user id modules use storage functions when module is parsed but gdpr enforcement hook is not added at that stage as setConfig callbacks are yet to be called. Hence for such calls we execute all the stored callbacks just before requestBids. At this hook point we will know for sure that gdprEnforcement module is added or not +// This hook will execute all storage callbacks which were registered before gdpr enforcement hook was added. Some bidders, user id modules use storage functions when module is parsed but gdpr enforcement hook is not added at that stage as setConfig callbacks are yet to be called. Hence for such calls we execute all the stored callbacks just before requestBids. At this hook point we will know for sure that tcfControl module is added or not pbjsInstance.requestBids.before(executeCallbacks, 49); /** @@ -717,12 +801,14 @@ pbjsInstance.getEvents = function () { * Wrapper to register bidderAdapter externally (adapterManager.registerBidAdapter()) * @param {Function} bidderAdaptor [description] * @param {string} bidderCode [description] + * @param {object} spec [description] * @alias module:pbjs.registerBidAdapter */ -pbjsInstance.registerBidAdapter = function (bidderAdaptor, bidderCode) { +pbjsInstance.registerBidAdapter = function (bidderAdaptor, bidderCode, spec) { logInfo('Invoking $$PREBID_GLOBAL$$.registerBidAdapter', arguments); try { - adapterManager.registerBidAdapter(bidderAdaptor(), bidderCode); + const bidder = spec ? newBidder(spec) : bidderAdaptor(); + adapterManager.registerBidAdapter(bidder, bidderCode); } catch (e) { logError('Error registering bidder adapter : ' + e.message); } @@ -867,41 +953,41 @@ pbjsInstance.getHighestCpmBids = function (adUnitCode) { return targeting.getWinningBids(adUnitCode); }; +pbjsInstance.clearAllAuctions = function () { + auctionManager.clearAllAuctions(); +}; + if (FEATURES.VIDEO) { /** * Mark the winning bid as used, should only be used in conjunction with video * @typedef {Object} MarkBidRequest * @property {string} adUnitCode The ad unit code * @property {string} adId The id representing the ad we want to mark + * @property {boolean} events If true, fires tracking pixels and BID_WON handlers + * @property {boolean} analytics alias of `events` (for backwards compat) * * @alias module:pbjs.markWinningBidAsUsed */ - pbjsInstance.markWinningBidAsUsed = function (markBidRequest) { - const bids = fetchReceivedBids(markBidRequest, 'Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); - + pbjsInstance.markWinningBidAsUsed = function ({adId, adUnitCode, analytics = false, events = false}) { + let bids; + if (adUnitCode && adId == null) { + bids = targeting.getWinningBids(adUnitCode); + } else if (adId) { + bids = auctionManager.getBidsReceived().filter(bid => bid.adId === adId) + } else { + logWarn('Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); + } if (bids.length > 0) { - auctionManager.addWinningBid(bids[0]); + if (analytics || events) { + markWinningBid(bids[0]); + } else { + auctionManager.addWinningBid(bids[0]); + } + markBidAsRendered(bids[0]) } } } -const fetchReceivedBids = (bidRequest, warningMessage) => { - let bids = []; - - if (bidRequest.adUnitCode && bidRequest.adId) { - bids = auctionManager.getBidsReceived() - .filter(bid => bid.adId === bidRequest.adId && bid.adUnitCode === bidRequest.adUnitCode); - } else if (bidRequest.adUnitCode) { - bids = targeting.getWinningBids(bidRequest.adUnitCode); - } else if (bidRequest.adId) { - bids = auctionManager.getBidsReceived().filter(bid => bid.adId === bidRequest.adId); - } else { - logWarn(warningMessage); - } - - return bids; -}; - /** * Get Prebid config options * @param {Object} options @@ -938,12 +1024,12 @@ pbjsInstance.que.push(() => listenMessagesFromCreative()); * by prebid once it's done loading. If it runs after prebid loads, then this monkey-patch causes their * function to execute immediately. * - * @memberof pbjs * @param {function} command A function which takes no arguments. This is guaranteed to run exactly once, and only after * the Prebid script has been fully loaded. * @alias module:pbjs.cmd.push + * @alias module:pbjs.que.push */ -pbjsInstance.cmd.push = function (command) { +function quePush(command) { if (typeof command === 'function') { try { command.call(); @@ -953,9 +1039,7 @@ pbjsInstance.cmd.push = function (command) { } else { logError('Commands written into $$PREBID_GLOBAL$$.cmd.push must be wrapped in a function'); } -}; - -pbjsInstance.que.push = pbjsInstance.cmd.push; +} function processQueue(queue) { queue.forEach(function (cmd) { @@ -973,28 +1057,24 @@ function processQueue(queue) { /** * @alias module:pbjs.processQueue */ -pbjsInstance.processQueue = function () { +pbjsInstance.processQueue = delayIfPrerendering(() => getGlobal().delayPrerendering, function () { + pbjsInstance.que.push = pbjsInstance.cmd.push = quePush; + insertLocatorFrame(); hook.ready(); processQueue(pbjsInstance.que); processQueue(pbjsInstance.cmd); -}; +}); /** * @alias module:pbjs.triggerBilling */ -pbjsInstance.triggerBilling = (winningBid) => { - const bids = fetchReceivedBids(winningBid, 'Improper use of triggerBilling. It requires a bid with at least an adUnitCode or an adId to function.'); - const triggerBillingBid = bids.find(bid => bid.requestId === winningBid.requestId) || bids[0]; - - if (bids.length > 0 && triggerBillingBid) { - try { - adapterManager.callBidBillableBidder(triggerBillingBid); - } catch (e) { - logError('Error when triggering billing :', e); - } - } else { - logWarn('The bid provided to triggerBilling did not match any bids received.'); - } +pbjsInstance.triggerBilling = ({adId, adUnitCode}) => { + auctionManager.getAllWinningBids() + .filter((bid) => bid.adId === adId || (adId == null && bid.adUnitCode === adUnitCode)) + .forEach((bid) => { + adapterManager.triggerBilling(bid); + renderIfDeferred(bid); + }); }; export default pbjsInstance; diff --git a/src/prebid.public.js b/src/prebid.public.js new file mode 100644 index 00000000000..f05e671ac24 --- /dev/null +++ b/src/prebid.public.js @@ -0,0 +1 @@ +export {default} from './prebid.js'; diff --git a/src/refererDetection.js b/src/refererDetection.js index 93ebf085dd5..bfe7fb02671 100644 --- a/src/refererDetection.js +++ b/src/refererDetection.js @@ -34,9 +34,11 @@ export function ensureProtocol(url, win = window) { /** * Extract the domain portion from a URL. - * @param url - * @param noLeadingWww: if true, remove 'www.' appearing at the beginning of the domain. - * @param noPort: if true, do not include the ':[port]' portion + * @param {string} url - The URL to extract the domain from. + * @param {Object} options - Options for parsing the domain. + * @param {boolean} options.noLeadingWww - If true, remove 'www.' appearing at the beginning of the domain. + * @param {boolean} options.noPort - If true, do not include the ':[port]' portion. + * @return {string|undefined} - The extracted domain or undefined if the URL is invalid. */ export function parseDomain(url, {noLeadingWww = false, noPort = false} = {}) { try { @@ -108,13 +110,13 @@ export function detectReferer(win) { * @property {string|null} ref the referrer (document.referrer) to the current page, or null if not available (due to cross-origin restrictions) * @property {string} topmostLocation of the top-most frame for which we could guess the location. Outside of cross-origin scenarios, this is equivalent to `location`. * @property {number} numIframes number of steps between window.self and window.top - * @property {Array[string|null]} stack our best guess at the location for each frame, in the direction top -> self. + * @property {Array} stack our best guess at the location for each frame, in the direction top -> self. */ /** * Walk up the windows to get the origin stack and best available referrer, canonical URL, etc. * - * @returns {refererInfo} + * @returns {refererInfo} An object containing referer information. */ function refererInfo() { const stack = []; diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 96ace0792e4..aeb0ecc6a19 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -3,19 +3,22 @@ access to a publisher page from creative payloads. */ -import * as events from './events.js'; import {getAllAssetsMessage, getAssetMessage} from './native.js'; -import { BID_STATUS, EVENTS, MESSAGES } from './constants.js'; +import {BID_STATUS, MESSAGES} from './constants.js'; import {isApnGetTagDefined, isGptPubadsDefined, logError, logWarn} from './utils.js'; -import {auctionManager} from './auctionManager.js'; import {find, includes} from './polyfill.js'; -import {handleCreativeEvent, handleNativeMessage, handleRender} from './adRendering.js'; -import {getCreativeRendererSource} from './creativeRenderers.js'; +import { + deferRendering, + getBidToRender, + handleCreativeEvent, + handleNativeMessage, + handleRender, + markWinner +} from './adRendering.js'; +import {getCreativeRendererSource, PUC_MIN_VERSION} from './creativeRenderers.js'; const { REQUEST, RESPONSE, NATIVE, EVENT } = MESSAGES; -const BID_WON = EVENTS.BID_WON; - const HANDLER_MAP = { [REQUEST]: handleRenderRequest, [EVENT]: handleEventRequest, @@ -28,7 +31,9 @@ if (FEATURES.NATIVE) { } export function listenMessagesFromCreative() { - window.addEventListener('message', receiveMessage, false); + window.addEventListener('message', function (ev) { + receiveMessage(ev); + }, false); } export function getReplier(ev) { @@ -49,6 +54,12 @@ export function getReplier(ev) { } } +function ensureAdId(adId, reply) { + return function (data, ...args) { + return reply(Object.assign({}, data, {adId}), ...args); + } +} + export function receiveMessage(ev) { var key = ev.message ? 'message' : 'data'; var data = {}; @@ -58,19 +69,19 @@ export function receiveMessage(ev) { return; } - if (data && data.adId && data.message) { - const adObject = find(auctionManager.getBidsReceived(), function (bid) { - return bid.adId === data.adId; - }); - if (HANDLER_MAP.hasOwnProperty(data.message)) { - HANDLER_MAP[data.message](getReplier(ev), data, adObject); - } + if (data && data.adId && data.message && HANDLER_MAP.hasOwnProperty(data.message)) { + return getBidToRender(data.adId, data.message === MESSAGES.REQUEST).then(adObject => { + HANDLER_MAP[data.message](ensureAdId(data.adId, getReplier(ev)), data, adObject); + }) } } -function getResizer(bidResponse) { +function getResizer(adId, bidResponse) { + // in some situations adId !== bidResponse.adId + // the first is the one that was requested and is tied to the element + // the second is the one that is being rendered (sometimes different, e.g. in some paapi setups) return function (width, height) { - resizeRemoteCreative({...bidResponse, width, height}); + resizeRemoteCreative({...bidResponse, width, height, adId}); } } function handleRenderRequest(reply, message, bidResponse) { @@ -78,10 +89,11 @@ function handleRenderRequest(reply, message, bidResponse) { renderFn(adData) { reply(Object.assign({ message: RESPONSE, - renderer: getCreativeRendererSource(bidResponse) + renderer: getCreativeRendererSource(bidResponse), + rendererVersion: PUC_MIN_VERSION }, adData)); }, - resizeFn: getResizer(bidResponse), + resizeFn: getResizer(message.adId, bidResponse), options: message.options, adId: message.adId, bidResponse @@ -98,21 +110,16 @@ function handleNativeRequest(reply, data, adObject) { logError(`Cannot find ad for x-origin event request: '${data.adId}'`); return; } - - if (adObject.status !== BID_STATUS.RENDERED) { - auctionManager.addWinningBid(adObject); - events.emit(BID_WON, adObject); - } - switch (data.action) { case 'assetRequest': - reply(getAssetMessage(data, adObject)); + deferRendering(adObject, () => reply(getAssetMessage(data, adObject))); break; case 'allAssetRequest': - reply(getAllAssetsMessage(data, adObject)); + deferRendering(adObject, () => reply(getAllAssetsMessage(data, adObject))); break; default: - handleNativeMessage(data, adObject, {resizeFn: getResizer(adObject)}) + handleNativeMessage(data, adObject, {resizeFn: getResizer(data.adId, adObject)}); + markWinner(adObject); } } @@ -128,7 +135,10 @@ function handleEventRequest(reply, data, adObject) { return handleCreativeEvent(data, adObject); } -export function resizeRemoteCreative({adId, adUnitCode, width, height}) { +export function resizeRemoteCreative({instl, adId, adUnitCode, width, height}) { + // do not resize interstitials - the creative frame takes the full screen and sizing of the ad should + // be handled within it. + if (instl) return; function getDimension(value) { return value ? value + 'px' : '100%'; } @@ -141,7 +151,7 @@ export function resizeRemoteCreative({adId, adUnitCode, width, height}) { elementStyle.width = getDimension(width) elementStyle.height = getDimension(height); } else { - logWarn(`Unable to locate matching page element for adUnitCode ${adUnitCode}. Can't resize it to ad's dimensions. Please review setup.`); + logError(`Unable to locate matching page element for adUnitCode ${adUnitCode}. Can't resize it to ad's dimensions. Please review setup.`); } }); diff --git a/src/storageManager.js b/src/storageManager.js index 87d714f77b8..eea653bcb34 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -18,6 +18,8 @@ export const STORAGE_TYPE_COOKIES = 'cookie'; export let storageCallbacks = []; +/* eslint-disable no-restricted-properties */ + /* * Storage manager constructor. Consumers should prefer one of `getStorageManager` or `getCoreStorageManager`. */ @@ -56,6 +58,7 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is * If not specified, defaults to the host portion of the current document location. * If a domain is specified, subdomains are always included. * Domain must match the domain of the JavaScript origin. Setting cookies to foreign domains will be silently ignored. + * @param {function} [done] */ const setCookie = function (key, value, expires, sameSite, domain, done) { let cb = function (result) { @@ -72,6 +75,7 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is /** * @param {string} name + * @param {function} [done] * @returns {(string|null)} */ const getCookie = function(name, done) { @@ -86,27 +90,7 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is }; /** - * @returns {boolean} - */ - const localStorageIsEnabled = function (done) { - let cb = function (result) { - if (result && result.valid) { - try { - localStorage.setItem('prebid.cookieTest', '1'); - return localStorage.getItem('prebid.cookieTest') === '1'; - } catch (error) { - } finally { - try { - localStorage.removeItem('prebid.cookieTest'); - } catch (error) {} - } - } - return false; - } - return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); - } - - /** + * @param {function} [done] * @returns {boolean} */ const cookiesAreEnabled = function (done) { @@ -119,67 +103,77 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is return schedule(cb, STORAGE_TYPE_COOKIES, done); } - /** - * @param {string} key - * @param {string} value - */ - const setDataInLocalStorage = function (key, value, done) { - let cb = function (result) { - if (result && result.valid && hasLocalStorage()) { - window.localStorage.setItem(key, value); - } - } - return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); - } + function storageMethods(name) { + const capName = name.charAt(0).toUpperCase() + name.substring(1); + const backend = () => window[name]; - /** - * @param {string} key - * @returns {(string|null)} - */ - const getDataFromLocalStorage = function (key, done) { - let cb = function (result) { - if (result && result.valid && hasLocalStorage()) { - return window.localStorage.getItem(key); - } - return null; - } - return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); - } - - /** - * @param {string} key - */ - const removeDataFromLocalStorage = function (key, done) { - let cb = function (result) { - if (result && result.valid && hasLocalStorage()) { - window.localStorage.removeItem(key); + const hasStorage = function (done) { + let cb = function (result) { + if (result && result.valid) { + try { + return !!backend(); + } catch (e) { + logError(`${name} api disabled`); + } + } + return false; } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } - return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); - } - /** - * @returns {boolean} - */ - const hasLocalStorage = function (done) { - let cb = function (result) { - if (result && result.valid) { - try { - return !!window.localStorage; - } catch (e) { - logError('Local storage api disabled'); + return { + [`has${capName}`]: hasStorage, + [`${name}IsEnabled`](done) { + let cb = function (result) { + if (result && result.valid) { + try { + backend().setItem('prebid.cookieTest', '1'); + return backend().getItem('prebid.cookieTest') === '1'; + } catch (error) { + } finally { + try { + backend().removeItem('prebid.cookieTest'); + } catch (error) {} + } + } + return false; } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); + }, + [`setDataIn${capName}`](key, value, done) { + let cb = function (result) { + if (result && result.valid && hasStorage()) { + backend().setItem(key, value); + } + } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); + }, + [`getDataFrom${capName}`](key, done) { + let cb = function (result) { + if (result && result.valid && hasStorage()) { + return backend().getItem(key); + } + return null; + } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); + }, + [`removeDataFrom${capName}`](key, done) { + let cb = function (result) { + if (result && result.valid && hasStorage()) { + backend().removeItem(key); + } + } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } - return false; } - return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** * Returns all cookie values from the jar whose names contain the `keyLike` * Needs to exist in `utils.js` as it follows the StorageHandler interface defined in live-connect-js. If that module were to be removed, this function can go as well. * @param {string} keyLike - * @return {[]} + * @param {function} [done] + * @returns {string[]} */ const findSimilarCookies = function(keyLike, done) { let cb = function (result) { @@ -207,12 +201,9 @@ export function newStorageManager({moduleName, moduleType} = {}, {isAllowed = is return { setCookie, getCookie, - localStorageIsEnabled, cookiesAreEnabled, - setDataInLocalStorage, - getDataFromLocalStorage, - removeDataFromLocalStorage, - hasLocalStorage, + ...storageMethods('localStorage'), + ...storageMethods('sessionStorage'), findSimilarCookies } } diff --git a/src/targeting.js b/src/targeting.js index acb3ddb09ff..14495c59962 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -1,3 +1,21 @@ +import { auctionManager } from './auctionManager.js'; +import { getBufferedTTL } from './bidTTL.js'; +import { bidderSettings } from './bidderSettings.js'; +import { config } from './config.js'; +import { + BID_STATUS, + DEFAULT_TARGETING_KEYS, + EVENTS, + JSON_MAPPING, + NATIVE_KEYS, + STATUS, + TARGETING_KEYS +} from './constants.js'; +import * as events from './events.js'; +import { hook } from './hook.js'; +import { ADPOD } from './mediaTypes.js'; +import { NATIVE_TARGETING_KEYS } from './native.js'; +import { find, includes } from './polyfill.js'; import { deepAccess, deepClone, @@ -11,19 +29,11 @@ import { logInfo, logMessage, logWarn, + sortByHighestCpm, timestamp, uniques, } from './utils.js'; -import {config} from './config.js'; -import {NATIVE_TARGETING_KEYS} from './native.js'; -import {auctionManager} from './auctionManager.js'; -import {ADPOD} from './mediaTypes.js'; -import {hook} from './hook.js'; -import {bidderSettings} from './bidderSettings.js'; -import {find, includes} from './polyfill.js'; -import { BID_STATUS, JSON_MAPPING, DEFAULT_TARGETING_KEYS, TARGETING_KEYS, NATIVE_KEYS, STATUS } from './constants.js'; -import {getHighestCpm, getOldestHighestCpmBid} from './utils/reducers.js'; -import {getTTL} from './bidTTL.js'; +import { getHighestCpm, getOldestHighestCpmBid } from './utils/reducers.js'; var pbTargetingKeys = []; @@ -38,7 +48,7 @@ export const TARGETING_KEYS_ARR = Object.keys(TARGETING_KEYS).map( ); // return unexpired bids -const isBidNotExpired = (bid) => (bid.responseTimestamp + getTTL(bid) * 1000) > timestamp(); +const isBidNotExpired = (bid) => (bid.responseTimestamp + getBufferedTTL(bid) * 1000) > timestamp(); // return bids whose status is not set. Winning bids can only have a status of `rendered`. const isUnusedBid = (bid) => bid && ((bid.status && !includes([BID_STATUS.RENDERED], bid.status)) || !bid.status); @@ -58,7 +68,7 @@ export function isBidUsable(bid) { // If two bids are found for same adUnitCode, we will use the highest one to take part in auction // This can happen in case of concurrent auctions // If adUnitBidLimit is set above 0 return top N number of bids -export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { +export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, winReducer, adUnitBidLimit = 0, hasModified = false, winSorter = sortByHighestCpm) { if (!hasModified) { const bids = []; const dealPrioritization = config.getConfig('sendBidsControl.dealPrioritization'); @@ -67,13 +77,14 @@ export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, // filter top bid for each bucket by bidder Object.keys(buckets).forEach(bucketKey => { let bucketBids = []; - let bidsByBidder = groupBy(buckets[bucketKey], 'bidderCode'); - Object.keys(bidsByBidder).forEach(key => bucketBids.push(bidsByBidder[key].reduce(highestCpmCallback))); + let 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 > 0) { + if (adUnitBidLimit) { bucketBids = dealPrioritization ? bucketBids.sort(sortByDealAndPriceBucketOrCpm(true)) : bucketBids.sort((a, b) => b.cpm - a.cpm); bids.push(...bucketBids.slice(0, adUnitBidLimit)); } else { + bucketBids = bucketBids.sort(winSorter) bids.push(...bucketBids); } }); @@ -124,6 +135,33 @@ export function sortByDealAndPriceBucketOrCpm(useCpm = false) { } } +/** + * Return a map where each code in `adUnitCodes` maps to a list of GPT slots that match it. + * + * @param {Array} adUnitCodes + * @param customSlotMatching + * @param getSlots + * @return {Object.} + */ +export function getGPTSlotsForAdUnits(adUnitCodes, customSlotMatching, getSlots = () => window.googletag.pubads().getSlots()) { + return getSlots().reduce((auToSlots, slot) => { + const customMatch = isFn(customSlotMatching) && customSlotMatching(slot); + Object.keys(auToSlots).filter(isFn(customMatch) ? customMatch : isAdUnitCodeMatchingSlot(slot)).forEach(au => auToSlots[au].push(slot)); + return auToSlots; + }, Object.fromEntries(adUnitCodes.map(au => [au, []]))); +} + +/** + * Clears targeting for bids + */ +function clearTargeting(slot) { + pbTargetingKeys.forEach(key => { + if (slot.getTargeting(key)) { + slot.clearTargeting(key) + } + }) +} + /** * @typedef {Object.} targeting * @property {string} targeting_key @@ -144,22 +182,11 @@ export function newTargeting(auctionManager) { targeting.resetPresetTargeting = function(adUnitCode, customSlotMatching) { if (isGptPubadsDefined()) { const adUnitCodes = getAdUnitCodes(adUnitCode); - const adUnits = auctionManager.getAdUnits().filter(adUnit => includes(adUnitCodes, adUnit.code)); - let unsetKeys = pbTargetingKeys.reduce((reducer, key) => { - reducer[key] = null; - return reducer; - }, {}); - window.googletag.pubads().getSlots().forEach(slot => { - let customSlotMatchingFunc = isFn(customSlotMatching) && customSlotMatching(slot); - // reset only registered adunits - adUnits.forEach(unit => { - if (unit.code === slot.getAdUnitPath() || - unit.code === slot.getSlotElementId() || - (isFn(customSlotMatchingFunc) && customSlotMatchingFunc(unit.code))) { - slot.updateTargetingFromMap(unsetKeys); - } - }); - }); + Object.values(getGPTSlotsForAdUnits(adUnitCodes, customSlotMatching)).forEach((slots) => { + slots.forEach(slot => { + clearTargeting(slot) + }) + }) } }; @@ -180,39 +207,33 @@ export function newTargeting(auctionManager) { }); }; - /** - * checks if bid has targeting set and belongs based on matching ad unit codes - * @return {boolean} true or false - */ - function bidShouldBeAddedToTargeting(bid, adUnitCodes) { - return bid.adserverTargeting && adUnitCodes && - ((isArray(adUnitCodes) && includes(adUnitCodes, bid.adUnitCode)) || - (typeof adUnitCodes === 'string' && bid.adUnitCode === adUnitCodes)); - }; + function addBidToTargeting(bids, enableSendAllBids = false, deals = false) { + const standardKeys = FEATURES.NATIVE ? TARGETING_KEYS_ARR.concat(NATIVE_TARGETING_KEYS) : TARGETING_KEYS_ARR.slice(); + const allowSendAllBidsTargetingKeys = config.getConfig('targetingControls.allowSendAllBidsTargetingKeys'); - /** - * Returns targeting for any bids which have deals if alwaysIncludeDeals === true - */ - function getDealBids(adUnitCodes, bidsReceived) { - if (config.getConfig('targetingControls.alwaysIncludeDeals') === true) { - const standardKeys = FEATURES.NATIVE ? TARGETING_KEYS_ARR.concat(NATIVE_TARGETING_KEYS) : TARGETING_KEYS_ARR.slice(); - - // we only want the top bid from bidders who have multiple entries per ad unit code - const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm); - - // populate targeting keys for the remaining bids if they have a dealId - return bids.map(bid => { - if (bid.dealId && bidShouldBeAddedToTargeting(bid, adUnitCodes)) { - return { - [bid.adUnitCode]: getTargetingMap(bid, standardKeys.filter( - key => typeof bid.adserverTargeting[key] !== 'undefined') - ) - }; + const allowedSendAllBidTargeting = allowSendAllBidsTargetingKeys + ? allowSendAllBidsTargetingKeys.map((key) => TARGETING_KEYS[key]) + : standardKeys; + + return bids.reduce((result, bid) => { + if (enableSendAllBids || (deals && bid.dealId)) { + const targetingValue = getTargetingMap(bid, standardKeys.filter( + key => typeof bid.adserverTargeting[key] !== 'undefined' && + (deals || allowedSendAllBidTargeting.indexOf(key) !== -1))); + + if (targetingValue) { + result.push({[bid.adUnitCode]: targetingValue}) } - }).filter(bid => bid); // removes empty elements in array - } - return []; - }; + } + return result; + }, []); + } + + function getBidderTargeting(bids) { + const alwaysIncludeDeals = config.getConfig('targetingControls.alwaysIncludeDeals'); + const enableSendAllBids = config.getConfig('enableSendAllBids'); + return addBidToTargeting(bids, enableSendAllBids, alwaysIncludeDeals); + } /** * Returns filtered ad server targeting for custom and allowed keys. @@ -261,26 +282,15 @@ export function newTargeting(auctionManager) { * @param {string=} adUnitCode * @return {Object.} targeting */ - targeting.getAllTargeting = function(adUnitCode, bidsReceived = getBidsReceived()) { + targeting.getAllTargeting = function(adUnitCode, bidLimit, bidsReceived, winReducer = getHighestCpm, winSorter = sortByHighestCpm) { + bidsReceived ||= getBidsReceived(winReducer, winSorter); const adUnitCodes = getAdUnitCodes(adUnitCode); - - // Get targeting for the winning bid. Add targeting for any bids that have - // `alwaysUseBid=true`. If sending all bids is enabled, add targeting for losing bids. - var targeting = getWinningBidTargeting(adUnitCodes, bidsReceived) - .concat(getCustomBidTargeting(adUnitCodes, bidsReceived)) - .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)) - .concat(getAdUnitTargeting(adUnitCodes)); - - // store a reference of the targeting keys - targeting.map(adUnitCode => { - Object.keys(adUnitCode).map(key => { - adUnitCode[key].map(targetKey => { - if (pbTargetingKeys.indexOf(Object.keys(targetKey)[0]) === -1) { - pbTargetingKeys = Object.keys(targetKey).concat(pbTargetingKeys); - } - }); - }); - }); + const sendAllBids = config.getConfig('enableSendAllBids'); + const bidLimitConfigValue = config.getConfig('sendBidsControl.bidLimit'); + const adUnitBidLimit = (sendAllBids && (bidLimit || bidLimitConfigValue)) || 0; + const { customKeysByUnit, filteredBids } = getfilteredBidsAndCustomKeys(adUnitCodes, bidsReceived); + const bidsSorted = getHighestCpmBidsFromBidPool(filteredBids, winReducer, adUnitBidLimit, undefined, winSorter); + let targeting = getTargetingLevels(bidsSorted, customKeysByUnit, adUnitCodes); const defaultKeys = Object.keys(Object.assign({}, DEFAULT_TARGETING_KEYS, NATIVE_KEYS)); let allowedKeys = config.getConfig(CFG_ALLOW_TARGETING_KEYS); @@ -316,6 +326,68 @@ export function newTargeting(auctionManager) { return targeting; }; + function updatePBTargetingKeys(adUnitCode) { + (Object.keys(adUnitCode)).forEach(key => { + adUnitCode[key].forEach(targetKey => { + const targetKeys = Object.keys(targetKey); + if (pbTargetingKeys.indexOf(targetKeys[0]) === -1) { + pbTargetingKeys = targetKeys.concat(pbTargetingKeys); + } + }); + }); + } + + function getTargetingLevels(bidsSorted, customKeysByUnit, adUnitCodes) { + const useAllBidsCustomTargeting = config.getConfig('targetingControls.allBidsCustomTargeting') !== false; + + const targeting = getWinningBidTargeting(bidsSorted, adUnitCodes) + .concat(getBidderTargeting(bidsSorted)) + .concat(getAdUnitTargeting(adUnitCodes)); + + if (useAllBidsCustomTargeting) { + targeting.push(...getCustomBidTargeting(bidsSorted, customKeysByUnit)) + } + + targeting.forEach(adUnitCode => { + updatePBTargetingKeys(adUnitCode); + }); + + return targeting; + } + + function getfilteredBidsAndCustomKeys(adUnitCodes, bidsReceived) { + const filteredBids = []; + const customKeysByUnit = {}; + const alwaysIncludeDeals = config.getConfig('targetingControls.alwaysIncludeDeals'); + + bidsReceived.forEach(bid => { + const adUnitIsEligible = includes(adUnitCodes, bid.adUnitCode); + const cpmAllowed = bidderSettings.get(bid.bidderCode, 'allowZeroCpmBids') === true ? bid.cpm >= 0 : bid.cpm > 0; + const isPreferredDeal = alwaysIncludeDeals && bid.dealId; + + if (adUnitIsEligible && (isPreferredDeal || cpmAllowed)) { + filteredBids.push(bid); + Object.keys(bid.adserverTargeting) + .filter(getCustomKeys()) + .forEach(key => { + const truncKey = key.substring(0, MAX_DFP_KEYLENGTH); + const data = customKeysByUnit[bid.adUnitCode] || {}; + const value = [bid.adserverTargeting[key]]; + + if (data[truncKey]) { + data[truncKey] = data[truncKey].concat(value).filter(uniques); + } else { + data[truncKey] = value; + } + + customKeysByUnit[bid.adUnitCode] = data; + }) + } + }); + + return {filteredBids, customKeysByUnit}; + } + // warn about conflicting configuration config.getConfig('targetingControls', function (config) { if (deepAccess(config, CFG_ALLOW_TARGETING_KEYS) != null && deepAccess(config, CFG_ADD_TARGETING_KEYS) != null) { @@ -407,35 +479,56 @@ export function newTargeting(auctionManager) { }; }).reduce((p, c) => Object.assign(c, p), {}) }; - }).reduce(function (accumulator, targeting) { + }) + + targetingObj = targetingObj.reduce(function (accumulator, targeting) { var key = Object.keys(targeting)[0]; accumulator[key] = Object.assign({}, accumulator[key], targeting[key]); return accumulator; }, {}); + return targetingObj; } - /** - * Sets targeting for DFP - * @param {Object.>} targetingConfig - */ - targeting.setTargetingForGPT = function(targetingConfig, customSlotMatching) { - window.googletag.pubads().getSlots().forEach(slot => { - Object.keys(targetingConfig).filter(customSlotMatching ? customSlotMatching(slot) : isAdUnitCodeMatchingSlot(slot)) - .forEach(targetId => { - Object.keys(targetingConfig[targetId]).forEach(key => { - let value = targetingConfig[targetId][key]; - if (typeof value === 'string' && value.indexOf(',') !== -1) { - // due to the check the array will be formed only if string has ',' else plain string will be assigned as value - value = value.split(','); - } - targetingConfig[targetId][key] = value; - }); - logMessage(`Attempting to set targeting-map for slot: ${slot.getSlotElementId()} with targeting-map:`, targetingConfig[targetId]); - slot.updateTargetingFromMap(targetingConfig[targetId]) - }) + targeting.setTargetingForGPT = hook('sync', function (adUnit, customSlotMatching) { + // get our ad unit codes + let targetingSet = targeting.getAllTargeting(adUnit); + + let resetMap = Object.fromEntries(pbTargetingKeys.map(key => [key, null])); + + Object.entries(getGPTSlotsForAdUnits(Object.keys(targetingSet), customSlotMatching)).forEach(([targetId, slots]) => { + slots.forEach(slot => { + // now set new targeting keys + Object.keys(targetingSet[targetId]).forEach(key => { + let value = targetingSet[targetId][key]; + if (typeof value === 'string' && value.indexOf(',') !== -1) { + // due to the check the array will be formed only if string has ',' else plain string will be assigned as value + value = value.split(','); + } + targetingSet[targetId][key] = value; + }); + logMessage(`Attempting to set targeting-map for slot: ${slot.getSlotElementId()} with targeting-map:`, targetingSet[targetId]); + slot.updateTargetingFromMap(Object.assign({}, resetMap, targetingSet[targetId])) + }) }) - }; + + Object.keys(targetingSet).forEach((adUnitCode) => { + Object.keys(targetingSet[adUnitCode]).forEach((targetingKey) => { + if (targetingKey === 'hb_adid') { + auctionManager.setStatusForBids(targetingSet[adUnitCode][targetingKey], BID_STATUS.BID_TARGETING_SET); + } + }); + }); + + targeting.targetingDone(targetingSet); + + // emit event + events.emit(EVENTS.SET_TARGETING, targetingSet); + }, 'setTargetingForGPT'); + + targeting.targetingDone = hook('sync', function (targetingSet) { + return targetingSet; + }, 'targetingDone'); /** * normlizes input to a `adUnit.code` array @@ -451,41 +544,38 @@ export function newTargeting(auctionManager) { return auctionManager.getAdUnitCodes() || []; } - function getBidsReceived() { - let bidsReceived = auctionManager.getBidsReceived(); - - if (!config.getConfig('useBidCache')) { - // don't use bid cache (i.e. filter out bids not in the latest auction) - bidsReceived = bidsReceived.filter(bid => latestAuctionForAdUnit[bid.adUnitCode] === bid.auctionId) - } else { - // if custom bid cache filter function exists, run for each bid from - // previous auctions. If it returns true, include bid in bid pool + function getBidsReceived(winReducer = getOldestHighestCpmBid, winSorter = undefined) { + let bidsReceived = auctionManager.getBidsReceived().reduce((bids, bid) => { + const bidCacheEnabled = config.getConfig('useBidCache'); const filterFunction = config.getConfig('bidCacheFilterFunction'); - if (typeof filterFunction === 'function') { - bidsReceived = bidsReceived.filter(bid => latestAuctionForAdUnit[bid.adUnitCode] === bid.auctionId || !!filterFunction(bid)) - } - } + const isBidFromLastAuction = latestAuctionForAdUnit[bid.adUnitCode] === bid.auctionId; + const filterFunctionResult = bidCacheEnabled && !isBidFromLastAuction && typeof filterFunction === 'function' ? !!filterFunction(bid) : true; + const cacheFilter = bidCacheEnabled || isBidFromLastAuction; + const bidFilter = cacheFilter && filterFunctionResult; - bidsReceived = bidsReceived - .filter(bid => deepAccess(bid, 'video.context') !== ADPOD) - .filter(isBidUsable); - - bidsReceived - .forEach(bid => { + if (bidFilter && deepAccess(bid, 'video.context') !== ADPOD && isBidUsable(bid)) { bid.latestTargetedAuctionId = latestAuctionForAdUnit[bid.adUnitCode]; - return bid; - }); + bids.push(bid) + } + + return bids; + }, []); - return getHighestCpmBidsFromBidPool(bidsReceived, getOldestHighestCpmBid); + return getHighestCpmBidsFromBidPool(bidsReceived, winReducer, undefined, undefined, undefined, winSorter); } /** * Returns top bids for a given adUnit or set of adUnits. * @param {(string|string[])} adUnitCode adUnitCode or array of adUnitCodes - * @return {[type]} [description] + * @param {(Array|undefined)} bids - The received bids, defaulting to the result of getBidsReceived(). + * @param {function(Array): Array} [winReducer = getHighestCpm] - reducer method + * @param {function(Array): Array} [winSorter = sortByHighestCpm] - sorter method + * @return {Array} - An array of winning bids. */ - targeting.getWinningBids = function(adUnitCode, bidsReceived = getBidsReceived()) { + targeting.getWinningBids = function(adUnitCode, bids, winReducer = getHighestCpm, winSorter = sortByHighestCpm) { + const bidsReceived = bids || getBidsReceived(winReducer, winSorter); const adUnitCodes = getAdUnitCodes(adUnitCode); + return bidsReceived .filter(bid => includes(adUnitCodes, bid.adUnitCode)) .filter(bid => (bidderSettings.get(bid.bidderCode, 'allowZeroCpmBids') === true) ? bid.cpm >= 0 : bid.cpm > 0) @@ -530,10 +620,11 @@ export function newTargeting(auctionManager) { /** * Get targeting key value pairs for winning bid. - * @param {string[]} adUnitCodes code array - * @return {targetingArray} winning bids targeting + * @param {Array} bidsReceived code array + * @param {string[]} adUnitCodes code array + * @return {targetingArray} winning bids targeting */ - function getWinningBidTargeting(adUnitCodes, bidsReceived) { + function getWinningBidTargeting(bidsReceived, adUnitCodes) { let winners = targeting.getWinningBids(adUnitCodes, bidsReceived); let standardKeys = getStandardKeys(); @@ -566,44 +657,6 @@ export function newTargeting(auctionManager) { .concat(TARGETING_KEYS_ARR).filter(uniques); // standard keys defined in the library. } - /** - * Merge custom adserverTargeting with same key name for same adUnitCode. - * e.g: Appnexus defining custom keyvalue pair foo:bar and Rubicon defining custom keyvalue pair foo:baz will be merged to foo: ['bar','baz'] - * - * @param {Object[]} acc Accumulator for reducer. It will store updated bidResponse objects - * @param {Object} bid BidResponse - * @param {number} index current index - * @param {Array} arr original array - */ - function mergeAdServerTargeting(acc, bid, index, arr) { - function concatTargetingValue(key) { - return function(currentBidElement) { - if (!isArray(currentBidElement.adserverTargeting[key])) { - currentBidElement.adserverTargeting[key] = [currentBidElement.adserverTargeting[key]]; - } - currentBidElement.adserverTargeting[key] = currentBidElement.adserverTargeting[key].concat(bid.adserverTargeting[key]).filter(uniques); - delete bid.adserverTargeting[key]; - } - } - - function hasSameAdunitCodeAndKey(key) { - return function(currentBidElement) { - return currentBidElement.adUnitCode === bid.adUnitCode && currentBidElement.adserverTargeting[key] - } - } - - Object.keys(bid.adserverTargeting) - .filter(getCustomKeys()) - .forEach(key => { - if (acc.length) { - acc.filter(hasSameAdunitCodeAndKey(key)) - .forEach(concatTargetingValue(key)); - } - }); - acc.push(bid); - return acc; - } - function getCustomKeys() { let standardKeys = getStandardKeys(); if (FEATURES.NATIVE) { @@ -614,73 +667,44 @@ export function newTargeting(auctionManager) { } } - function truncateCustomKeys(bid) { - return { - [bid.adUnitCode]: Object.keys(bid.adserverTargeting) - // Get only the non-standard keys of the losing bids, since we - // don't want to override the standard keys of the winning bid. - .filter(getCustomKeys()) - .map(key => { - return { - [key.substring(0, MAX_DFP_KEYLENGTH)]: [bid.adserverTargeting[key]] - }; - }) - } - } - /** * Get custom targeting key value pairs for bids. - * @param {string[]} adUnitCodes code array - * @return {targetingArray} bids with custom targeting defined in bidderSettings - */ - function getCustomBidTargeting(adUnitCodes, bidsReceived) { - return bidsReceived - .filter(bid => includes(adUnitCodes, bid.adUnitCode)) - .map(bid => Object.assign({}, bid)) - .reduce(mergeAdServerTargeting, []) - .map(truncateCustomKeys) - .filter(bid => bid); // removes empty elements in array; - } - - /** - * Get targeting key value pairs for non-winning bids. - * @param {string[]} adUnitCodes code array - * @return {targetingArray} all non-winning bids targeting + * @param {Array} bidsSorted code array + * @param {Object} customKeysByUnit code array + * @return {targetingArray} bids with custom targeting defined in bidderSettings */ - function getBidLandscapeTargeting(adUnitCodes, bidsReceived) { - const standardKeys = FEATURES.NATIVE ? TARGETING_KEYS_ARR.concat(NATIVE_TARGETING_KEYS) : TARGETING_KEYS_ARR.slice(); - const adUnitBidLimit = config.getConfig('sendBidsControl.bidLimit'); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, adUnitBidLimit); - const allowSendAllBidsTargetingKeys = config.getConfig('targetingControls.allowSendAllBidsTargetingKeys'); + function getCustomBidTargeting(bidsSorted, customKeysByUnit) { + return bidsSorted + .reduce((acc, bid) => { + const newBid = Object.assign({}, bid); + const customKeysForUnit = customKeysByUnit[newBid.adUnitCode]; + const targeting = []; + + if (customKeysForUnit) { + Object.keys(customKeysForUnit).forEach(key => { + if (key && customKeysForUnit[key]) targeting.push({[key]: customKeysForUnit[key]}); + }) + } - const allowedSendAllBidTargeting = allowSendAllBidsTargetingKeys - ? allowSendAllBidsTargetingKeys.map((key) => TARGETING_KEYS[key]) - : standardKeys; + acc.push({[newBid.adUnitCode]: targeting}); - // populate targeting keys for the remaining bids - return bids.map(bid => { - if (bidShouldBeAddedToTargeting(bid, adUnitCodes)) { - return { - [bid.adUnitCode]: getTargetingMap(bid, standardKeys.filter( - key => typeof bid.adserverTargeting[key] !== 'undefined' && - allowedSendAllBidTargeting.indexOf(key) !== -1) - ) - }; - } - }).filter(bid => bid); // removes empty elements in array + return acc; + }, []); } function getTargetingMap(bid, keys) { - return keys.map(key => { - return { - [`${key}_${bid.bidderCode}`.substring(0, MAX_DFP_KEYLENGTH)]: [bid.adserverTargeting[key]] - }; - }); + return keys.reduce((targeting, key) => { + const value = bid.adserverTargeting[key]; + if (value) { + targeting.push({[`${key}_${bid.bidderCode}`.substring(0, MAX_DFP_KEYLENGTH)]: [bid.adserverTargeting[key]]}) + } + return targeting; + }, []); } function getAdUnitTargeting(adUnitCodes) { function getTargetingObj(adUnit) { - return deepAccess(adUnit, JSON_MAPPING.ADSERVER_TARGETING); + return adUnit?.[JSON_MAPPING.ADSERVER_TARGETING]; } function getTargetingValues(adUnit) { @@ -695,10 +719,13 @@ export function newTargeting(auctionManager) { } return auctionManager.getAdUnits() - .filter(adUnit => includes(adUnitCodes, adUnit.code) && getTargetingObj(adUnit)) - .map(adUnit => { - return {[adUnit.code]: getTargetingValues(adUnit)} - }); + .filter(adUnit => adUnitCodes.includes(adUnit.code) && getTargetingObj(adUnit)) + .reduce((result, adUnit) => { + const targetingValues = getTargetingValues(adUnit); + + if (targetingValues)result.push({[adUnit.code]: targetingValues}); + return result; + }, []); } targeting.isApntagDefined = function() { diff --git a/src/userSync.js b/src/userSync.js index 1b684de6de0..d8f2238007d 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -25,7 +25,7 @@ export const USERSYNC_DEFAULT_CONFIG = { }, syncsPerBidder: 5, syncDelay: 3000, - auctionDelay: 0 + auctionDelay: 500 }; // Set userSync default values diff --git a/src/utils.js b/src/utils.js index abb69209020..263be7a8bbe 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,11 +1,12 @@ import {config} from './config.js'; -import clone from 'just-clone'; +import {klona} from 'klona/json'; import {includes} from './polyfill.js'; -import { EVENTS, S2S } from './constants.js'; -import {GreedyPromise} from './utils/promise.js'; +import {EVENTS} from './constants.js'; +import {PbPromise} from './utils/promise.js'; import {getGlobal} from './prebidGlobal.js'; +import { default as deepAccess } from 'dlv/index.js'; -export { default as deepAccess } from 'dlv/index.js'; +export { deepAccess }; export { dset as deepSetValue } from 'dset'; var tStr = 'String'; @@ -21,6 +22,7 @@ let consoleWarnExists = Boolean(consoleExists && window.console.warn); let consoleErrorExists = Boolean(consoleExists && window.console.error); let eventEmitter; +let windowDimensions; const pbjsInstance = getGlobal(); @@ -35,12 +37,61 @@ function emitEvent(...args) { } } +export const getWinDimensions = (function() { + let lastCheckTimestamp; + const CHECK_INTERVAL_MS = 20; + return () => { + if (!windowDimensions || !lastCheckTimestamp || (Date.now() - lastCheckTimestamp > CHECK_INTERVAL_MS)) { + internal.resetWinDimensions(); + lastCheckTimestamp = Date.now(); + } + return windowDimensions; + } +})(); + +export function resetWinDimensions() { + const top = canAccessWindowTop() ? internal.getWindowTop() : internal.getWindowSelf(); + + windowDimensions = { + screen: { + width: top.screen?.width, + height: top.screen?.height, + availWidth: top.screen?.availWidth, + availHeight: top.screen?.availHeight, + colorDepth: top.screen?.colorDepth, + }, + innerHeight: top.innerHeight, + innerWidth: top.innerWidth, + outerWidth: top.outerWidth, + outerHeight: top.outerHeight, + visualViewport: { + height: top.visualViewport?.height, + width: top.visualViewport?.width, + }, + document: { + documentElement: { + clientWidth: top.document?.documentElement?.clientWidth, + clientHeight: top.document?.documentElement?.clientHeight, + scrollTop: top.document?.documentElement?.scrollTop, + scrollLeft: top.document?.documentElement?.scrollLeft, + }, + body: { + scrollTop: document.body?.scrollTop, + scrollLeft: document.body?.scrollLeft, + clientWidth: document.body?.clientWidth, + clientHeight: document.body?.clientHeight, + }, + } + }; +} + // this allows stubbing of utility functions that are used internally by other utility functions export const internal = { checkCookieSupport, createTrackPixelIframeHtml, getWindowSelf, getWindowTop, + canAccessWindowTop, getWindowLocation, insertUserSyncIframe, insertElement, @@ -52,7 +103,8 @@ export const internal = { logInfo, parseQS, formatQS, - deepEqual + deepEqual, + resetWinDimensions }; let prebidInternal = {}; @@ -129,37 +181,55 @@ export function transformAdServerTargetingObj(targeting) { } /** - * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' - * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] - * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` + * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of width, height tuples `[[300, 250]]` or '[[300,250], [970,90]]' */ -export function parseSizesInput(sizeObj) { - if (typeof sizeObj === 'string') { +export function sizesToSizeTuples(sizes) { + if (typeof sizes === 'string') { // multiple sizes will be comma-separated - return sizeObj.split(',').filter(sz => sz.match(/^(\d)+x(\d)+$/i)) - } else if (typeof sizeObj === 'object') { - if (sizeObj.length === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { - return [parseGPTSingleSizeArray(sizeObj)]; - } else { - return sizeObj.map(parseGPTSingleSizeArray) + return sizes + .split(/\s*,\s*/) + .map(sz => sz.match(/^(\d+)x(\d+)$/i)) + .filter(match => match) + .map(([_, w, h]) => [parseInt(w, 10), parseInt(h, 10)]) + } else if (Array.isArray(sizes)) { + if (isValidGPTSingleSize(sizes)) { + return [sizes] } + return sizes.filter(isValidGPTSingleSize); } return []; } +/** + * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' + * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] + * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` + */ +export function parseSizesInput(sizeObj) { + return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString); +} + +export function sizeTupleToSizeString(size) { + return size[0] + 'x' + size[1] +} + // Parse a GPT style single size array, (i.e [300, 250]) // into an AppNexus style string, (i.e. 300x250) export function parseGPTSingleSizeArray(singleSize) { if (isValidGPTSingleSize(singleSize)) { - return singleSize[0] + 'x' + singleSize[1]; + return sizeTupleToSizeString(singleSize); } } +export function sizeTupleToRtbSize(size) { + return {w: size[0], h: size[1]}; +} + // Parse a GPT style single size array, (i.e [300, 250]) // into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250}) export function parseGPTSingleSizeArrayToRtbSize(singleSize) { if (isValidGPTSingleSize(singleSize)) { - return {w: singleSize[0], h: singleSize[1]}; + return sizeTupleToRtbSize(singleSize) } } @@ -180,6 +250,20 @@ export function getWindowLocation() { return window.location; } +export function getDocument() { + return document; +} + +export function canAccessWindowTop() { + try { + if (internal.getWindowTop().location.href) { + return true; + } + } catch (e) { + return false; + } +} + /** * Wrappers to console.(log | info | warn | error). Takes N arguments, the same as the native methods */ @@ -353,7 +437,8 @@ export function isEmptyStr(str) { * Iterate object with the function * falls back to es5 `forEach` * @param {Array|Object} object - * @param {Function(value, key, object)} fn + * @param {Function} fn - The function to execute for each element. It receives three arguments: value, key, and the original object. + * @returns {void} */ export function _each(object, fn) { if (isFn(object?.forEach)) return object.forEach(fn, this); @@ -368,7 +453,7 @@ export function contains(a, obj) { * Map an array or object into another array * given a function * @param {Array|Object} object - * @param {Function(value, key, object)} callback + * @param {Function} callback - The function to execute for each element. It receives three arguments: value, key, and the original object. * @return {Array} */ export function _map(object, callback) { @@ -412,7 +497,7 @@ export function insertElement(elm, doc, target, asLastChildChild) { */ export function waitForElementToLoad(element, timeout) { let timer = null; - return new GreedyPromise((resolve) => { + return new PbPromise((resolve) => { const onLoad = function() { element.removeEventListener('load', onLoad); element.removeEventListener('error', onLoad); @@ -443,12 +528,6 @@ export function triggerPixel(url, done, timeout) { img.src = url; } -export function callBurl({ source, burl }) { - if (source === S2S.SRC && burl) { - internal.triggerPixel(burl); - } -} - /** * Inserts an empty iframe with the specified `html`, primarily used for tracking purposes * (though could be for other purposes) @@ -471,7 +550,6 @@ export function insertHtmlIntoIframe(htmlCode) { /** * Inserts empty iframe with the specified `url` for cookie sync * @param {string} url URL to be requested - * @param {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process * @param {Number} [timeout] an optional timeout in milliseconds for the iframe to load before calling `done` */ @@ -582,6 +660,10 @@ export function isApnGetTagDefined() { } } +export const sortByHighestCpm = (a, b) => { + return b.cpm - a.cpm; +} + /** * Fisher–Yates shuffle * http://stackoverflow.com/a/6274398 @@ -609,7 +691,7 @@ export function shuffle(array) { } export function deepClone(obj) { - return clone(obj); + return klona(obj) || {}; } export function inIframe() { @@ -620,6 +702,33 @@ export function inIframe() { } } +/** + * https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf + */ +export function isSafeFrameWindow() { + if (!inIframe()) { + return false; + } + + const ws = internal.getWindowSelf(); + return !!(ws.$sf && ws.$sf.ext); +} + +/** + * Returns the result of calling the function $sf.ext.geom() if it exists + * @see https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf — 5.4 Function $sf.ext.geom + * @returns {Object | undefined} geometric information about the container + */ +export function getSafeframeGeometry() { + try { + const ws = getWindowSelf(); + return (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : undefined; + } catch (e) { + logError('Error getting SafeFrame geometry', e); + return undefined; + } +} + export function isSafariBrowser() { return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); } @@ -652,6 +761,33 @@ export function getPerformanceNow() { return (window.performance && window.performance.now && window.performance.now()) || 0; } +/** + * Retuns the difference between `timing.domLoading` and `timing.navigationStart`. + * This function uses the deprecated `Performance.timing` API and should be removed in future. + * It has not been updated yet because it is still used in some modules. + * @deprecated + * @param {Window} w The window object used to perform the api call. default to window.self + * @returns {number} + */ +export function getDomLoadingDuration(w) { + let domLoadingDuration = -1; + + w = w || getWindowSelf(); + + const performance = w.performance; + + if (w.performance?.timing) { + if (w.performance.timing.navigationStart > 0) { + const val = performance.timing.domLoading - performance.timing.navigationStart; + if (val > 0) { + domLoadingDuration = val; + } + } + } + + return domLoadingDuration; +} + /** * When the deviceAccess flag config option is false, no cookies should be read or set * @returns {boolean} @@ -664,6 +800,7 @@ export function hasDeviceAccess() { * @returns {(boolean|undefined)} */ export function checkCookieSupport() { + // eslint-disable-next-line no-restricted-properties if (window.navigator.cookieEnabled || !!document.cookie.length) { return true; } @@ -696,7 +833,6 @@ export function delayExecution(func, numRequiredCalls) { /** * https://stackoverflow.com/a/34890276/428704 - * @export * @param {Array} xs * @param {string} key * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}} @@ -912,67 +1048,107 @@ export function buildUrl(obj) { * This function deeply compares two objects checking for their equivalence. * @param {Object} obj1 * @param {Object} obj2 - * @param checkTypes {boolean} if set, two objects with identical properties but different constructors will *not* - * be considered equivalent. - * @returns {boolean} + * @param {Object} [options] - Options for comparison. + * @param {boolean} [options.checkTypes=false] - If set, two objects with identical properties but different constructors will *not* be considered equivalent. + * @returns {boolean} - Returns `true` if the objects are equivalent, `false` otherwise. */ -export function deepEqual(obj1, obj2, {checkTypes = false} = {}) { +export function deepEqual(obj1, obj2, { checkTypes = false } = {}) { + // Quick reference check if (obj1 === obj2) return true; - else if ( - (typeof obj1 === 'object' && obj1 !== null) && - (typeof obj2 === 'object' && obj2 !== null) && - (!checkTypes || (obj1.constructor === obj2.constructor)) + + // If either is null or not an object, do a direct equality check + if ( + typeof obj1 !== 'object' || obj1 === null || + typeof obj2 !== 'object' || obj2 === null ) { - const props1 = Object.keys(obj1); - if (props1.length !== Object.keys(obj2).length) return false; - for (let prop of props1) { - if (obj2.hasOwnProperty(prop)) { - if (!deepEqual(obj1[prop], obj2[prop], {checkTypes})) { - return false; - } - } else { + return false; + } + // Cache the Array checks + const isArr1 = Array.isArray(obj1); + const isArr2 = Array.isArray(obj2); + // Special case: both are arrays + if (isArr1 && isArr2) { + if (obj1.length !== obj2.length) return false; + for (let i = 0; i < obj1.length; i++) { + if (!deepEqual(obj1[i], obj2[i], { checkTypes })) { return false; } } return true; - } else { + } else if (isArr1 || isArr2) { + return false; + } + + // If we’re checking types, compare constructors (e.g., plain object vs. Date) + if (checkTypes && obj1.constructor !== obj2.constructor) { return false; } + + // Compare object keys. Cache keys for both to avoid repeated calls. + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + if (keys1.length !== keys2.length) return false; + + for (const key of keys1) { + // If `obj2` doesn't have this key or sub-values aren't equal, bail out. + if (!Object.prototype.hasOwnProperty.call(obj2, key)) { + return false; + } + if (!deepEqual(obj1[key], obj2[key], { checkTypes })) { + return false; + } + } + + return true; } export function mergeDeep(target, ...sources) { - if (!sources.length) return target; - const source = sources.shift(); - - if (isPlainObject(target) && isPlainObject(source)) { - for (const key in source) { - if (isPlainObject(source[key])) { - if (!target[key]) Object.assign(target, { [key]: {} }); - mergeDeep(target[key], source[key]); - } else if (isArray(source[key])) { - if (!target[key]) { - Object.assign(target, { [key]: [...source[key]] }); - } else if (isArray(target[key])) { - source[key].forEach(obj => { - let addItFlag = 1; - for (let i = 0; i < target[key].length; i++) { - if (deepEqual(target[key][i], obj)) { - addItFlag = 0; - break; - } - } - if (addItFlag) { - target[key].push(obj); - } - }); - } + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + if (!isPlainObject(source)) { + continue; + } + mergeDeepHelper(target, source); + } + return target; +} + +function mergeDeepHelper(target, source) { + // quick check + if (!isPlainObject(target) || !isPlainObject(source)) { + return; + } + + const keys = Object.keys(source); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key === '__proto__' || key === 'constructor') { + continue; + } + const val = source[key]; + + if (isPlainObject(val)) { + if (!target[key]) { + target[key] = {}; + } + mergeDeepHelper(target[key], val); + } else if (Array.isArray(val)) { + if (!Array.isArray(target[key])) { + target[key] = [...val]; } else { - Object.assign(target, { [key]: source[key] }); + // deduplicate + val.forEach(obj => { + if (!target[key].some(item => deepEqual(item, obj))) { + target[key].push(obj); + } + }); } + } else { + // direct assignment + target[key] = val; } } - - return mergeDeep(target, ...sources); } /** @@ -1026,6 +1202,14 @@ export function safeJSONParse(data) { } catch (e) {} } +export function safeJSONEncode(data) { + try { + return JSON.stringify(data); + } catch (e) { + return ''; + } +} + /** * Returns a memoized version of `fn`. * @@ -1047,9 +1231,36 @@ export function memoize(fn, key = function (arg) { return arg; }) { return memoized; } +/** + * Returns a Unix timestamp for given time value and unit. + * @param {number} timeValue numeric value, defaults to 0 (which means now) + * @param {string} timeUnit defaults to days (or 'd'), use 'm' for minutes. Any parameter that isn't 'd' or 'm' will return Date.now(). + * @returns {number} + */ +export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') { + const acceptableUnits = ['m', 'd']; + if (acceptableUnits.indexOf(timeUnit) < 0) { + return Date.now(); + } + const multiplication = timeValue / (timeUnit === 'm' ? 1440 : 1); + return Date.now() + (timeValue && timeValue > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0); +} + +/** + * Converts given object into an array, so {key: 1, anotherKey: 'fred', third: ['fred']} is turned + * into [{key: 1}, {anotherKey: 'fred'}, {third: ['fred']}] + * @param {Object} obj the object + * @returns {Array} + */ +export function convertObjectToArray(obj) { + return Object.keys(obj).map(key => { + return {[key]: obj[key]}; + }); +} + /** * Sets dataset attributes on a script - * @param {Script} script + * @param {HTMLScriptElement} script * @param {object} attributes */ export function setScriptAttributes(script, attributes) { @@ -1083,3 +1294,87 @@ export function binarySearch(arr, el, key = (el) => el) { } return left; } + +/** + * Checks if an object has non-serializable properties. + * Non-serializable properties are functions and RegExp objects. + * + * @param {Object} obj - The object to check. + * @param {Set} checkedObjects - A set of properties that have already been checked. + * @returns {boolean} - Returns true if the object has non-serializable properties, false otherwise. + */ +export function hasNonSerializableProperty(obj, checkedObjects = new Set()) { + for (const key in obj) { + const value = obj[key]; + const type = typeof value; + + if ( + value === undefined || + type === 'function' || + type === 'symbol' || + value instanceof RegExp || + value instanceof Map || + value instanceof Set || + value instanceof Date || + (value !== null && type === 'object' && value.hasOwnProperty('toJSON')) + ) { + return true; + } + if (value !== null && type === 'object' && value.constructor === Object) { + if (checkedObjects.has(value)) { + // circular reference, means we have a non-serializable property + return true; + } + checkedObjects.add(value); + if (hasNonSerializableProperty(value, checkedObjects)) { + return true; + } + } + } + return false; +} + +/** + * Returns the value of a nested property in an array of objects. + * + * @param {Array} collection - Array of objects. + * @param {String} key - Key of nested property. + * @returns {any|undefined} - Value of nested property. + */ +export function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = deepAccess(collection[i], key); + if (result) { + return result; + } + } + return undefined; +} + +export function extractDomainFromHost(pageHost) { + let domain = null; + try { + let domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); + if (domains != null && domains.length > 0) { + domain = domains[0]; + for (let i = 1; i < domains.length; i++) { + if (domains[i].length > domain.length) { + domain = domains[i]; + } + } + } + } catch (e) { + domain = null; + } + return domain; +} + +export function triggerNurlWithCpm(bid, cpm) { + if (isStr(bid.nurl) && bid.nurl !== '') { + bid.nurl = bid.nurl.replace( + /\${AUCTION_PRICE}/, + cpm + ); + triggerPixel(bid.nurl); + } +} diff --git a/src/utils/focusTimeout.js b/src/utils/focusTimeout.js new file mode 100644 index 00000000000..0c54bacec97 --- /dev/null +++ b/src/utils/focusTimeout.js @@ -0,0 +1,52 @@ +let outOfFocusStart = null; // enforce null otherwise it could be undefined and the callback wouldn't execute +let timeOutOfFocus = 0; +let suspendedTimeouts = []; + +function trackTimeOutOfFocus() { + if (document.hidden) { + outOfFocusStart = Date.now() + } else { + timeOutOfFocus += Date.now() - (outOfFocusStart ?? 0); // when the page is loaded in hidden state outOfFocusStart is undefined, which results in timeoutOffset being NaN + outOfFocusStart = null; + suspendedTimeouts.forEach(({ callback, startTime, setTimerId }) => setTimerId(setFocusTimeout(callback, timeOutOfFocus - startTime)())); + suspendedTimeouts = []; + } +} + +document.addEventListener('visibilitychange', trackTimeOutOfFocus); + +export function reset() { + outOfFocusStart = null; + timeOutOfFocus = 0; + suspendedTimeouts = []; + document.removeEventListener('visibilitychange', trackTimeOutOfFocus); + document.addEventListener('visibilitychange', trackTimeOutOfFocus); +} + +/** + * Wraps native setTimeout function in order to count time only when page is focused + * + * @param {function(*): ()} [callback] - A function that will be invoked after passed time + * @param {number} [milliseconds] - Minimum duration (in milliseconds) that the callback will be executed after + * @returns {function(*): (number)} - Getter function for current timer id + */ +export function setFocusTimeout(callback, milliseconds) { + const startTime = timeOutOfFocus; + let timerId = setTimeout(() => { + if (timeOutOfFocus === startTime && outOfFocusStart == null) { + callback(); + } else if (outOfFocusStart != null) { + // case when timeout ended during page is out of focus + suspendedTimeouts.push({ + callback, + startTime, + setTimerId(newId) { + timerId = newId; + } + }) + } else { + timerId = setFocusTimeout(callback, timeOutOfFocus - startTime)(); + } + }, milliseconds); + return () => timerId; +} diff --git a/src/utils/gpdr.js b/src/utils/gdpr.js similarity index 72% rename from src/utils/gpdr.js rename to src/utils/gdpr.js index 19c7126b7d7..6151861a67b 100644 --- a/src/utils/gpdr.js +++ b/src/utils/gdpr.js @@ -1,5 +1,3 @@ -import {deepAccess} from '../utils.js'; - /** * Check if GDPR purpose 1 consent was given. * @@ -8,7 +6,7 @@ import {deepAccess} from '../utils.js'; */ export function hasPurpose1Consent(gdprConsent) { if (gdprConsent?.gdprApplies) { - return deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true; + return gdprConsent?.vendorData?.purpose?.consents?.[1] === true; } return true; } diff --git a/src/utils/ipUtils.js b/src/utils/ipUtils.js new file mode 100644 index 00000000000..600c0e75087 --- /dev/null +++ b/src/utils/ipUtils.js @@ -0,0 +1,52 @@ +export function scrubIPv4(ip) { + if (!ip) { + return null; + } + + const ones = 24; + + let ipParts = ip.split('.').map(Number) + + if (ipParts.length != 4) { + return null; + } + + let mask = []; + for (let i = 0; i < 4; i++) { + let n = Math.max(0, Math.min(8, ones - (i * 8))); + mask.push((0xff << (8 - n)) & 0xff); + } + + let maskedIP = ipParts.map((part, i) => part & mask[i]); + + return maskedIP.join('.'); +} + +export function scrubIPv6(ip) { + if (!ip) { + return null; + } + + const ones = 64; + + let ipParts = ip.split(':').map(part => parseInt(part, 16)); + + ipParts = ipParts.map(part => isNaN(part) ? 0 : part); + while (ipParts.length < 8) { + ipParts.push(0); + } + + if (ipParts.length != 8) { + return null; + } + + let mask = []; + for (let i = 0; i < 8; i++) { + let n = Math.max(0, Math.min(16, ones - (i * 16))); + mask.push((0xffff << (16 - n)) & 0xffff); + } + + let maskedIP = ipParts.map((part, i) => part & mask[i]); + + return maskedIP.map(part => part.toString(16)).join(':'); +} diff --git a/src/utils/perfMetrics.js b/src/utils/perfMetrics.js index b1fdb38effe..220b6084d1e 100644 --- a/src/utils/perfMetrics.js +++ b/src/utils/perfMetrics.js @@ -63,9 +63,9 @@ export function metricsFactory({now = getTime, mkNode = makeNode, mkTimer = make /** * Get the tame passed since `checkpoint`, and optionally save it as a metric. * - * @param checkpoint checkpoint name - * @param metric? metric name - * @return {number} time between now and `checkpoint` + * @param {string} checkpoint checkpoint name + * @param {string} [metric] - The name of the metric to save. Optional. + * @returns {number|null} - The time in milliseconds between now and the checkpoint, or `null` if the checkpoint is not found. */ function timeSince(checkpoint, metric) { const ts = getTimestamp(checkpoint); @@ -79,10 +79,10 @@ export function metricsFactory({now = getTime, mkNode = makeNode, mkTimer = make /** * Get the time passed between `startCheckpoint` and `endCheckpoint`, optionally saving it as a metric. * - * @param startCheckpoint begin checkpoint - * @param endCheckpoint end checkpoint - * @param metric? metric name - * @return {number} time passed between `startCheckpoint` and `endCheckpoint` + * @param {string} startCheckpoint - The name of the starting checkpoint. + * @param {string} endCheckpoint - The name of the ending checkpoint. + * @param {string} [metric] - The name of the metric to save. Optional. + * @returns {number|null} - The time in milliseconds between `startCheckpoint` and `endCheckpoint`, or `null` if either checkpoint is not found. */ function timeBetween(startCheckpoint, endCheckpoint, metric) { const start = getTimestamp(startCheckpoint); @@ -128,12 +128,12 @@ export function metricsFactory({now = getTime, mkNode = makeNode, mkTimer = make } /** - * @typedef {function: T} HookFn - * @property {function(T): void} bail + * @typedef {Function} HookFn + * @property {Function(T): void} bail * * @template T - * @typedef {T: HookFn} TimedHookFn - * @property {function(): void} stopTiming + * @typedef {HookFn} TimedHookFn + * @property {Function(): void} stopTiming * @property {T} untimed */ @@ -141,12 +141,12 @@ export function metricsFactory({now = getTime, mkNode = makeNode, mkTimer = make * Convenience method for measuring time spent in a `.before` or `.after` hook. * * @template T - * @param name metric name - * @param {HookFn} next the hook's `next` (first) argument - * @param {function(TimedHookFn): T} fn a function that will be run immediately; it takes `next`, + * @param {string} name - The metric name. + * @param {HookFn} next - The hook's `next` (first) argument. + * @param {function(TimedHookFn): T} fn - A function that will be run immediately; it takes `next`, * where both `next` and `next.bail` automatically * call `stopTiming` before continuing with the original hook. - * @return {T} fn's return value + * @return {T} - The return value of `fn`. */ function measureHookTime(name, next, fn) { const stopTiming = startTiming(name); @@ -208,10 +208,11 @@ export function metricsFactory({now = getTime, mkNode = makeNode, mkTimer = make * ``` * * - * @param propagate if false, the forked metrics will not be propagated here - * @param stopPropagation if true, propagation from the new metrics is stopped here - instead of - * continuing up the chain (if for example these metrics were themselves created through `.fork()`) - * @param includeGroups if true, the forked metrics will also replicate metrics that were propagated + * @param {Object} [options={}] - Options for forking the metrics. + * @param {boolean} [options.propagate=true] - If false, the forked metrics will not be propagated here. + * @param {boolean} [options.stopPropagation=false] - If true, propagation from the new metrics is stopped here, instead of + * continuing up the chain (if for example these metrics were themselves created through `.fork()`). + * @param {boolean} [options.includeGroups=false] - If true, the forked metrics will also replicate metrics that were propagated * here from elsewhere. For example: * ``` * const metrics = newMetrics(); @@ -222,6 +223,7 @@ export function metricsFactory({now = getTime, mkNode = makeNode, mkTimer = make * withoutGroups.getMetrics() // {} * withGroups.getMetrics() // {foo: ['bar']} * ``` + * @returns {Object} - The new metrics object. */ function fork({propagate = true, stopPropagation = false, includeGroups = false} = {}) { return makeMetrics(mkNode([[self, {propagate, stopPropagation, includeGroups}]]), rename); @@ -297,7 +299,6 @@ function makeTimer(now, cb) { let done = false; function stopTiming() { if (!done) { - // eslint-disable-next-line standard/no-callback-literal cb(now() - start); done = true; } diff --git a/src/utils/prerendering.js b/src/utils/prerendering.js new file mode 100644 index 00000000000..b89b8d895eb --- /dev/null +++ b/src/utils/prerendering.js @@ -0,0 +1,22 @@ +import {logInfo} from '../utils.js'; + +/** + * Returns a wrapper around fn that delays execution until the page if activated, if it was prerendered and isDelayEnabled returns true. + * https://developer.chrome.com/docs/web-platform/prerender-pages + */ +export function delayIfPrerendering(isDelayEnabled, fn) { + return function () { + if (document.prerendering && isDelayEnabled()) { + const that = this; + const args = Array.from(arguments); + return new Promise((resolve) => { + document.addEventListener('prerenderingchange', () => { + logInfo(`Auctions were suspended while page was prerendering`) + resolve(fn.apply(that, args)) + }, {once: true}) + }) + } else { + return Promise.resolve(fn.apply(this, arguments)); + } + } +} diff --git a/src/utils/promise.js b/src/utils/promise.js index 0cf0a47eb8e..5c13c09c78d 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -1,128 +1,19 @@ -const SUCCESS = 0; -const FAIL = 1; +import {GreedyPromise, greedySetTimeout} from '../../libraries/greedy/greedyPromise.js'; +import {getGlobal} from '../prebidGlobal.js'; -/** - * A version of Promise that runs callbacks synchronously when it can (i.e. after it's been fulfilled or rejected). - */ -export class GreedyPromise { - #result; - #callbacks; - - /** - * Convenience wrapper for setTimeout; takes care of returning an already fulfilled GreedyPromise when the delay is zero. - * - * @param {Number} delayMs delay in milliseconds - * @returns {GreedyPromise} a promise that resolves (to undefined) in `delayMs` milliseconds - */ - static timeout(delayMs = 0) { - return new GreedyPromise((resolve) => { - delayMs === 0 ? resolve() : setTimeout(resolve, delayMs); - }); - } - - constructor(resolver) { - if (typeof resolver !== 'function') { - throw new Error('resolver not a function'); - } - const result = []; - const callbacks = []; - let [resolve, reject] = [SUCCESS, FAIL].map((type) => { - return function (value) { - if (type === SUCCESS && typeof value?.then === 'function') { - value.then(resolve, reject); - } else if (!result.length) { - result.push(type, value); - while (callbacks.length) callbacks.shift()(); - } - } - }); - try { - resolver(resolve, reject); - } catch (e) { - reject(e); - } - this.#result = result; - this.#callbacks = callbacks; - } - - then(onSuccess, onError) { - const result = this.#result; - return new this.constructor((resolve, reject) => { - const continuation = () => { - let value = result[1]; - let [handler, resolveFn] = result[0] === SUCCESS ? [onSuccess, resolve] : [onError, reject]; - if (typeof handler === 'function') { - try { - value = handler(value); - } catch (e) { - reject(e); - return; - } - resolveFn = resolve; - } - resolveFn(value); - } - result.length ? continuation() : this.#callbacks.push(continuation); - }); - } - - catch(onError) { - return this.then(null, onError); - } - - finally(onFinally) { - let val; - return this.then( - (v) => { val = v; return onFinally(); }, - (e) => { val = this.constructor.reject(e); return onFinally() } - ).then(() => val); - } +export const pbSetTimeout = getGlobal().setTimeout ?? (FEATURES.GREEDY ? greedySetTimeout : setTimeout) +export const PbPromise = getGlobal().Promise ?? (FEATURES.GREEDY ? GreedyPromise : Promise); - static #collect(promises, collector, done) { - let cnt = promises.length; - function clt() { - collector.apply(this, arguments); - if (--cnt <= 0 && done) done(); - } - promises.length === 0 && done ? done() : promises.forEach((p, i) => this.resolve(p).then( - (val) => clt(true, val, i), - (err) => clt(false, err, i) - )); - } - - static race(promises) { - return new this((resolve, reject) => { - this.#collect(promises, (success, result) => success ? resolve(result) : reject(result)); - }) - } - - static all(promises) { - return new this((resolve, reject) => { - let res = []; - this.#collect(promises, (success, val, i) => success ? res[i] = val : reject(val), () => resolve(res)); - }) - } - - static allSettled(promises) { - return new this((resolve) => { - let res = []; - this.#collect(promises, (success, val, i) => res[i] = success ? {status: 'fulfilled', value: val} : {status: 'rejected', reason: val}, () => resolve(res)) - }) - } - - static resolve(value) { - return new this(resolve => resolve(value)) - } - - static reject(error) { - return new this((resolve, reject) => reject(error)) - } +export function delay(delayMs = 0) { + return new PbPromise((resolve) => { + pbSetTimeout(resolve, delayMs); + }); } /** * @returns a {promise, resolve, reject} trio where `promise` is resolved by calling `resolve` or `reject`. */ -export function defer({promiseFactory = (resolver) => new GreedyPromise(resolver)} = {}) { +export function defer({promiseFactory = (resolver) => new PbPromise(resolver)} = {}) { function invoker(delegate) { return (val) => delegate(val); } diff --git a/src/utils/ttlCollection.js b/src/utils/ttlCollection.js index 0972d175848..6a3b13c1159 100644 --- a/src/utils/ttlCollection.js +++ b/src/utils/ttlCollection.js @@ -1,22 +1,30 @@ -import {GreedyPromise} from './promise.js'; +import {PbPromise} from './promise.js'; import {binarySearch, logError, timestamp} from '../utils.js'; +import {setFocusTimeout} from './focusTimeout.js'; /** * Create a set-like collection that automatically forgets items after a certain time. * - * @param {({}) => Number|Promise} startTime? a function taking an item added to this collection, + * @param {function(*): (number|Promise)} [startTime=timestamp] - A function taking an item added to this collection, * and returning (a promise to) a timestamp to be used as the starting time for the item * (the item will be dropped after `ttl(item)` milliseconds have elapsed since this timestamp). * Defaults to the time the item was added to the collection. - * @param {({}) => Number|void|Promise} ttl a function taking an item added to this collection, + * @param {function(*): (number|void|Promise)} [ttl=() => null] - A function taking an item added to this collection, * and returning (a promise to) the duration (in milliseconds) the item should be kept in it. * May return null to indicate that the item should be persisted indefinitely. - * @param {boolean} monotonic? set to true for better performance, but only if, given any two items A and B in this collection: + * @param {boolean} [monotonic=false] - Set to true for better performance, but only if, given any two items A and B in this collection: * if A was added before B, then: * - startTime(A) + ttl(A) <= startTime(B) + ttl(B) * - Promise.all([startTime(A), ttl(A)]) never resolves later than Promise.all([startTime(B), ttl(B)]) - * @param {number} slack? maximum duration (in milliseconds) that an item is allowed to persist + * @param {number} [slack=5000] - Maximum duration (in milliseconds) that an item is allowed to persist * once past its TTL. This is also roughly the interval between "garbage collection" sweeps. + * @returns {Object} A set-like collection with automatic TTL expiration. + * @returns {function(*): void} return.add - Add an item to the collection. + * @returns {function(): void} return.clear - Clear the collection. + * @returns {function(): Array<*>} return.toArray - Get all the items in the collection, in insertion order. + * @returns {function(): void} return.refresh - Refresh the TTL for each item in the collection. + * @returns {function(function(*)): function(): void} return.onExpiry - Register a callback to be run when an item has expired and is about to be + * removed from the collection. Returns an un-registration function */ export function ttlCollection( { @@ -39,7 +47,7 @@ export function ttlCollection( if (pendingPurge.length > 0) { const now = timestamp(); nextPurge = Math.max(now, pendingPurge[0].expiry + slack); - task = setTimeout(() => { + task = setFocusTimeout(() => { const now = timestamp(); let cnt = 0; for (const entry of pendingPurge) { @@ -85,7 +93,7 @@ export function ttlCollection( let currentCall; return function() { const thisCall = currentCall = {}; - GreedyPromise.resolve(getter(item)).then((val) => { + PbPromise.resolve(getter(item)).then((val) => { if (thisCall === currentCall) { values[field] = val; update(); diff --git a/src/video.js b/src/video.js index ff137892a2b..3df69d3aabd 100644 --- a/src/video.js +++ b/src/video.js @@ -1,4 +1,4 @@ -import {deepAccess, logError} from './utils.js'; +import {isArrayOfNums, isInteger, isNumber, isPlainObject, isStr, logError, logWarn} from './utils.js'; import {config} from '../src/config.js'; import {hook} from './hook.js'; import {auctionManager} from './auctionManager.js'; @@ -6,6 +6,47 @@ import {auctionManager} from './auctionManager.js'; export const OUTSTREAM = 'outstream'; export const INSTREAM = 'instream'; +/** + * List of OpenRTB 2.x video object properties with simple validators. + * Not included: `companionad`, `durfloors`, `ext` + * reference: https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md + */ +export const ORTB_VIDEO_PARAMS = new Map([ + [ 'mimes', value => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string') ], + [ 'minduration', isInteger ], + [ 'maxduration', isInteger ], + [ 'startdelay', isInteger ], + [ 'maxseq', isInteger ], + [ 'poddur', isInteger ], + [ 'protocols', isArrayOfNums ], + [ 'w', isInteger ], + [ 'h', isInteger ], + [ 'podid', isStr ], + [ 'podseq', isInteger ], + [ 'rqddurs', isArrayOfNums ], + [ 'placement', isInteger ], // deprecated, see plcmt + [ 'plcmt', isInteger ], + [ 'linearity', isInteger ], + [ 'skip', value => [1, 0].includes(value) ], + [ 'skipmin', isInteger ], + [ 'skipafter', isInteger ], + [ 'sequence', isInteger ], // deprecated + [ 'slotinpod', isInteger ], + [ 'mincpmpersec', isNumber ], + [ 'battr', isArrayOfNums ], + [ 'maxextended', isInteger ], + [ 'minbitrate', isInteger ], + [ 'maxbitrate', isInteger ], + [ 'boxingallowed', isInteger ], + [ 'playbackmethod', isArrayOfNums ], + [ 'playbackend', isInteger ], + [ 'delivery', isArrayOfNums ], + [ 'pos', isInteger ], + [ 'api', isArrayOfNums ], + [ 'companiontype', isArrayOfNums ], + [ 'poddedupe', isArrayOfNums ] +]); + export function fillVideoDefaults(adUnit) { const video = adUnit?.mediaTypes?.video; if (video != null && video.plcmt == null) { @@ -17,6 +58,42 @@ export function fillVideoDefaults(adUnit) { } } +/** + * validateOrtbVideoFields mutates the `adUnit.mediaTypes.video` object by removing invalid ortb properties (default). + * The onInvalidParam callback can be used to handle invalid properties differently. + * Other properties are ignored and kept as is. + * + * @param {Object} adUnit - The adUnit object. + * @param {Function} onInvalidParam - The callback function to be called with key, value, and adUnit. + * @returns {void} + */ +export function validateOrtbVideoFields(adUnit, onInvalidParam) { + const videoParams = adUnit?.mediaTypes?.video; + + if (!isPlainObject(videoParams)) { + logWarn(`validateOrtbVideoFields: videoParams must be an object.`); + return; + } + + if (videoParams != null) { + Object.entries(videoParams) + .forEach(([key, value]) => { + if (!ORTB_VIDEO_PARAMS.has(key)) { + return + } + const isValid = ORTB_VIDEO_PARAMS.get(key)(value); + if (!isValid) { + if (typeof onInvalidParam === 'function') { + onInvalidParam(key, value, adUnit); + } else { + delete videoParams[key]; + logWarn(`Invalid prop in adUnit "${adUnit.code}": Invalid value for mediaTypes.video.${key} ORTB property. The property has been removed.`); + } + } + }); + } +} + /** * @typedef {object} VideoBid * @property {string} adId id of the bid @@ -25,13 +102,14 @@ export function fillVideoDefaults(adUnit) { /** * Validate that the assets required for video context are present on the bid * @param {VideoBid} bid Video bid to validate - * @param index + * @param {Object} [options] - Options object + * @param {Object} [options.index=auctionManager.index] - Index object, defaulting to `auctionManager.index` * @return {Boolean} If object is valid */ export function isValidVideoBid(bid, {index = auctionManager.index} = {}) { - const videoMediaType = deepAccess(index.getMediaTypes(bid), 'video'); - const context = videoMediaType && deepAccess(videoMediaType, 'context'); - const useCacheKey = videoMediaType && deepAccess(videoMediaType, 'useCacheKey'); + const videoMediaType = index.getMediaTypes(bid)?.video; + const context = videoMediaType && videoMediaType?.context; + const useCacheKey = videoMediaType && videoMediaType?.useCacheKey; const adUnit = index.getAdUnit(bid); // if context not defined assume default 'instream' for video bids @@ -42,10 +120,12 @@ export function isValidVideoBid(bid, {index = auctionManager.index} = {}) { export const checkVideoBidSetup = hook('sync', function(bid, adUnit, videoMediaType, context, useCacheKey) { if (videoMediaType && (useCacheKey || context !== OUTSTREAM)) { // xml-only video bids require a prebid cache url - if (!config.getConfig('cache.url') && bid.vastXml && !bid.vastUrl) { + const { url, useLocal } = config.getConfig('cache') || {}; + if ((!url && !useLocal) && bid.vastXml && !bid.vastUrl) { logError(` This bid contains only vastXml and will not work when a prebid cache url is not specified. - Try enabling prebid cache with $$PREBID_GLOBAL$$.setConfig({ cache: {url: "..."} }); + Try enabling either prebid cache with $$PREBID_GLOBAL$$.setConfig({ cache: {url: "..."} }); + or local cache with $$PREBID_GLOBAL$$.setConfig({ cache: { useLocal: true }}); `); return false; } diff --git a/src/videoCache.js b/src/videoCache.js index ce03f2f624e..6f0076ca02b 100644 --- a/src/videoCache.js +++ b/src/videoCache.js @@ -12,6 +12,8 @@ import {ajaxBuilder} from './ajax.js'; import {config} from './config.js'; import {auctionManager} from './auctionManager.js'; +import {generateUUID, logError, logWarn} from './utils.js'; +import {addBidToAuction} from './auction.js'; /** * Might be useful to be configurable in the future @@ -19,6 +21,8 @@ import {auctionManager} from './auctionManager.js'; */ const ttlBufferInSeconds = 15; +export const vastLocalCache = new Map(); + /** * @typedef {object} CacheableUrlBid * @property {string} vastUrl A URL which loads some valid VAST XML. @@ -39,7 +43,7 @@ const ttlBufferInSeconds = 15; * Function which wraps a URI that serves VAST XML, so that it can be loaded. * * @param {string} uri The URI where the VAST content can be found. - * @param {string} impUrl An impression tracker URL for the delivery of the video ad + * @param {(string|string[])} impTrackerURLs An impression tracker URL for the delivery of the video ad * @return A VAST URL which loads XML from the given URI. */ function wrapURI(uri, impTrackerURLs) { @@ -65,10 +69,12 @@ function wrapURI(uri, impTrackerURLs) { * the bid can't be converted cleanly. * * @param {CacheableBid} bid - * @param index + * @param {Object} [options] - Options object. + * @param {Object} [options.index=auctionManager.index] - Index object, defaulting to `auctionManager.index`. + * @return {Object|null} - The payload to be sent to the prebid-server endpoints, or null if the bid can't be converted cleanly. */ function toStorageRequest(bid, {index = auctionManager.index} = {}) { - const vastValue = bid.vastXml ? bid.vastXml : wrapURI(bid.vastUrl, bid.vastImpUrl); + const vastValue = getVastXml(bid); const auction = index.getAuction(bid); const ttlWithBuffer = Number(bid.ttl) + ttlBufferInSeconds; let payload = { @@ -136,6 +142,10 @@ function shimStorageCallback(done) { } } +function getVastXml(bid) { + return bid.vastXml ? bid.vastXml : wrapURI(bid.vastUrl, bid.vastImpUrl); +}; + /** * If the given bid is for a Video ad, generate a unique ID and cache it somewhere server-side. * @@ -157,3 +167,100 @@ export function store(bids, done, getAjax = ajaxBuilder) { export function getCacheUrl(id) { return `${config.getConfig('cache.url')}?uuid=${id}`; } + +export const storeLocally = (bid) => { + const vastXml = getVastXml(bid); + const bidVastUrl = URL.createObjectURL(new Blob([vastXml], { type: 'text/xml' })); + + assignVastUrlAndCacheId(bid, bidVastUrl); + + vastLocalCache.set(bid.videoCacheKey, bidVastUrl); +}; + +const assignVastUrlAndCacheId = (bid, vastUrl, videoCacheKey) => { + bid.videoCacheKey = videoCacheKey || generateUUID(); + if (!bid.vastUrl) { + bid.vastUrl = vastUrl; + } +} + +export const _internal = { + store +} + +export function storeBatch(batch) { + const bids = batch.map(entry => entry.bidResponse) + function err(msg) { + logError(`Failed to save to the video cache: ${msg}. Video bids will be discarded:`, bids) + } + _internal.store(bids, function (error, cacheIds) { + if (error) { + err(error) + } else if (batch.length !== cacheIds.length) { + logError(`expected ${batch.length} cache IDs, got ${cacheIds.length} instead`) + } else { + cacheIds.forEach((cacheId, i) => { + const {auctionInstance, bidResponse, afterBidAdded} = batch[i]; + if (cacheId.uuid === '') { + logWarn(`Supplied video cache key was already in use by Prebid Cache; caching attempt was rejected. Video bid must be discarded.`); + } else { + assignVastUrlAndCacheId(bidResponse, getCacheUrl(bidResponse.videoCacheKey), cacheId.uuid); + addBidToAuction(auctionInstance, bidResponse); + afterBidAdded(); + } + }); + } + }); +}; + +let batchSize, batchTimeout, cleanupHandler; +if (FEATURES.VIDEO) { + config.getConfig('cache', ({cache}) => { + batchSize = typeof cache.batchSize === 'number' && cache.batchSize > 0 + ? cache.batchSize + : 1; + batchTimeout = typeof cache.batchTimeout === 'number' && cache.batchTimeout > 0 + ? cache.batchTimeout + : 0; + + // removing blobs that are not going to be used + if (cache.useLocal && !cleanupHandler) { + cleanupHandler = auctionManager.onExpiry((auction) => { + auction.getBidsReceived() + .forEach((bid) => { + const vastUrl = vastLocalCache.get(bid.videoCacheKey) + if (vastUrl && vastUrl.startsWith('blob')) { + URL.revokeObjectURL(vastUrl); + } + vastLocalCache.delete(bid.videoCacheKey); + }) + }); + } + }); +} + +export const batchingCache = (timeout = setTimeout, cache = storeBatch) => { + let batches = [[]]; + let debouncing = false; + const noTimeout = cb => cb(); + + return function (auctionInstance, bidResponse, afterBidAdded) { + const batchFunc = batchTimeout > 0 ? timeout : noTimeout; + if (batches[batches.length - 1].length >= batchSize) { + batches.push([]); + } + + batches[batches.length - 1].push({auctionInstance, bidResponse, afterBidAdded}); + + if (!debouncing) { + debouncing = true; + batchFunc(() => { + batches.forEach(cache); + batches = [[]]; + debouncing = false; + }, batchTimeout); + } + }; +}; + +export const batchAndStore = batchingCache(); diff --git a/test/.eslintrc.js b/test/.eslintrc.js deleted file mode 100644 index abb34438653..00000000000 --- a/test/.eslintrc.js +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = { - env: { - browser: true, - mocha: true - }, - extends: 'standard', - globals: { - '$$PREBID_GLOBAL$$': false - }, - parserOptions: { - sourceType: 'module', - ecmaVersion: 2018 - }, - rules: { - 'comma-dangle': 'off', - semi: 'off', - 'space-before-function-paren': 'off', - - // Exceptions below this line are temporary, so that eslint can be added into the CI process. - // Violations of these styles should be fixed, and the exceptions removed over time. - // - // See Issue #1111. - camelcase: 'off', - eqeqeq: 'off', - 'no-mixed-spaces-and-tabs': 'off', - 'no-tabs': 'off', - 'no-unused-expressions': 'off', - 'import/no-duplicates': 'off', - 'import/extensions': 'off', - 'no-template-curly-in-string': 'off', - 'no-global-assign': 'off', - 'no-path-concat': 'off', - 'no-redeclare': 'off', - 'node/no-deprecated-api': 'off', - 'no-return-assign': 'off', - 'no-undef': 'off', - 'no-unused-vars': 'off', - 'no-use-before-define': 'off', - 'no-useless-escape': 'off', - 'one-var': 'off' - } -}; diff --git a/test/fixtures/fixtures.js b/test/fixtures/fixtures.js index fb6cfe036e5..b282d9006a6 100644 --- a/test/fixtures/fixtures.js +++ b/test/fixtures/fixtures.js @@ -822,6 +822,68 @@ export function getAdUnits() { ]; }; +export function getTwinAdUnits() { + return [ + { + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 728, + 90 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'pubmatic', + 'params': { + 'publisherId': 1234567, + 'adSlot': '1234567@728x90' + } + }, + { + 'bidder': 'medianet', + 'params': { + 'cid': '8CUWQS47C', + 'crid': '241882766' + }, + }, + ] + }, + { + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 970, + 90 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '543221' + } + }, + { + 'bidder': 'medianet', + 'params': { + 'cid': '8CUWQS47C', + 'crid': '241882764' + }, + }, + ] + } + ] +} + export function getBidResponsesFromAPI() { return { '/19968336/header-bid-tag-0': { @@ -966,7 +1028,7 @@ export function getBidResponsesFromAPI() { export function getAdServerTargeting() { return { '/19968336/header-bid-tag-0': convertTargetingsFromOldToNew({ - 'foobar': '0x0,300x250,300x600', + 'foobar': '300x250,300x600,0x0', 'hb_size': '300x250', 'hb_pb': '10.00', 'hb_adid': '233bcbee889d46d', @@ -1035,7 +1097,7 @@ export function getTargetingKeys() { ], [ 'foobar', - ['0x0', '300x250', '300x600'] + ['300x250', '300x600', '0x0'] ] ]; } @@ -1062,7 +1124,7 @@ export function getTargetingKeysBidLandscape() { ], [ 'foobar', - ['0x0', '300x250', '300x600'] + ['300x250', '300x600', '0x0'] ], [ TARGETING_KEYS.BIDDER + '_triplelift', diff --git a/test/fixtures/liveIntentAuctionEvents.js b/test/fixtures/liveIntentAuctionEvents.js new file mode 100644 index 00000000000..cb0198f7caa --- /dev/null +++ b/test/fixtures/liveIntentAuctionEvents.js @@ -0,0 +1,2347 @@ +export const AUCTION_INIT_EVENT = { + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'timestamp': 1739969798557, + 'auctionStatus': 'inProgress', + 'adUnits': [ + { + 'code': 'test-div', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'userId': { + 'lipb': { + 'nonId': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'sovrn': 'testSovrn', + 'medianet': 'testMadianet', + 'bidswitch': 'testBidswitch', + 'magnite': 'testMagnite', + 'lipbid': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==' + }, + 'bidswitch': { + 'id': 'testBidswitch', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'medianet': { + 'id': 'testMadianet', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'magnite': { + 'id': 'testMagnite', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'sovrn': { + 'id': 'testSovrn', + 'ext': { + 'provider': 'liveintent.com' + } + } + }, + 'userIdAsEids': [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + } + ] + } + ], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'adUnitId': 'dfeffb52-99fc-4b81-b29c-2c275cd856f5', + 'transactionId': '2a3815e5-5285-415f-8d16-efdbe7eb9be5', + 'ortb2Imp': { + 'ext': { + 'tid': '2a3815e5-5285-415f-8d16-efdbe7eb9be5' + } + } + }, + { + 'code': 'test-div2', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 728, + 90 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'userId': { + 'lipb': { + 'nonId': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'sovrn': 'testSovrn', + 'medianet': 'testMadianet', + 'bidswitch': 'testBidswitch', + 'magnite': 'testMagnite', + 'lipbid': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==' + }, + 'bidswitch': { + 'id': 'testBidswitch', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'medianet': { + 'id': 'testMadianet', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'magnite': { + 'id': 'testMagnite', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'sovrn': { + 'id': 'testSovrn', + 'ext': { + 'provider': 'liveintent.com' + } + } + }, + 'userIdAsEids': [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + } + ] + } + ], + 'sizes': [ + [ + 728, + 90 + ] + ], + 'adUnitId': '64ed0035-8f8b-49a9-956c-4d21be034175', + 'transactionId': '744fa14d-62a6-45a2-a880-c0a2a5aa6c01', + 'ortb2Imp': { + 'ext': { + 'tid': '744fa14d-62a6-45a2-a880-c0a2a5aa6c01' + } + } + } + ], + 'adUnitCodes': [ + 'test-div', + 'test-div2' + ], + 'bidderRequests': [ + { + 'bidderCode': 'appnexus', + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'bidderRequestId': '16ed05f7946bed', + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placement_id': 13144370 + }, + 'userId': { + 'lipb': { + 'nonId': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'sovrn': 'testSovrn', + 'medianet': 'testMadianet', + 'bidswitch': 'testBidswitch', + 'magnite': 'testMagnite', + 'lipbid': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==' + }, + 'bidswitch': { + 'id': 'testBidswitch', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'medianet': { + 'id': 'testMadianet', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'magnite': { + 'id': 'testMagnite', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'sovrn': { + 'id': 'testSovrn', + 'ext': { + 'provider': 'liveintent.com' + } + } + }, + 'userIdAsEids': [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + } + ], + 'ortb2Imp': { + 'ext': {} + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'test-div', + 'transactionId': '2a3815e5-5285-415f-8d16-efdbe7eb9be5', + 'adUnitId': 'dfeffb52-99fc-4b81-b29c-2c275cd856f5', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '2635cc051f8d47', + 'bidderRequestId': '16ed05f7946bed', + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'src': 'client', + 'metrics': { + 'userId.init.consent': [ + 0 + ], + 'userId.mod.init': [ + 2.0999999940395355 + ], + 'userId.mods.liveIntentId.init': [ + 2.0999999940395355 + ], + 'userId.init.modules': [ + 2.2999999821186066 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 626.0999999940395 + ], + 'userId.mods.liveIntentId.callback': [ + 626.0999999940395 + ], + 'userId.callbacks.total': [ + 626.3000000119209 + ], + 'userId.total': [ + 629.5999999940395 + ], + 'requestBids.userId': 623.7999999821186, + 'requestBids.validate': 0.20000001788139343, + 'requestBids.makeRequests': 1, + 'adapter.client.validate': 0.10000002384185791, + 'adapters.client.appnexus.validate': 0.10000002384185791, + 'adapter.client.buildRequests': 1.0999999940395355, + 'adapters.client.appnexus.buildRequests': 1.0999999940395355 + }, + 'auctionsCount': 1, + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'deferBilling': false, + 'ortb2': { + 'source': {}, + 'site': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com', + 'publisher': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com' + }, + 'page': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + }, + 'device': { + 'w': 3840, + 'h': 2160, + 'dnt': 1, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + 'language': 'en', + 'ext': { + 'vpw': 150, + 'vph': 1573 + }, + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Not(A:Brand', + 'version': [ + '99' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '133' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '133' + ] + } + ], + 'mobile': 0 + } + }, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + } + ] + } + } + } + }, + { + 'bidder': 'appnexus', + 'params': { + 'placement_id': 13144370 + }, + 'userId': { + 'lipb': { + 'nonId': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'sovrn': 'testSovrn', + 'medianet': 'testMadianet', + 'bidswitch': 'testBidswitch', + 'magnite': 'testMagnite', + 'lipbid': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==' + }, + 'bidswitch': { + 'id': 'testBidswitch', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'medianet': { + 'id': 'testMadianet', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'magnite': { + 'id': 'testMagnite', + 'ext': { + 'provider': 'liveintent.com' + } + }, + 'sovrn': { + 'id': 'testSovrn', + 'ext': { + 'provider': 'liveintent.com' + } + } + }, + 'userIdAsEids': [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + } + ], + 'ortb2Imp': { + 'ext': {} + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 728, + 90 + ] + ] + } + }, + 'adUnitCode': 'test-div2', + 'transactionId': '744fa14d-62a6-45a2-a880-c0a2a5aa6c01', + 'adUnitId': '64ed0035-8f8b-49a9-956c-4d21be034175', + 'sizes': [ + [ + 728, + 90 + ] + ], + 'bidId': '39ca9c9224aa1a', + 'bidderRequestId': '16ed05f7946bed', + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'src': 'client', + 'metrics': { + 'userId.init.consent': [ + 0 + ], + 'userId.mod.init': [ + 2.0999999940395355 + ], + 'userId.mods.liveIntentId.init': [ + 2.0999999940395355 + ], + 'userId.init.modules': [ + 2.2999999821186066 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 626.0999999940395 + ], + 'userId.mods.liveIntentId.callback': [ + 626.0999999940395 + ], + 'userId.callbacks.total': [ + 626.3000000119209 + ], + 'userId.total': [ + 629.5999999940395 + ], + 'requestBids.userId': 623.7999999821186, + 'requestBids.validate': 0.20000001788139343, + 'requestBids.makeRequests': 1, + 'adapter.client.validate': 0.10000002384185791, + 'adapters.client.appnexus.validate': 0.10000002384185791, + 'adapter.client.buildRequests': 1.0999999940395355, + 'adapters.client.appnexus.buildRequests': 1.0999999940395355 + }, + 'auctionsCount': 1, + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'deferBilling': false, + 'ortb2': { + 'source': {}, + 'site': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com', + 'publisher': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com' + }, + 'page': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + }, + 'device': { + 'w': 3840, + 'h': 2160, + 'dnt': 1, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + 'language': 'en', + 'ext': { + 'vpw': 150, + 'vph': 1573 + }, + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Not(A:Brand', + 'version': [ + '99' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '133' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '133' + ] + } + ], + 'mobile': 0 + } + }, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + } + ] + } + } + } + } + ], + 'auctionStart': 1739969798557, + 'timeout': 2000, + 'refererInfo': { + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + ], + 'topmostLocation': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html', + 'location': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html', + 'canonicalUrl': null, + 'page': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html', + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com', + 'ref': null, + 'legacy': { + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + ], + 'referer': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html', + 'canonicalUrl': null + } + }, + 'metrics': { + 'userId.init.consent': [ + 0 + ], + 'userId.mod.init': [ + 2.0999999940395355 + ], + 'userId.mods.liveIntentId.init': [ + 2.0999999940395355 + ], + 'userId.init.modules': [ + 2.2999999821186066 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 626.0999999940395 + ], + 'userId.mods.liveIntentId.callback': [ + 626.0999999940395 + ], + 'userId.callbacks.total': [ + 626.3000000119209 + ], + 'userId.total': [ + 629.5999999940395 + ], + 'requestBids.userId': 623.7999999821186, + 'requestBids.validate': 0.20000001788139343, + 'requestBids.makeRequests': 1, + 'adapter.client.validate': 0.10000002384185791, + 'adapters.client.appnexus.validate': 0.10000002384185791, + 'adapter.client.buildRequests': 1.0999999940395355, + 'adapters.client.appnexus.buildRequests': 1.0999999940395355 + }, + 'ortb2': { + 'source': {}, + 'site': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com', + 'publisher': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com' + }, + 'page': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + }, + 'device': { + 'w': 3840, + 'h': 2160, + 'dnt': 1, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + 'language': 'en', + 'ext': { + 'vpw': 150, + 'vph': 1573 + }, + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Not(A:Brand', + 'version': [ + '99' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '133' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '133' + ] + } + ], + 'mobile': 0 + } + }, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'liveintent.com' + } + } + ] + } + ] + } + } + }, + 'start': 1739969798558 + } + ], + 'noBids': [], + 'bidsReceived': [], + 'bidsRejected': [], + 'winningBids': [], + 'timeout': 2000, + 'metrics': { + 'userId.init.consent': [ + 0 + ], + 'userId.mod.init': [ + 2.0999999940395355 + ], + 'userId.mods.liveIntentId.init': [ + 2.0999999940395355 + ], + 'userId.init.modules': [ + 2.2999999821186066 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 626.0999999940395 + ], + 'userId.mods.liveIntentId.callback': [ + 626.0999999940395 + ], + 'userId.callbacks.total': [ + 626.3000000119209 + ], + 'userId.total': [ + 629.5999999940395 + ], + 'adapter.client.validate': [ + 0.10000002384185791 + ], + 'adapters.client.appnexus.validate': [ + 0.10000002384185791 + ], + 'adapter.client.buildRequests': [ + 1.0999999940395355 + ], + 'adapters.client.appnexus.buildRequests': [ + 1.0999999940395355 + ], + 'requestBids.userId': 623.7999999821186, + 'requestBids.validate': 0.20000001788139343, + 'requestBids.makeRequests': 1 + }, + 'seatNonBids': [] +} + +export const AUCTION_INIT_EVENT_NOT_LI = { + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'timestamp': 1739969798557, + 'auctionStatus': 'inProgress', + 'adUnits': [ + { + 'code': 'test-div', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'userId': { + 'lipb': { + 'nonId': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'sovrn': 'testSovrn', + 'medianet': 'testMadianet', + 'bidswitch': 'testBidswitch', + 'magnite': 'testMagnite', + 'lipbid': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==' + }, + 'bidswitch': { + 'id': 'testBidswitch', + 'ext': { + 'provider': 'NOTNOTliveintent.com' + } + }, + 'medianet': { + 'id': 'testMadianet', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'magnite': { + 'id': 'testMagnite', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'sovrn': { + 'id': 'testSovrn', + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + }, + 'userIdAsEids': [ + { + 'source': 'NOTliveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + } + ] + } + ], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'adUnitId': 'dfeffb52-99fc-4b81-b29c-2c275cd856f5', + 'transactionId': '2a3815e5-5285-415f-8d16-efdbe7eb9be5', + 'ortb2Imp': { + 'ext': { + 'tid': '2a3815e5-5285-415f-8d16-efdbe7eb9be5' + } + } + }, + { + 'code': 'test-div2', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 728, + 90 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'userId': { + 'lipb': { + 'nonId': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'sovrn': 'testSovrn', + 'medianet': 'testMadianet', + 'bidswitch': 'testBidswitch', + 'magnite': 'testMagnite', + 'lipbid': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==' + }, + 'bidswitch': { + 'id': 'testBidswitch', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'medianet': { + 'id': 'testMadianet', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'magnite': { + 'id': 'testMagnite', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'sovrn': { + 'id': 'testSovrn', + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + }, + 'userIdAsEids': [ + { + 'source': 'NOTliveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + } + ] + } + ], + 'sizes': [ + [ + 728, + 90 + ] + ], + 'adUnitId': '64ed0035-8f8b-49a9-956c-4d21be034175', + 'transactionId': '744fa14d-62a6-45a2-a880-c0a2a5aa6c01', + 'ortb2Imp': { + 'ext': { + 'tid': '744fa14d-62a6-45a2-a880-c0a2a5aa6c01' + } + } + } + ], + 'adUnitCodes': [ + 'test-div', + 'test-div2' + ], + 'bidderRequests': [ + { + 'bidderCode': 'appnexus', + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'bidderRequestId': '16ed05f7946bed', + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placement_id': 13144370 + }, + 'userId': { + 'lipb': { + 'nonId': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'sovrn': 'testSovrn', + 'medianet': 'testMadianet', + 'bidswitch': 'testBidswitch', + 'magnite': 'testMagnite', + 'lipbid': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==' + }, + 'bidswitch': { + 'id': 'testBidswitch', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'medianet': { + 'id': 'testMadianet', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'magnite': { + 'id': 'testMagnite', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'sovrn': { + 'id': 'testSovrn', + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + }, + 'userIdAsEids': [ + { + 'source': 'NOTliveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + } + ], + 'ortb2Imp': { + 'ext': {} + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'test-div', + 'transactionId': '2a3815e5-5285-415f-8d16-efdbe7eb9be5', + 'adUnitId': 'dfeffb52-99fc-4b81-b29c-2c275cd856f5', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '2635cc051f8d47', + 'bidderRequestId': '16ed05f7946bed', + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'src': 'client', + 'metrics': { + 'userId.init.consent': [ + 0 + ], + 'userId.mod.init': [ + 2.0999999940395355 + ], + 'userId.mods.liveIntentId.init': [ + 2.0999999940395355 + ], + 'userId.init.modules': [ + 2.2999999821186066 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 626.0999999940395 + ], + 'userId.mods.liveIntentId.callback': [ + 626.0999999940395 + ], + 'userId.callbacks.total': [ + 626.3000000119209 + ], + 'userId.total': [ + 629.5999999940395 + ], + 'requestBids.userId': 623.7999999821186, + 'requestBids.validate': 0.20000001788139343, + 'requestBids.makeRequests': 1, + 'adapter.client.validate': 0.10000002384185791, + 'adapters.client.appnexus.validate': 0.10000002384185791, + 'adapter.client.buildRequests': 1.0999999940395355, + 'adapters.client.appnexus.buildRequests': 1.0999999940395355 + }, + 'auctionsCount': 1, + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'deferBilling': false, + 'ortb2': { + 'source': {}, + 'site': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com', + 'publisher': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com' + }, + 'page': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + }, + 'device': { + 'w': 3840, + 'h': 2160, + 'dnt': 1, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + 'language': 'en', + 'ext': { + 'vpw': 150, + 'vph': 1573 + }, + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Not(A:Brand', + 'version': [ + '99' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '133' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '133' + ] + } + ], + 'mobile': 0 + } + }, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'NOTliveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + } + ] + } + } + } + }, + { + 'bidder': 'appnexus', + 'params': { + 'placement_id': 13144370 + }, + 'userId': { + 'lipb': { + 'nonId': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'sovrn': 'testSovrn', + 'medianet': 'testMadianet', + 'bidswitch': 'testBidswitch', + 'magnite': 'testMagnite', + 'lipbid': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==' + }, + 'bidswitch': { + 'id': 'testBidswitch', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'medianet': { + 'id': 'testMadianet', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'magnite': { + 'id': 'testMagnite', + 'ext': { + 'provider': 'NOTliveintent.com' + } + }, + 'sovrn': { + 'id': 'testSovrn', + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + }, + 'userIdAsEids': [ + { + 'source': 'NOTliveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + } + ], + 'ortb2Imp': { + 'ext': {} + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 728, + 90 + ] + ] + } + }, + 'adUnitCode': 'test-div2', + 'transactionId': '744fa14d-62a6-45a2-a880-c0a2a5aa6c01', + 'adUnitId': '64ed0035-8f8b-49a9-956c-4d21be034175', + 'sizes': [ + [ + 728, + 90 + ] + ], + 'bidId': '39ca9c9224aa1a', + 'bidderRequestId': '16ed05f7946bed', + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'src': 'client', + 'metrics': { + 'userId.init.consent': [ + 0 + ], + 'userId.mod.init': [ + 2.0999999940395355 + ], + 'userId.mods.liveIntentId.init': [ + 2.0999999940395355 + ], + 'userId.init.modules': [ + 2.2999999821186066 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 626.0999999940395 + ], + 'userId.mods.liveIntentId.callback': [ + 626.0999999940395 + ], + 'userId.callbacks.total': [ + 626.3000000119209 + ], + 'userId.total': [ + 629.5999999940395 + ], + 'requestBids.userId': 623.7999999821186, + 'requestBids.validate': 0.20000001788139343, + 'requestBids.makeRequests': 1, + 'adapter.client.validate': 0.10000002384185791, + 'adapters.client.appnexus.validate': 0.10000002384185791, + 'adapter.client.buildRequests': 1.0999999940395355, + 'adapters.client.appnexus.buildRequests': 1.0999999940395355 + }, + 'auctionsCount': 1, + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'deferBilling': false, + 'ortb2': { + 'source': {}, + 'site': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com', + 'publisher': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com' + }, + 'page': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + }, + 'device': { + 'w': 3840, + 'h': 2160, + 'dnt': 1, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + 'language': 'en', + 'ext': { + 'vpw': 150, + 'vph': 1573 + }, + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Not(A:Brand', + 'version': [ + '99' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '133' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '133' + ] + } + ], + 'mobile': 0 + } + }, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'NOTliveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + } + ] + } + } + } + } + ], + 'auctionStart': 1739969798557, + 'timeout': 2000, + 'refererInfo': { + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + ], + 'topmostLocation': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html', + 'location': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html', + 'canonicalUrl': null, + 'page': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html', + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com', + 'ref': null, + 'legacy': { + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + ], + 'referer': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html', + 'canonicalUrl': null + } + }, + 'metrics': { + 'userId.init.consent': [ + 0 + ], + 'userId.mod.init': [ + 2.0999999940395355 + ], + 'userId.mods.liveIntentId.init': [ + 2.0999999940395355 + ], + 'userId.init.modules': [ + 2.2999999821186066 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 626.0999999940395 + ], + 'userId.mods.liveIntentId.callback': [ + 626.0999999940395 + ], + 'userId.callbacks.total': [ + 626.3000000119209 + ], + 'userId.total': [ + 629.5999999940395 + ], + 'requestBids.userId': 623.7999999821186, + 'requestBids.validate': 0.20000001788139343, + 'requestBids.makeRequests': 1, + 'adapter.client.validate': 0.10000002384185791, + 'adapters.client.appnexus.validate': 0.10000002384185791, + 'adapter.client.buildRequests': 1.0999999940395355, + 'adapters.client.appnexus.buildRequests': 1.0999999940395355 + }, + 'ortb2': { + 'source': {}, + 'site': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com', + 'publisher': { + 'domain': 'vdreiling-lab-test.s3.us-east-1.amazonaws.com' + }, + 'page': 'https://vdreiling-lab-test.s3.us-east-1.amazonaws.com/prebid-presentation/pageWith2.html' + }, + 'device': { + 'w': 3840, + 'h': 2160, + 'dnt': 1, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + 'language': 'en', + 'ext': { + 'vpw': 150, + 'vph': 1573 + }, + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Not(A:Brand', + 'version': [ + '99' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '133' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '133' + ] + } + ], + 'mobile': 0 + } + }, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'NOTliveintent.com', + 'uids': [ + { + 'id': '12-hMaZCgZ9Q99h3cGDam21dEfxTawgIafx1J1zxvXTHwnrXUHWudCwcgIjkbTbZpRd2DV9ivSyz2L9t27VTNWXYAvYmk82zmrmciW71dSFq00zHg==', + 'atype': 3 + } + ] + }, + { + 'source': 'bidswitch.net', + 'uids': [ + { + 'id': 'testBidswitch', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'media.net', + 'uids': [ + { + 'id': 'testMadianet', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'rubiconproject.com', + 'uids': [ + { + 'id': 'testMagnite', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + }, + { + 'source': 'liveintent.sovrn.com', + 'uids': [ + { + 'id': 'testSovrn', + 'atype': 3, + 'ext': { + 'provider': 'NOTliveintent.com' + } + } + ] + } + ] + } + } + }, + 'start': 1739969798558 + } + ], + 'noBids': [], + 'bidsReceived': [], + 'bidsRejected': [], + 'winningBids': [], + 'timeout': 2000, + 'metrics': { + 'userId.init.consent': [ + 0 + ], + 'userId.mod.init': [ + 2.0999999940395355 + ], + 'userId.mods.liveIntentId.init': [ + 2.0999999940395355 + ], + 'userId.init.modules': [ + 2.2999999821186066 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 626.0999999940395 + ], + 'userId.mods.liveIntentId.callback': [ + 626.0999999940395 + ], + 'userId.callbacks.total': [ + 626.3000000119209 + ], + 'userId.total': [ + 629.5999999940395 + ], + 'adapter.client.validate': [ + 0.10000002384185791 + ], + 'adapters.client.appnexus.validate': [ + 0.10000002384185791 + ], + 'adapter.client.buildRequests': [ + 1.0999999940395355 + ], + 'adapters.client.appnexus.buildRequests': [ + 1.0999999940395355 + ], + 'requestBids.userId': 623.7999999821186, + 'requestBids.validate': 0.20000001788139343, + 'requestBids.makeRequests': 1 + }, + 'seatNonBids': [] +} + +export const BID_WON_EVENT = { + 'bidderCode': 'appnexus', + 'width': 728, + 'height': 90, + 'statusMessage': 'Bid available', + 'adId': '4e02072b881823', + 'requestId': '3fae2718fd70f', + 'transactionId': '6daa1dac-9eea-47ce-82ce-ce9681df1ec5', + 'adUnitId': 'afc6bc6a-3082-4940-b37f-d22e1b026e48', + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 1.5, + 'creativeId': 98493734, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': 'test-div2', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885, + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [ + { + 'bsid': '9325' + } + ] + }, + 'brandId': 555545 + }, + 'ad': "
", + 'metrics': { + 'userId.init.consent': [ + 0.09999999403953552 + ], + 'userId.mod.init': [ + 1.7999999821186066 + ], + 'userId.mods.liveIntentId.init': [ + 1.7999999821186066 + ], + 'userId.init.modules': [ + 1.9000000059604645 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 149.69999998807907 + ], + 'userId.mods.liveIntentId.callback': [ + 149.69999998807907 + ], + 'userId.callbacks.total': [ + 149.80000001192093 + ], + 'userId.total': [ + 152.90000000596046 + ], + 'requestBids.userId': 147.2999999821186, + 'requestBids.validate': 0.30000001192092896, + 'requestBids.makeRequests': 0.699999988079071, + 'requestBids.total': 215.7999999821186, + 'requestBids.callBids': 65, + 'adapter.client.validate': 0, + 'adapters.client.appnexus.validate': 0, + 'adapter.client.buildRequests': 1.199999988079071, + 'adapters.client.appnexus.buildRequests': 1.199999988079071, + 'adapter.client.total': 62.69999998807907, + 'adapters.client.appnexus.total': 62.69999998807907, + 'adapter.client.net': 59.5, + 'adapters.client.appnexus.net': 59.5, + 'adapter.client.interpretResponse': 0.5, + 'adapters.client.appnexus.interpretResponse': 0.5, + 'addBidResponse.validate': 0.09999999403953552, + 'addBidResponse.total': 0.9000000059604645, + 'render.deferred': null, + 'render.pending': 130.60000002384186, + 'render.e2e': 346.40000000596046, + 'adserver.pending': 130.80000001192093, + 'adserver.e2e': 346.59999999403954 + }, + 'adapterCode': 'appnexus', + 'originalCpm': 1.5, + 'originalCurrency': 'USD', + 'deferBilling': false, + 'deferRendering': false, + 'responseTimestamp': 1739971147806, + 'requestTimestamp': 1739971147744, + 'bidder': 'appnexus', + 'timeToRespond': 62, + 'pbLg': '1.50', + 'pbMg': '1.50', + 'pbHg': '1.50', + 'pbAg': '1.50', + 'pbDg': '1.50', + 'pbCg': '', + 'size': '728x90', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '4e02072b881823', + 'hb_pb': '1.50', + 'hb_size': '728x90', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': '', + 'hb_crid': 98493734 + }, + 'latestTargetedAuctionId': 'a8ac8297-9f6f-4e35-bb42-8405ea908e92', + 'status': 'rendered', + 'params': [ + { + 'placementId': 13144370 + } + ] +} + +export const BID_WON_EVENT_UNDEFINED = { + 'bidderCode': undefined, + 'width': 728, + 'height': 90, + 'statusMessage': 'Bid available', + 'adId': '4e02072b881823', + 'requestId': '3fae2718fd70f', + 'transactionId': '6daa1dac-9eea-47ce-82ce-ce9681df1ec5', + 'adUnitId': undefined, + 'auctionId': '87b4a93d-19ae-432a-96f0-8c2d4cc1c539', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': undefined, + 'creativeId': 98493734, + 'currency': undefined, + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': undefined, + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885, + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [ + { + 'bsid': '9325' + } + ] + }, + 'brandId': 555545 + }, + 'ad': "
", + 'metrics': { + 'userId.init.consent': [ + 0.09999999403953552 + ], + 'userId.mod.init': [ + 1.7999999821186066 + ], + 'userId.mods.liveIntentId.init': [ + 1.7999999821186066 + ], + 'userId.init.modules': [ + 1.9000000059604645 + ], + 'userId.callbacks.pending': [ + 0 + ], + 'userId.mod.callback': [ + 149.69999998807907 + ], + 'userId.mods.liveIntentId.callback': [ + 149.69999998807907 + ], + 'userId.callbacks.total': [ + 149.80000001192093 + ], + 'userId.total': [ + 152.90000000596046 + ], + 'requestBids.userId': 147.2999999821186, + 'requestBids.validate': 0.30000001192092896, + 'requestBids.makeRequests': 0.699999988079071, + 'requestBids.total': 215.7999999821186, + 'requestBids.callBids': 65, + 'adapter.client.validate': 0, + 'adapters.client.appnexus.validate': 0, + 'adapter.client.buildRequests': 1.199999988079071, + 'adapters.client.appnexus.buildRequests': 1.199999988079071, + 'adapter.client.total': 62.69999998807907, + 'adapters.client.appnexus.total': 62.69999998807907, + 'adapter.client.net': 59.5, + 'adapters.client.appnexus.net': 59.5, + 'adapter.client.interpretResponse': 0.5, + 'adapters.client.appnexus.interpretResponse': 0.5, + 'addBidResponse.validate': 0.09999999403953552, + 'addBidResponse.total': 0.9000000059604645, + 'render.deferred': null, + 'render.pending': 130.60000002384186, + 'render.e2e': 346.40000000596046, + 'adserver.pending': 130.80000001192093, + 'adserver.e2e': 346.59999999403954 + }, + 'adapterCode': 'appnexus', + 'originalCpm': 1.5, + 'originalCurrency': 'USD', + 'deferBilling': false, + 'deferRendering': false, + 'responseTimestamp': undefined, + 'requestTimestamp': undefined, + 'bidder': undefined, + 'timeToRespond': 62, + 'pbLg': '1.50', + 'pbMg': '1.50', + 'pbHg': '1.50', + 'pbAg': '1.50', + 'pbDg': '1.50', + 'pbCg': '', + 'size': '728x90', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '4e02072b881823', + 'hb_pb': '1.50', + 'hb_size': '728x90', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': '', + 'hb_crid': 98493734 + }, + 'latestTargetedAuctionId': 'a8ac8297-9f6f-4e35-bb42-8405ea908e92', + 'status': 'rendered', + 'params': [ + { + 'placementId': 13144370 + } + ] +} diff --git a/test/helpers/consentData.js b/test/helpers/consentData.js index c708e397bd6..3ebb4506704 100644 --- a/test/helpers/consentData.js +++ b/test/helpers/consentData.js @@ -1,12 +1,13 @@ -import {gdprDataHandler} from 'src/adapterManager.js'; -import {GreedyPromise} from '../../src/utils/promise.js'; +import {gdprDataHandler, gppDataHandler} from 'src/adapterManager.js'; +import {PbPromise} from '../../src/utils/promise.js'; export function mockGdprConsent(sandbox, getConsentData = () => null) { sandbox.stub(gdprDataHandler, 'enabled').get(() => true) - sandbox.stub(gdprDataHandler, 'promise').get(() => GreedyPromise.resolve(getConsentData())); + sandbox.stub(gdprDataHandler, 'promise').get(() => PbPromise.resolve(getConsentData())); sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(getConsentData) } beforeEach(() => { gdprDataHandler.reset(); + gppDataHandler.reset(); }) diff --git a/test/helpers/cookies.js b/test/helpers/cookies.js new file mode 100644 index 00000000000..a0d9da3c595 --- /dev/null +++ b/test/helpers/cookies.js @@ -0,0 +1,5 @@ +export function clearAllCookies() { + document.cookie.split(';').forEach(function (c) { + document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/'); + }); +} diff --git a/test/helpers/fpd.js b/test/helpers/fpd.js index 89755f26541..7a29c9fc92d 100644 --- a/test/helpers/fpd.js +++ b/test/helpers/fpd.js @@ -1,5 +1,5 @@ import {dep, enrichFPD} from 'src/fpd/enrichment.js'; -import {GreedyPromise} from '../../src/utils/promise.js'; +import {PbPromise} from '../../src/utils/promise.js'; import {deepClone} from '../../src/utils.js'; import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; @@ -13,7 +13,7 @@ export function mockFpdEnrichments(sandbox, overrides = {}) { return window }, getHighEntropySUA() { - return GreedyPromise.resolve() + return PbPromise.resolve() } }, overrides) Object.entries(overrides) @@ -35,11 +35,9 @@ export function mockFpdEnrichments(sandbox, overrides = {}) { export function addFPDEnrichments(ortb2 = {}, overrides) { const sandbox = sinon.sandbox.create(); mockFpdEnrichments(sandbox, overrides) - return enrichFPD(GreedyPromise.resolve(deepClone(ortb2))).finally(() => sandbox.restore()); + return enrichFPD(PbPromise.resolve(deepClone(ortb2))).finally(() => sandbox.restore()); } -export const syncAddFPDEnrichments = synchronize(addFPDEnrichments); - export function addFPDToBidderRequest(bidderRequest, overrides) { overrides = Object.assign({}, { getRefererInfo() { @@ -59,13 +57,3 @@ export function addFPDToBidderRequest(bidderRequest, overrides) { } }); } - -export const syncAddFPDToBidderRequest = synchronize(addFPDToBidderRequest); - -function synchronize(fn) { - return function () { - let result; - fn.apply(this, arguments).then(res => { result = res }); - return result; - } -} diff --git a/test/helpers/global_hooks.js b/test/helpers/global_hooks.js new file mode 100644 index 00000000000..3df3d27d032 --- /dev/null +++ b/test/helpers/global_hooks.js @@ -0,0 +1,24 @@ +import {clearEvents} from '../../src/events.js'; + +window.describe = window.context = ((orig) => { + let level = 0; + return function (name, fn, ...args) { + try { + if (level++ === 0) { + fn = ((orig) => { + return function (...args) { + const result = orig.apply(this, args); + after(() => { + // run this after each top-level "describe", roughly equivalent to each file + clearEvents(); + }); + return result; + } + })(fn) + } + return orig.call(this, name, fn, ...args); + } finally { + level--; + } + } +})(window.describe); diff --git a/test/helpers/index_adapter_utils.js b/test/helpers/index_adapter_utils.js index f01145b573d..0eb7af88d14 100644 --- a/test/helpers/index_adapter_utils.js +++ b/test/helpers/index_adapter_utils.js @@ -1,3 +1,5 @@ +import { deepClone } from '../../src/utils'; + var AllowedAdUnits = [[728, 90], [120, 600], [300, 250], [160, 600], [336, 280], [234, 60], [300, 600], [300, 50], [320, 50], [970, 250], [300, 1050], [970, 90], [180, 150]]; var UnsupportedAdUnits = [[700, 100], [100, 600], [300, 200], [100, 600], [300, 200], [200, 60], [900, 200], [300, 1000], [900, 90], [100, 100]]; @@ -117,7 +119,7 @@ exports.getExpectedIndexSlots = function(bids) { } function clone(x) { - return JSON.parse(JSON.stringify(x)); + return deepClone(x); } // returns the difference(lhs, rhs), difference(rhs,lhs), and intersection(lhs, rhs) based on the object keys diff --git a/test/helpers/karma-init.js b/test/helpers/karma-init.js deleted file mode 100644 index 56e936aa741..00000000000 --- a/test/helpers/karma-init.js +++ /dev/null @@ -1,6 +0,0 @@ -(function (window) { - if (!window.parent.pbjsKarmaInitDone && window.location.pathname === '/context.html') { - window.parent.pbjsKarmaInitDone = true; - window.open('/debug.html', '_blank'); - } -})(window); diff --git a/test/helpers/testing-utils.js b/test/helpers/testing-utils.js index 3f59411ff6c..ac2e390c1d2 100644 --- a/test/helpers/testing-utils.js +++ b/test/helpers/testing-utils.js @@ -1,4 +1,4 @@ -/* eslint-disable no-console */ + const {expect} = require('chai'); const DEFAULT_TIMEOUT = 2000; const utils = { diff --git a/test/mocks/adloaderStub.js b/test/mocks/adloaderStub.js index 6e7f5756100..24f5781dfc4 100644 --- a/test/mocks/adloaderStub.js +++ b/test/mocks/adloaderStub.js @@ -8,9 +8,7 @@ export let loadExternalScriptStub = createStub(); function createStub() { return sinon.stub(adloader, 'loadExternalScript').callsFake((...args) => { - if (typeof args[2] === 'function') { - args[2](); - } else if (typeof args[3] === 'function') { + if (typeof args[3] === 'function') { args[3](); } return document.createElement('script'); diff --git a/test/mocks/ortbConverter.js b/test/mocks/ortbConverter.js new file mode 100644 index 00000000000..446fac4629a --- /dev/null +++ b/test/mocks/ortbConverter.js @@ -0,0 +1,8 @@ +import {defaultProcessors} from '../../libraries/ortbConverter/converter.js'; +import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js'; + +beforeEach(() => { + // disable caching of default processors so that tests do not freeze a subset for other tests + defaultProcessors.clear(); + pbsExtensions.clear(); +}); diff --git a/test/mocks/videoCacheStub.js b/test/mocks/videoCacheStub.js index 7ce899cae35..acae5cd6a32 100644 --- a/test/mocks/videoCacheStub.js +++ b/test/mocks/videoCacheStub.js @@ -1,4 +1,4 @@ -import * as videoCache from 'src/videoCache.js'; +import {_internal as videoCache} from 'src/videoCache.js'; /** * Function which can be called from unit tests to stub out the video cache. diff --git a/test/mocks/xhr.js b/test/mocks/xhr.js index e7b1d96f0a4..8c0d5a27b19 100644 --- a/test/mocks/xhr.js +++ b/test/mocks/xhr.js @@ -1,5 +1,5 @@ import {getUniqueIdentifierStr} from '../../src/utils.js'; -import {GreedyPromise} from '../../src/utils/promise.js'; +import {GreedyPromise} from 'libraries/greedy/greedyPromise.js'; import {fakeXhr} from 'nise'; import {dep} from 'src/ajax.js'; diff --git a/test/pages/banner_sync.html b/test/pages/banner_sync.html new file mode 100644 index 00000000000..71403ba5570 --- /dev/null +++ b/test/pages/banner_sync.html @@ -0,0 +1,97 @@ + + + + + + + Prebid.js Banner Example + + + + + + + + + + + + + + + +

Prebid.js Banner Ad Unit Test

+
+ +
+
+ + + diff --git a/test/pages/consent_mgt_gdpr.html b/test/pages/consent_mgt_gdpr.html index c55a2b9236f..6ff24938bf3 100644 --- a/test/pages/consent_mgt_gdpr.html +++ b/test/pages/consent_mgt_gdpr.html @@ -1,6 +1,6 @@ - + ", + 'cpm': 0.68, + 'nurl': nurl, + 'creativeId': 'test', + 'currency': 'USD', + 'dealId': '', + 'meta': { + 'advertiserDomains': [], + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [ + { + 'name': 'redtram' + } + ] + } + }, + 'netRevenue': true, + 'ttl': 120, + 'metrics': {}, + 'adapterCode': 'redtram', + 'originalCpm': 0.68, + 'originalCurrency': 'USD', + 'responseTimestamp': 1668162732297, + 'requestTimestamp': 1668162732292, + 'bidder': 'redtram', + 'adUnitCode': 'div-prebid', + 'timeToRespond': 5, + 'pbLg': '0.50', + 'pbMg': '0.60', + 'pbHg': '0.68', + 'pbAg': '0.65', + 'pbDg': '0.68', + 'pbCg': '', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'redtram', + 'hb_adid': '5691dd18ba6ab6', + 'hb_pb': '0.68', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': '' + }, + 'status': 'rendered', + 'params': [ + { + 'placementId': 23611 + } + ] + }; + spec.onBidWon(bid); + expect(bid.nurl).to.deep.equal('nurl/?ap=0.68'); + }); + }); +}); diff --git a/test/spec/libraries/precisoUtils/bidUtils_spec.js b/test/spec/libraries/precisoUtils/bidUtils_spec.js new file mode 100644 index 00000000000..05c480f41bc --- /dev/null +++ b/test/spec/libraries/precisoUtils/bidUtils_spec.js @@ -0,0 +1,150 @@ + +import { expect } from 'chai'; +import { buildRequests, interpretResponse } from '../../../../libraries/precisoUtils/bidUtils.js'; + +const DEFAULT_PRICE = 1 +const DEFAULT_CURRENCY = 'USD' +const DEFAULT_BANNER_WIDTH = 300 +const DEFAULT_BANNER_HEIGHT = 250 +const BIDDER_CODE = 'preciso'; +const TESTDOMAIN = 'test.org' +const bidEndPoint = `https://${TESTDOMAIN}/bid_request/openrtb`; + +describe('bidUtils', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: BIDDER_CODE, + buyerUid: 'testuid', + mediaTypes: { + banner: { + sizes: [[DEFAULT_BANNER_WIDTH, DEFAULT_BANNER_HEIGHT]] + } + }, + params: { + host: 'prebid', + sourceid: '0', + publisherId: '0', + mediaType: 'banner', + region: 'IND' + + }, + userId: { + pubcid: '12355454test' + + }, + user: { + geo: { + region: 'IND', + } + }, + ortb2: { + device: { + w: 1920, + h: 166, + dnt: 0, + }, + site: { + domain: 'localHost' + }, + source: { + tid: 'eaff09b-a1ab-4ec6-a81e-c5a75bc1dde3' + } + + } + + }; + + const spec = { + // isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(bidEndPoint), + interpretResponse, + // buildUserSyncs: buildUserSyncs(syncEndPoint) + }; + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + 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 valid URL', function () { + expect(serverRequest.url).to.equal(`https://${TESTDOMAIN}/bid_request/openrtb`); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data.device).to.be.a('object'); + expect(data.user).to.be.a('object'); + expect(data.source).to.be.a('object'); + expect(data.site).to.be.a('object'); + }); + it('Returns data.device is undefined if no valid device object is passed', function () { + delete bid.ortb2.device; + serverRequest = spec.buildRequests([bid]); + let data = serverRequest.data; + expect(data.device).to.be.undefined; + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid response', function () { + let response = { + bidderRequestId: 'f6adb85f-4e19-45a0-b41e-2a5b9a48f23a', + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + price: DEFAULT_PRICE, + adm: 'hi', + cid: 'test_cid', + crid: 'test_banner_crid', + w: DEFAULT_BANNER_WIDTH, + h: DEFAULT_BANNER_HEIGHT, + adomain: [], + } + ], + seat: BIDDER_CODE + } + ], + } + let expectedResponse = [ + { + requestId: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + cpm: DEFAULT_PRICE, + width: DEFAULT_BANNER_WIDTH, + height: DEFAULT_BANNER_HEIGHT, + creativeId: 'test_banner_crid', + ad: 'hi', + currency: DEFAULT_CURRENCY, + netRevenue: true, + ttl: 300, + meta: { advertiserDomains: [] }, + } + ] + let result = spec.interpretResponse({ body: response }) + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])) + }) + }); + // describe('getUserSyncs', function () { + // const syncUrl = `https://${TESTDOMAIN}/rtb/user/usersync.aspx?/iframe?pbjs=1&coppa=0`; + // const syncOptions = { + // iframeEnabled: true + // }; + // let userSync = spec.buildUserSyncs(syncOptions); + // it('Returns valid URL and type', function () { + // expect(userSync).to.be.an('array').with.lengthOf(1); + // expect(userSync[0].type).to.exist; + // expect(userSync[0].url).to.exist; + // expect(userSync).to.deep.equal([ + // { type: 'iframe', url: syncUrl } + // ]); + // }); + // }); +}); diff --git a/test/spec/libraries/processResponse_spec.js b/test/spec/libraries/processResponse_spec.js new file mode 100644 index 00000000000..8f682bd87f5 --- /dev/null +++ b/test/spec/libraries/processResponse_spec.js @@ -0,0 +1,60 @@ +import { getBidFromResponse } from '../../../libraries/processResponse/index.js'; +import {expect} from 'chai/index.js'; + +describe('processResponse', function () { + const respItem = { + 'bid': [ + { + 'price': 0.504, + 'ext': { + 'visx': { + 'events': { + 'runtime': '//t.visx.net/track/status/RFTFjZflStSUyuXuyT2IKOZMVPUIiPkzebpPWYwKvNkE_IybYfFxk2P5feBnt9LhiR7291KTG11JjrnyHyhVKfolH_VRCmGppbnHXHfHJ9AgNqjhFB_yTg3m18wGO9k4LOddGAg3mk8qc5zYEIzNsPFnZzos1EkHh5WNs0EjrBpwCgTERUqM3PJD_Zy60nMDA-LCuq-Z4JNBGC_GHx4LwvwXipQsjdGHS-HkqHHf9sES45OlRrW4wMf69dsmey1gvwqFAhJwii2lzo9wfOohLCMRa3Vxd-zvzx-uw71maWOyKnJXWiP6c5xkyrfV4gukNYaDUgrHc0mA0yhqyiHxe8KzEl32rxQXJRCg4FoJcJ1g9jmpZQBnIh2QrKm5iC159elwzwf31_v3Uw97Zpek8j0CCLa8FjxSjvXm1Mq8x4jcwlt0ngfWU6WwyyKwX_GMbKWuAL_nrfxSvs1hZCb4eunEFyXb2lN2olWo8ezMEzZ8YRxF_mx0hDB3NXyV0Tb4b6KXQq7tvxV-1rKPRt7DySRTbLPht0hO3mjTHxutfihnuL6ROEr372gSAiDodnbdCq_lPsCsUSEpG7DmN-4In10uSp2MemjfbqI6tllOCO-j6Pm9mhdl_rT4anHmRG2DG_dLsfD7pLaAsgf2zl2bpawhxxLVjTxikoWjNKAvr_GNh4adHGj5EHbqaBaHovB573Yk-koHkyBNrebeiy-1-Knc28MWOpFi9XKjNsXx756jAXLx2H098ptaXF3mFiuT2Iv6sTVjqOI/{STATUS_CODE}' + } + }, + 'prebid': { + 'events': { + 'pending': '//t.visx.net/track/pending/RFTFjZflStSUyuXuyT2IKOZMVPUIiPkzebpPWYwKvNkE_IybYfFxk2P5feBnt9LhiR7291KTG11JjrnyHyhVKfolH_VRCmGppbnHXHfHJ9AgNqjhFB_yTg3m18wGO9k4LOddGAg3mk8qc5zYEIzNsPFnZzos1EkHh5WNs0EjrBpwCgTERUqM3PJD_Zy60nMDA-LCuq-Z4JNBGC_GHx4LwvwXipQsjdGHS-HkqHHf9sES45OlRrW4wMf69dsmey1gvwqFAhJwii2lzo9wfOohLCMRa3Vxd-zvzx-uw71maWOyKnJXWiP6c5xkyrfV4gukNYaDUgrHc0mA0yhqyiHxe8KzEl32rxQXJRCg4FoJcJ1g9jmpZQBnIh2QrKm5iC159elwzwf31_v3Uw97Zpek8j0CCLa8FjxSjvXm1Mq8x4jcwlt0ngfWU6WwyyKwX_GMbKWuAL_nrfxSvs1hZCb4eunEFyXb2lN2olWo8ezMEzZ8YRxF_mx0hDB3NXyV0Tb4b6KXQq7tvxV-1rKPRt7DySRTbLPht0hO3mjTHxutfihnuL6ROEr372gSAiDodnbdCq_lPsCsUSEpG7DmN-4In10uSp2MemjfbqI6tllOCO-j6Pm9mhdl_rT4anHmRG2DG_dLsfD7pLaAsgf2zl2bpawhxxLVjTxikoWjNKAvr_GNh4adHGj5EHbqaBaHovB573Yk-koHkyBNrebeiy-1-Knc28MWOpFi9XKjNsXx756jAXLx2H098ptaXF3mFiuT2Iv6sTVjqOI/', + 'win': '//t.visx.net/track/win/RFTFjZflStSUyuXuyT2IKOZMVPUIiPkzebpPWYwKvNkE_IybYfFxk2P5feBnt9LhiR7291KTG11JjrnyHyhVKfolH_VRCmGppbnHXHfHJ9AgNqjhFB_yTg3m18wGO9k4LOddGAg3mk8qc5zYEIzNsPFnZzos1EkHh5WNs0EjrBpwCgTERUqM3PJD_Zy60nMDA-LCuq-Z4JNBGC_GHx4LwvwXipQsjdGHS-HkqHHf9sES45OlRrW4wMf69dsmey1gvwqFAhJwii2lzo9wfOohLCMRa3Vxd-zvzx-uw71maWOyKnJXWiP6c5xkyrfV4gukNYaDUgrHc0mA0yhqyiHxe8KzEl32rxQXJRCg4FoJcJ1g9jmpZQBnIh2QrKm5iC159elwzwf31_v3Uw97Zpek8j0CCLa8FjxSjvXm1Mq8x4jcwlt0ngfWU6WwyyKwX_GMbKWuAL_nrfxSvs1hZCb4eunEFyXb2lN2olWo8ezMEzZ8YRxF_mx0hDB3NXyV0Tb4b6KXQq7tvxV-1rKPRt7DySRTbLPht0hO3mjTHxutfihnuL6ROEr372gSAiDodnbdCq_lPsCsUSEpG7DmN-4In10uSp2MemjfbqI6tllOCO-j6Pm9mhdl_rT4anHmRG2DG_dLsfD7pLaAsgf2zl2bpawhxxLVjTxikoWjNKAvr_GNh4adHGj5EHbqaBaHovB573Yk-koHkyBNrebeiy-1-Knc28MWOpFi9XKjNsXx756jAXLx2H098ptaXF3mFiuT2Iv6sTVjqOI/', + 'bid_timeout': '//t.visx.net/track/bid_timeout/RFTFjZflStSUyuXuyT2IKOZMVPUIiPkzebpPWYwKvNkE_IybYfFxk2P5feBnt9LhiR7291KTG11JjrnyHyhVKfolH_VRCmGppbnHXHfHJ9AgNqjhFB_yTg3m18wGO9k4LOddGAg3mk8qc5zYEIzNsPFnZzos1EkHh5WNs0EjrBpwCgTERUqM3PJD_Zy60nMDA-LCuq-Z4JNBGC_GHx4LwvwXipQsjdGHS-HkqHHf9sES45OlRrW4wMf69dsmey1gvwqFAhJwii2lzo9wfOohLCMRa3Vxd-zvzx-uw71maWOyKnJXWiP6c5xkyrfV4gukNYaDUgrHc0mA0yhqyiHxe8KzEl32rxQXJRCg4FoJcJ1g9jmpZQBnIh2QrKm5iC159elwzwf31_v3Uw97Zpek8j0CCLa8FjxSjvXm1Mq8x4jcwlt0ngfWU6WwyyKwX_GMbKWuAL_nrfxSvs1hZCb4eunEFyXb2lN2olWo8ezMEzZ8YRxF_mx0hDB3NXyV0Tb4b6KXQq7tvxV-1rKPRt7DySRTbLPht0hO3mjTHxutfihnuL6ROEr372gSAiDodnbdCq_lPsCsUSEpG7DmN-4In10uSp2MemjfbqI6tllOCO-j6Pm9mhdl_rT4anHmRG2DG_dLsfD7pLaAsgf2zl2bpawhxxLVjTxikoWjNKAvr_GNh4adHGj5EHbqaBaHovB573Yk-koHkyBNrebeiy-1-Knc28MWOpFi9XKjNsXx756jAXLx2H098ptaXF3mFiuT2Iv6sTVjqOI/', + 'runtime': '//t.visx.net/track/status/RFTFjZflStSUyuXuyT2IKOZMVPUIiPkzebpPWYwKvNkE_IybYfFxk2P5feBnt9LhiR7291KTG11JjrnyHyhVKfolH_VRCmGppbnHXHfHJ9AgNqjhFB_yTg3m18wGO9k4LOddGAg3mk8qc5zYEIzNsPFnZzos1EkHh5WNs0EjrBpwCgTERUqM3PJD_Zy60nMDA-LCuq-Z4JNBGC_GHx4LwvwXipQsjdGHS-HkqHHf9sES45OlRrW4wMf69dsmey1gvwqFAhJwii2lzo9wfOohLCMRa3Vxd-zvzx-uw71maWOyKnJXWiP6c5xkyrfV4gukNYaDUgrHc0mA0yhqyiHxe8KzEl32rxQXJRCg4FoJcJ1g9jmpZQBnIh2QrKm5iC159elwzwf31_v3Uw97Zpek8j0CCLa8FjxSjvXm1Mq8x4jcwlt0ngfWU6WwyyKwX_GMbKWuAL_nrfxSvs1hZCb4eunEFyXb2lN2olWo8ezMEzZ8YRxF_mx0hDB3NXyV0Tb4b6KXQq7tvxV-1rKPRt7DySRTbLPht0hO3mjTHxutfihnuL6ROEr372gSAiDodnbdCq_lPsCsUSEpG7DmN-4In10uSp2MemjfbqI6tllOCO-j6Pm9mhdl_rT4anHmRG2DG_dLsfD7pLaAsgf2zl2bpawhxxLVjTxikoWjNKAvr_GNh4adHGj5EHbqaBaHovB573Yk-koHkyBNrebeiy-1-Knc28MWOpFi9XKjNsXx756jAXLx2H098ptaXF3mFiuT2Iv6sTVjqOI/{STATUS_CODE}' + }, + 'meta': { + 'mediaType': 'banner' + } + } + }, + 'impid': '2b642c27bdcf8f', + 'auid': 929004, + 'h': 250, + 'cur': 'EUR', + 'adomain': [ + '' + ], + 'w': 300, + 'id': '9b6c7e04-0a09-4add-8ba9-0c8b98304de3' + } + ], + 'seat': '1429601' + }; + const LOG_ERROR_MESS = { + 'noAuid': 'Bid from response has no auid parameter - ', + 'noAdm': 'Bid from response has no adm parameter - ', + 'noBid': 'Array of bid objects is empty', + 'noImpId': 'Bid from response has no impid parameter - ', + 'noPlacementCode': 'Can\'t find in requested bids the bid with auid - ', + 'emptyUids': 'Uids should not be empty', + 'emptySeatbid': 'Seatbid array from response has an empty item', + 'emptyResponse': 'Response is empty', + 'hasEmptySeatbidArray': 'Response has empty seatbid array', + 'hasNoArrayOfBids': 'Seatbid from response has no array of bid objects - ', + 'notAllowedCurrency': 'Currency is not supported - ', + 'currencyMismatch': 'Currency from the request is not match currency from the response - ', + 'onlyVideoInstream': 'Only video instream supported', + 'videoMissing': 'Bid request videoType property is missing - ' + }; + it('returns bid when respItem and LOG_ERROR_MESS is passed', function () { + let response = getBidFromResponse(respItem, LOG_ERROR_MESS); + expect(response).not.include.any.keys('emptyResponse', 'hasNoArrayOfBids', 'emptySeatbid'); + }); +}); diff --git a/test/spec/libraries/teqblazeUtils/bidderUtils_spec.js b/test/spec/libraries/teqblazeUtils/bidderUtils_spec.js new file mode 100644 index 00000000000..4bc74f50de5 --- /dev/null +++ b/test/spec/libraries/teqblazeUtils/bidderUtils_spec.js @@ -0,0 +1,545 @@ +import { expect } from 'chai'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../../../../libraries/teqblazeUtils/bidderUtils.js'; +import { BANNER, VIDEO, NATIVE } from '../../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../../src/utils.js'; + +const bidder = 'bidder'; +const DOMAIN = 'test.org'; +const AD_URL = `https://${DOMAIN}/pbjs`; +const SYNC_URL = `https://${DOMAIN}`; + +describe('TeqBlazeBidderUtils', 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 + }; + + const spec = { + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) + }; + + 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 () { + let 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 + } + ]; + + let 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); + let 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); + let 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; + }); + + it('Handles ORTB2 device data', function () { + const ortb2Device = { + 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'}, + }; + const _bidderRequest = JSON.parse(JSON.stringify(bidderRequest)); + _bidderRequest.ortb2.device = ortb2Device; + const _request = spec.buildRequests(bids, _bidderRequest); + + expect(_request.data.device).to.deep.equal(ortb2Device); + }); + }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + + 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 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let 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 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let 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' + }] + }; + + let 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' + }] + }; + let 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', + }] + }; + let 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' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal(`https://${DOMAIN}/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0`) + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal(`https://${DOMAIN}/image?pbjs=1&ccpa_consent=1---&coppa=0`) + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal(`https://${DOMAIN}/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0`) + }); + }); +}); diff --git a/test/spec/libraries/timeToFirstBytesUtils.js b/test/spec/libraries/timeToFirstBytesUtils.js new file mode 100644 index 00000000000..ea737e925c4 --- /dev/null +++ b/test/spec/libraries/timeToFirstBytesUtils.js @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { getTimeToFirstByte } from './timeToFirstBytesUtils'; + +describe('getTimeToFirstByte', () => { + let win; + + beforeEach(() => { + win = { + performance: { + getEntriesByType: sinon.stub(), + timing: { + responseStart: 0, + requestStart: 0 + } + } + }; + }); + + it('should return TTFB using Navigation Timing Level 2 API', () => { + win.performance.getEntriesByType.withArgs('navigation').returns([{ + responseStart: 100, + requestStart: 50 + }]); + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal('50'); + }); + + it('should return TTFB using Navigation Timing Level 1 API', () => { + win.performance.getEntriesByType.returns([]); + win.performance.timing.responseStart = 100; + win.performance.timing.requestStart = 50; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal('50'); + }); + + it('should return an empty string if TTFB cannot be determined', () => { + win.performance.getEntriesByType.returns([]); + win.performance.timing.responseStart = 0; + win.performance.timing.requestStart = 0; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal(''); + }); + + it('should return an empty string if performance object is not available', () => { + win.performance = null; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal(''); + }); +}); diff --git a/test/spec/libraries/userAgentUtils_spec.js b/test/spec/libraries/userAgentUtils_spec.js new file mode 100644 index 00000000000..0a72b51588b --- /dev/null +++ b/test/spec/libraries/userAgentUtils_spec.js @@ -0,0 +1,109 @@ +/* globals describe, beforeEach, afterEach, sinon */ +import { expect } from 'chai'; +import { getDeviceType, getBrowser, getOS } from 'libraries/userAgentUtils'; +import { deviceTypes, browserTypes, osTypes } from 'libraries/userAgentUtils/userAgentTypes.enums'; + +const ORIGINAL_USER_AGENT = window.navigator.userAgent; +const ORIGINAL_VENDOR = window.navigator.vendor; +const ORIGINAL_APP_VERSION = window.navigator.appVersion; + +describe('Test user agent categorization', () => { + afterEach(() => { + window.navigator.__defineGetter__('userAgent', () => ORIGINAL_USER_AGENT); + window.navigator.__defineGetter__('vendor', () => ORIGINAL_VENDOR); + window.navigator.__defineGetter__('appVersion', () => ORIGINAL_APP_VERSION); + }) + + describe('test getDeviceType', () => { + it('user agent device type is tablet', () => { + const tabletUserAgent = 'Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4.17.9 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3' + window.navigator.__defineGetter__('userAgent', () => tabletUserAgent); + expect(getDeviceType()).to.equal(deviceTypes.TABLET); + }) + it('user agent device type is mobile', () => { + const mobileUserAgent = 'Mozilla/5.0 (Linux; Android 12; M2102J20SG) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36' + window.navigator.__defineGetter__('userAgent', () => mobileUserAgent); + expect(getDeviceType()).to.equal(deviceTypes.MOBILE); + }) + it('user agent device type is desktop', () => { + const desktopUserAgent = 'Mozilla/5.0 (X11; CrOS x86_64 8172.45.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.64 Safari/537.36' + window.navigator.__defineGetter__('userAgent', () => desktopUserAgent); + expect(getDeviceType()).to.equal(deviceTypes.DESKTOP); + }) + }) + + describe('test getBrowser', () => { + it('user agent browser is edge', () => { + const edgeUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10532' + window.navigator.__defineGetter__('userAgent', () => edgeUserAgent); + expect(getBrowser()).to.equal(browserTypes.EDGE); + }) + it('user agent browser is chrome', () => { + const chromeUserAgent = 'Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/44.0.2403.67 Mobile/12H143 Safari/600.1.4' + window.navigator.__defineGetter__('userAgent', () => chromeUserAgent); + expect(getBrowser()).to.equal(browserTypes.CHROME); + }) + it('user agent browser is firefox', () => { + const firefoxUserAgent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:40.0) Gecko/20100101 Firefox/40.0.2 Waterfox/40.0.2' + window.navigator.__defineGetter__('userAgent', () => firefoxUserAgent); + expect(getBrowser()).to.equal(browserTypes.FIREFOX); + }) + it('user agent browser is safari', () => { + const safariUserAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36' + window.navigator.__defineGetter__('userAgent', () => safariUserAgent); + window.navigator.__defineGetter__('vendor', () => 'Apple Computer, Inc.'); + expect(getBrowser()).to.equal(browserTypes.SAFARI); + }) + it('user agent browser is internet explorer', () => { + const internetexplorerUserAgent = 'Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko' + window.navigator.__defineGetter__('userAgent', () => internetexplorerUserAgent); + expect(getBrowser()).to.equal(browserTypes.INTERNET_EXPLORER); + }) + it('user agent is other', () => { + const otherUserAgent = 'Dalvik/2.1.0 (Linux; U; Android 9; ADT-2 Build/PTT5.181126.002)' + window.navigator.__defineGetter__('userAgent', () => otherUserAgent); + expect(getBrowser()).to.equal(browserTypes.OTHER); + }) + }) + + describe('test getOS', () => { + it('user agent is android', () => { + const androidUserAgent = 'Mozilla/5.0 (Android; Mobile; rv:40.0) Gecko/40.0 Firefox/40.0' + window.navigator.__defineGetter__('userAgent', () => androidUserAgent); + expect(getOS()).to.equal(osTypes.ANDROID); + }) + it('user agent is ios', () => { + const iosUserAgent = 'Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4' + window.navigator.__defineGetter__('userAgent', () => iosUserAgent); + expect(getOS()).to.equal(osTypes.IOS); + }) + it('user agent is windows', () => { + const windowsUserAgent = 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36' + window.navigator.__defineGetter__('userAgent', () => windowsUserAgent); + expect(getOS()).to.equal(osTypes.WINDOWS); + }) + it('user agent is mac', () => { + const macUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0' + window.navigator.__defineGetter__('userAgent', () => macUserAgent); + expect(getOS()).to.equal(osTypes.MAC); + }) + it('user agent is linux', () => { + const linuxUserAgent = 'Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0' + window.navigator.__defineGetter__('userAgent', () => linuxUserAgent); + expect(getOS()).to.equal(osTypes.LINUX); + }) + it('user agent is unix', () => { + const unixUserAgent = 'Mozilla/5.0 (X11; CrOS armv7l 7077.134.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.156 Safari/537.36' + const unixappVersion = '5.0 (X11; CrOS armv7l 7077.134.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.156 Safari/537.36' + window.navigator.__defineGetter__('userAgent', () => unixUserAgent); + window.navigator.__defineGetter__('appVersion', () => unixappVersion); + expect(getOS()).to.equal(osTypes.UNIX); + }) + it('user agent is other', () => { + const otherUserAgent = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' + window.navigator.__defineGetter__('userAgent', () => otherUserAgent); + window.navigator.__defineGetter__('appVersion', () => ''); + expect(getOS()).to.equal(osTypes.OTHER); + }) + }) +}) diff --git a/test/spec/libraries/vastTrackers_spec.js b/test/spec/libraries/vastTrackers_spec.js index 3849ea75b02..a359f32a2d4 100644 --- a/test/spec/libraries/vastTrackers_spec.js +++ b/test/spec/libraries/vastTrackers_spec.js @@ -1,33 +1,88 @@ -import {addImpUrlToTrackers, getVastTrackers, insertVastTrackers, registerVastTrackers} from 'libraries/vastTrackers/vastTrackers.js'; +import { + addImpUrlToTrackers, + addTrackersToResponse, + getVastTrackers, + insertVastTrackers, + registerVastTrackers, + reset, cacheVideoBidHook, + disable +} from 'libraries/vastTrackers/vastTrackers.js'; import {MODULE_TYPE_ANALYTICS} from '../../../src/activities/modules.js'; +import {AuctionIndex} from '../../../src/auctionIndex.js'; describe('vast trackers', () => { - it('insert into tracker list', function() { - let trackers = getVastTrackers({'cpm': 1.0}); - if (!trackers || !trackers.get('impressions')) { - registerVastTrackers(MODULE_TYPE_ANALYTICS, 'test', function(bidResponse) { - return [ - {'event': 'impressions', 'url': `https://vasttracking.mydomain.com/vast?cpm=${bidResponse.cpm}`} - ]; - }); + let sandbox, tracker, auction, bid, bidRequest, index; + beforeEach(() => { + bid = { + requestId: 'bid', + cpm: 1.0, + auctionId: 'aid', + mediaType: 'video', } - trackers = getVastTrackers({'cpm': 1.0}); + bidRequest = { + auctionId: 'aid', + bidId: 'bid', + } + auction = { + getAuctionId() { + return 'aid'; + }, + getProperties() { + return {auction: 'props'}; + }, + getBidRequests() { + return [{bids: [bidRequest]}] + } + }; + sandbox = sinon.sandbox.create(); + index = new AuctionIndex(() => [auction]); + tracker = sinon.stub().callsFake(function (bidResponse) { + return [ + {'event': 'impressions', 'url': `https://vasttracking.mydomain.com/vast?cpm=${bidResponse.cpm}`} + ]; + }); + registerVastTrackers(MODULE_TYPE_ANALYTICS, 'test', tracker); + }) + afterEach(() => { + reset(); + sandbox.restore(); + }); + + after(disable); + + it('insert into tracker list', function () { + const trackers = getVastTrackers(bid, {index}); expect(trackers).to.be.a('map'); expect(trackers.get('impressions')).to.exists; expect(trackers.get('impressions').has('https://vasttracking.mydomain.com/vast?cpm=1')).to.be.true; }); - it('insert trackers in vastXml', function() { - const trackers = getVastTrackers({'cpm': 1.0}); + it('insert trackers in vastXml', function () { + const trackers = getVastTrackers(bid, {index}); let vastXml = ''; vastXml = insertVastTrackers(trackers, vastXml); expect(vastXml).to.equal(''); }); - it('test addImpUrlToTrackers', function() { - const trackers = addImpUrlToTrackers({'vastImpUrl': 'imptracker.com'}, getVastTrackers({'cpm': 1.0})); + it('should pass request and auction properties to trackerFn', () => { + const bid = {requestId: 'bid', auctionId: 'aid'}; + getVastTrackers(bid, {index}); + sinon.assert.calledWith(tracker, bid, sinon.match({auction: auction.getProperties(), bidRequest})) + }) + + it('test addImpUrlToTrackers', function () { + const trackers = addImpUrlToTrackers({'vastImpUrl': 'imptracker.com'}, getVastTrackers(bid, {index})); expect(trackers).to.be.a('map'); expect(trackers.get('impressions')).to.exists; expect(trackers.get('impressions').has('imptracker.com')).to.be.true; }); + + if (FEATURES.VIDEO) { + it('should add trackers to bid response', () => { + cacheVideoBidHook({index})(sinon.stub(), 'au', bid); + expect(bid.vastImpUrl).to.eql([ + 'https://vasttracking.mydomain.com/vast?cpm=1' + ]) + }); + } }) diff --git a/test/spec/libraries/weakStore_spec.js b/test/spec/libraries/weakStore_spec.js new file mode 100644 index 00000000000..407b83391ef --- /dev/null +++ b/test/spec/libraries/weakStore_spec.js @@ -0,0 +1,32 @@ +import {weakStore} from '../../../libraries/weakStore/weakStore.js'; + +describe('weakStore', () => { + let targets, store; + beforeEach(() => { + targets = { + id: {} + }; + store = weakStore((id) => targets[id]); + }); + + it('returns undef if getter returns undef', () => { + expect(store('missing')).to.not.exist; + }); + + it('inits to empty object by default', () => { + expect(store('id')).to.eql({}); + }); + + it('inits to given value', () => { + expect(store('id', {initial: 'value'})).to.eql({'initial': 'value'}); + }); + + it('returns the same object as long as the target does not change', () => { + expect(store('id')).to.equal(store('id')); + }); + + it('ignores init value if already initialized', () => { + store('id', {initial: 'value'}); + expect(store('id', {second: 'value'})).to.eql({initial: 'value'}); + }) +}); diff --git a/test/spec/modules/1plusXRtdProvider_spec.js b/test/spec/modules/1plusXRtdProvider_spec.js index 4e4092ea26e..1059f43fb4d 100644 --- a/test/spec/modules/1plusXRtdProvider_spec.js +++ b/test/spec/modules/1plusXRtdProvider_spec.js @@ -12,6 +12,7 @@ import { updateBidderConfig, } from 'modules/1plusXRtdProvider'; import {deepClone} from '../../../src/utils.js'; +import { STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE } from 'src/storageManager.js'; describe('1plusXRtdProvider', () => { // Fake server config @@ -126,6 +127,7 @@ describe('1plusXRtdProvider', () => { const customerId = 'test'; const timeout = 1000; const bidders = ['appnexus']; + const fpidStorageType = STORAGE_TYPE_LOCALSTORAGE it('Throws an error if no customerId is specified', () => { const moduleConfig = { params: { timeout, bidders } }; @@ -141,13 +143,14 @@ describe('1plusXRtdProvider', () => { expect(() => extractConfig(moduleConfig, reqBidsConfigEmpty)).to.throw(); }) it('Returns an object containing the parameters specified', () => { - const moduleConfig = { params: { customerId, timeout, bidders } }; - const expectedKeys = ['customerId', 'timeout', 'bidders'] + const moduleConfig = { params: { customerId, timeout, bidders, fpidStorageType } }; + const expectedKeys = ['customerId', 'timeout', 'bidders', 'fpidStorageType'] const extractedConfig = extractConfig(moduleConfig, reqBidsConfigObj); expect(extractedConfig).to.be.an('object').and.to.have.all.keys(expectedKeys); expect(extractedConfig.customerId).to.equal(customerId); expect(extractedConfig.timeout).to.equal(timeout); expect(extractedConfig.bidders).to.deep.equal(bidders); + expect(extractedConfig.fpidStorageType).to.equal(fpidStorageType) }) /* 1plusX RTD module may only use bidders that are both specified in : - the bid request configuration @@ -166,6 +169,20 @@ describe('1plusXRtdProvider', () => { const moduleConfig = { params: { customerId, timeout, bidders } }; expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); }) + it('Throws an error if wrong fpidStorageType is provided', () => { + const moduleConfig = { params: { customerId, timeout, bidders, fpidStorageType: 'bogus' } }; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj).to.throw()) + }) + it('Defaults fpidStorageType to localStorage', () => { + const moduleConfig = { params: { customerId, timeout, bidders } }; + const extractedConfig = extractConfig(moduleConfig, reqBidsConfigObj); + expect(extractedConfig.fpidStorageType).to.equal(STORAGE_TYPE_LOCALSTORAGE) + }) + it('Correctly instantiates fpidStorageType to cookie store if instructed', () => { + const moduleConfig = { params: { customerId, timeout, bidders, fpidStorageType: STORAGE_TYPE_COOKIES } }; + const extractedConfig = extractConfig(moduleConfig, reqBidsConfigObj); + expect(extractedConfig.fpidStorageType).to.equal(STORAGE_TYPE_COOKIES) + }) }) describe('buildOrtb2Updates', () => { @@ -284,17 +301,6 @@ describe('1plusXRtdProvider', () => { }) }) - describe('extractFpid', () => { - it('correctly extracts an ope fpid if present', () => { - window.localStorage.setItem('ope_fpid', 'oneplusx_test_key') - const id1 = extractFpid() - window.localStorage.removeItem('ope_fpid') - const id2 = extractFpid() - expect(id1).to.equal('oneplusx_test_key') - expect(id2).to.equal(null) - }) - }) - describe('getPapiUrl', () => { const customer = 'acme' const consent = { diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index 9cc038428bc..d4ad0184a17 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -1,9 +1,11 @@ import { expect } from 'chai'; import * as utils from 'src/utils.js'; +import { internal } from 'src/utils.js'; import { config } from 'src/config.js'; import { spec } from 'modules/33acrossBidAdapter.js'; +import { resetWinDimensions } from '../../../src/utils'; function validateBuiltServerRequest(builtReq, expectedReq) { expect(builtReq.url).to.equal(expectedReq.url); @@ -121,7 +123,7 @@ describe('33acrossBidAdapter:', function () { video: { w: 300, h: 250, - placement: 2, + plcmt: 2, ...params } }); @@ -499,12 +501,15 @@ describe('33acrossBidAdapter:', function () { sandbox = sinon.sandbox.create(); sandbox.stub(Date, 'now').returns(1); sandbox.stub(document, 'getElementById').returns(element); + sandbox.stub(internal, 'getWindowTop').returns(win); + sandbox.stub(internal, 'getWindowSelf').returns(win); sandbox.stub(utils, 'getWindowTop').returns(win); sandbox.stub(utils, 'getWindowSelf').returns(win); bidderRequest = {bidderRequestId: 'r1'}; }); afterEach(function() { + resetWinDimensions(); sandbox.restore(); }); describe('isBidRequestValid:', function() { @@ -527,27 +532,6 @@ describe('33acrossBidAdapter:', function () { }); }); - it('returns false for invalid bidder name values', function() { - const invalidBidderName = [ - undefined, - '33', - '33x', - 'thirtythree', - '' - ]; - - invalidBidderName.forEach((bidderName) => { - const bid = { - bidder: bidderName, - params: { - siteId: 'sample33xGUID123456789' - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - }); - it('returns true for valid guid values', function() { // NOTE: We ignore whitespace at the start and end since // in our experience these are common typos @@ -733,6 +717,11 @@ describe('33acrossBidAdapter:', function () { 'foo' ]; + invalidPlacement.forEach((placement) => { + this.bid.mediaTypes.video.plcmt = placement; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + invalidPlacement.forEach((placement) => { this.bid.mediaTypes.video.placement = placement; expect(spec.isBidRequestValid(this.bid)).to.be.false; @@ -1018,6 +1007,8 @@ describe('33acrossBidAdapter:', function () { win.innerHeight = 728; win.innerWidth = 727; + resetWinDimensions(); + const [ buildRequest ] = spec.buildRequests(bidRequests, bidderRequest); validateBuiltServerRequest(buildRequest, serverRequest); @@ -1040,6 +1031,7 @@ describe('33acrossBidAdapter:', function () { utils.getWindowTop.restore(); win.document.visibilityState = 'hidden'; sandbox.stub(utils, 'getWindowTop').returns(win); + resetWinDimensions(); const [ buildRequest ] = spec.buildRequests(bidRequests, bidderRequest); validateBuiltServerRequest(buildRequest, serverRequest); @@ -1520,89 +1512,141 @@ describe('33acrossBidAdapter:', function () { }); }); - context('when mediaType has video only and context is instream', function() { - it('builds instream request with default params', function() { - const bidRequests = ( - new BidRequestsBuilder() - .withVideo({context: 'instream'}) - .build() - ); + context('when mediaType has video only', function() { + context('and context is instream', function() { + it('builds instream request with default params', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream'}) + .build() + ); - const ttxRequest = new TtxRequestBuilder() - .withVideo() - .withProduct('instream') - .build(); + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('instream') + .build(); - ttxRequest.imp[0].video.placement = 1; - ttxRequest.imp[0].video.startdelay = 0; + ttxRequest.imp[0].video.startdelay = 0; - const serverRequest = new ServerRequestBuilder() - .withData(ttxRequest) - .build(); - const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - validateBuiltServerRequest(builtServerRequest, serverRequest); - }); + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); - it('builds instream request with params passed', function() { - const bidRequests = ( - new BidRequestsBuilder() - .withVideo({context: 'instream', startdelay: -2}) - .build() - ); + it('builds instream request with params passed', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream', startdelay: -2}) + .build() + ); - const ttxRequest = new TtxRequestBuilder() - .withVideo({startdelay: -2, placement: 1}) - .withProduct('instream') - .build(); + const ttxRequest = new TtxRequestBuilder() + .withVideo({startdelay: -2}) + .withProduct('instream') + .build(); - const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + }); + + context('when the placement is still specified in the DEPRECATED `placement` field', function() { + it('does not overwrite its value and does not set it in the recent `plcmt` field as well', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({ + placement: 2, // Incorrect placement for an instream video + context: 'instream' + }) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('instream') + .build(); + + ttxRequest.imp[0].video.placement = 2; + ttxRequest.imp[0].video.startdelay = 0; + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + }); + }); }); - }); - context('when mediaType has video only and context is outstream', function() { - it('builds siab request with video only with default params', function() { - const bidRequests = ( - new BidRequestsBuilder() - .withVideo({context: 'outstream'}) - .build() - ); + context('and context is outstream', function() { + it('builds siab request with video only with default params', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); - const ttxRequest = new TtxRequestBuilder() - .withVideo() - .withProduct('siab') - .build(); + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('siab') + .build(); - ttxRequest.imp[0].video.placement = 2; + // No placement specified, final value should default to 2. + ttxRequest.imp[0].video.plcmt = 2; - const serverRequest = new ServerRequestBuilder() - .withData(ttxRequest) - .build(); - const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - validateBuiltServerRequest(builtServerRequest, serverRequest); - }); + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); - it('builds siab request with video params passed', function() { - const bidRequests = ( - new BidRequestsBuilder() - .withVideo({context: 'outstream', placement: 3, playbackmethod: [2]}) - .build() - ); + it('builds siab request with video params passed', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream', plcmt: 3, playbackmethod: [2]}) + .build() + ); - const ttxRequest = new TtxRequestBuilder() - .withVideo({placement: 3, playbackmethod: [2]}) - .withProduct('siab') - .build(); + const ttxRequest = new TtxRequestBuilder() + .withVideo({plcmt: 3, playbackmethod: [2]}) + .withProduct('siab') + .build(); - const serverRequest = new ServerRequestBuilder() - .withData(ttxRequest) - .build(); - const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - validateBuiltServerRequest(builtServerRequest, serverRequest); + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + + context('and the placement is specified in the DEPRECATED `placement` field', function() { + it('sets the recent `plcmt` field', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream', placement: 3, playbackmethod: [2]}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo({plcmt: 3, placement: 3, playbackmethod: [2]}) + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); }); }); @@ -1686,7 +1730,7 @@ describe('33acrossBidAdapter:', function () { .withProduct('siab') .build(); - ttxRequest.imp[0].video.placement = 2; + ttxRequest.imp[0].video.plcmt = 2; const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 364f7d2845c..cd70dfd9e25 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -1,27 +1,30 @@ -import { thirthyThreeAcrossIdSubmodule, storage } from 'modules/33acrossIdSystem.js'; +import { thirtyThreeAcrossIdSubmodule, storage, domainUtils } from 'modules/33acrossIdSystem.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; import { uspDataHandler, coppaDataHandler, gppDataHandler } from 'src/adapterManager.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; +import {attachIdSystem} from '../../../modules/userId/index.js'; describe('33acrossIdSystem', () => { describe('name', () => { it('should expose the name of the submodule', () => { - expect(thirthyThreeAcrossIdSubmodule.name).to.equal('33acrossId'); + expect(thirtyThreeAcrossIdSubmodule.name).to.equal('33acrossId'); }); }); describe('gvlid', () => { it('should expose the vendor id', () => { - expect(thirthyThreeAcrossIdSubmodule.gvlid).to.equal(58); + expect(thirtyThreeAcrossIdSubmodule.gvlid).to.equal(58); }); }); describe('getId', () => { - it('should call endpoint and handle valid response', () => { + it('should call endpoint', () => { const completeCallback = sinon.spy(); - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -47,100 +50,411 @@ describe('33acrossIdSystem', () => { const regExp = new RegExp('https://lexicon.33across.com/v1/envelope\\?pid=12345&gdpr=\\d&src=pbjs&ver=$prebid.version$'); expect(request.url).to.match(regExp); - expect(completeCallback.calledOnceWithExactly('foo')).to.be.true; }); - context('if the use of a first-party ID has been enabled', () => { - context('and the response includes a first-party ID', () => { - context('and the storage type is "cookie"', () => { - it('should store the provided first-party ID in a cookie', () => { + context('when there\'s a successful response containing an ID', () => { + it('should execute the callback and pass the ID', () => { + const completeCallback = sinon.spy(); + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo' + }, + expires: 1645667805067 + })); + + expect(completeCallback.calledOnceWithExactly('foo')).to.be.true; + }); + + it('should NOT wipe any stored hashed email', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + callback(completeCallback); + + const [request] = server.requests; + + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo' + }, + expires: 1645667805067 + })); + + expect(removeDataFromLocalStorage.calledWithExactly('33acrossIdHm')).to.be.false; + expect(setCookie.calledWithExactly('33acrossIdHm', '', sinon.match.string, 'Lax', 'foo.com')).to.be.false; + + removeDataFromLocalStorage.restore(); + setCookie.restore(); + domainUtils.domainOverride.restore(); + }); + }); + + const additionalOptions = { + 'by an option': { storeFpid: true, storeTpid: true }, + 'by default': { } // No storeFpid, default value should be true + }; + + Object.entries(additionalOptions).forEach(([caseTitle, opts]) => { + context(`if the use of a first-party ID has been enabled ${caseTitle}`, () => { + context('and the response includes a first-party ID', () => { + context('and the enabled storage types include "cookie"', () => { + it('should store the provided first-party ID in a cookie', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + ...opts + }, + enabledStorageTypes: [ 'cookie' ], + storage: { expires: 30 } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + + setCookie.restore(); + domainUtils.domainOverride.restore(); + }); + }); + + context('and the enabled storage types include "html5"', () => { + it('should store the provided first-party ID in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + ...opts + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledWithExactly('33acrossIdFp', 'bar')).to.be.true; + + setDataInLocalStorage.restore(); + }); + }); + + context('and the enabled storage types are "cookie" and "html5"', () => { + it('should store the provided first-party ID in each storage type', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + ...opts + }, + enabledStorageTypes: [ 'cookie', 'html5' ], + storage: {} + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + expect(setDataInLocalStorage.calledWithExactly('33acrossIdFp', 'bar')).to.be.true; + + setCookie.restore(); + domainUtils.domainOverride.restore(); + setDataInLocalStorage.restore(); + }); + }); + }); + + context('and the response lacks a first-party ID', () => { + it('should wipe any existing first-party ID from storage', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345', - storeFpid: true + ...opts }, - storage: { - type: 'cookie', - expires: 30 - } + enabledStorageTypes: [ 'html5' ], + storage: {} }); callback(completeCallback); const [request] = server.requests; + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); const setCookie = sinon.stub(storage, 'setCookie'); - const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ succeeded: true, data: { - envelope: 'foo', - fp: 'bar' + envelope: 'foo' // no 'fp' field }, expires: 1645667805067 })); - expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.true; + expect(removeDataFromLocalStorage.calledWithExactly('33acrossIdFp')).to.be.true; + expect(setCookie.calledWithExactly('33acrossIdFp', '', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + removeDataFromLocalStorage.restore(); setCookie.restore(); - cookiesAreEnabled.restore(); + domainUtils.domainOverride.restore(); }); }); + }); + + context(`if the use of a supplemental third-party ID has been enabled ${caseTitle}`, () => { + context('and the response includes a third-party ID', () => { + context('and the enabled storage type include "cookie"', () => { + it('should store the provided third-party ID in a cookie', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + ...opts + }, + enabledStorageTypes: [ 'cookie' ], + storage: { expires: 30 } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + tp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledWithExactly('33acrossIdTp', 'bar', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + + setCookie.restore(); + domainUtils.domainOverride.restore(); + }); + }); + + context('and the enabled storage types include "html5"', () => { + it('should store the provided third-party ID in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + ...opts + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + callback(completeCallback); - context('and the storage type is "html5"', () => { - it('should store the provided first-party ID in local storage', () => { + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + tp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledWithExactly('33acrossIdTp', 'bar')).to.be.true; + + setDataInLocalStorage.restore(); + }); + }); + + context('and the enabled storage types are "cookie" and "html5"', () => { + it('should store the provided third-party ID in each storage type', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + ...opts + }, + enabledStorageTypes: [ 'cookie', 'html5' ], + storage: {} + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + tp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledWithExactly('33acrossIdTp', 'bar', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + expect(setDataInLocalStorage.calledWithExactly('33acrossIdTp', 'bar')).to.be.true; + + setCookie.restore(); + domainUtils.domainOverride.restore(); + setDataInLocalStorage.restore(); + }); + }); + }); + + context('and the response lacks a third-party ID', () => { + it('should wipe any existing third-party ID from storage', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345', - storeFpid: true + ...opts }, - storage: { - type: 'html5' - } + enabledStorageTypes: [ 'html5' ], + storage: {} }); callback(completeCallback); const [request] = server.requests; - const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ succeeded: true, data: { - envelope: 'foo', - fp: 'bar' + envelope: 'foo' // no 'tp' field }, expires: 1645667805067 })); - expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + expect(removeDataFromLocalStorage.calledWithExactly('33acrossIdTp')).to.be.true; + expect(setCookie.calledWithExactly('33acrossIdTp', '', sinon.match.string, 'Lax', 'foo.com')).to.be.true; - setDataInLocalStorage.restore(); + removeDataFromLocalStorage.restore(); + setCookie.restore(); + domainUtils.domainOverride.restore(); }); }); }); + }); - context('and the response lacks a first-party ID', () => { - it('should wipe any existing first-party ID from storage', () => { + context('if the use of a first-party ID has been disabled', () => { + context('and the response includes a first-party ID', () => { + it('should not store the provided first-party ID in a cookie', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345', - storeFpid: true + storeFpid: false }, + enabledStorageTypes: [ 'cookie' ], storage: { - type: 'html5' + expires: 30 } }); @@ -148,42 +462,74 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; - const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); const setCookie = sinon.stub(storage, 'setCookie'); - const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ succeeded: true, data: { - envelope: 'foo' // no 'fp' field + envelope: 'foo', + fp: 'bar' }, expires: 1645667805067 })); - expect(removeDataFromLocalStorage.calledOnceWithExactly('33acrossIdFp')).to.be.true; - expect(setCookie.calledOnceWithExactly('33acrossIdFp', '', sinon.match.string, 'Lax')).to.be.true; + expect(setCookie.calledWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax', 'foo.com')).to.be.false; - removeDataFromLocalStorage.restore(); setCookie.restore(); - cookiesAreEnabled.restore(); + domainUtils.domainOverride.restore(); + }); + + it('should not store the provided first-party ID in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + storeFpid: false + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledWithExactly('33acrossIdFp', 'bar')).to.be.false; + + setDataInLocalStorage.restore(); }); }); }); - context('if the use of a first-party ID has been disabled (default value)', () => { - context('and the response includes a first-party ID', () => { - it('should not store the provided first-party ID in a cookie', () => { + context('if the use of a supplemental third-party ID has been disabled', () => { + context('and the response includes a third-party ID', () => { + it('should not store the provided third-party ID in a cookie', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { - pid: '12345' - // no storeFpid param + pid: '12345', + storeTpid: false }, + enabledStorageTypes: [ 'cookie' ], storage: { - type: 'cookie', expires: 30 } }); @@ -193,7 +539,6 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; const setCookie = sinon.stub(storage, 'setCookie'); - const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); request.respond(200, { 'Content-Type': 'application/json' @@ -201,28 +546,26 @@ describe('33acrossIdSystem', () => { succeeded: true, data: { envelope: 'foo', - fp: 'bar' + tp: 'bar' }, expires: 1645667805067 })); - expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.false; + expect(setCookie.calledWithExactly('33acrossIdTp', 'bar', sinon.match.string, 'Lax')).to.be.false; setCookie.restore(); - cookiesAreEnabled.restore(); }); - it('should not store the provided first-party ID in local storage', () => { + it('should not store the provided third-party ID in local storage', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { - pid: '12345' - // no storeFpid param + pid: '12345', + storeTpid: false }, - storage: { - type: 'html5' - } + enabledStorageTypes: [ 'html5' ], + storage: {} }); callback(completeCallback); @@ -237,12 +580,12 @@ describe('33acrossIdSystem', () => { succeeded: true, data: { envelope: 'foo', - fp: 'bar' + tp: 'bar' }, expires: 1645667805067 })); - expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.false; + expect(setDataInLocalStorage.calledWithExactly('33acrossIdTp', 'bar')).to.be.false; setDataInLocalStorage.restore(); }); @@ -253,13 +596,12 @@ describe('33acrossIdSystem', () => { it('should wipe any existing "envelope" ID from storage', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' }, - storage: { - type: 'html5' - } + enabledStorageTypes: [ 'html5' ], + storage: {} }); callback(completeCallback); @@ -268,7 +610,7 @@ describe('33acrossIdSystem', () => { const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); const setCookie = sinon.stub(storage, 'setCookie'); - const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); request.respond(200, { 'Content-Type': 'application/json' @@ -280,12 +622,14 @@ describe('33acrossIdSystem', () => { expires: 1645667805067 })); - expect(removeDataFromLocalStorage.calledWith('33acrossId')).to.be.true; - expect(setCookie.calledWith('33acrossId', '', sinon.match.string, 'Lax')).to.be.true; + ['', '_last', '_exp', '_cst'].forEach(suffix => { + expect(removeDataFromLocalStorage.calledWith(`33acrossId${suffix}`)).to.be.true; + expect(setCookie.calledWithExactly(`33acrossId${suffix}`, '', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + }); removeDataFromLocalStorage.restore(); setCookie.restore(); - cookiesAreEnabled.restore(); + domainUtils.domainOverride.restore(); }); }); @@ -293,12 +637,12 @@ describe('33acrossIdSystem', () => { it('should log a warning and don\'t expect a call to the endpoint', () => { const logWarnSpy = sinon.spy(utils, 'logWarn'); - const result = thirthyThreeAcrossIdSubmodule.getId({ + const result = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } }, { - gdprApplies: true + gdpr: {gdprApplies: true} }); expect(logWarnSpy.calledOnceWithExactly('33acrossId: Submodule cannot be used where GDPR applies')).to.be.true; @@ -311,12 +655,12 @@ describe('33acrossIdSystem', () => { context('when GDPR doesn\'t apply', () => { it('should call endpoint with \'gdpr=0\'', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } }, { - gdprApplies: false + gdpr: {gdprApplies: false} }); callback(completeCallback); @@ -329,13 +673,15 @@ describe('33acrossIdSystem', () => { context('but the GDPR consent string is given', () => { it('should call endpoint with the GDPR consent string', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } }, { - gdprApplies: false, - consentString: 'foo' + gdpr: { + gdprApplies: false, + consentString: 'foo' + } }); callback(completeCallback); @@ -350,7 +696,7 @@ describe('33acrossIdSystem', () => { context('when a valid US Privacy string is given', () => { it('should call endpoint with the US Privacy parameter', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -371,7 +717,7 @@ describe('33acrossIdSystem', () => { context('when an invalid US Privacy is given', () => { it('should call endpoint without the US Privacy parameter', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -393,7 +739,7 @@ describe('33acrossIdSystem', () => { context('when coppa is enabled', () => { it('should call endpoint with an enabled coppa signal', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -414,7 +760,7 @@ describe('33acrossIdSystem', () => { context('when coppa is not enabled', () => { it('should call endpoint with coppa signal not enabled', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -448,7 +794,7 @@ describe('33acrossIdSystem', () => { { gppString: 'foo', expected: 'foo' }, ].forEach(({ gppString, expected }, index) => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -474,7 +820,7 @@ describe('33acrossIdSystem', () => { { applicableSections: ['1', '2'], expected: '1%2C2' }, ].forEach(({ applicableSections, expected }, index) => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -495,13 +841,12 @@ describe('33acrossIdSystem', () => { context('when a first-party ID is present in local storage', () => { it('should call endpoint with the encoded first-party ID included', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' }, - storage: { - type: 'html5' - } + enabledStorageTypes: [ 'html5' ], + storage: {} }); sinon.stub(storage, 'getDataFromLocalStorage') @@ -521,13 +866,12 @@ describe('33acrossIdSystem', () => { context('when a first-party ID is present in cookie storage', () => { it('should call endpoint with the first-party ID included', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' }, - storage: { - type: 'cookie' - } + enabledStorageTypes: [ 'cookie' ], + storage: {} }); sinon.stub(storage, 'getCookie') @@ -547,7 +891,7 @@ describe('33acrossIdSystem', () => { context('when a first-party ID is not present in storage', () => { it('should not call endpoint with the first-party ID included', () => { const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -561,11 +905,359 @@ describe('33acrossIdSystem', () => { }); }); + context('when a third-party ID is present in local storage', () => { + it('should call endpoint with the encoded third-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + sinon.stub(storage, 'getDataFromLocalStorage') + .withArgs('33acrossIdTp') + .returns('33acrossIdTpValue+'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('tp=33acrossIdTpValue%2B'); + + storage.getDataFromLocalStorage.restore(); + }); + }); + + context('when a third-party ID is present in cookie storage', () => { + it('should call endpoint with the third-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + enabledStorageTypes: [ 'cookie' ], + storage: {} + }); + + sinon.stub(storage, 'getCookie') + .withArgs('33acrossIdTp') + .returns('33acrossIdTpValue'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('tp=33acrossIdTpValue'); + + storage.getCookie.restore(); + }); + }); + + context('when a third-party ID is not present in storage', () => { + it('should not call endpoint with the third-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).not.to.contain('tp='); + }); + }); + + context('when a hashed email is provided via configuration options', () => { + it('should call endpoint with the hashed email included', () => { + const completeCallback = () => {}; + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + hem: '33acrossIdHmValue+' + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + // Prioritizes the hem given via config options over the one stored in LS. + sinon.stub(storage, 'getDataFromLocalStorage') + .withArgs('33acrossIdHm') + .returns('33acrossIdHmValueLS'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('sha256=33acrossIdHmValue%2B'); + + storage.getDataFromLocalStorage.restore(); + }); + + context('and the enabled storage types include "html5"', () => { + it('should store the hashed email in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + hem: '33acrossIdHmValue+' + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledWithExactly('33acrossIdHm', '33acrossIdHmValue+')).to.be.true; + + setDataInLocalStorage.restore(); + }); + }); + + context('and the enabled storage types include "cookie"', () => { + it('should store the hashed email in a cookie', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + hem: '33acrossIdHmValue+' + }, + enabledStorageTypes: [ 'cookie' ], + storage: { expires: 30 } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledWithExactly('33acrossIdHm', '33acrossIdHmValue+', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + + setCookie.restore(); + domainUtils.domainOverride.restore(); + }); + }); + }); + + context('when a hashed email is NOT provided via configuration options', () => { + context('but it\'s provided via global 33across object', () => { + beforeEach(() => { + window._33across = { + hem: { + sha256: 'fake-sha256-hashed-email+' + } + } + }); + + afterEach(() => { + delete window._33across; + }); + + it('should call endpoint with the hashed email included', () => { + const completeCallback = () => {}; + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + // No hashed email via config option. + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + // Prioritizes the hashed email given via global object over the one stored in LS. + sinon.stub(storage, 'getDataFromLocalStorage') + .withArgs('33acrossIdHm') + .returns('33acrossIdHmValueLS'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('sha256=fake-sha256-hashed-email%2B'); + + storage.getDataFromLocalStorage.restore(); + }); + + context('and the enabled storage types include "html5"', () => { + it('should store the hashed email in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledWithExactly('33acrossIdHm', 'fake-sha256-hashed-email+')).to.be.true; + + setDataInLocalStorage.restore(); + }); + }); + + context('and the enabled storage types include "cookie"', () => { + it('should store the hashed email in a cookie', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + enabledStorageTypes: [ 'cookie' ], + storage: { expires: 30 } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledWithExactly('33acrossIdHm', 'fake-sha256-hashed-email+', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + + setCookie.restore(); + domainUtils.domainOverride.restore(); + }); + }); + }); + + context('but it\'s provided via local storage', () => { + it('should call endpoint with the hashed email included', () => { + const completeCallback = () => {}; + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + sinon.stub(storage, 'getDataFromLocalStorage') + .withArgs('33acrossIdHm') + .returns('33acrossIdHmValue+'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('sha256=33acrossIdHmValue%2B'); + + storage.getDataFromLocalStorage.restore(); + }); + }); + + context('but it\'s provided via cookie storage', () => { + it('should call endpoint with the hashed email included', () => { + const completeCallback = () => {}; + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + enabledStorageTypes: [ 'cookie' ], + storage: {} + }); + + sinon.stub(storage, 'getCookie') + .withArgs('33acrossIdHm') + .returns('33acrossIdHmValue'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('sha256=33acrossIdHmValue'); + + storage.getCookie.restore(); + }); + }); + + context('and hashed email is not present in storage', () => { + it('should not call endpoint with the hashed email included', () => { + const completeCallback = () => {}; + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).not.to.contain('sha256='); + }); + }); + }); + context('when the partner ID is not given', () => { it('should log an error', () => { const logErrorSpy = sinon.spy(utils, 'logError'); - thirthyThreeAcrossIdSubmodule.getId({ + thirtyThreeAcrossIdSubmodule.getId({ params: { /* No 'pid' param */ } }); @@ -579,7 +1271,7 @@ describe('33acrossIdSystem', () => { it('should log an error', () => { const logErrorSpy = sinon.spy(utils, 'logError'); - thirthyThreeAcrossIdSubmodule.getId({ + thirtyThreeAcrossIdSubmodule.getId({ params: { pid: 123456 // PID must be a string } @@ -596,7 +1288,7 @@ describe('33acrossIdSystem', () => { const logErrorSpy = sinon.spy(utils, 'logError'); const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -610,7 +1302,7 @@ describe('33acrossIdSystem', () => { 'Content-Type': 'application/json' }, 'invalid response'); - expect(logErrorSpy.lastCall.args[0]).to.eq(`${thirthyThreeAcrossIdSubmodule.name}: ID reading error:`); + expect(logErrorSpy.lastCall.args[0]).to.eq(`${thirtyThreeAcrossIdSubmodule.name}: ID reading error:`); logErrorSpy.restore(); }); @@ -618,7 +1310,7 @@ describe('33acrossIdSystem', () => { it('should execute complete callback with undefined value', () => { const completeCallback = sinon.spy(); - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -639,7 +1331,7 @@ describe('33acrossIdSystem', () => { context('when an endpoint override is given', () => { it('should call that endpoint', () => { const completeCallback = sinon.spy(); - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345', apiUrl: 'https://staging-lexicon.33across.com/v1/envelope' @@ -669,7 +1361,7 @@ describe('33acrossIdSystem', () => { const logErrorSpy = sinon.spy(utils, 'logError'); const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -686,7 +1378,7 @@ describe('33acrossIdSystem', () => { error: 'foo' })); - expect(logErrorSpy.calledOnceWithExactly(`${thirthyThreeAcrossIdSubmodule.name}: Unsuccessful response foo`)).to.be.true; + expect(logErrorSpy.calledOnceWithExactly(`${thirtyThreeAcrossIdSubmodule.name}: Unsuccessful response foo`)).to.be.true; logErrorSpy.restore(); }); @@ -694,7 +1386,7 @@ describe('33acrossIdSystem', () => { it('should execute complete callback with undefined value', () => { const completeCallback = sinon.spy(); - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -720,7 +1412,7 @@ describe('33acrossIdSystem', () => { const logMessageSpy = sinon.spy(utils, 'logMessage'); const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -737,7 +1429,7 @@ describe('33acrossIdSystem', () => { data: {} })); - expect(logMessageSpy.calledOnceWithExactly(`${thirthyThreeAcrossIdSubmodule.name}: No envelope was received`)).to.be.true; + expect(logMessageSpy.calledOnceWithExactly(`${thirtyThreeAcrossIdSubmodule.name}: No envelope was received`)).to.be.true; logMessageSpy.restore(); }); @@ -745,7 +1437,7 @@ describe('33acrossIdSystem', () => { it('should execute complete callback with undefined value', () => { const completeCallback = sinon.spy(); - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -764,6 +1456,43 @@ describe('33acrossIdSystem', () => { expect(completeCallback.calledOnceWithExactly(undefined)).to.be.true; }); + + it('should wipe any stored hashed email', () => { + const completeCallback = () => {}; + + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + enabledStorageTypes: [ 'html5' ], + storage: {} + }); + + callback(completeCallback); + + const [request] = server.requests; + + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); + const setCookie = sinon.stub(storage, 'setCookie'); + sinon.stub(domainUtils, 'domainOverride').returns('foo.com'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + // no envelope field + }, + expires: 1645667805067 + })); + + expect(removeDataFromLocalStorage.calledWithExactly('33acrossIdHm')).to.be.true; + expect(setCookie.calledWithExactly('33acrossIdHm', '', sinon.match.string, 'Lax', 'foo.com')).to.be.true; + + removeDataFromLocalStorage.restore(); + setCookie.restore(); + domainUtils.domainOverride.restore(); + }); }); context('when the server returns an error status code', () => { @@ -771,7 +1500,7 @@ describe('33acrossIdSystem', () => { const logErrorSpy = sinon.spy(utils, 'logError'); const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -783,7 +1512,7 @@ describe('33acrossIdSystem', () => { request.respond(404); - expect(logErrorSpy.calledOnceWithExactly(`${thirthyThreeAcrossIdSubmodule.name}: ID error response`, 'Not Found')).to.be.true; + expect(logErrorSpy.calledOnceWithExactly(`${thirtyThreeAcrossIdSubmodule.name}: ID error response`, 'Not Found')).to.be.true; logErrorSpy.restore(); }); @@ -791,7 +1520,7 @@ describe('33acrossIdSystem', () => { it('should execute complete callback without any value', () => { const completeCallback = sinon.spy(); - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + const { callback } = thirtyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } @@ -810,11 +1539,32 @@ describe('33acrossIdSystem', () => { describe('decode', () => { it('should wrap the given value inside an object literal', () => { - expect(thirthyThreeAcrossIdSubmodule.decode('foo')).to.deep.equal({ - [thirthyThreeAcrossIdSubmodule.name]: { + expect(thirtyThreeAcrossIdSubmodule.decode('foo')).to.deep.equal({ + [thirtyThreeAcrossIdSubmodule.name]: { envelope: 'foo' } }); }); }); + describe('eid', () => { + before(() => { + attachIdSystem(thirtyThreeAcrossIdSubmodule); + }) + it('33acrossId', function() { + const userId = { + '33acrossId': { + envelope: 'some-random-id-value' + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: '33across.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + }) }); diff --git a/test/spec/modules/51DegreesRtdProvider_spec.js b/test/spec/modules/51DegreesRtdProvider_spec.js new file mode 100644 index 00000000000..7b60a08b906 --- /dev/null +++ b/test/spec/modules/51DegreesRtdProvider_spec.js @@ -0,0 +1,485 @@ +import { + extractConfig, + get51DegreesJSURL, + is51DegreesMetaPresent, + deepSetNotEmptyValue, + convert51DegreesDataToOrtb2, + convert51DegreesDeviceToOrtb2, + getBidRequestData, + fiftyOneDegreesSubmodule, +} from 'modules/51DegreesRtdProvider'; +import {mergeDeep} from '../../../src/utils'; + +const inject51DegreesMeta = () => { + const meta = document.createElement('meta'); + meta.httpEquiv = 'Delegate-CH'; + meta.content = 'sec-ch-ua-full-version-list https://cloud.51degrees.com; sec-ch-ua-model https://cloud.51degrees.com; sec-ch-ua-platform https://cloud.51degrees.com; sec-ch-ua-platform-version https://cloud.51degrees.com'; + document.head.appendChild(meta); +}; + +describe('51DegreesRtdProvider', function() { + const fiftyOneDegreesDevice = { + screenpixelswidth: 5120, + screenpixelsheight: 1440, + hardwarevendor: 'Apple', + hardwaremodel: 'Macintosh', + hardwarename: [ + 'Macintosh', + ], + platformname: 'macOS', + platformversion: '14.1.2', + screeninchesheight: 13.27, + screenincheswidth: 47.17, + devicetype: 'Desktop', + pixelratio: 1, + deviceid: '17595-131215-132535-18092', + }; + + const fiftyOneDegreesDeviceX2scaling = { + ...fiftyOneDegreesDevice, + screenpixelsheight: fiftyOneDegreesDevice.screenpixelsheight / 2, + screenpixelswidth: fiftyOneDegreesDevice.screenpixelswidth / 2, + screenpixelsphysicalheight: fiftyOneDegreesDevice.screenpixelsheight, + screenpixelsphysicalwidth: fiftyOneDegreesDevice.screenpixelswidth, + pixelratio: fiftyOneDegreesDevice.pixelratio * 2, + }; + + const fiftyOneDegreesData = { + device: fiftyOneDegreesDevice, + }; + + const expectedORTB2DeviceResult = { + device: { + devicetype: 2, + make: 'Apple', + model: 'Macintosh', + os: 'macOS', + osv: '14.1.2', + h: 1440, + w: 5120, + ppi: 109, + pxratio: 1, + ext: { + fiftyonedegrees_deviceId: '17595-131215-132535-18092', + }, + }, + }; + + const expectedORTB2Result = {}; + mergeDeep( + expectedORTB2Result, + expectedORTB2DeviceResult, + // placeholder for the next 51Degrees RTD submodule update + ); + + describe('extractConfig', function() { + it('returns the resourceKey from the moduleConfig', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: {resourceKey: 'TEST_RESOURCE_KEY'}}; + expect(extractConfig(moduleConfig, reqBidsConfigObj)).to.deep.equal({ + resourceKey: 'TEST_RESOURCE_KEY', + onPremiseJSUrl: undefined, + }); + }); + + it('returns the onPremiseJSUrl from the moduleConfig', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: {onPremiseJSUrl: 'https://example.com/51Degrees.core.js'}}; + expect(extractConfig(moduleConfig, reqBidsConfigObj)).to.deep.equal({ + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + resourceKey: undefined, + }); + }); + + it('throws an error if neither resourceKey nor onPremiseJSUrl is provided', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: {}}; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); + }); + + it('throws an error if both resourceKey and onPremiseJSUrl are provided', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: { + resourceKey: 'TEST_RESOURCE_KEY', + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + }}; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); + }); + + it('throws an error if the resourceKey is equal to "" from example', function() { + const reqBidsConfigObj = {}; + const moduleConfig = {params: {resourceKey: ''}}; + expect(() => extractConfig(moduleConfig, reqBidsConfigObj)).to.throw(); + }); + + it('sets the resourceKey to undefined if it was set to "0"', function() { + const moduleConfig = {params: { + resourceKey: '0', + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + }}; + expect(extractConfig(moduleConfig, {})).to.deep.equal({ + resourceKey: undefined, + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + }); + }); + + it('sets the onPremiseJSUrl to undefined if it was set to "0"', function() { + const moduleConfig = {params: { + resourceKey: 'TEST_RESOURCE_KEY', + onPremiseJSUrl: '0', + }}; + expect(extractConfig(moduleConfig, {})).to.deep.equal({ + resourceKey: 'TEST_RESOURCE_KEY', + onPremiseJSUrl: undefined, + }); + }); + + it('throws an error if the onPremiseJSUrl is not a valid URL', function() { + expect(() => extractConfig({ + params: {onPremiseJSUrl: 'invalid URL'} + }, {})).to.throw(); + expect(() => extractConfig({ + params: {onPremiseJSUrl: 'www.example.com/51Degrees.core.js'} + }, {})).to.throw(); + }); + + it('allows the onPremiseJSUrl to be a valid URL', function() { + const VALID_URLS = [ + 'https://www.example.com/51Degrees.core.js', + 'http://example.com/51Degrees.core.js', + '//example.com/51Degrees.core.js', + '/51Degrees.core.js', + ]; + + VALID_URLS.forEach(url => { + expect(() => extractConfig({ + params: {onPremiseJSUrl: url} + }, {})).to.not.throw(); + }); + }); + }); + + describe('get51DegreesJSURL', function() { + const hev = { + 'brands': [ + { + 'brand': 'Chromium', + 'version': '130' + }, + { + 'brand': 'Google Chrome', + 'version': '130' + }, + { + 'brand': 'Not?A_Brand', + 'version': '99' + } + ], + 'fullVersionList': [ + { + 'brand': 'Chromium', + 'version': '130.0.6723.92' + }, + { + 'brand': 'Google Chrome', + 'version': '130.0.6723.92' + }, + { + 'brand': 'Not?A_Brand', + 'version': '99.0.0.0' + } + ], + 'mobile': false, + 'model': '', + 'platform': 'macOS', + 'platformVersion': '14.6.1' + }; + const mockWindow = { + ...window, + screen: { + height: 1117, + width: 1728, + }, + devicePixelRatio: 2, + }; + + it('returns the cloud URL if the resourceKey is provided', function() { + const config = {resourceKey: 'TEST_RESOURCE_KEY'}; + expect(get51DegreesJSURL(config, mockWindow)).to.equal( + 'https://cloud.51degrees.com/api/v4/TEST_RESOURCE_KEY.js?' + + `51D_ScreenPixelsHeight=${mockWindow.screen.height}&` + + `51D_ScreenPixelsWidth=${mockWindow.screen.width}&` + + `51D_PixelRatio=${mockWindow.devicePixelRatio}` + ); + }); + + it('returns the on-premise URL if the onPremiseJSUrl is provided', function () { + const config = {onPremiseJSUrl: 'https://example.com/51Degrees.core.js'}; + expect(get51DegreesJSURL(config, mockWindow)).to.equal( + `https://example.com/51Degrees.core.js?` + + `51D_ScreenPixelsHeight=${mockWindow.screen.height}&` + + `51D_ScreenPixelsWidth=${mockWindow.screen.width}&` + + `51D_PixelRatio=${mockWindow.devicePixelRatio}` + ); + }); + + it('doesn\'t override static query string parameters', function () { + const config = {onPremiseJSUrl: 'https://example.com/51Degrees.core.js?test=1'}; + expect(get51DegreesJSURL(config, mockWindow)).to.equal( + `https://example.com/51Degrees.core.js?test=1&` + + `51D_ScreenPixelsHeight=${mockWindow.screen.height}&` + + `51D_ScreenPixelsWidth=${mockWindow.screen.width}&` + + `51D_PixelRatio=${mockWindow.devicePixelRatio}` + ); + }); + + it('adds high entropy values to the query string, if available', async function () { + const config = { + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + hev, + }; + expect(get51DegreesJSURL(config, mockWindow)).to.equal( + `https://example.com/51Degrees.core.js?` + + `51D_GetHighEntropyValues=${btoa(JSON.stringify(hev))}&` + + `51D_ScreenPixelsHeight=${mockWindow.screen.height}&` + + `51D_ScreenPixelsWidth=${mockWindow.screen.width}&` + + `51D_PixelRatio=${mockWindow.devicePixelRatio}` + ); + }); + + it('doesn\'t add high entropy values to the query string if object is empty', function () { + const config = { + onPremiseJSUrl: 'https://example.com/51Degrees.core.js', + hev: {}, + }; + expect(get51DegreesJSURL(config, mockWindow)).to.equal( + `https://example.com/51Degrees.core.js?` + + `51D_ScreenPixelsHeight=${mockWindow.screen.height}&` + + `51D_ScreenPixelsWidth=${mockWindow.screen.width}&` + + `51D_PixelRatio=${mockWindow.devicePixelRatio}` + ); + }); + + it('keeps the original URL if none of the additional parameters are available', function () { + // delete screen and devicePixelRatio properties to test the case when they are not available + delete mockWindow.screen; + delete mockWindow.devicePixelRatio; + + const config = {onPremiseJSUrl: 'https://example.com/51Degrees.core.js'}; + expect(get51DegreesJSURL(config, mockWindow)).to.equal('https://example.com/51Degrees.core.js'); + expect(get51DegreesJSURL(config, window)).to.not.equal('https://example.com/51Degrees.core.js'); + }); + }); + + describe('is51DegreesMetaPresent', function() { + let initialHeadInnerHTML; + + before(function() { + initialHeadInnerHTML = document.head.innerHTML; + }); + + afterEach(function() { + document.head.innerHTML = initialHeadInnerHTML; + }); + + it('returns true if the 51Degrees meta tag is present', function () { + inject51DegreesMeta(); + expect(is51DegreesMetaPresent()).to.be.true; + }); + + it('returns false if the 51Degrees meta tag is not present', function() { + expect(is51DegreesMetaPresent()).to.be.false; + }); + + it('works with multiple meta tags, even if those are not to include any `content`', function() { + const meta1 = document.createElement('meta'); + meta1.httpEquiv = 'Delegate-CH'; + document.head.appendChild(meta1); + + inject51DegreesMeta(); + + const meta2 = document.createElement('meta'); + meta2.httpEquiv = 'Delegate-CH'; + document.head.appendChild(meta2); + + expect(is51DegreesMetaPresent()).to.be.true; + }); + }); + + describe('deepSetNotEmptyValue', function() { + it('sets value of ORTB2 key if it is not empty', function() { + const data = {}; + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', 'TEST_ORTB2_VALUE'); + expect(data).to.deep.equal({TEST_ORTB2_KEY: 'TEST_ORTB2_VALUE'}); + deepSetNotEmptyValue(data, 'test2.TEST_ORTB2_KEY_2', 'TEST_ORTB2_VALUE_2'); + expect(data).to.deep.equal({ + TEST_ORTB2_KEY: 'TEST_ORTB2_VALUE', + test2: { + TEST_ORTB2_KEY_2: 'TEST_ORTB2_VALUE_2' + }, + }); + }); + + it('throws an error if the key is empty', function() { + const data = {}; + expect(() => deepSetNotEmptyValue(data, '', 'TEST_ORTB2_VALUE')).to.throw(); + }); + + it('does not set value of ORTB2 key if it is empty', function() { + const data = {}; + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', ''); + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', 0); + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', null); + deepSetNotEmptyValue(data, 'TEST_ORTB2_KEY', undefined); + deepSetNotEmptyValue(data, 'TEST.TEST_ORTB2_KEY', undefined); + expect(data).to.deep.equal({}); + }); + }); + + describe('convert51DegreesDataToOrtb2', function() { + it('returns empty object if data is null, undefined or empty', () => { + expect(convert51DegreesDataToOrtb2(null)).to.deep.equal({}); + expect(convert51DegreesDataToOrtb2(undefined)).to.deep.equal({}); + expect(convert51DegreesDataToOrtb2({})).to.deep.equal({}); + }); + + it('converts all 51Degrees data to ORTB2 format', function() { + expect(convert51DegreesDataToOrtb2(fiftyOneDegreesData)).to.deep.equal(expectedORTB2Result); + }); + }); + + describe('convert51DegreesDeviceToOrtb2', function() { + it('converts 51Degrees device data to ORTB2 format', function() { + expect( + convert51DegreesDeviceToOrtb2(fiftyOneDegreesDevice) + ).to.deep.equal(expectedORTB2DeviceResult); + }); + + it('returns an empty object if the device data is not provided', function() { + expect(convert51DegreesDeviceToOrtb2()).to.deep.equal({}); + }); + + it('does not set the deviceid if it is not provided', function() { + const device = {...fiftyOneDegreesDevice}; + delete device.deviceid; + expect(convert51DegreesDeviceToOrtb2(device).device).to.not.have.any.keys('ext'); + }); + + it('sets the model to hardwarename if hardwaremodel is not provided', function() { + const device = {...fiftyOneDegreesDevice}; + delete device.hardwaremodel; + expect(convert51DegreesDeviceToOrtb2(device).device).to.deep.include({model: 'Macintosh'}); + }); + + it('does not set the model if hardwarename is empty', function() { + const device = {...fiftyOneDegreesDevice}; + delete device.hardwaremodel; + device.hardwarename = []; + expect(convert51DegreesDeviceToOrtb2(device).device).to.not.have.any.keys('model'); + }); + + it('does not set the ppi if screeninchesheight is not provided', function() { + const device = {...fiftyOneDegreesDevice}; + delete device.screeninchesheight; + expect(convert51DegreesDeviceToOrtb2(device).device).to.not.have.any.keys('ppi'); + }); + + it('sets correct ppi if screenpixelsphysicalheight & screeninchesheight are provided', function() { + expect(convert51DegreesDeviceToOrtb2(fiftyOneDegreesDeviceX2scaling).device).to.deep.include({ + ppi: expectedORTB2DeviceResult.device.ppi, + }); + }); + + it('if screenpixelsphysical properties are available, use them for screen size', function() { + expect(fiftyOneDegreesDevice.screenpixelswidth).to.not.equal(fiftyOneDegreesDeviceX2scaling.screenpixelswidth); + expect(fiftyOneDegreesDevice.screenpixelsheight).to.not.equal(fiftyOneDegreesDeviceX2scaling.screenpixelsheight); + expect(fiftyOneDegreesDevice.screenpixelsphysicalwidth).to.equal(undefined); + expect(fiftyOneDegreesDevice.screenpixelsphysicalheight).to.equal(undefined); + expect(convert51DegreesDeviceToOrtb2(fiftyOneDegreesDeviceX2scaling).device).to.deep.include({ + h: expectedORTB2DeviceResult.device.h, + w: expectedORTB2DeviceResult.device.w, + }); + }); + }); + + describe('getBidRequestData', function() { + let initialHeadInnerHTML; + let reqBidsConfigObj = {}; + const resetReqBidsConfigObj = () => { + reqBidsConfigObj = { + ortb2Fragments: { + global: { + device: {}, + }, + }, + }; + }; + + before(function() { + initialHeadInnerHTML = document.head.innerHTML; + + const mockScript = document.createElement('script'); + mockScript.innerHTML = ` + window.fod = {complete: (_callback) => _callback(${JSON.stringify(fiftyOneDegreesData)})}; + `; + document.head.appendChild(mockScript); + }); + + beforeEach(function() { + resetReqBidsConfigObj(); + }); + + after(function() { + document.head.innerHTML = initialHeadInnerHTML; + }); + + it('calls the callback even if submodule fails (wrong config)', function() { + const callback = sinon.spy(); + const moduleConfig = {params: {}}; + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + expect(callback.calledOnce).to.be.true; + }); + + it('calls the callback even if submodule fails (on-premise, non-working URL)', async function() { + const callback = sinon.spy(); + const moduleConfig = {params: {onPremiseJSUrl: 'http://localhost:12345/test/51Degrees.core.js'}}; + + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(callback.calledOnce).to.be.true; + }); + + it('calls the callback even if submodule fails (invalid resource key)', async function() { + const callback = sinon.spy(); + const moduleConfig = {params: {resourceKey: 'INVALID_RESOURCE_KEY'}}; + + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(callback.calledOnce).to.be.true; + }); + + it('works with Delegate-CH meta tag', async function() { + inject51DegreesMeta(); + const callback = sinon.spy(); + const moduleConfig = {params: {resourceKey: 'INVALID_RESOURCE_KEY'}}; + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(callback.calledOnce).to.be.true; + }); + + it('has the correct ORTB2 data', async function() { + const callback = sinon.spy(); + const moduleConfig = {params: {resourceKey: 'INVALID_RESOURCE_KEY'}}; + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(callback.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.equal(expectedORTB2Result); + }); + }); + + describe('init', function() { + it('initialises the 51Degrees RTD provider', function() { + expect(fiftyOneDegreesSubmodule.init()).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js b/test/spec/modules/AsteriobidPbmAnalyticsAdapter_spec.js similarity index 98% rename from test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js rename to test/spec/modules/AsteriobidPbmAnalyticsAdapter_spec.js index 9241fda8c81..57fb5b9a32b 100644 --- a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js +++ b/test/spec/modules/AsteriobidPbmAnalyticsAdapter_spec.js @@ -1,4 +1,4 @@ -import prebidmanagerAnalytics, {storage} from 'modules/prebidmanagerAnalyticsAdapter.js'; +import prebidmanagerAnalytics, {storage} from 'modules/AsteriobidPbmAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; import * as utils from 'src/utils.js'; diff --git a/test/spec/modules/BTBidAdapter_spec.js b/test/spec/modules/BTBidAdapter_spec.js index e0306abb7f0..2ec0acc424e 100644 --- a/test/spec/modules/BTBidAdapter_spec.js +++ b/test/spec/modules/BTBidAdapter_spec.js @@ -7,11 +7,10 @@ import 'modules/currency.js'; import 'modules/userId/index.js'; import 'modules/multibid/index.js'; import 'modules/priceFloors.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'modules/consentManagementUsp.js'; import 'modules/consentManagementGpp.js'; -import 'modules/enrichmentFpdModule.js'; -import 'modules/gdprEnforcement.js'; +import 'modules/tcfControl.js'; import 'modules/gppControl_usnat.js'; import 'modules/schain.js'; diff --git a/test/spec/modules/a1MediaBidAdapter_spec.js b/test/spec/modules/a1MediaBidAdapter_spec.js index e1db2b9ad8d..8031b584d65 100644 --- a/test/spec/modules/a1MediaBidAdapter_spec.js +++ b/test/spec/modules/a1MediaBidAdapter_spec.js @@ -75,7 +75,8 @@ const getConvertedBidReq = () => { }, bidfloor: 0, bidfloorcur: 'JPY', - id: '2e9f38ea93bb9e' + id: '2e9f38ea93bb9e', + secure: 1 } ], test: 0, diff --git a/test/spec/modules/acuityadsBidAdapter_spec.js b/test/spec/modules/acuityadsBidAdapter_spec.js index 31ef9dd6466..40de7db69c3 100644 --- a/test/spec/modules/acuityadsBidAdapter_spec.js +++ b/test/spec/modules/acuityadsBidAdapter_spec.js @@ -1,11 +1,21 @@ import { expect } from 'chai'; -import { spec } from '../../../modules/acuityadsBidAdapter'; +import { spec } from '../../../modules/acuityadsBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'acuityads' +const bidder = 'acuityads'; describe('AcuityAdsBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('AcuityAdsBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('AcuityAdsBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('AcuityAdsBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -73,12 +86,22 @@ describe('AcuityAdsBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' }, - timeout: 500, - ortb2: {} + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } + }, + timeout: 500 }; describe('isBidRequestValid', function () { @@ -113,6 +136,7 @@ describe('AcuityAdsBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -121,7 +145,11 @@ describe('AcuityAdsBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -130,7 +158,7 @@ describe('AcuityAdsBidAdapter', function () { 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('string'); + 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); @@ -146,6 +174,56 @@ describe('AcuityAdsBidAdapter', function () { 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 + } + ]; + + let 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'); @@ -171,8 +249,10 @@ describe('AcuityAdsBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -187,40 +267,38 @@ describe('AcuityAdsBidAdapter', function () { expect(data.ccpa).to.equal(bidderRequest.uspConsent); expect(data.gdpr).to.not.exist; }); + }); - describe('Returns data with gppConsent', function () { - it('bidderRequest.gppConsent', () => { - bidderRequest.gppConsent = { - gppString: 'abc123', - applicableSections: [8] - }; + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; - serverRequest = spec.buildRequests(bids, bidderRequest); - let 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; - }) + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); - it('bidderRequest.ortb2.regs.gpp', () => { - bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; - bidderRequest.ortb2.regs.gpp = 'abc123'; - bidderRequest.ortb2.regs.gpp_sid = [8]; + delete bidderRequest.gppConsent; + }) - serverRequest = spec.buildRequests(bids, bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - expect(data).to.have.property('gpp'); - expect(data).to.have.property('gpp_sid'); - }) - }); + 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]; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -424,5 +502,17 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/adagioAnalyticsAdapter_spec.js b/test/spec/modules/adagioAnalyticsAdapter_spec.js index c14393e267b..d1058170f44 100644 --- a/test/spec/modules/adagioAnalyticsAdapter_spec.js +++ b/test/spec/modules/adagioAnalyticsAdapter_spec.js @@ -1,9 +1,9 @@ -import adagioAnalyticsAdapter from 'modules/adagioAnalyticsAdapter.js'; -import { expect } from 'chai'; -import * as utils from 'src/utils.js'; -import { server } from 'test/mocks/xhr.js'; import * as prebidGlobal from 'src/prebidGlobal.js'; +import * as utils from 'src/utils.js'; +import adagioAnalyticsAdapter, { _internal } from 'modules/adagioAnalyticsAdapter.js'; import { EVENTS } from 'src/constants.js'; +import { expect } from 'chai'; +import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; let events = require('src/events'); @@ -14,30 +14,35 @@ describe('adagio analytics adapter - adagio.js', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - sandbox.stub(events, 'getEvents').returns([]); - const w = utils.getWindowTop(); + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + installedModules: ['adagioRtdProvider', 'rtdModule'] + }); adapterManager.registerAnalyticsAdapter({ code: 'adagio', adapter: adagioAnalyticsAdapter }); - w.ADAGIO = w.ADAGIO || {}; - w.ADAGIO.queue = w.ADAGIO.queue || []; + _internal.getAdagioNs().pageviewId = 'a68e6d70-213b-496c-be0a-c468ff387106'; - adagioQueuePushSpy = sandbox.spy(w.ADAGIO.queue, 'push'); + adagioQueuePushSpy = sandbox.spy(_internal.getAdagioNs().queue, 'push'); }); afterEach(() => { + _internal.getAdagioNs().queue = []; sandbox.restore(); }); describe('track', () => { beforeEach(() => { adapterManager.enableAnalytics({ - provider: 'adagio' + provider: 'adagio', + options: { + organizationId: '1001', + site: 'test-com', + } }); }); @@ -93,7 +98,6 @@ describe('adagio analytics adapter - adagio.js', () => { // Step 1-3: Send events Object.entries(testEvents).forEach(([ev, payload]) => events.emit(ev, payload)); - function eventItem(eventName, args) { return sinon.match({ action: 'pb-analytics-event', @@ -108,81 +112,15 @@ describe('adagio analytics adapter - adagio.js', () => { Object.entries(testEvents).forEach(([ev, payload]) => sinon.assert.calledWith(adagioQueuePushSpy, eventItem(ev, payload))); }); }); - - describe('no track', () => { - beforeEach(() => { - sandbox.stub(utils, 'getWindowTop').throws(); - - adapterManager.enableAnalytics({ - provider: 'adagio' - }); - }); - - afterEach(() => { - adagioAnalyticsAdapter.disableAnalytics(); - sandbox.restore(); - }); - - it('builds and sends auction data', () => { - let bidRequest = { - bids: [{ - adUnitCode: 'div-1', - params: { - features: { - siteId: '2', - placement: 'pave_top', - pagetype: 'article', - category: 'IAB12,IAB12-2', - device: '2', - } - } - }, { - adUnitCode: 'div-2', - params: { - features: { - siteId: '2', - placement: 'ban_top', - pagetype: 'article', - category: 'IAB12,IAB12-2', - device: '2', - } - }, - }], - }; - let bidResponse = { - bidderCode: 'adagio', - width: 300, - height: 250, - statusMessage: 'Bid available', - cpm: 6.2189757658226075, - currency: '', - netRevenue: false, - adUnitCode: 'div-1', - timeToRespond: 132, - }; - - // Step 1: Send bid requested event - events.emit(EVENTS.BID_REQUESTED, bidRequest); - - // Step 2: Send bid response event - events.emit(EVENTS.BID_RESPONSE, bidResponse); - - // Step 3: Send auction end event - events.emit(EVENTS.AUCTION_END, {}); - - utils.getWindowTop.restore(); - - sandbox.assert.callCount(adagioQueuePushSpy, 0); - }); - }); }); const AUCTION_ID = '25c6d7f5-699a-4bfc-87c9-996f915341fa'; -const AUCTION_ID_ADAGIO = '6fc53663-bde5-427b-ab63-baa9ed296f47' +const RTD_AUCTION_ID = '753b3784-12a1-44c2-9d08-d0e4ee910e69'; +const RTD_AUCTION_ID_CACHE = '04d991be-8f7d-4491-930b-2b7eefb3c447'; const AUCTION_ID_CACHE = 'b43d24a0-13d4-406d-8176-3181402bafc4'; -const AUCTION_ID_CACHE_ADAGIO = 'a9cae98f-efb5-477e-9259-27350044f8db'; +const SESSION_ID = 'c4f9e517-a592-45af-9560-ca191823d591'; -const BID_ADAGIO = Object.assign({}, BID_ADAGIO, { +const BID_ADAGIO = { bidder: 'adagio', auctionId: AUCTION_ID, adUnitCode: '/19968336/header-bid-tag-1', @@ -215,9 +153,9 @@ const BID_ADAGIO = Object.assign({}, BID_ADAGIO, { sid: '42', e_pba_test: true } -}); +}; -const BID_ANOTHER = Object.assign({}, BID_ANOTHER, { +const BID_ANOTHER = { bidder: 'another', auctionId: AUCTION_ID, adUnitCode: '/19968336/header-bid-tag-1', @@ -246,7 +184,7 @@ const BID_ANOTHER = Object.assign({}, BID_ANOTHER, { meta: { advertiserDomains: ['example.com'] } -}); +}; const BID_CACHED = Object.assign({}, BID_ADAGIO, { auctionId: AUCTION_ID_CACHE, @@ -254,14 +192,20 @@ const BID_CACHED = Object.assign({}, BID_ADAGIO, { }); const PARAMS_ADG = { - organizationId: '1001', - site: 'test-com', - pageviewId: 'a68e6d70-213b-496c-be0a-c468ff387106', environment: 'desktop', +}; + +const ORTB_DATA = { pagetype: 'article', - placement: 'pave_top', - testName: 'test', - testVersion: 'version', +}; + +const ADG_RTD = { + 'uid': RTD_AUCTION_ID, + 'session': { + 'testName': 'test', + 'testVersion': 'version', + 'id': SESSION_ID, + } }; const AUCTION_INIT_ANOTHER = { @@ -300,8 +244,20 @@ const AUCTION_INIT_ANOTHER = { 'params': { ...PARAMS_ADG }, + }, { + 'bidder': 'anotherWithAlias', + 'params': { + 'publisherId': '1001' + }, }, ], - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'ortb2Imp': { + 'ext': { + 'data': { + 'placement': 'pave_top', + } + } + }, }, { 'code': '/19968336/footer-bid-tag-1', 'mediaTypes': { @@ -321,103 +277,202 @@ const AUCTION_INIT_ANOTHER = { 'publisherId': '1001' }, } ], - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'ortb2Imp': { + 'ext': { + 'data': { + 'placement': 'pave_top', + } + } + }, } ], 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], - 'bidderRequests': [ { - 'bidderCode': 'another', - 'auctionId': AUCTION_ID, - 'bidderRequestId': '1be65d7958826a', - 'bids': [ { - 'bidder': 'another', - 'params': { - 'publisherId': '1001', + 'bidderRequests': [ + { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, + { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[640, 480]] + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + ...ADG_RTD + }, + ...ORTB_DATA + } + } } - }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', - 'sizes': [[640, 480]], - 'bidId': '2ecff0db240757', - 'bidderRequestId': '1be65d7958826a', + } + }, + { + 'bidderCode': 'nobid', 'auctionId': AUCTION_ID, - 'src': 'client', - 'bidRequestsCount': 1 - }, { - 'bidder': 'another', - 'params': { - 'publisherId': '1001' + 'bidderRequestId': '1be65d7958826a', + 'bids': [{ + 'bidder': 'nobid', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[640, 480]] + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + ...ADG_RTD + }, + ...ORTB_DATA + } + } } - }, - 'adUnitCode': '/19968336/footer-bid-tag-1', - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', - 'sizes': [[640, 480]], - 'bidId': '2ecff0db240757', - 'bidderRequestId': '1be65d7958826a', + } + }, + { + bidderCode: 'anotherWithAlias', 'auctionId': AUCTION_ID, - 'src': 'client', - 'bidRequestsCount': 1 - }, { - 'bidder': 'nobid', - 'params': { - 'publisherId': '1001' + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'anotherWithAlias', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[640, 480]] + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + ...ADG_RTD + }, + ...ORTB_DATA + } + } } - }, - 'adUnitCode': '/19968336/footer-bid-tag-1', - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', - 'sizes': [[640, 480]], - 'bidId': '2ecff0db240757', - 'bidderRequestId': '1be65d7958826a', + } + }, + { + 'bidderCode': 'adagio', 'auctionId': AUCTION_ID, - 'src': 'client', - 'bidRequestsCount': 1 - } - ], - 'timeout': 3000, - 'refererInfo': { - 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] - } - }, { - 'bidderCode': 'adagio', - 'auctionId': AUCTION_ID, - 'bidderRequestId': '1be65d7958826a', - 'bids': [ { - 'bidder': 'adagio', - 'params': { - ...PARAMS_ADG, - adagioAuctionId: AUCTION_ID_ADAGIO + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[640, 480]] + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + ...ADG_RTD + }, + ...ORTB_DATA + } + } } - }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', - 'sizes': [[640, 480]], - 'bidId': '2ecff0db240757', - 'bidderRequestId': '1be65d7958826a', - 'auctionId': AUCTION_ID, - 'src': 'client', - 'bidRequestsCount': 1 - } - ], - 'timeout': 3000, - 'refererInfo': { - 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } } - } ], 'bidsReceived': [], 'winningBids': [], @@ -456,7 +511,14 @@ const AUCTION_INIT_CACHE = { ...PARAMS_ADG }, }, ], - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'ortb2Imp': { + 'ext': { + 'data': { + 'placement': 'pave_top', + } + } + }, }, { 'code': '/19968336/footer-bid-tag-1', 'mediaTypes': { @@ -476,7 +538,14 @@ const AUCTION_INIT_CACHE = { 'publisherId': '1001' }, } ], - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'ortb2Imp': { + 'ext': { + 'data': { + 'placement': 'pave_top', + } + } + }, } ], 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], 'bidderRequests': [ { @@ -524,6 +593,19 @@ const AUCTION_INIT_CACHE = { 'timeout': 3000, 'refererInfo': { 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + }, + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + ...ADG_RTD, + 'uid': RTD_AUCTION_ID_CACHE + }, + ...ORTB_DATA + } + } + } } }, { 'bidderCode': 'adagio', @@ -532,8 +614,7 @@ const AUCTION_INIT_CACHE = { 'bids': [ { 'bidder': 'adagio', 'params': { - ...PARAMS_ADG, - adagioAuctionId: AUCTION_ID_CACHE_ADAGIO + ...PARAMS_ADG }, 'mediaTypes': { 'banner': { @@ -553,6 +634,19 @@ const AUCTION_INIT_CACHE = { 'timeout': 3000, 'refererInfo': { 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + }, + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + ...ADG_RTD, + 'uid': RTD_AUCTION_ID_CACHE + }, + ...ORTB_DATA + } + } + } } } ], @@ -569,6 +663,22 @@ const AUCTION_END_ANOTHER_NOBID = Object.assign({}, AUCTION_INIT_ANOTHER, { bidsReceived: [] }); +const PBS_ANALYTICS_ANOTHER = { + atag: [ + { + stage: 'auction-response', + module: 'adg-pba', + pba: { + '/19968336/header-bid-tag-1': { + st_id: '53', + splt_cs_id: '731' + } + } + } + ], + auctionId: AUCTION_ID, +} + const MOCK = { SET_TARGETING: { [BID_ADAGIO.adUnitCode]: BID_ADAGIO.adserverTargeting, @@ -582,6 +692,20 @@ const MOCK = { adagio: BID_ADAGIO, another: BID_ANOTHER }, + BID_TIMEOUT: { + another: [ + { + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidder: 'another', + }, + { + auctionId: AUCTION_ID, + adUnitCode: '/19968336/footer-bid-tag-1', + bidder: 'another', + }, + ] + }, AUCTION_END: { another: AUCTION_END_ANOTHER, another_nobid: AUCTION_END_ANOTHER_NOBID @@ -621,10 +745,27 @@ describe('adagio analytics adapter', () => { let sandbox; beforeEach(() => { - sandbox = sinon.sandbox.create(); + sandbox = sinon.createSandbox(); sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + installedModules: ['adagioRtdProvider', 'rtdModule'], + convertCurrency: (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + } + }); + + _internal.getAdagioNs().pageviewId = 'a68e6d70-213b-496c-be0a-c468ff387106'; + adapterManager.registerAnalyticsAdapter({ code: 'adagio', adapter: adagioAnalyticsAdapter @@ -632,35 +773,28 @@ describe('adagio analytics adapter', () => { }); afterEach(() => { + _internal.getAdagioNs().queue = []; sandbox.restore(); }); describe('track', () => { beforeEach(() => { adapterManager.enableAnalytics({ - provider: 'adagio' + provider: 'adagio', + options: { + organizationId: '1001', + site: 'test-com', + } }); + adapterManager.aliasRegistry['anotherWithAlias'] = 'another'; }); afterEach(() => { adagioAnalyticsAdapter.disableAnalytics(); + delete adapterManager.aliasRegistry['anotherWithAlias']; }); it('builds and sends auction data', () => { - sandbox.stub(prebidGlobal, 'getGlobal').returns({ - convertCurrency: (cpm, from, to) => { - const convKeys = { - 'GBP-EUR': 0.7, - 'EUR-GBP': 1.3, - 'USD-EUR': 0.8, - 'EUR-USD': 1.2, - 'USD-GBP': 0.6, - 'GBP-USD': 1.6, - }; - return cpm * (convKeys[`${from}-${to}`] || 1); - } - }); - events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); events.emit(EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); events.emit(EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); @@ -668,7 +802,7 @@ describe('adagio analytics adapter', () => { events.emit(EVENTS.BID_WON, MOCK.BID_WON.another); events.emit(EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED.another); - expect(server.requests.length).to.equal(3, 'requests count'); + expect(server.requests.length).to.equal(5, 'requests count'); { const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); expect(protocol).to.equal('https'); @@ -676,7 +810,8 @@ describe('adagio analytics adapter', () => { expect(pathname).to.equal('/pba.gif'); expect(search.v).to.equal('1'); expect(search.pbjsv).to.equal('$prebid.version$'); - expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + 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'); @@ -686,7 +821,9 @@ describe('adagio analytics adapter', () => { expect(search.plcmt).to.equal('pave_top'); expect(search.mts).to.equal('ban'); expect(search.ban_szs).to.equal('640x100,640x480'); - expect(search.bdrs).to.equal('adagio,another,nobid'); + 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'); } @@ -695,44 +832,52 @@ describe('adagio analytics adapter', () => { expect(protocol).to.equal('https'); expect(hostname).to.equal('c.4dex.io'); expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); expect(search.v).to.equal('2'); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); expect(search.e_sid).to.equal('42'); expect(search.e_pba_test).to.equal('true'); - expect(search.bdrs_bid).to.equal('1,1,0'); - expect(search.bdrs_cpm).to.equal('1.42,2.052,'); + expect(search.bdrs_bid).to.equal('1,1,0,0'); + expect(search.bdrs_cpm).to.equal('1.42,2.052,,'); + expect(search.bdrs_timeout).to.equal('0,0,0,0'); } { - const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[3].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.auct_id).to.equal(RTD_AUCTION_ID); + expect(search.adu_code).to.equal('/19968336/footer-bid-tag-1'); + expect(search.bdrs_timeout).to.equal('0'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[4].url); expect(protocol).to.equal('https'); expect(hostname).to.equal('c.4dex.io'); expect(pathname).to.equal('/pba.gif'); expect(search.v).to.equal('3'); - expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + 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.win_mt).to.equal('ban'); expect(search.win_ban_sz).to.equal('728x90'); expect(search.win_net_cpm).to.equal('2.052'); expect(search.win_og_cpm).to.equal('2.592'); + expect(search.bdrs_timeout).to.equal('0,0,0,0'); } }); it('builds and sends auction data with a cached bid win', () => { - sandbox.stub(prebidGlobal, 'getGlobal').returns({ - convertCurrency: (cpm, from, to) => { - const convKeys = { - 'GBP-EUR': 0.7, - 'EUR-GBP': 1.3, - 'USD-EUR': 0.8, - 'EUR-USD': 1.2, - 'USD-GBP': 0.6, - 'GBP-USD': 1.6, - }; - return cpm * (convKeys[`${from}-${to}`] || 1); - } - }); - events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.bidcached); events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); events.emit(EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); @@ -741,15 +886,17 @@ describe('adagio analytics adapter', () => { events.emit(EVENTS.BID_WON, MOCK.BID_WON.bidcached); events.emit(EVENTS.AD_RENDER_FAILED, MOCK.AD_RENDER_FAILED.bidcached); - expect(server.requests.length).to.equal(5, 'requests count'); + expect(server.requests.length).to.equal(8, 'requests count'); { + // the first request is getting cached we expect to see its auction id later when it's re-used 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.auct_id).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.auct_id).to.equal(RTD_AUCTION_ID_CACHE); + expect(search.s_id).to.equal(SESSION_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'); @@ -760,6 +907,8 @@ describe('adagio analytics adapter', () => { expect(search.mts).to.equal('ban'); expect(search.ban_szs).to.equal('640x100,640x480'); expect(search.bdrs).to.equal('adagio,another'); + expect(search.bdrs_code).to.equal('adagio,another'); + expect(search.bdrs_timeout).to.not.exist; expect(search.adg_mts).to.equal('ban'); expect(search.t_n).to.equal('test'); expect(search.t_v).to.equal('version'); @@ -772,7 +921,30 @@ describe('adagio analytics adapter', () => { expect(pathname).to.equal('/pba.gif'); expect(search.v).to.equal('1'); expect(search.pbjsv).to.equal('$prebid.version$'); - expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id).to.equal(RTD_AUCTION_ID_CACHE); + expect(search.adu_code).to.equal('/19968336/footer-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('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x480'); + expect(search.bdrs).to.equal('another'); + expect(search.bdrs_code).to.equal('another'); + expect(search.bdrs_timeout).to.not.exist; + expect(search.adg_mts).to.not.exist; + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].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.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'); @@ -782,30 +954,59 @@ describe('adagio analytics adapter', () => { expect(search.plcmt).to.equal('pave_top'); expect(search.mts).to.equal('ban'); expect(search.ban_szs).to.equal('640x100,640x480'); - expect(search.bdrs).to.equal('adagio,another,nobid'); + 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'); } { - const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[3].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.auct_id).to.equal(RTD_AUCTION_ID); + expect(search.adu_code).to.equal('/19968336/footer-bid-tag-1'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.bdrs_timeout).to.not.exist; + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[4].url); expect(protocol).to.equal('https'); expect(hostname).to.equal('c.4dex.io'); expect(pathname).to.equal('/pba.gif'); expect(search.v).to.equal('2'); + expect(search.auct_id).to.equal(RTD_AUCTION_ID); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); expect(search.e_sid).to.equal('42'); expect(search.e_pba_test).to.equal('true'); - expect(search.bdrs_bid).to.equal('0,0,0'); - expect(search.bdrs_cpm).to.equal(',,'); + expect(search.bdrs_bid).to.equal('0,0,0,0'); + expect(search.bdrs_cpm).to.equal(',,,'); + expect(search.bdrs_timeout).to.equal('0,0,0,0'); } { - const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[3].url); + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[5].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.auct_id).to.equal(RTD_AUCTION_ID); + expect(search.adu_code).to.equal('/19968336/footer-bid-tag-1'); + expect(search.rndr).to.not.exist; + expect(search.bdrs_timeout).to.equal('0'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[6].url); expect(protocol).to.equal('https'); expect(hostname).to.equal('c.4dex.io'); expect(pathname).to.equal('/pba.gif'); expect(search.v).to.equal('3'); - expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); - expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.auct_id).to.equal(RTD_AUCTION_ID); + expect(search.auct_id_c).to.equal(RTD_AUCTION_ID_CACHE); expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); expect(search.win_bdr).to.equal('adagio'); expect(search.win_mt).to.equal('ban'); @@ -813,23 +1014,33 @@ describe('adagio analytics adapter', () => { expect(search.win_net_cpm).to.equal('1.42'); expect(search.win_og_cpm).to.equal('1.42'); expect(search.rndr).to.not.exist; + expect(search.bdrs_timeout).to.equal('0,0,0,0'); } { - const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[4].url); + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[7].url); expect(protocol).to.equal('https'); expect(hostname).to.equal('c.4dex.io'); expect(pathname).to.equal('/pba.gif'); expect(search.v).to.equal('4'); - expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); - expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.auct_id).to.equal(RTD_AUCTION_ID); + expect(search.auct_id_c).to.equal(RTD_AUCTION_ID_CACHE); expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.win_bdr).to.equal('adagio'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_net_cpm).to.equal('1.42'); + expect(search.win_og_cpm).to.equal('1.42'); expect(search.rndr).to.equal('0'); + expect(search.bdrs_timeout).to.equal('0,0,0,0'); } }); it('send an "empty" cpm when adserver currency != USD and convertCurrency() is undefined', () => { - sandbox.stub(prebidGlobal, 'getGlobal').returns({}); + sandbox.restore(); + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + installedModules: ['adagioRtdProvider', 'rtdModule'] + }); events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); events.emit(EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); @@ -838,19 +1049,207 @@ describe('adagio analytics adapter', () => { events.emit(EVENTS.BID_WON, MOCK.BID_WON.another); events.emit(EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED.another); - expect(server.requests.length).to.equal(3, 'requests count'); + expect(server.requests.length).to.equal(5, 'requests count'); // fail to compute bidder cpm and send an "empty" cpm { - const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); expect(protocol).to.equal('https'); expect(hostname).to.equal('c.4dex.io'); expect(pathname).to.equal('/pba.gif'); + expect(search.s_id).to.equal(SESSION_ID); expect(search.v).to.equal('2'); expect(search.e_sid).to.equal('42'); expect(search.e_pba_test).to.equal('true'); - expect(search.bdrs_bid).to.equal('1,1,0'); - expect(search.bdrs_cpm).to.equal('1.42,,'); + expect(search.bdrs_bid).to.equal('1,1,0,0'); + expect(search.bdrs_cpm).to.equal('1.42,,,'); + } + }); + + it('set adg-pbs aTags in beacon', () => { + events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(EVENTS.PBS_ANALYTICS, PBS_ANALYTICS_ANOTHER); + events.emit(EVENTS.AUCTION_END, MOCK.AUCTION_END.another); + + expect(server.requests.length).to.equal(4, 'requests count'); + + // server.requests[0] -> AUCTION_INIT - AdUnit header-bid-tag-1 + { + const { search } = utils.parseUrl(server.requests[0].url); + + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.v).to.equal('1'); + + expect(search.e_st_id).to.be.undefined; + expect(search.e_splt_cs_id).to.be.undefined; + } + + // server.requests[1] -> AUCTION_INIT - AdUnit footer-bid-tag-1 + { + const { search } = utils.parseUrl(server.requests[1].url); + + expect(search.adu_code).to.equal('/19968336/footer-bid-tag-1'); + expect(search.v).to.equal('1'); + + expect(search.e_st_id).to.be.undefined; + expect(search.e_splt_cs_id).to.be.undefined; + } + + // server.requests[2] -> AUCTION_END - AdUnit header-bid-tag-1 + { + const { search } = utils.parseUrl(server.requests[2].url); + + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.v).to.equal('2'); + + // The adg-pbs aTags fields are set in the beacon ! + expect(search.e_st_id).to.equal('53'); + expect(search.e_splt_cs_id).to.equal('731'); + } + + // server.requests[3] -> AUCTION_END - AdUnit footer-bid-tag-1 + { + const { search } = utils.parseUrl(server.requests[3].url); + + expect(search.adu_code).to.equal('/19968336/footer-bid-tag-1'); + expect(search.v).to.equal('2'); + + expect(search.e_st_id).to.be.undefined; + expect(search.e_splt_cs_id).to.be.undefined; + } + }); + + it('builds and sends auction data with a bid timeout', () => { + events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(EVENTS.BID_TIMEOUT, MOCK.BID_TIMEOUT.another); + events.emit(EVENTS.AUCTION_END, MOCK.AUCTION_END.another); + + expect(server.requests.length).to.equal(4, 'requests count'); + { + 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.bdrs_timeout).to.not.exist; + } + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].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.bdrs_timeout).to.not.exist; + } + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.bdrs).to.equal('adagio,another,anotherWithAlias,nobid'); + expect(search.bdrs_timeout).to.equal('0,1,0,0'); + } + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[3].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.bdrs).to.equal('another'); + expect(search.bdrs_timeout).to.equal('1'); + } + }); + + it('builds and sends auction data with GAM slot callback', () => { + events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + _internal.gamSlotCallback({ + slot: { + getAdUnitPath() { + return '/19968336/header-bid-tag-1' + }, + getSlotElementId() { + return '/19968336/header-bid-tag-1' + } + }, + isEmpty: true, + }); + events.emit(EVENTS.AUCTION_END, MOCK.AUCTION_END.another); + + expect(server.requests.length).to.equal(4, 'requests count'); + { + const { search } = utils.parseUrl(server.requests[0].url); + expect(search.adsrv).to.not.exist; + expect(search.adsrv_empty).to.not.exist; + } + { + const { search } = utils.parseUrl(server.requests[1].url); + expect(search.v).to.equal('1'); + expect(search.adsrv).to.not.exist; + expect(search.adsrv_empty).to.not.exist; + } + { + const { search } = utils.parseUrl(server.requests[2].url); + expect(search.v).to.equal('2'); + expect(search.adsrv).to.equal('gam'); + expect(search.adsrv_empty).to.equal('true'); + } + { + const { search } = utils.parseUrl(server.requests[3].url); + expect(search.v).to.equal('2'); + expect(search.adsrv).to.not.exist; + expect(search.adsrv_empty).to.not.exist; + } + }); + + it('builds and sends auction data with GAM slot callback after auction ended', () => { + events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(EVENTS.AUCTION_END, MOCK.AUCTION_END.another); + _internal.gamSlotCallback({ + slot: { + getAdUnitPath() { + return '/19968336/header-bid-tag-1' + }, + getSlotElementId() { + return '/19968336/header-bid-tag-1' + } + }, + isEmpty: true, + }); + + expect(server.requests.length).to.equal(5, 'requests count'); + { + const { search } = utils.parseUrl(server.requests[0].url); + expect(search.adsrv).to.not.exist; + expect(search.adsrv_empty).to.not.exist; + } + { + const { search } = utils.parseUrl(server.requests[1].url); + expect(search.v).to.equal('1'); + expect(search.adsrv).to.not.exist; + expect(search.adsrv_empty).to.not.exist; + } + { + const { search } = utils.parseUrl(server.requests[2].url); + expect(search.v).to.equal('2'); + expect(search.adsrv).to.not.exist; + expect(search.adsrv_empty).to.not.exist; + } + { + const { search } = utils.parseUrl(server.requests[3].url); + expect(search.v).to.equal('2'); + expect(search.adsrv).to.not.exist; + expect(search.adsrv_empty).to.not.exist; + } + { + const { search } = utils.parseUrl(server.requests[4].url); + expect(search.v).to.equal('3'); + expect(search.adsrv).to.equal('gam'); + expect(search.adsrv_empty).to.equal('true'); } }); }); diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 13c02cc9bae..e9cf58f3fed 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -1,22 +1,16 @@ -import { expect } from 'chai'; +import * as utils from '../../../src/utils.js'; import { - _features, - internal as adagio, - adagioScriptFromLocalStorageCb, - getAdagioScript, - storage, - setExtraParam, - spec, + BB_RENDERER_URL, ENDPOINT, VERSION, - BB_RENDERER_URL, - GlobalExchange + _internal, + setExtraParam, + spec } from '../../../modules/adagioBidAdapter.js'; -import { loadExternalScript } from '../../../src/adloader.js'; -import * as utils from '../../../src/utils.js'; -import { config } from '../../../src/config.js'; import { NATIVE } from '../../../src/mediaTypes.js'; +import { config } from '../../../src/config.js'; import { executeRenderer } from '../../../src/Renderer.js'; +import { expect } from 'chai'; import { userSync } from '../../../src/userSync.js'; const BidRequestBuilder = function BidRequestBuilder(options) { @@ -73,7 +67,6 @@ const BidderRequestBuilder = function BidderRequestBuilder(options) { }; describe('Adagio bid adapter', () => { - let adagioMock; let utilsMock; let sandbox; let fakeRenderer; @@ -119,26 +112,16 @@ describe('Adagio bid adapter', () => { window.ADAGIO.versions.adagioBidderAdapter = VERSION; window.ADAGIO.pageviewId = 'dda61753-4059-4f75-b0bf-3f60bd2c4d9a'; - GlobalExchange.clearFeatures(); - GlobalExchange.clearExchangeData(); - - adagioMock = sinon.mock(adagio); utilsMock = sinon.mock(utils); - $$PREBID_GLOBAL$$.bidderSettings = { - adagio: { - storageAllowed: true - } - }; - sandbox = sinon.createSandbox(); + utilsMock = sandbox.mock(utils); }); afterEach(() => { window.ADAGIO = undefined; $$PREBID_GLOBAL$$.bidderSettings = {}; - adagioMock.restore(); utilsMock.restore(); sandbox.restore(); @@ -192,7 +175,7 @@ describe('Adagio bid adapter', () => { }); it('should compute organizationId and site params from global BidderSettings config', function() { - sandbox.stub(adagio, 'getRefererInfo').returns({ reachedTop: true }); + sandbox.stub(_internal, 'getRefererInfo').returns({ reachedTop: true }); sandbox.stub(config, 'getConfig').withArgs('adagio').returns({ siteId: '1000:SITE-NAME' }); @@ -266,10 +249,9 @@ describe('Adagio bid adapter', () => { 'user', 'schain', 'prebidVersion', - 'featuresVersion', + 'hasRtd', 'data', 'usIfr', - 'adgjs', ]; it('groups requests by organizationId', function() { @@ -293,10 +275,8 @@ describe('Adagio bid adapter', () => { }); it('should send bid request to ENDPOINT_PB via POST', function() { - sandbox.stub(adagio, 'getDevice').returns({ a: 'a' }); - sandbox.stub(adagio, 'getSite').returns({ domain: 'adagio.io', 'page': 'https://adagio.io/hb' }); - sandbox.stub(adagio, 'getPageviewId').returns('1234-567'); - sandbox.stub(utils, 'generateUUID').returns('blabla'); + sandbox.stub(_internal, 'getDevice').returns({ a: 'a' }); + sandbox.stub(_internal, 'getSite').returns({ domain: 'adagio.io', 'page': 'https://adagio.io/hb' }); const bid01 = new BidRequestBuilder().withParams().build(); const bidderRequest = new BidderRequestBuilder().build(); @@ -310,70 +290,52 @@ describe('Adagio bid adapter', () => { expect(requests[0].data).to.have.all.keys(expectedDataKeys); }); - it('should use a custom generated auctionId and remove transactionId', function() { + it('should use a custom generated auctionId from ortb2.site.ext.data.adg_rtd.uid when available', function() { const expectedAuctionId = '373bcda7-9794-4f1c-be2c-0d223d11d579' - sandbox.stub(utils, 'generateUUID').returns(expectedAuctionId); const bid01 = new BidRequestBuilder().withParams().build(); - const bidderRequest = new BidderRequestBuilder().build(); + let ortb = { + ortb2: { + site: { + ext: { + data: { + adg_rtd: { + uid: expectedAuctionId + } + } + } + } + } + } + const bidderRequest = new BidderRequestBuilder(ortb).build(); const requests = spec.buildRequests([bid01], bidderRequest); expect(requests[0].data.adUnits[0].auctionId).eq(expectedAuctionId); expect(requests[0].data.adUnits[0].transactionId).to.not.exist; }); - it('should enrich prebid bid requests params', function() { + it('should use a custom generated auctionId when ortb2.site.ext.data.adg_rtd.uid is absent and remove transactionId', function() { const expectedAuctionId = '373bcda7-9794-4f1c-be2c-0d223d11d579' - const expectedPageviewId = '56befc26-8cf0-472d-b105-73896df8eb89'; sandbox.stub(utils, 'generateUUID').returns(expectedAuctionId); - sandbox.stub(adagio, 'getPageviewId').returns(expectedPageviewId); const bid01 = new BidRequestBuilder().withParams().build(); const bidderRequest = new BidderRequestBuilder().build(); - spec.buildRequests([bid01], bidderRequest); - - expect(bid01.params.adagioAuctionId).eq(expectedAuctionId); - expect(bid01.params.pageviewId).eq(expectedPageviewId); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.adUnits[0].auctionId).eq(expectedAuctionId); + expect(requests[0].data.adUnits[0].transactionId).to.not.exist; }); - it('should enqueue computed features for collect usage', function() { - sandbox.stub(Date, 'now').returns(12345); + it('should enrich prebid bid requests params', function() { + const expectedPageviewId = '56befc26-8cf0-472d-b105-73896df8eb89'; + sandbox.stub(_internal, 'getAdagioNs').returns({ pageviewId: expectedPageviewId }); const bid01 = new BidRequestBuilder().withParams().build(); const bidderRequest = new BidderRequestBuilder().build(); - adagioMock.expects('enqueue').withArgs(sinon.match({ action: 'features' })).atLeast(1); - - const requests = spec.buildRequests([bid01], bidderRequest); - - expect(requests[0].data).to.have.all.keys(expectedDataKeys); - - adagioMock.verify(); - }); - - it('should filter some props in case refererDetection.reachedTop is false', function() { - const bid01 = new BidRequestBuilder().withParams().build(); - const bidderRequest = new BidderRequestBuilder({ - refererInfo: { - numIframes: 2, - reachedTop: false, - referer: 'http://example.com/iframe1.html', - stack: [ - null, - 'http://example.com/iframe1.html', - 'http://example.com/iframe2.html' - ], - canonicalUrl: '' - } - }).build(); - - const requests = spec.buildRequests([bid01], bidderRequest); + spec.buildRequests([bid01], bidderRequest); - expect(requests).to.have.lengthOf(1); - expect(requests[0].data).to.have.all.keys(expectedDataKeys); - expect(requests[0].data.adUnits[0].features).to.exist; - expect(requests[0].data.adUnits[0].features.url).to.not.exist; + expect(bid01.params.pageviewId).eq(expectedPageviewId); }); it('should force split keyword param into a string', function() { @@ -446,10 +408,102 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[3].params.dataLayer).to.not.exist; }); + describe('with adagioRtdProvider enrichments', function() { + const adUnitRtdEnrichments = { + ortb2: { + site: { + ext: { + data: { + adg_rtd: { + features: { + page_dimensions: '1024x768', + viewport_dimensions: '1024x768', + user_timestamp: '111111111', + dom_loading: '111111111', + } + } + }}} + }, + ortb2Imp: { + ext: { + data: { + adg_rtd: { + adunit_position: '1x1' + } + } + } + } + } + const rtdEnrichments = { + ortb2: { + site: { + ext: { + data: { + adg_rtd: { + session: { + new: true, + rnd: 0.0666 + }, + } + } + } + } + } + } + + it('should add features and data to the request if exists', function() { + const bid01 = new BidRequestBuilder(adUnitRtdEnrichments).withParams().build(); + const bidderRequest = new BidderRequestBuilder(rtdEnrichments).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.data).to.deep.equal({ + session: { + new: true, + rnd: 0.0666 + } + }); + + expect(requests[0].data.adUnits[0].features).to.deep.equal({ + page_dimensions: '1024x768', + viewport_dimensions: '1024x768', + user_timestamp: '111111111', + dom_loading: '111111111', + adunit_position: '1x1', + print_number: '1' + }) + }); + + it('should add an only "print_number" in features object if ortb2 is not properly defined', function() { + const bid01 = new BidRequestBuilder({ + ortb2: {}, + bidderRequestsCount: 2 + }).withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.adUnits[0].features).to.deep.equal({ + print_number: '2' + }); + }); + + it('should send data.session with default if the ortb2 ext is not properly defined', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + sandbox.stub(Math, 'random').returns(0.444); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.data.session).to.exist; + expect(requests[0].data.data.session.new).to.equal(true); + expect(requests[0].data.data.session.rnd).to.equal(0.444); + }); + }); + describe('With video mediatype', function() { context('Outstream video', function() { - it('should logWarn if user does not set renderer.backupOnly: true', function() { - sandbox.spy(utils, 'logWarn'); + it('should set playerName = "other" if user does not set renderer.backupOnly: true', function() { const bid01 = new BidRequestBuilder({ adUnitCode: 'adunit-code-01', mediaTypes: { @@ -468,7 +522,39 @@ describe('Adagio bid adapter', () => { const request = spec.buildRequests([bid01], bidderRequest)[0]; expect(request.data.adUnits[0].mediaTypes.video.playerName).to.equal('other'); - sinon.assert.calledWith(utils.logWarn, 'Adagio: renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.'); + }); + + it('should set playerName = "adagio" if user does not set a renderer or set `renderer.backupOnly: true`', function() { + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + } + }, + }).withParams().build(); + const bid02 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-02', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + renderer: { + url: 'https://url.tld', + render: () => true, + backupOnly: true + } + } + }, + }).withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + const request = spec.buildRequests([bid01, bid02], bidderRequest)[0]; + + expect(request.data.adUnits[0].mediaTypes.video.playerName).to.equal('adagio'); + expect(request.data.adUnits[1].mediaTypes.video.playerName).to.equal('adagio'); }); }); @@ -493,7 +579,7 @@ describe('Adagio bid adapter', () => { skipafter: 4, minduration: 10, maxduration: 30, - placement: 3, + plcmt: 4, protocols: [8] } }).build(); @@ -508,7 +594,7 @@ describe('Adagio bid adapter', () => { skipafter: 4, minduration: 10, maxduration: 30, - placement: 3, + plcmt: 4, protocols: [8], w: 300, h: 250 @@ -517,7 +603,6 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); expect(requests).to.have.lengthOf(1); expect(requests[0].data.adUnits[0].mediaTypes.video).to.deep.equal(expected); - sinon.assert.calledTwice(utils.logWarn.withArgs(sinon.match(new RegExp(/^Adagio: The OpenRTB/)))); }); }); @@ -715,77 +800,24 @@ describe('Adagio bid adapter', () => { describe('with GPP', function() { const bid01 = new BidRequestBuilder().withParams().build(); - const regsGpp = 'regs_gpp_consent_string'; - const regsApplicableSections = [2]; - - const ortb2Gpp = 'ortb2_gpp_consent_string'; - const ortb2GppSid = [1]; - - context('When GPP in regs module', function() { - it('send gpp and gppSid to the server', function() { - const bidderRequest = new BidderRequestBuilder({ - gppConsent: { - gppString: regsGpp, - applicableSections: regsApplicableSections, - } - }).build(); - - const requests = spec.buildRequests([bid01], bidderRequest); - - expect(requests[0].data.regs.gpp).to.equal(regsGpp); - expect(requests[0].data.regs.gppSid).to.equal(regsApplicableSections); - }); - }); - - context('When GPP partially defined in regs module', function() { - it('send gpp and gppSid coming from ortb2 to the server', function() { - const bidderRequest = new BidderRequestBuilder({ - gppConsent: { - gppString: regsGpp, - }, - ortb2: { - regs: { - gpp: ortb2Gpp, - gpp_sid: ortb2GppSid, - } - } - }).build(); - - const requests = spec.buildRequests([bid01], bidderRequest); - - expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); - expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); - }); + const gpp = 'gpp_consent_string'; + const gppSid = [1]; - it('send empty gpp and gppSid if no ortb2 fields to the server', function() { - const bidderRequest = new BidderRequestBuilder({ - gppConsent: { - gppString: regsGpp, - } - }).build(); - - const requests = spec.buildRequests([bid01], bidderRequest); - - expect(requests[0].data.regs.gpp).to.equal(''); - expect(requests[0].data.regs.gppSid).to.be.empty; - }); - }); - - context('When GPP defined in ortb2 module', function() { + context('When GPP is defined', function() { it('send gpp and gppSid coming from ortb2 to the server', function() { const bidderRequest = new BidderRequestBuilder({ ortb2: { regs: { - gpp: ortb2Gpp, - gpp_sid: ortb2GppSid, + gpp, + gpp_sid: gppSid, } } }).build(); const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); - expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + expect(requests[0].data.regs.gpp).to.equal(gpp); + expect(requests[0].data.regs.gppSid).to.equal(gppSid); }); }); @@ -1010,6 +1042,98 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.regs.dsa).to.be.undefined; }); }) + + describe('with ORTB2', function() { + it('should add ortb2 device data to the request', function() { + const ortb2 = { + device: { + 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'}, + }, + }; + + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder({ortb2}).build(); + const requests = spec.buildRequests([bid01], bidderRequest); + + const expectedData = { + ...ortb2.device, + language: navigator[navigator.language ? 'language' : 'userLanguage'], + js: 1, + geo: {}, + userAgent: navigator.userAgent, + }; + + expect(requests[0].data.device).to.deep.equal(expectedData); + }); + }); + + describe('with `rwdd` and `instl` signals', function() { + const tests = [ + { + n: 'Should set signals in bidRequest if value is 1', + ortb2Imp: { + rwdd: 1, + instl: '1' + }, + expected: { + rwdd: 1, + instl: 1 + } + }, + { + n: 'Should not set signals in bidRequest if value is 0', + ortb2Imp: { + rwdd: 0, + instl: '0' + }, + expected: { + rwdd: undefined, + instl: undefined + } + }, + { + n: 'Should not set if rwdd and instl are missformated', + ortb2Imp: { + rwdd: 'a', + ext: { instl: 1 } + }, + expected: { + rwdd: undefined, + instl: undefined + } + }, + { + n: 'Should not set rwdd and instl in bidRequest if undefined', + ortb2Imp: {}, + expected: { + rwdd: undefined, + instl: undefined + } + } + ] + + tests.forEach((t) => { + it(t.n, function() { + const bid01 = new BidRequestBuilder().withParams().build(); + bid01.ortb2Imp = t.ortb2Imp; + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + const expected = t.expected; + expect(requests[0].data.adUnits[0].rwdd).to.equal(expected.rwdd); + expect(requests[0].data.adUnits[0].instl).to.equal(expected.instl); + }); + }) + }) }); describe('interpretResponse()', function() { @@ -1143,25 +1267,24 @@ describe('Adagio bid adapter', () => { it('should populate ADAGIO queue with ssp-data', function() { sandbox.stub(Date, 'now').returns(12345); + sandbox.stub(_internal, 'hasRtd').returns(true); + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') - adagioMock.expects('enqueue').withExactArgs({ + spec.interpretResponse(serverResponse, bidRequest); + + expect(spy.withArgs({ action: 'ssp-data', ts: 12345, data: serverResponse.body.data - }).once(); - - spec.interpretResponse(serverResponse, bidRequest); - - adagioMock.verify(); + }).calledOnce).to.be.true; }); it('should properly try-catch an exception and return an empty array', function() { - sandbox.stub(adagio, 'enqueue').throws(); - utilsMock.expects('logError').once(); - + sandbox.stub(_internal, 'hasRtd').returns(true); + sandbox.stub(_internal, 'getAdagioNs').returns({ queue: () => { throw new Error('test') } }); + const spy = sandbox.spy(utils, 'logError'); expect(spec.interpretResponse(serverResponse, bidRequest)).to.be.an('array').length(0); - - utilsMock.verify(); + expect(spy.calledOnce).to.be.true; }); describe('Response with video outstream', function() { @@ -1465,215 +1588,6 @@ describe('Adagio bid adapter', () => { }); }); - describe('transformBidParams', function() { - it('Compute additional params in s2s mode', function() { - const adUnit = { - code: 'adunit-code', - params: { - organizationId: '1000' - } - }; - const bid01 = new BidRequestBuilder({ - 'mediaTypes': { - banner: { sizes: [[300, 250]] }, - video: { - context: 'outstream', - playerSize: [300, 250], - renderer: { - url: 'https://url.tld', - render: () => true - } - } - } - }).withParams().build(); - - const params = spec.transformBidParams({ param01: 'test' }, true, adUnit, [{ bidderCode: 'adagio', auctionId: bid01.auctionId, bids: [bid01] }]); - expect(params.param01).eq('test'); - }); - }); - - describe('Adagio features when prebid in top.window', function() { - it('should return all expected features when all expected bidder params are available', function() { - sandbox.stub(window.top.document, 'getElementById').returns( - fixtures.getElementById() - ); - sandbox.stub(window.top, 'getComputedStyle').returns({ display: 'block' }); - sandbox.stub(utils, 'inIframe').returns(false); - - const bidRequest = new BidRequestBuilder({ - 'mediaTypes': { - banner: { sizes: [[300, 250]] } - } - }).withParams().build(); - - const bidderRequest = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bidRequest], bidderRequest); - const result = requests[0].data.adUnits[0].features; - - expect(result.adunit_position).to.match(/^[\d]+x[\d]+$/); - expect(result.page_dimensions).to.match(/^[\d]+x[\d]+$/); - expect(result.viewport_dimensions).to.match(/^[\d]+x[\d]+$/); - expect(result.print_number).to.be.a('String'); - expect(result.dom_loading).to.be.a('String'); - expect(result.user_timestamp).to.be.a('String'); - expect(result.url).to.not.exist; - expect(result.device).to.not.exist; - expect(result.os).to.not.exist; - expect(result.browser).to.not.exist; - }); - - it('should return all expected features when `adUnitElementId` param is not available', function() { - sandbox.stub(utils, 'inIframe').returns(false); - - const bidRequest = new BidRequestBuilder({ - params: { - organizationId: '1000', - placement: 'PAVE_ATF', - site: 'SITE-NAME' - }, - 'mediaTypes': { - banner: { sizes: [[300, 250]] } - } - }).build(); - - const bidderRequest = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bidRequest], bidderRequest); - const result = requests[0].data.adUnits[0].features; - - expect(result.adunit_position).to.not.exist; - expect(result.page_dimensions).to.be.a('String'); - expect(result.viewport_dimensions).to.be.a('String'); - expect(result.print_number).to.be.a('String'); - expect(result.dom_loading).to.be.a('String'); - expect(result.user_timestamp).to.be.a('String'); - }); - - it('should return `adunit_position` feature when the slot is hidden with value 0x0', function () { - const elem = fixtures.getElementById('0', '0', '0', '0'); - sandbox.stub(window.top.document, 'getElementById').returns(elem); - sandbox.stub(window.top, 'getComputedStyle').returns({ display: 'none' }); - sandbox.stub(utils, 'inIframe').returns(false); - - const bidRequest = new BidRequestBuilder({ - mediaTypes: { - banner: { sizes: [[300, 250]] }, - }, - }) - .withParams() - .build(); - - const bidderRequest = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bidRequest], bidderRequest); - const result = requests[0].data.adUnits[0].features; - - expect(result.adunit_position).to.equal('0x0'); - }); - }); - - describe('Adagio features when prebid in Safeframe', function() { - beforeEach(function () { - window.$sf = $sf; - }); - - afterEach(function () { - delete window.$sf; - }); - - it('should return all expected features when prebid is in safeframe iframe', function() { - sandbox.stub(window.$sf.ext, 'geom').returns({ - win: {t: 23, r: 1920, b: 1200, l: 0, w: 1920, h: 1177}, - self: {t: 210, r: 1159, b: 460, l: 859, w: 300, h: 250}, - }); - - const bidRequest = new BidRequestBuilder({ - 'mediaTypes': { - banner: { sizes: [[300, 250]] } - } - }).withParams().build(); - - const bidderRequest = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bidRequest], bidderRequest); - const result = requests[0].data.adUnits[0].features; - - expect(result.page_dimensions).to.not.exist; - expect(result.viewport_dimensions).to.be.a('String'); - expect(result.print_number).to.be.a('String'); - expect(result.dom_loading).to.be.a('String'); - expect(result.user_timestamp).to.be.a('String'); - expect(result.adunit_position).to.exist; - }); - - it('should return all expected features when prebid safeframe api not properly implemented', function() { - const bidRequest = new BidRequestBuilder({ - 'mediaTypes': { - banner: { sizes: [[300, 250]] } - } - }).withParams().build(); - - const bidderRequest = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bidRequest], bidderRequest); - const result = requests[0].data.adUnits[0].features; - - expect(result.page_dimensions).to.not.exist; - expect(result.viewport_dimensions).to.not.exist; - expect(result.print_number).to.be.a('String'); - expect(result.dom_loading).to.be.a('String'); - expect(result.user_timestamp).to.be.a('String'); - expect(result.adunit_position).to.not.exist; - }); - - it('should return all expected features when prebid safeframe api not properly implemented bis', function() { - window.$sf.ext.geom = undefined; - - const bidRequest = new BidRequestBuilder({ - 'mediaTypes': { - banner: { sizes: [[300, 250]] } - } - }).withParams().build(); - - const bidderRequest = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bidRequest], bidderRequest); - const result = requests[0].data.adUnits[0].features; - - expect(result.page_dimensions).to.not.exist; - expect(result.viewport_dimensions).to.not.exist; - expect(result.print_number).to.be.a('String'); - expect(result.dom_loading).to.be.a('String'); - expect(result.user_timestamp).to.be.a('String'); - expect(result.adunit_position).to.not.exist; - }); - }); - - describe('Adagio features when prebid in crossdomain iframe', function() { - it('should return all expected features', function() { - sandbox.stub(utils, 'getWindowTop').throws(); - - const bidRequest = new BidRequestBuilder({ - 'mediaTypes': { - banner: { sizes: [[300, 250]] } - } - }).withParams().build(); - - const bidderRequest = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bidRequest], bidderRequest); - const result = requests[0].data.adUnits[0].features; - - expect(result.page_dimensions).to.not.exist; - expect(result.viewport_dimensions).to.not.exist; - expect(result.print_number).to.be.a('String'); - expect(result.dom_loading).to.be.a('String'); - expect(result.user_timestamp).to.be.a('String'); - expect(result.adunit_position).to.not.exist; - }); - }); - describe('site information using refererDetection or window.top', function() { it('should returns domain, page and window.referrer in a window.top context', function() { const bidderRequest = new BidderRequestBuilder({ @@ -1687,7 +1601,7 @@ describe('Adagio bid adapter', () => { } }).build(); - expect(adagio.getSite(bidderRequest)).to.deep.equal({ + expect(_internal.getSite(bidderRequest)).to.deep.equal({ domain: 'test.io', page: 'https://test.io/article/a.html', referrer: 'https://google.com', @@ -1696,7 +1610,7 @@ describe('Adagio bid adapter', () => { }); it('should returns domain and page in a cross-domain w/ top domain reached context', function() { - sandbox.stub(utils, 'getWindowTop').throws(); + sandbox.stub(utils, 'canAccessWindowTop').returns(false); sandbox.stub(utils, 'getWindowSelf').returns({ document: { referrer: 'https://google.com' @@ -1722,7 +1636,7 @@ describe('Adagio bid adapter', () => { refererInfo: info }).build(); - expect(adagio.getSite(bidderRequest)).to.deep.equal({ + expect(_internal.getSite(bidderRequest)).to.deep.equal({ domain: 'level.io', page: 'http://level.io/', referrer: 'https://google.com', @@ -1731,7 +1645,7 @@ describe('Adagio bid adapter', () => { }); it('should return info in a cross-domain w/o top domain reached and w/o ancestor context', function() { - sandbox.stub(utils, 'getWindowTop').throws(); + sandbox.stub(utils, 'canAccessWindowTop').returns(false); const info = { numIframes: 2, @@ -1752,119 +1666,11 @@ describe('Adagio bid adapter', () => { refererInfo: info }).build(); - const s = adagio.getSite(bidderRequest) + const s = _internal.getSite(bidderRequest) expect(s.domain).equal('example.com') expect(s.page).equal('http://example.com/iframe1.html') expect(s.referrer).match(/^https?:\/\/.+/); expect(s.top).equal(false) }); }); - - describe('adagioScriptFromLocalStorageCb()', function() { - const VALID_HASH = 'Lddcw3AADdQDrPtbRJkKxvA+o1CtScGDIMNRpHB3NnlC/FYmy/9RKXelKrYj/sjuWusl5YcOpo+lbGSkk655i8EKuDiOvK6ae/imxSrmdziIp+S/TA6hTFJXcB8k1Q9OIp4CMCT52jjXgHwX6G0rp+uYoCR25B1jHaHnpH26A6I='; - const INVALID_HASH = 'invalid'; - const VALID_SCRIPT_CONTENT = 'var _ADAGIO=function(){};(_ADAGIO)();\n'; - const INVALID_SCRIPT_CONTENT = 'var _ADAGIO=function(){//corrupted};(_ADAGIO)();\n'; - const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; - - beforeEach(function() { - localStorage.removeItem(ADAGIO_LOCALSTORAGE_KEY); - }); - - describe('getAdagioScript', function() { - it('should run storage.getDataFromLocalStorage callback and call adagioScriptFromLocalStorageCb() ', function() { - sandbox.spy(adagio, 'adagioScriptFromLocalStorageCb'); - const getDataFromLocalStorageStub = sandbox.stub(storage, 'getDataFromLocalStorage').callsArg(1); - localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, '// hash: ' + VALID_HASH + '\n' + VALID_SCRIPT_CONTENT); - - getAdagioScript(); - - sinon.assert.callCount(getDataFromLocalStorageStub, 1); - sinon.assert.callCount(adagio.adagioScriptFromLocalStorageCb, 1); - }); - - it('should load external script if the user consent', function() { - sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, true); - getAdagioScript(); - - expect(loadExternalScript.called).to.be.true; - }); - - it('should not load external script if the user does not consent', function() { - sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, false); - getAdagioScript(); - - expect(loadExternalScript.called).to.be.false; - }); - - it('should remove the localStorage key if exists and the user does not consent', function() { - sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, false); - localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, 'the script'); - - getAdagioScript(); - - expect(loadExternalScript.called).to.be.false; - expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; - }); - }); - - it('should verify valid hash with valid script', function () { - localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, '// hash: ' + VALID_HASH + '\n' + VALID_SCRIPT_CONTENT); - - utilsMock.expects('logInfo').withExactArgs('Adagio: start script.').once(); - utilsMock.expects('logWarn').withExactArgs('Adagio: no hash found.').never(); - utilsMock.expects('logWarn').withExactArgs('Adagio: invalid script found.').never(); - - adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)); - - expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.equals('// hash: ' + VALID_HASH + '\n' + VALID_SCRIPT_CONTENT); - utilsMock.verify(); - }); - - it('should verify valid hash with invalid script', function () { - localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, '// hash: ' + VALID_HASH + '\n' + INVALID_SCRIPT_CONTENT); - - utilsMock.expects('logInfo').withExactArgs('Adagio: start script').never(); - utilsMock.expects('logWarn').withExactArgs('Adagio: no hash found.').never(); - utilsMock.expects('logWarn').withExactArgs('Adagio: invalid script found.').once(); - - adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)); - - expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; - utilsMock.verify(); - }); - - it('should verify invalid hash with valid script', function () { - localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, '// hash: ' + INVALID_HASH + '\n' + VALID_SCRIPT_CONTENT); - - utilsMock.expects('logInfo').withExactArgs('Adagio: start script').never(); - utilsMock.expects('logWarn').withExactArgs('Adagio: no hash found.').never(); - utilsMock.expects('logWarn').withExactArgs('Adagio: invalid script found.').once(); - - adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)); - - expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; - utilsMock.verify(); - }); - - it('should verify missing hash', function () { - localStorage.setItem(ADAGIO_LOCALSTORAGE_KEY, VALID_SCRIPT_CONTENT); - - utilsMock.expects('logInfo').withExactArgs('Adagio: start script').never(); - utilsMock.expects('logWarn').withExactArgs('Adagio: no hash found.').once(); - utilsMock.expects('logWarn').withExactArgs('Adagio: invalid script found.').never(); - - adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)); - - expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; - utilsMock.verify(); - }); - - it('should return false if content script does not exist in localStorage', function() { - sandbox.spy(utils, 'logWarn'); - expect(adagioScriptFromLocalStorageCb(null)).to.be.undefined; - sinon.assert.callCount(utils.logWarn, 1); - sinon.assert.calledWith(utils.logWarn, 'Adagio: script not found.'); - }); - }); }); diff --git a/test/spec/modules/adagioRtdProvider_spec.js b/test/spec/modules/adagioRtdProvider_spec.js new file mode 100644 index 00000000000..aea76ef4c3d --- /dev/null +++ b/test/spec/modules/adagioRtdProvider_spec.js @@ -0,0 +1,732 @@ +import { + PLACEMENT_SOURCES, + _internal, + adagioRtdSubmodule, + storage, +} from 'modules/adagioRtdProvider.js'; +import * as utils from 'src/utils.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { expect } from 'chai'; +import { getGlobal } from '../../../src/prebidGlobal.js'; + +describe('Adagio Rtd Provider', function () { + const SUBMODULE_NAME = 'adagio'; + + function getElementByIdMock(width, height, x, y) { + const obj = { + x: x || 800, + y: y || 300, + width: width || 300, + height: height || 250, + }; + + return { + ...obj, + getBoundingClientRect: () => { + return { + width: obj.width, + height: obj.height, + left: obj.x, + top: obj.y, + right: obj.x + obj.width, + bottom: obj.y + obj.height + }; + } + }; + } + + let sandbox; + let clock; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + }); + + afterEach(function () { + clock.restore(); + sandbox.restore(); + }); + + describe('submodule `init`', function () { + const config = { + name: SUBMODULE_NAME, + params: { + organizationId: '1000', + site: 'mysite' + } + }; + + it('exists', function () { + expect(adagioRtdSubmodule.init).to.be.a('function'); + }); + + it('returns false missing config params', function () { + const value = adagioRtdSubmodule.init({ + name: SUBMODULE_NAME, + }); + expect(value).to.equal(false); + }); + + it('returns false if missing providers param', function () { + const value = adagioRtdSubmodule.init({ + name: SUBMODULE_NAME, + params: {} + }); + expect(value).to.equal(false); + }); + + it('returns false if organizationId param is not a string', function () { + const value = adagioRtdSubmodule.init({ + name: SUBMODULE_NAME, + params: { + organizationId: 1000, + site: 'mysite' + } + }); + expect(value).to.equal(false); + }); + + it('returns false if `site` param is not a string', function () { + const value = adagioRtdSubmodule.init({ + name: SUBMODULE_NAME, + params: { + organizationId: '1000', + site: 123 + } + }); + expect(value).to.equal(false); + }); + + it('returns true if `organizationId` and `site` params included', function () { + const value = adagioRtdSubmodule.init(config); + expect(value).to.equal(true); + }); + + it('load an external script if localStorageIsEnabled is enabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, true) + adagioRtdSubmodule.init(config); + expect(loadExternalScript.called).to.be.true; + }); + + it('do not load an external script if localStorageIsEnabled is disabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsArgWith(0, false) + adagioRtdSubmodule.init(config); + expect(loadExternalScript.called).to.be.false; + }); + + describe('store session data in localStorage', function () { + const session = { + expiry: 1714116530700, + id: 'uid-1234', + rnd: 0.5697, + vwSmplg: 0.1, + vwSmplgNxt: 0.1, + pages: 1, + v: 2 + }; + + it('store new session data for further usage', function () { + const storageValue = JSON.stringify({abTest: {}}); + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Date, 'now').returns(1714116520710); + sandbox.stub(Math, 'random').returns(0.8); + sandbox.stub(utils, 'generateUUID').returns('uid-1234'); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + v: 2, + new: true, + id: utils.generateUUID(), + rnd: Math.random(), + pages: 1, + } + } + + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + + it('store existing session data for further usage', function () { + const storageValue = JSON.stringify({session: session, abTest: {}}); + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Date, 'now').returns(1714116520710); + sandbox.stub(Math, 'random').returns(0.8); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + ...session, + new: false, + } + } + + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + + it('store new session if old session has expired data for further usage', function () { + const storageValue = JSON.stringify({session: session, abTest: {}}); + sandbox.stub(Date, 'now').returns(1715679344351); + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Math, 'random').returns(0.8); + sandbox.stub(utils, 'generateUUID').returns('uid-5678'); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + ...session, + new: true, + id: utils.generateUUID(), + rnd: Math.random(), + } + } + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + }); + + describe('store session data in localStorage for old snippet', function () { + it('store new session data for further usage', function () { + const storageValue = null; + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Date, 'now').returns(1714116520710); + sandbox.stub(Math, 'random').returns(0.8); + sandbox.stub(utils, 'generateUUID').returns('uid-1234'); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + new: true, + id: utils.generateUUID(), + rnd: Math.random(), + pages: 1 + } + } + + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + + it('update session data for further usage', function () { + const storageValue = JSON.stringify({ + session: { + new: true, + id: 'uid-1234', + rnd: 0.8, + pages: 1, + expiry: 1714116520710, + testName: 't', + testVersion: 'clt' + } + }); + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Date, 'now').returns(1714116520710); + sandbox.stub(Math, 'random').returns(0.8); + sandbox.stub(utils, 'generateUUID').returns('uid-1234'); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + new: false, + expiry: 1714116520710, + id: utils.generateUUID(), + rnd: Math.random(), + pages: 1, + testName: 't', + testVersion: 'clt' + } + } + + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + }); + + describe('update session data in localStorage from old snippet to new version', function () { + it('update session data for new snippet', function () { + const storageValue = JSON.stringify({ + session: { + new: false, + id: 'uid-1234', + rnd: 0.8, + pages: 1, + expiry: 1714116520710, + testName: 't', + testVersion: 'clt' + }, + abTest: { + expiry: 1714116520810, + testName: 't', + testVersion: 'srv' + } + }); + sandbox.stub(storage, 'getDataFromLocalStorage').callsArgWith(1, storageValue); + sandbox.stub(Date, 'now').returns(1714116520710); + sandbox.stub(Math, 'random').returns(0.8); + sandbox.stub(utils, 'generateUUID').returns('uid-1234'); + + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + + adagioRtdSubmodule.init(config); + + const expected = { + session: { + new: false, + expiry: 1714116520710, + id: utils.generateUUID(), + rnd: Math.random(), + pages: 1, + testName: 't', + testVersion: 'srv', + v: 2 + } + } + + expect(spy.withArgs({ + action: 'session', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + }); + }); + + describe('submodule `getBidRequestData`', function () { + const bidReqConfig = { + 'timeout': 700, + 'adUnits': [ + { + 'code': 'div-gpt-ad-1460505748561-0', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'ortb2Imp': {}, + 'bids': [ + { + 'bidder': 'adagio', + 'params': { + 'organizationId': '1004', + 'site': 'maville', + 'useAdUnitCodeAsPlacement': true, + 'adUnitElementId': 'div-gpt-ad-1460505748561-0', + 'pagetype': 'article', + } + }, + { + 'bidder': 'another', + 'params': { + 'pubid': 'xxx', + } + } + ] + } + ], + 'adUnitCodes': [ + 'div-gpt-ad-1460505748561-0' + ], + 'ortb2Fragments': { + 'global': { + 'regs': { + 'ext': { + 'gdpr': 1 + } + }, + 'site': { + 'domain': 'example.com', + 'publisher': { + 'domain': 'example.com' + }, + 'page': 'http://example.com/page.html', + }, + 'device': { + 'w': 1359, + 'h': 1253, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'fr' + } + }, + 'bidder': {} + } + }; + + function cb() {} + + beforeEach(function() { + _internal.getFeatures().reset(); + }); + + it('exists', function () { + expect(adagioRtdSubmodule.getBidRequestData).to.be.a('function'); + }); + + it('update the ortb2Fragments object with adg_rtd signals', function() { + const bidRequest = utils.deepClone(bidReqConfig); + + sandbox.stub(window.top.document, 'getElementById').returns(getElementByIdMock()); + sandbox.stub(window.top, 'getComputedStyle').returns({ display: 'block' }); + sandbox.stub(utils, 'inIframe').returns(false); + + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + const signals = bidRequest.ortb2Fragments.global.site.ext.data.adg_rtd; + expect(signals).to.have.property('features'); + expect(signals).to.have.property('session'); + expect(signals).to.have.property('uid'); + expect(signals.features.viewport_dimensions).to.match(/\d+x\d+/); + expect(signals.features.page_dimensions).to.match(/\d+x\d+/); + + expect(bidRequest.adUnits[0]).to.have.property('ortb2Imp'); + expect(bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd.adunit_position).to.match(/\d+x\d+/); + }); + + describe('update the ortb2Fragments object a SafeFrame context', function() { + it('update', function() { + sandbox.stub(utils, 'isSafeFrameWindow').returns(true); + sandbox.stub(utils, 'canAccessWindowTop').returns(false); + + window.$sf = { + ext: { + geom() { + return { + win: {t: 23, r: 1920, b: 1200, l: 0, w: 1920, h: 1177}, + self: {t: 210, r: 1159, b: 460, l: 859, w: 300, h: 250}, + } + } + } + }; + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const fragmentExt = bidRequest.ortb2Fragments.global.site.ext.data.adg_rtd; + expect(fragmentExt.features.viewport_dimensions).equal('1920x1177'); + expect(fragmentExt.features.page_dimensions).equal(''); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal('210x859'); + + window.$sf = undefined; + }); + + it('handle missformated $sf object and update', function() { + sandbox.stub(utils, 'isSafeFrameWindow').returns(true); + sandbox.stub(utils, 'canAccessWindowTop').returns(false); + + window.$sf = { + ext: { + geom: '' + } + }; + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const fragmentExt = bidRequest.ortb2Fragments.global.site.ext.data.adg_rtd; + expect(fragmentExt.features.viewport_dimensions).equal(''); + expect(fragmentExt.features.page_dimensions).equal(''); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal(''); + + window.$sf = undefined; + }); + }); + + describe('update the ortb2Fragments object in a "inIframe" context', function() { + it('update when window.top is accessible', function() { + sandbox.stub(utils, 'canAccessWindowTop').returns(true); + sandbox.stub(utils, 'isSafeFrameWindow').returns(false); + sandbox.stub(utils, 'inIframe').returns(true); + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal(''); + }); + + it('catch error when window.top is accessible', function() { + sandbox.stub(utils, 'canAccessWindowTop').returns(true); + sandbox.stub(utils, 'isSafeFrameWindow').returns(false); + sandbox.stub(window.document, 'getElementById').throws(); + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal(''); + }); + }); + + it('update the ortb2Fragments object when window.top is not accessible', function() { + sandbox.stub(utils, 'canAccessWindowTop').returns(false); + sandbox.stub(utils, 'isSafeFrameWindow').returns(false); + + const bidRequest = utils.deepClone(bidReqConfig); + adagioRtdSubmodule.getBidRequestData(bidRequest, cb); + + const fragmentExt = bidRequest.ortb2Fragments.global.site.ext.data.adg_rtd; + expect(fragmentExt.features.viewport_dimensions).equal(''); + expect(fragmentExt.features.page_dimensions).equal(''); + + const ortb2ImpExt = bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd; + expect(ortb2ImpExt.adunit_position).equal(''); + }); + + describe('update the ortb2Imp.ext.data.placement if not present', function() { + const config = { + name: SUBMODULE_NAME, + params: { + organizationId: '1000', + site: 'mysite' + } + }; + + it('update the placement value with the adUnit.code value', function() { + const configCopy = utils.deepClone(config); + configCopy.params.placementSource = PLACEMENT_SOURCES.ADUNITCODE; + + 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.equal('div-gpt-ad-1460505748561-0'); + }); + + it('update the placement value with the gpid value', function() { + const configCopy = utils.deepClone(config); + configCopy.params.placementSource = PLACEMENT_SOURCES.GPID; + + 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); + }); + + it('update the placement value the legacy adUnit[].bids adagio.params.placement value', function() { + const placement = 'placement-value'; + + const configCopy = utils.deepClone(config); + + 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(placement); + }); + + it('it does not populate `ortb2Imp.ext.data.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; + }); + + it('ensure we create the `ortb2Imp` object if it does not exist', function() { + const configCopy = utils.deepClone(config); + configCopy.params.placementSource = PLACEMENT_SOURCES.ADUNITCODE; + + const bidRequest = utils.deepClone(bidReqConfig); + delete bidRequest.adUnits[0].ortb2Imp; + + 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'); + }); + }); + }); + + describe('submodule `onBidRequestEvent`', function() { + const bidderRequest = { + 'bidderCode': 'adagio', + 'auctionId': '3de10dc0-fe75-480f-95cc-f15f2c4929fe', + 'bidderRequestId': '4ecd1f17cf829b', + 'bids': [ + { + 'bidder': 'adagio', + 'params': { + 'organizationId': '1000', + 'site': 'example', + 'adUnitElementId': 'div-gpt-ad-1460505748561-0', + 'pagetype': 'article', + 'environment': 'desktop', + 'placement': 'div-gpt-ad-1460505748561-0', + 'adagioAuctionId': '4c259968-0158-443d-af93-551bac594b6c', + 'pageviewId': 'dfb9b067-e5c4-4212-97bb-c67d6313ecaf' + }, + 'ortb2Imp': { + 'ext': { + 'tid': '235c991e-fcc4-416b-95d3-f60e53575bee', + 'data': { + 'adserver': { + 'name': 'gam', + 'adslot': '/19968336/header-bid-tag-0' + }, + 'pbadslot': '/19968336/header-bid-tag-0', + 'adunit_position': '8x95' + }, + 'gpid': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '235c991e-fcc4-416b-95d3-f60e53575bee', + 'adUnitId': '79ab5904-0b21-4235-965a-f4905af072b7', + 'bidId': '534aa529a44e0e', + 'bidderRequestId': '4ecd1f17cf829b', + 'auctionId': '3de10dc0-fe75-480f-95cc-f15f2c4929fe', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + 'uid': 'dfb9b067-e5c4-4212-97bb-c67d6313ecaf', + 'features': { + 'page_dimensions': '1359x1353', + 'viewport_dimensions': '1359x1253', + 'user_timestamp': '1715621032', + 'dom_loading': '28' + }, + 'session': { + 'new': true, + 'rnd': 0.020644826280300954, + 'vwSmplg': 0.1, + 'vwSmplgNxt': 0.1, + 'pages': 1 + } + } + } + } + } + }, + }, + ], + 'auctionStart': 1715613832791, + 'timeout': 700, + 'ortb2': { + 'site': { + 'ext': { + 'data': { + 'adg_rtd': { + 'features': { + 'page_dimensions': '1359x1353', + 'viewport_dimensions': '1359x1253', + 'user_timestamp': '1715621032', + 'dom_loading': '28' + }, + 'session': { + 'new': true, + 'rnd': 0.020644826280300954, + 'vwSmplg': 0.1, + 'vwSmplgNxt': 0.1, + 'pages': 1 + } + } + } + } + } + }, + 'start': 1715613832796 + } + + it('store a copy of computed property', function() { + const spy = sandbox.spy(_internal.getAdagioNs().queue, 'push') + sandbox.stub(Date, 'now').returns(12345); + + _internal.getGuard().clear(); + + const config = { + params: { + organizationId: '1000', + site: 'example' + } + }; + const bidderRequestCopy = utils.deepClone(bidderRequest); + adagioRtdSubmodule.onBidRequestEvent(bidderRequestCopy, config); + + clock.tick(1); + + const { + bidder, + adUnitCode, + mediaTypes, + params, + auctionId, + bidderRequestsCount } = bidderRequestCopy.bids[0]; + + const expected = { + bidder, + adUnitCode, + mediaTypes, + ortb2: bidderRequestCopy.bids[0].ortb2.site.ext.data, + ortb2Imp: bidderRequestCopy.bids[0].ortb2Imp.ext.data, + params, + auctionId, + bidderRequestsCount, + organizationId: config.params.organizationId, + site: config.params.site, + localPbjs: 'pbjs', + localPbjsRef: getGlobal() + } + + expect(spy.withArgs({ + action: 'store', + ts: Date.now(), + data: expected, + }).calledOnce).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/adbookpspBidAdapter_spec.js b/test/spec/modules/adbookpspBidAdapter_spec.js deleted file mode 100755 index 3f26cd7749f..00000000000 --- a/test/spec/modules/adbookpspBidAdapter_spec.js +++ /dev/null @@ -1,1344 +0,0 @@ -import { expect } from 'chai'; -import * as utils from '../../../src/utils.js'; -import { - spec, - storage, - DEFAULT_BIDDER_CONFIG, - VERSION, - common, -} from '../../../modules/adbookpspBidAdapter.js'; - -describe('adbookpsp bid adapter', () => { - let sandbox; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - - sandbox - .stub(common, 'generateUUID') - .returns('54444444-5444-4444-9444-544444444444'); - sandbox.stub(common, 'getWindowDimensions').returns({ - innerWidth: 100, - innerHeight: 100, - }); - }); - - afterEach(function () { - sandbox.restore(); - }); - - describe('isBidRequestValid()', () => { - it('should return false when there is no banner in mediaTypes', () => { - const bid = utils.deepClone(bannerBid); - delete bid.mediaTypes.banner; - - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when orgId and placementId is not defined', () => { - const bid = utils.deepClone(bannerBid); - delete bid.params.placementId; - delete bid.params.orgId; - - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - - it('should return true when orgId is set in config', () => { - const bid = utils.deepClone(bannerBid); - - delete bid.params.placementId; - delete bid.params.orgId; - - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.orgId') - .returns('129576'); - - expect(spec.isBidRequestValid(bid)).to.be.true; - }); - - it('should return true when required params found', () => { - expect(spec.isBidRequestValid(bannerBid)).to.equal(true); - expect(spec.isBidRequestValid(videoBid)).to.equal(true); - expect(spec.isBidRequestValid(mixedBid)).to.equal(true); - }); - - it('should return false when sizes for banner are not specified', () => { - const bid = utils.deepClone(bannerBid); - delete bid.mediaTypes.banner.sizes; - - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when sizes for banner are invalid', () => { - const bid = utils.deepClone(bannerBid); - delete bid.mediaTypes.banner.sizes; - - bid.mediaTypes.banner.sizes = [['123', 'foo']]; - - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return true if player size is set via playerSize', () => { - expect(spec.isBidRequestValid(videoBid)).to.equal(true); - }); - - it('should return true if player size is set via w and h', () => { - const bid = utils.deepClone(videoBid); - delete bid.mediaTypes.video.playerSize; - - bid.mediaTypes.video.w = 400; - bid.mediaTypes.video.h = 300; - - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - - it('should reutrn false if player size is not set', () => { - const bid = utils.deepClone(videoBid); - delete bid.mediaTypes.video.playerSize; - - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - }); - - describe('buildRequests()', () => { - it('should build correct request for banner bid', () => { - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.orgId') - .returns(undefined) - .withArgs('adbookpsp.exchangeUrl') - .returns('https://ex.fattail.com/openrtb2'); - - const requests = spec.buildRequests([bannerBid], bidderRequest); - - expect(requests).to.have.lengthOf(1); - expect(requests[0]).to.deep.include({ - method: 'POST', - url: 'https://ex.fattail.com/openrtb2', - options: { - contentType: 'application/json', - withCredentials: true, - }, - }); - expect(JSON.parse(requests[0].data)).to.deep.equal(bannerExchangeRequest); - }); - - it('should build correct request for video bid', () => { - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp') - .returns(DEFAULT_BIDDER_CONFIG) - .withArgs('adbookpsp.exchangeUrl') - .returns(DEFAULT_BIDDER_CONFIG.exchangeUrl) - .withArgs('adbookpsp.orgId') - .returns(undefined); - - const requests = spec.buildRequests([videoBid], bidderRequest); - - expect(requests).to.have.lengthOf(1); - expect(requests[0]).to.deep.include({ - method: 'POST', - url: 'https://ex.fattail.com/openrtb2', - options: { - contentType: 'application/json', - withCredentials: true, - }, - }); - expect(JSON.parse(requests[0].data)).to.deep.include({ - ...videoExchangeRequest, - ext: { - adbook: { - config: DEFAULT_BIDDER_CONFIG, - version: { - adapter: VERSION, - prebid: '$prebid.version$', - }, - }, - }, - }); - }); - - it('should build correct request for video bid with w and h', () => { - const bid = utils.deepClone(videoBid); - - delete bid.mediaTypes.video.playerSize; - - bid.mediaTypes.video.w = 400; - bid.mediaTypes.video.h = 300; - - const [request] = spec.buildRequests([bid], bidderRequest); - const requestData = JSON.parse(request.data); - - expect(requestData.imp[0].video.w).to.equal(400); - expect(requestData.imp[0].video.h).to.equal(300); - }); - - it('should build correct request for video bid with both w, h and playerSize', () => { - const bid = utils.deepClone(videoBid); - - bid.mediaTypes.video.w = 640; - bid.mediaTypes.video.h = 480; - - const [request] = spec.buildRequests([bid], bidderRequest); - const requestData = JSON.parse(request.data); - - expect(requestData.imp[0].video.w).to.equal(640); - expect(requestData.imp[0].video.h).to.equal(480); - }); - - it('should build correct request for mixed bid', () => { - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.orgId') - .returns(undefined) - .withArgs('adbookpsp.exchangeUrl') - .returns('https://ex.fattail.com/openrtb2'); - - const requests = spec.buildRequests([mixedBid], bidderRequest); - - expect(requests).to.have.lengthOf(1); - expect(requests[0]).to.deep.include({ - method: 'POST', - url: 'https://ex.fattail.com/openrtb2', - options: { - contentType: 'application/json', - withCredentials: true, - }, - }); - expect(JSON.parse(requests[0].data)).to.deep.include( - mixedExchangeRequest - ); - }); - - it('should use orgId from config', () => { - const bid = utils.deepClone(bannerBid); - - delete bid.params; - - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.orgId') - .returns('129576'); - - const requests = spec.buildRequests([bid], bidderRequest); - const request = JSON.parse(requests[0].data); - - expect(request.imp[0].ext).to.deep.include({ - adbook: { - orgId: '129576', - }, - }); - }); - - it('should use orgId from adUnit when orgId is also set in config', () => { - const bid = utils.deepClone(bannerBid); - - delete bid.params.placementId; - - bid.params.orgId = 'adUnitOrgId'; - - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.orgId') - .returns('configOrgId'); - - const requests = spec.buildRequests([bid], bidderRequest); - const request = JSON.parse(requests[0].data); - - expect(request.imp[0].ext).to.deep.include({ - adbook: { - orgId: 'adUnitOrgId', - }, - }); - }); - - it('should include in request GDPR options if available', () => { - const request = utils.deepClone(bidderRequest); - - delete request.uspConsent; - - const requests = spec.buildRequests([bannerBid, mixedBid], request); - - expect(JSON.parse(requests[0].data)).to.deep.include({ - regs: { - coppa: 0, - ext: { - gdpr: 1, - gdprConsentString: 'gdprConsentString', - }, - }, - }); - }); - - it('should include in request USP (CPPA) options if available', () => { - const request = utils.deepClone(bidderRequest); - - delete request.gdprConsent; - - const requests = spec.buildRequests([bannerBid, mixedBid], request); - - expect(JSON.parse(requests[0].data)).to.deep.include({ - regs: { - coppa: 0, - ext: { - us_privacy: 'uspConsentString', - }, - }, - }); - }); - - it('should pass valid coppa flag based on config', () => { - sandbox.stub(common, 'getConfig').withArgs('coppa').returns(true); - - const request = utils.deepClone(bidderRequest); - - delete request.gdprConsent; - delete request.uspConsent; - - const requests = spec.buildRequests([bannerBid, mixedBid], request); - - expect(JSON.parse(requests[0].data)).to.deep.include({ - regs: { - coppa: 1, - }, - }); - }); - - it('should pass GDPR, USP (CCPA) and COPPA options', () => { - sandbox.stub(common, 'getConfig').withArgs('coppa').returns(true); - - const requests = spec.buildRequests([bannerBid, mixedBid], bidderRequest); - - expect(JSON.parse(requests[0].data)).to.deep.include({ - regs: { - coppa: 1, - ext: { - gdpr: 1, - gdprConsentString: 'gdprConsentString', - us_privacy: 'uspConsentString', - }, - }, - }); - }); - - it('should generate and pass user id when is not present in cookie and local storage is not enabled', () => { - sandbox.stub(storage, 'localStorageIsEnabled').returns(false); - const requests = spec.buildRequests([bannerBid, mixedBid], bidderRequest); - const rtbRequest = JSON.parse(requests[0].data); - - expect(rtbRequest.user.id).to.have.lengthOf(36); - }); - - it('should pass user id when is present in cookie', () => { - sandbox.stub(storage, 'localStorageIsEnabled').returns(false); - sandbox - .stub(storage, 'getCookie') - .returns('e35da6bb-f2f8-443b-aeff-3375bef45c9d'); - const requests = spec.buildRequests([bannerBid, mixedBid], bidderRequest); - const rtbRequest = JSON.parse(requests[0].data); - - expect(rtbRequest.user.id).to.equal( - 'e35da6bb-f2f8-443b-aeff-3375bef45c9d' - ); - }); - - it('should pass user id if is present in local storage', () => { - sandbox.stub(storage, 'localStorageIsEnabled').returns(true); - sandbox - .stub(storage, 'getDataFromLocalStorage') - .returns('e35da6bb-f2f8-443b-aeff-3375bef45c9d'); - - const requests = spec.buildRequests([bannerBid, mixedBid], bidderRequest); - const rtbRequest = JSON.parse(requests[0].data); - expect(rtbRequest.user.id).to.equal( - 'e35da6bb-f2f8-443b-aeff-3375bef45c9d' - ); - }); - - it('should regenerate user id if it is invalid', () => { - sandbox.stub(storage, 'localStorageIsEnabled').returns(true); - sandbox.stub(storage, 'getDataFromLocalStorage').returns('foo'); - - const requests = spec.buildRequests([bannerBid, mixedBid], bidderRequest); - const rtbRequest = JSON.parse(requests[0].data); - expect(rtbRequest.user.id).to.have.lengthOf(36); - }); - - it('should pass schain if available', () => { - const bid = utils.deepClone(bannerBid); - const schain = { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'exchange1.com', - sid: '1234', - hp: 1, - rid: 'bid-request-1', - name: 'publisher', - domain: 'publisher.com', - }, - ], - }; - - bid.schain = schain; - - const requests = spec.buildRequests([bid], bidderRequest); - - expect(JSON.parse(requests[0].data).source).to.deep.include({ - ext: { - schain, - }, - }); - }); - - it('return empty array if there are no valid bid requests', () => { - const requests = spec.buildRequests([], bidderRequest); - - expect(requests).to.deep.equal([]); - }); - - it('should prioritize device information set in config', () => { - const ua = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1'; - - sandbox.stub(common, 'getConfig').withArgs('device').returns({ - ua, - }); - - const requests = spec.buildRequests([bannerBid], bidderRequest); - - expect(JSON.parse(requests[0].data).device.ua).to.equal(ua); - }); - - it('should include bidder config', () => { - const bidderConfig = { - bidTTL: 500, - defaultCurrency: 'USD', - exchangeUrl: 'https://exsb.fattail.com/openrtb2', - winTrackingEnabled: true, - winTrackingUrl: 'https://evsb.fattail.com/wins', - orgId: '129576', - }; - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp') - .returns(bidderConfig); - - const requests = spec.buildRequests([bannerBid], bidderRequest); - const request = JSON.parse(requests[0].data); - - expect(request.ext).to.deep.include({ - adbook: { - config: bidderConfig, - version: { - adapter: VERSION, - prebid: '$prebid.version$', - }, - }, - }); - }); - - it('should use bidder video params if they are set', () => { - const videoBidWithParams = utils.deepClone(videoBid); - const bidderVideoParams = { - api: [1, 2], - mimes: ['video/mp4', 'video/x-flv'], - playbackmethod: [3, 4], - protocols: [5, 6], - minduration: 10, - maxduration: 30, - }; - videoBidWithParams.params.video = bidderVideoParams; - - const requests = spec.buildRequests([videoBidWithParams], bidderRequest); - const request = JSON.parse(requests[0].data); - - expect(request.imp[0]).to.deep.include({ - video: { - ...bidderVideoParams, - w: videoBidWithParams.mediaTypes.video.playerSize[0][0], - h: videoBidWithParams.mediaTypes.video.playerSize[0][1], - }, - }); - }); - }); - - describe('interpretResponse()', () => { - it('should correctly interpret valid response', () => { - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.defaultCurrency') - .returns(DEFAULT_BIDDER_CONFIG.defaultCurrency) - .withArgs('adbookpsp.bidTTL') - .returns(DEFAULT_BIDDER_CONFIG.bidTTL); - - const response = utils.deepClone(exchangeResponse); - const bids = spec.interpretResponse( - { body: response }, - { data: JSON.stringify(exchangeBidRequest) } - ); - - expect(bids).to.deep.equal([ - { - bidderRequestId: '999ccceeee11', - requestId: '9873kfse', - bidId: 'bid123456', - width: 300, - height: 250, - ttl: 300, - cpm: 0.5, - currency: 'USD', - creativeId: '123456789', - mediaType: 'banner', - meta: { - advertiserDomains: ['advertiser.com'], - mediaType: 'banner', - primaryCatId: 'IAB2-1', - secondaryCatIds: ['IAB2-2', 'IAB2-3'], - }, - netRevenue: true, - nurl: 'http://win.example.url', - adUnitCode: 'div-gpt-ad-837465923534-0', - ad: '
ad
', - adId: '5', - adserverTargeting: { - hb_ad_ord_adbookpsp: '0_0', // the value to the left of the underscore represents the index of the ad id and the number to the right represents the order index - hb_adid_c_adbookpsp: '5', - hb_deal_adbookpsp: 'werwetwerw', - hb_liid_adbookpsp: '2342345', - hb_ordid_adbookpsp: '567843', - }, - referrer: 'http://prebid-test-page.io:8080/banner.html', - lineItemId: '2342345', - }, - { - ad: '', - adId: '10', - adUnitCode: 'div-gpt-ad-837465923534-0', - adserverTargeting: { - hb_ad_ord_adbookpsp: '0_0', - hb_adid_c_adbookpsp: '10', - hb_deal_adbookpsp: 'dsfxcxcvxc', - hb_liid_adbookpsp: '2121221', - hb_ordid_adbookpsp: '5678234', - }, - bidId: 'bid4321', - bidderRequestId: '999ccceeee11', - cpm: 0.45, - creativeId: '543123', - currency: 'USD', - height: 250, - lineItemId: '2121221', - mediaType: 'video', - meta: { - advertiserDomains: ['advertiser.com', 'campaign.advertiser.com'], - mediaType: 'video', - primaryCatId: 'IAB2-3', - secondaryCatIds: [], - }, - netRevenue: true, - nurl: 'http://win.example.url', - referrer: 'http://prebid-test-page.io:8080/banner.html', - requestId: '120kfeske', - ttl: 300, - vastXml: - '', - width: 300, - }, - ]); - }); - - it('should place valid GAM targeting for all bids when multiple bids are present for multiple impressions', () => { - const response = utils.deepClone(exchangeResponse); - - const bids = spec.interpretResponse( - { body: response }, - { data: JSON.stringify(exchangeBidRequest) } - ); - - expect(bids).to.have.length(2); - expect(bids[0].adserverTargeting).to.deep.equal({ - hb_ad_ord_adbookpsp: '0_0', - hb_adid_c_adbookpsp: '5', - hb_deal_adbookpsp: 'werwetwerw', - hb_liid_adbookpsp: '2342345', - hb_ordid_adbookpsp: '567843', - }); - expect(bids[1].adserverTargeting).to.deep.equal({ - hb_ad_ord_adbookpsp: '0_0', - hb_adid_c_adbookpsp: '10', - hb_deal_adbookpsp: 'dsfxcxcvxc', - hb_liid_adbookpsp: '2121221', - hb_ordid_adbookpsp: '5678234', - }); - }); - - it('should place valid GAM targeting for all bids when multiple bids are present for single impression', () => { - const response = utils.deepClone(exchangeResponse); - - response.seatbid[1].bid[0].impid = '9873kfse'; - - const bids = spec.interpretResponse( - { body: response }, - { data: JSON.stringify(exchangeBidRequest) } - ); - - expect(bids).to.have.length(2); - for (const bid of bids) { - expect(bid.adserverTargeting).to.deep.equal({ - hb_ad_ord_adbookpsp: '0_0,1_0', - hb_adid_c_adbookpsp: '5,10', - hb_deal_adbookpsp: 'werwetwerw,dsfxcxcvxc', - hb_liid_adbookpsp: '2342345,2121221', - hb_ordid_adbookpsp: '567843,5678234', - }); - } - }); - - it('should return no bids if response id does not match bidderRequestId', () => { - const body = utils.deepClone(exchangeResponse); - body.id = '999'; - - const bids = spec.interpretResponse( - { body }, - { data: JSON.stringify(exchangeBidRequest) } - ); - - expect(bids).to.deep.equal([]); - }); - - it('should return no bids if response does not include seatbid', () => { - const body = utils.deepClone(exchangeResponse); - delete body.seatbid; - - const bids = spec.interpretResponse( - { body }, - { data: JSON.stringify(exchangeBidRequest) } - ); - - expect(bids).to.deep.equal([]); - }); - - it('should return no bids if response does not include any bids', () => { - const body = utils.deepClone(exchangeResponse); - body.seatbid = []; - - const bids = spec.interpretResponse( - { body }, - { data: JSON.stringify(exchangeBidRequest) } - ); - - expect(bids).to.deep.equal([]); - }); - - it('should exclude invalid video bids', () => { - const body = utils.deepClone(exchangeResponse); - - body.seatbid.shift(); - body.seatbid[0].bid[0].adid = 34; - - const bids = spec.interpretResponse( - { body }, - { data: JSON.stringify(exchangeBidRequest) } - ); - - expect(bids).to.deep.equal([]); - }); - - it('should exclude invalid banner bids', () => { - const body = utils.deepClone(exchangeResponse); - const request = utils.deepClone(exchangeBidRequest); - - body.seatbid.pop(); - - delete body.seatbid[0].bid[0].w; - delete body.seatbid[0].bid[0].h; - - request.imp[0].banner.format.push({ w: 300, h: 600 }); - - const bids = spec.interpretResponse( - { body }, - { data: JSON.stringify(request) } - ); - - expect(bids).to.deep.equal([]); - }); - - it('should not include invalid banner bids in targeting map', () => { - const body = utils.deepClone(exchangeResponse); - const request = utils.deepClone(exchangeBidRequest); - - body.seatbid[0].bid[0].h = '600'; - - request.imp[0].banner.format.push({ w: 300, h: 600 }); - - const bids = spec.interpretResponse( - { body }, - { data: JSON.stringify(exchangeBidRequest) } - ); - - expect(bids[0].adserverTargeting).to.deep.equal({ - hb_ad_ord_adbookpsp: '0_0', - hb_adid_c_adbookpsp: '10', - hb_deal_adbookpsp: 'dsfxcxcvxc', - hb_liid_adbookpsp: '2121221', - hb_ordid_adbookpsp: '5678234', - }); - }); - - it('should not validate banner bid dimensions if bid request has single size', () => { - const body = utils.deepClone(exchangeResponse); - const request = utils.deepClone(exchangeBidRequest); - - delete body.seatbid[1]; - delete body.seatbid[0].bid[0].h; - delete body.seatbid[0].bid[0].w; - - const bids = spec.interpretResponse( - { body }, - { data: JSON.stringify(request) } - ); - - expect(bids.length).to.equal(1); - }); - }); - - describe('getUserSyncs()', () => { - it('should return user syncs if there are included in the response and syncs are enabled', () => { - const syncs = spec.getUserSyncs( - { - pixelEnabled: true, - iframeEnabled: true, - }, - [{ body: exchangeResponse }] - ); - - expect(syncs).to.deep.equal([ - { - type: 'image', - url: 'http://sometest.com/sync/1234567', - }, - { - type: 'iframe', - url: 'http://sometest.com/sync/1234567', - }, - ]); - }); - - it('should not return user syncs if syncs are disabled', () => { - const syncs = spec.getUserSyncs( - { - pixelEnabled: false, - iframeEnabled: false, - }, - [{ body: exchangeResponse }] - ); - - expect(syncs).to.deep.equal([]); - }); - - it('should return image syncs if they are enabled', () => { - const syncs = spec.getUserSyncs( - { - pixelEnabled: true, - iframeEnabled: false, - }, - [{ body: exchangeResponse }] - ); - - expect(syncs).to.deep.equal([ - { - type: 'image', - url: 'http://sometest.com/sync/1234567', - }, - ]); - }); - - it('should return iframe syncs if they are enabled', () => { - const syncs = spec.getUserSyncs( - { - pixelEnabled: false, - iframeEnabled: true, - }, - [{ body: exchangeResponse }] - ); - - expect(syncs).to.deep.equal([ - { - type: 'iframe', - url: 'http://sometest.com/sync/1234567', - }, - ]); - }); - - it('should append COPPA status to sync url', () => { - sandbox.stub(common, 'getConfig').withArgs('coppa').returns(true); - const syncs = spec.getUserSyncs( - { - pixelEnabled: false, - iframeEnabled: true, - }, - [{ body: utils.deepClone(exchangeResponse) }] - ); - - expect(syncs).to.deep.equal([ - { - type: 'iframe', - url: 'http://sometest.com/sync/1234567?coppa=1', - }, - ]); - }); - - it('should append GDPR consent data to url', () => { - sandbox.stub(common, 'getConfig').withArgs('coppa').returns(false); - const syncs = spec.getUserSyncs( - { - pixelEnabled: false, - iframeEnabled: true, - }, - [{ body: utils.deepClone(exchangeResponse) }], - { gdprApplies: true, consentString: 'gdprConsentString' } - ); - - expect(syncs).to.deep.equal([ - { - type: 'iframe', - url: 'http://sometest.com/sync/1234567?gdpr=1&consentString=gdprConsentString', - }, - ]); - }); - - it('should append USP (CCPA) consent string to url', () => { - const syncs = spec.getUserSyncs( - { - pixelEnabled: false, - iframeEnabled: true, - }, - [{ body: utils.deepClone(exchangeResponse) }], - undefined, - 'uspConsentString' - ); - - expect(syncs).to.deep.equal([ - { - type: 'iframe', - url: 'http://sometest.com/sync/1234567?us_privacy=uspConsentString', - }, - ]); - }); - - it('should append COPPA, GDPR and USP (CCPA) url params', () => { - sandbox.stub(common, 'getConfig').withArgs('coppa').returns(true); - const syncs = spec.getUserSyncs( - { - pixelEnabled: true, - iframeEnabled: true, - }, - [{ body: utils.deepClone(exchangeResponse) }], - { gdprApplies: true, consentString: 'gdprConsentString' }, - 'uspConsentString' - ); - - expect(syncs).to.deep.equal([ - { - type: 'image', - url: 'http://sometest.com/sync/1234567?gdpr=1&consentString=gdprConsentString&us_privacy=uspConsentString&coppa=1', - }, - { - type: 'iframe', - url: 'http://sometest.com/sync/1234567?gdpr=1&consentString=gdprConsentString&us_privacy=uspConsentString&coppa=1', - }, - ]); - }); - - it('should respect url param syntax when appending params', () => { - sandbox.stub(common, 'getConfig').withArgs('coppa').returns(true); - - const response = utils.deepClone(exchangeResponse); - - response.ext.sync[0] = { - type: 'image', - url: 'http://sometest.com/sync/1234567?horseCount=4', - }; - - const syncs = spec.getUserSyncs( - { - pixelEnabled: true, - iframeEnabled: false, - }, - [{ body: response }], - { gdprApplies: true, consentString: 'gdprConsentString' }, - 'uspConsentString' - ); - - expect(syncs).to.deep.equal([ - { - type: 'image', - url: 'http://sometest.com/sync/1234567?horseCount=4&gdpr=1&consentString=gdprConsentString&us_privacy=uspConsentString&coppa=1', - }, - ]); - }); - }); - - describe('onBidWon()', () => { - it('should track win if win tracking is enabled', () => { - const spy = sandbox.spy(utils, 'triggerPixel'); - - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.winTrackingEnabled') - .returns(true) - .withArgs('adbookpsp.winTrackingUrl') - .returns('https://ev.fattail.com/wins'); - - spec.onBidWon({ - requestId: 'requestId', - bidderRequestId: 'bidderRequestId', - bidId: 'bidId', - }); - - expect( - spy.calledWith( - 'https://ev.fattail.com/wins?impId=requestId&reqId=bidderRequestId&bidId=bidId' - ) - ).to.equal(true); - }); - it('should call bid.nurl if win tracking is enabled', () => { - const spy = sandbox.spy(utils, 'triggerPixel'); - - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.winTrackingEnabled') - .returns(true) - .withArgs('adbookpsp.winTrackingUrl') - .returns('https://ev.fattail.com/wins'); - - spec.onBidWon({ - requestId: 'requestId', - bidderRequestId: 'bidderRequestId', - bidId: 'bidId', - nurl: 'http://win.example.url', - }); - - expect(spy.calledWith('http://win.example.url')).to.equal(true); - }); - it('should not track win nor call bid.nurl if win tracking is disabled', () => { - const spy = sandbox.spy(utils, 'triggerPixel'); - - sandbox - .stub(common, 'getConfig') - .withArgs('adbookpsp.winTrackingEnabled') - .returns(false) - .withArgs('adbookpsp.winTrackingUrl') - .returns('https://ev.fattail.com/wins'); - - spec.onBidWon({ - requestId: 'requestId', - bidderRequestId: 'bidderRequestId', - bidId: 'bidId', - nurl: 'http://win.example.url', - }); - - expect(spy.notCalled).to.equal(true); - }); - }); -}); - -const bidderRequest = { - auctionId: 'aaccee333311', - bidderRequestId: '999ccceeee11', - timeout: 200, - refererInfo: { - page: 'http://mock-page.com', - domain: 'mock-page.com', - ref: 'http://example-domain.com/foo', - }, - gdprConsent: { - gdprApplies: 1, - consentString: 'gdprConsentString', - }, - uspConsent: 'uspConsentString', - ortb2: { - source: { - tid: 'aaccee333311' - } - } -}; - -const bannerBid = { - bidder: 'adbookpsp', - params: { - placementId: '12390123', - }, - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - adUnitCode: 'div-gpt-ad-837465923534-0', - transactionId: 'sfsf89e-mck3-asf3-fe45-feksjfi123mfs', - bidId: '9873kfse', - bidderRequestId: '999ccceeee11', - auctionId: 'aaccee333311', - lineItemId: 123123123, -}; - -const bannerExchangeRequest = { - id: '999ccceeee11', - device: { - h: 100, - w: 100, - js: true, - ua: navigator.userAgent, - dnt: 0, - }, - regs: { - coppa: 0, - ext: { - gdpr: 1, - gdprConsentString: 'gdprConsentString', - us_privacy: 'uspConsentString', - }, - }, - site: { - domain: 'mock-page.com', - page: 'http://mock-page.com', - ref: 'http://example-domain.com/foo', - }, - source: { - fd: 1, - tid: 'aaccee333311', - }, - tmax: 200, - user: { - gdprConsentString: 'gdprConsentString', - id: '54444444-5444-4444-9444-544444444444', - }, - imp: [ - { - banner: { - format: [ - { - w: 300, - h: 250, - }, - { - w: 300, - h: 600, - }, - ], - w: 300, - h: 250, - topframe: 0, - pos: 0, - }, - ext: { - adbook: { - placementId: '12390123', - }, - }, - id: '9873kfse', - tagid: 'div-gpt-ad-837465923534-0', - }, - ], - ext: { - adbook: { - version: { - adapter: VERSION, - prebid: '$prebid.version$', - }, - }, - }, -}; - -const videoBid = { - bidder: 'adbookpsp', - params: { - placementId: '129576', - }, - mediaTypes: { - video: { - api: [1, 2, 4, 6], - mimes: ['video/mp4'], - playbackmethod: [2, 4, 6], - playerSize: [[400, 300]], - protocols: [3, 4, 7, 8, 10], - }, - }, - adUnitCode: 'div-gpt-ad-9383743831-6', - transactionId: 'aacc3fasf-fere-1335-8m1s-785393mc3fj', - bidId: '120kfeske', - bidderRequestId: '999ccceeee11', - auctionId: 'aaccee333311', - lineItemId: 321321321, -}; - -const videoExchangeRequest = { - id: '999ccceeee11', - device: { - h: 100, - w: 100, - js: true, - ua: navigator.userAgent, - dnt: 0, - }, - regs: { - coppa: 0, - ext: { - gdpr: 1, - gdprConsentString: 'gdprConsentString', - us_privacy: 'uspConsentString', - }, - }, - site: { - domain: 'mock-page.com', - page: 'http://mock-page.com', - ref: 'http://example-domain.com/foo', - }, - source: { - fd: 1, - tid: 'aaccee333311', - }, - tmax: 200, - user: { - gdprConsentString: 'gdprConsentString', - id: '54444444-5444-4444-9444-544444444444', - }, - imp: [ - { - video: { - api: [1, 2, 4, 6], - h: 300, - mimes: ['video/mp4'], - playbackmethod: [2, 4, 6], - protocols: [3, 4, 7, 8, 10], - w: 400, - }, - ext: { - adbook: { - placementId: '129576', - }, - }, - id: '120kfeske', - tagid: 'div-gpt-ad-9383743831-6', - }, - ], - ext: { - adbook: { - version: { - adapter: VERSION, - prebid: '$prebid.version$', - }, - }, - }, -}; - -const mixedBid = { - bidder: 'adbookpsp', - params: { - orgId: '129576', - }, - mediaTypes: { - banner: { - sizes: [[300, 600]], - }, - video: { - mimes: ['video/mp4'], - playerSize: [[300, 600]], - }, - }, - adUnitCode: 'div-gpt-ad-9383743831-5', - transactionId: 'aacc3fasf-fere-1335-8m1s-785393mc3fj', - bidId: '120kfeske', - bidderRequestId: '999ccceeee11', - auctionId: 'aaccee333311', - lineItemId: 12341234, -}; - -const mixedExchangeRequest = { - id: '999ccceeee11', - device: { - h: 100, - w: 100, - js: true, - ua: navigator.userAgent, - dnt: 0, - }, - regs: { - coppa: 0, - ext: { - gdpr: 1, - gdprConsentString: 'gdprConsentString', - us_privacy: 'uspConsentString', - }, - }, - site: { - domain: 'mock-page.com', - page: 'http://mock-page.com', - ref: 'http://example-domain.com/foo', - }, - source: { - fd: 1, - tid: 'aaccee333311', - }, - tmax: 200, - user: { - gdprConsentString: 'gdprConsentString', - id: '54444444-5444-4444-9444-544444444444', - }, - imp: [ - { - banner: { - format: [ - { - w: 300, - h: 600, - }, - ], - w: 300, - h: 600, - topframe: 0, - pos: 0, - }, - video: { - h: 600, - mimes: ['video/mp4'], - w: 300, - }, - ext: { - adbook: { - orgId: '129576', - }, - }, - id: '120kfeske', - tagid: 'div-gpt-ad-9383743831-5', - }, - ], - ext: { - adbook: { - version: { - adapter: VERSION, - prebid: '$prebid.version$', - }, - }, - }, -}; - -const exchangeBidRequest = { - id: '999ccceeee11', - tmax: 200, - imp: [ - { - id: '9873kfse', - banner: { - format: [ - { - w: 300, - h: 250, - }, - ], - }, - video: { - w: 300, - h: 250, - }, - tagid: 'div-gpt-ad-837465923534-0', - }, - { - id: '120kfeske', - banner: { - format: [ - { - w: 300, - h: 250, - }, - ], - }, - video: { - w: 300, - h: 250, - }, - tagid: 'div-gpt-ad-837465923534-0', - }, - ], - source: { - fd: 1, - tid: 'aaccee333311', - }, - site: { - domain: location.hostname, - page: location.href, - ref: 'http://prebid-test-page.io:8080/banner.html', - }, -}; - -const exchangeResponse = { - id: '999ccceeee11', - seatbid: [ - { - seat: 'adbookpsp', - group: 0, - bid: [ - { - id: 'bid123456', - w: 300, - h: 250, - impid: '9873kfse', - price: 0.5, - exp: 300, - crid: '123456789', - adm: '
ad
', - adid: '5', - dealid: 'werwetwerw', - nurl: 'http://win.example.url', - ext: { - liid: '2342345', - ordid: '567843', - }, - cat: ['IAB2-1', 'IAB2-2', 'IAB2-3'], - adomain: ['advertiser.com'], - }, - ], - }, - { - seat: 'adbookpsp', - group: 0, - bid: [ - { - id: 'bid4321', - impid: '120kfeske', - price: 0.45, - exp: 300, - crid: '543123', - adm: '', - adid: '10', - dealid: 'dsfxcxcvxc', - nurl: 'http://win.example.url', - ext: { - liid: '2121221', - ordid: '5678234', - }, - cat: ['IAB2-3'], - adomain: ['advertiser.com', 'campaign.advertiser.com'], - }, - ], - }, - ], - ext: { - sync: [ - { - type: 'image', - url: 'http://sometest.com/sync/1234567', - }, - { - type: 'iframe', - url: 'http://sometest.com/sync/1234567', - }, - ], - }, -}; diff --git a/test/spec/modules/adfBidAdapter_spec.js b/test/spec/modules/adfBidAdapter_spec.js index d4c5f5c3c38..2b8009a5d72 100644 --- a/test/spec/modules/adfBidAdapter_spec.js +++ b/test/spec/modules/adfBidAdapter_spec.js @@ -1,9 +1,10 @@ // jshint esversion: 6, es3: false, node: true -/* eslint-disable no-console */ + import { assert } from 'chai'; import { spec } from 'modules/adfBidAdapter.js'; import { config } from 'src/config.js'; -import { createEidsArray } from 'modules/userId/eids.js'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; describe('Adf adapter', function () { let bids = []; @@ -81,33 +82,45 @@ describe('Adf adapter', function () { }); describe('user privacy', function () { - it('should send GDPR Consent data to adform if gdprApplies', function () { + it('should send GDPR Consent data to adform', function () { let validBidRequests = [{ bidId: 'bidId', params: { test: 1 } }]; - let bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentDataString' }, refererInfo: { page: 'page' } }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, bidderRequest.gdprConsent.consentString); - assert.equal(request.regs.ext.gdpr, bidderRequest.gdprConsent.gdprApplies); - assert.equal(typeof request.regs.ext.gdpr, 'number'); - }); - - it('should send gdpr as number', function () { - let validBidRequests = [{ bidId: 'bidId', params: { test: 1 } }]; - let bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentDataString' }, refererInfo: { page: 'page' } }; + let ortb2 = { + regs: { + ext: { + gdpr: 1 + } + }, + user: { + ext: { + consent: 'consentDataString' + } + } + }; + let bidderRequest = { ortb2, refererInfo: { page: 'page' } }; let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - assert.equal(typeof request.regs.ext.gdpr, 'number'); + assert.equal(request.user.ext.consent, 'consentDataString'); assert.equal(request.regs.ext.gdpr, 1); }); it('should send CCPA Consent data to adform', function () { let validBidRequests = [{ bidId: 'bidId', params: { test: 1 } }]; - let bidderRequest = { uspConsent: '1YA-', refererInfo: { page: 'page' } }; + let ortb2 = { + regs: { + ext: { + us_privacy: '1YA-' + } + } + }; + let bidderRequest = { ortb2, refererInfo: { page: 'page' } }; let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); assert.equal(request.regs.ext.us_privacy, '1YA-'); - bidderRequest = { uspConsent: '1YA-', gdprConsent: { gdprApplies: true, consentString: 'consentDataString' }, refererInfo: { page: 'page' } }; + ortb2.regs.ext.gdpr = 1; + ortb2.user = { ext: { consent: 'consentDataString' } }; + + bidderRequest = { ortb2, refererInfo: { page: 'page' } }; request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); assert.equal(request.regs.ext.us_privacy, '1YA-'); @@ -115,34 +128,6 @@ describe('Adf adapter', function () { assert.equal(request.regs.ext.gdpr, 1); }); - it('should not send GDPR Consent data to adform if gdprApplies is undefined', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: { siteId: 'siteId' } - }]; - let bidderRequest = { gdprConsent: {gdprApplies: false, consentString: 'consentDataString'}, refererInfo: { page: 'page' } }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 0); - - bidderRequest = {gdprConsent: {consentString: 'consentDataString'}, refererInfo: { page: 'page' }}; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - it('should send default GDPR Consent data to adform', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: { siteId: 'siteId' } - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - it('should transfer DSA info', function () { let validBidRequests = [ { bidId: 'bidId', params: { siteId: 'siteId' } } ]; @@ -199,7 +184,7 @@ describe('Adf adapter', function () { }); it('should have default request structure', function () { - let keys = 'site,device,source,ext,imp'.split(','); + let keys = 'site,user,device,source,ext,imp,regs'.split(','); let validBidRequests = [{ bidId: 'bidId', params: { siteId: 'siteId' } @@ -224,34 +209,30 @@ describe('Adf adapter', function () { assert.equal(request.source.fd, 1); }); - it('should not set coppa when coppa is not provided or is set to false', function () { - config.setConfig({ - }); + it('should send coppa flag', function () { + let ortb2 = { regs: { coppa: 1 } }; let validBidRequests = [{ bidId: 'bidId', params: { test: 1 } }]; - let bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentDataString' }, refererInfo: { page: 'page' } }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); + let request = JSON.parse(spec.buildRequests(validBidRequests, { ortb2, refererInfo: { page: 'page' } }).data); - assert.equal(request.regs.coppa, undefined); - - config.setConfig({ - coppa: false - }); - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.coppa, undefined); + assert.equal(request.regs.coppa, 1); }); - it('should set coppa to 1 when coppa is provided with value true', function () { + it('should send info about device', function () { config.setConfig({ - coppa: true + device: { w: 100, h: 100 } }); - let validBidRequests = [{ bidId: 'bidId', params: { test: 1 } }]; + let validBidRequests = [{ + bidId: 'bidId', + params: { mid: '1000' } + }]; let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data); - assert.equal(request.regs.coppa, 1); + assert.equal(request.device.ua, navigator.userAgent); + assert.equal(request.device.w, 100); + assert.equal(request.device.h, 100); }); - it('should send info about device', function () { + it('should merge ortb2.device info', function () { config.setConfig({ device: { w: 100, h: 100 } }); @@ -259,11 +240,13 @@ describe('Adf adapter', function () { bidId: 'bidId', params: { mid: '1000' } }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data); + let ortb2 = { device: { ua: 'customUA', w: 200, geo: { lat: 1, lon: 1 } } }; + let request = JSON.parse(spec.buildRequests(validBidRequests, { ortb2, refererInfo: { page: 'page' } }).data); - assert.equal(request.device.ua, navigator.userAgent); - assert.equal(request.device.w, 100); + assert.equal(request.device.ua, 'customUA'); + assert.equal(request.device.w, 200); assert.equal(request.device.h, 100); + assert.deepEqual(request.device.geo, { lat: 1, lon: 1 }); }); it('should send app info', function () { @@ -321,26 +304,26 @@ describe('Adf adapter', function () { let validBidRequests = [{ bidId: 'bidId', params: {}, - userIdAsEids: createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE' - }) + userIdAsEids: [ + { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, + { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } + ] }]; let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data); - assert.deepEqual(request.user.ext.eids, [ - { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, - { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } - ]); + assert.deepEqual(request.user.ext.eids, validBidRequests[0].userIdAsEids); }); it('should send currency if defined', function () { - config.setConfig({ currency: { adServerCurrency: 'EUR' } }); let validBidRequests = [{ params: {} }]; let refererInfo = { page: 'page' }; - let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo }).data); - - assert.deepEqual(request.cur, [ 'EUR' ]); + const bidderRequest = { refererInfo }; + setCurrencyConfig({ adServerCurrency: 'EUR' }) + return addFPDToBidderRequest(bidderRequest).then(res => { + let request = JSON.parse(spec.buildRequests(validBidRequests, res).data); + assert.deepEqual(request.cur, [ 'EUR' ]); + setCurrencyConfig({}); + }); }); it('should pass supply chain object', function () { @@ -429,6 +412,18 @@ describe('Adf adapter', function () { } }); + it('should add first party data', function () { + let validBidRequests = [ + { bidId: 'bidId', params: { mid: 1000 }, mediaTypes: { video: {} }, ortb2Imp: { ext: { data: { some: 'value' } } } }, + { bidId: 'bidId2', params: { mid: 1001 }, mediaTypes: { video: {} }, ortb2Imp: { ext: { data: { some: 'value', another: 1 } } } }, + { bidId: 'bidId3', params: { mid: 1002 }, mediaTypes: { video: {} }, ortb2Imp: { ext: {} } } + ]; + let imps = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data).imp; + for (let i = 0; i < 3; i++) { + assert.deepEqual(imps[i].ext.data, validBidRequests[i].ortb2Imp.ext.data); + } + }); + describe('dynamic placement tag', function () { it('should add imp parameters correctly', function () { const validBidRequests = [ @@ -471,12 +466,14 @@ describe('Adf adapter', function () { }); it('should request floor price in adserver currency', function () { - config.setConfig({ currency: { adServerCurrency: 'DKK' } }); + setCurrencyConfig({ adServerCurrency: 'DKK' }) const validBidRequests = [ getBidWithFloor() ]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'DKK'); + return addFPDToBidderRequest(validBidRequests[0]).then(res => { + const imp = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' }, ...res }).data).imp[0]; + assert.equal(imp.bidfloor, undefined); + assert.equal(imp.bidfloorcur, 'DKK'); + setCurrencyConfig({}); + }); }); it('should add correct floor values', function () { @@ -496,30 +493,29 @@ describe('Adf adapter', function () { playerSize: [ 100, 200 ] } }; const expectedFloors = [ 1, 1.3, 0.5 ]; - config.setConfig({ currency: { adServerCurrency: 'DKK' } }); + setCurrencyConfig({ adServerCurrency: 'DKK' }); let validBidRequests = expectedFloors.map(getBidWithFloorTest); - getRequestImps(validBidRequests); - assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + return addFPDToBidderRequest(validBidRequests[0]).then(res => { + getRequestImps(validBidRequests, res); + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }) + mediaTypes = { banner: { + sizes: [ [100, 200], [300, 400] ] + }}; + getRequestImps(validBidRequests, res); - mediaTypes = { banner: { - sizes: [ [100, 200], [300, 400] ] - }}; - validBidRequests = expectedFloors.map(getBidWithFloorTest); - getRequestImps(validBidRequests); + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); - assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + mediaTypes = { native: {} }; + getRequestImps(validBidRequests, res); - mediaTypes = { native: {} }; - validBidRequests = expectedFloors.map(getBidWithFloorTest); - getRequestImps(validBidRequests); + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); - assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + mediaTypes = {}; + getRequestImps(validBidRequests, res); - mediaTypes = {}; - validBidRequests = expectedFloors.map(getBidWithFloorTest); - getRequestImps(validBidRequests); - - assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + setCurrencyConfig({}); + }); function getBidWithFloorTest(floor) { return { @@ -904,8 +900,8 @@ describe('Adf adapter', function () { }); }); - function getRequestImps(validBidRequests) { - return JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data).imp; + function getRequestImps(validBidRequests, enriched = {}) { + return JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' }, ...enriched }).data).imp; } }); @@ -1061,7 +1057,8 @@ describe('Adf adapter', function () { }], adrender: 1 } - } + }, + cat: [ 'IAB1', 'IAB2' ] } ] }], @@ -1121,6 +1118,8 @@ describe('Adf adapter', function () { assert.deepEqual(bids[0].currency, serverResponse.body.cur); assert.deepEqual(bids[0].mediaType, 'native'); assert.deepEqual(bids[0].meta.mediaType, 'native'); + assert.deepEqual(bids[0].meta.primaryCatId, 'IAB1'); + assert.deepEqual(bids[0].meta.secondaryCatIds, [ 'IAB2' ]); assert.deepEqual(bids[0].meta.advertiserDomains, [ 'demo.com' ]); assert.deepEqual(bids[0].meta.dsa, { behalf: 'some-behalf', diff --git a/test/spec/modules/adgenerationBidAdapter_spec.js b/test/spec/modules/adgenerationBidAdapter_spec.js index 9a3bf61fe23..f49b600c1bf 100644 --- a/test/spec/modules/adgenerationBidAdapter_spec.js +++ b/test/spec/modules/adgenerationBidAdapter_spec.js @@ -2,12 +2,15 @@ import {expect} from 'chai'; import {spec} from 'modules/adgenerationBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; import {NATIVE} from 'src/mediaTypes.js'; -import {config} from 'src/config.js'; import prebid from '../../../package.json'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; describe('AdgenerationAdapter', function () { const adapter = newBidder(spec); - const ENDPOINT = ['https://api-test.scaleout.jp/adsv/v1', 'https://d.socdm.com/adsv/v1']; + const ADGENE_PREBID_VERSION = '1.6.4'; + const ENDPOINT_STG = 'https://api-test.scaleout.jp/adgen/prebid'; + const ENDPOINT_RELEASE = 'https://d.socdm.com/adgen/prebid'; describe('inherited functions', function () { it('exists and is a function', function () { @@ -27,23 +30,53 @@ describe('AdgenerationAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); describe('buildRequests', function () { + const suaSample = { + source: 2, + platform: { + brand: 'macOS' + }, + browsers: [ + { + brand: 'Chromium', + version: ['112'] + }, + { + brand: 'Google Chrome', + version: ['112'] + }, + { + brand: 'Not:A-Brand', + version: ['99'] + } + ], + mobile: 0 + }; + const schainSmaple = {ver: '1.0', complete: 1, nodes: [{asi: 'indirectseller.com', sid: '00001', hp: 1}]}; const bidRequests = [ { // banner bidder: 'adg', params: { id: '58278', - currency: 'JPY', }, adUnitCode: 'adunit-code', - sizes: [[300, 250], [320, 100]], + mediaTypes: { + banner: { + sizes: [ + [ + 300, + 250 + ] + ] + } + }, bidId: '2f6ac468a9c15e', bidderRequestId: '14a9f773e30243', auctionId: '4aae9f05-18c6-4fcd-80cf-282708cd584a', @@ -53,7 +86,6 @@ describe('AdgenerationAdapter', function () { bidder: 'adg', params: { id: '58278', - currency: 'JPY', }, mediaTypes: { native: { @@ -152,30 +184,9 @@ describe('AdgenerationAdapter', function () { dnt: 0, ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML,like Gecko)Chrome / 112.0.0.0Safari / 537.36', language: 'ja', - sua: { - source: 2, - platform: { - brand: 'macOS' - }, - browsers: [ - { - brand: 'Chromium', - version: ['112'] - }, - { - brand: 'Google Chrome', - version: ['112'] - }, - { - brand: 'Not:A-Brand', - version: ['99'] - } - ], - mobile: 0 - } + sua: suaSample } }, - schain: {ver: '1.0', complete: 1, nodes: [{asi: 'indirectseller.com', sid: '00001', hp: 1}]} } ]; const bidderRequest = { @@ -183,149 +194,786 @@ describe('AdgenerationAdapter', function () { page: 'https://example.com' } }; - const data = { - banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.3&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.3&imark=1&tp=https%3A%2F%2Fexample.com`, - native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.3&tp=https%3A%2F%2Fexample.com`, - bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.3&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, - bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.3&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.3&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=AgAAAAVacu1uAxgAxH%2BHJ8%2BnWlS2H4uVqr6i%2BHBDCNREHD8WKsio%2Fx7D8xXFuq1cJycUU86yXfTH9Xe%2F4C8KkH%2B7UCiU7uQxhyD7Qxnv251pEs6K8oK%2BBPLYR%2B8BLY%2FsJKesa%2FkoKwx1FHgUzIBum582tSy2Oo%2B7C6wYUaaV4QcLr%2F4LPA%3D&gpid=%2F1111%2Fhomepage%23300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22indirectseller.com%22%2C%22sid%22%3A%2200001%22%2C%22hp%22%3A1%7D%5D%7D&imark=1&tp=https%3A%2F%2Fexample.com`, - }; - it('sends bid request to ENDPOINT via GET', function () { - const request = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(request.url).to.equal(ENDPOINT[1]); - expect(request.method).to.equal('GET'); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + // check banner request + for (const req of request) { + const url = new URL(req.url); + expect(url.origin + url.pathname).to.equal(ENDPOINT_RELEASE); + } }); - it('sends bid request to debug ENDPOINT via GET', function () { - bidRequests[0].params.debug = true; - const request = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(request.url).to.equal(ENDPOINT[0]); - expect(request.method).to.equal('GET'); + it('sends bid request to debug ENDPOINT via POST', function () { + // change the first bidRequest to debug mode + const copyBidRequests = JSON.parse(JSON.stringify(bidRequests)); + for (const copyBid of copyBidRequests) { + copyBid.params.debug = true + } + // check banner request + const request = spec.buildRequests(copyBidRequests, bidderRequest); + for (const req of request) { + const url = new URL(req.url); + expect(url.origin + url.pathname).to.equal(ENDPOINT_STG); + } }); it('should attache params to the banner request', function () { + const expectedMediaTypes = { + banner: { + sizes: [ + [ + 300, + 250 + ] + ] + } + } + const expectedBanner = { + topframe: 0, + format: [ + { + w: 300, + h: 250 + } + ] + } const request = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(request.data).to.equal(data.banner); + // check banner request + const url = new URL(request.url); + expect(url.searchParams.get('posall')).equal('SSPLOC'); + expect(url.searchParams.get('id')).equal('58278'); + expect(url.searchParams.get('sdktype')).equal('0'); + expect(request.method).to.equal('POST'); + + // check request data + expect(request.data.currency).to.equal('JPY'); + expect(request.data.pbver).to.equal(prebid.version); + expect(request.data.sdkname).to.equal('prebidjs'); + expect(request.data.adapterver).to.equal(ADGENE_PREBID_VERSION); + expect(request.data.imark).to.equal(1); + expect(request.data.ortb.imp[0].id).to.equal('2f6ac468a9c15e'); + expect(request.data.ortb.imp[0].ext.params.id).to.equal('58278'); + expect(request.data.ortb.imp[0].ext.mediaTypes).to.deep.equal(expectedMediaTypes); + expect(request.data.ortb.imp[0].banner).to.deep.equal(expectedBanner); }); it('should attache params to the native request', function () { + const expectedMediaTypes = { + native: { + image: { + required: true + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + body: { + required: true + }, + icon: { + required: true + } + } + } const request = spec.buildRequests(bidRequests, bidderRequest)[1]; - expect(request.data).to.equal(data.native); + // check native request + const url = new URL(request.url); + expect(url.searchParams.get('posall')).equal('SSPLOC'); + expect(url.searchParams.get('id')).equal('58278'); + expect(url.searchParams.get('sdktype')).equal('0'); + expect(request.method).to.equal('POST'); + + // check request data + expect(request.data.currency).to.equal('JPY'); + expect(request.data.pbver).to.equal(prebid.version); + expect(request.data.sdkname).to.equal('prebidjs'); + expect(request.data.adapterver).to.equal(ADGENE_PREBID_VERSION); + expect(request.data.ortb.imp[0].id).to.equal('2f6ac468a9c15e'); + expect(request.data.ortb.imp[0].ext.novatiqSyncResponse).to.equal(undefined); + expect(request.data.ortb.imp[0].ext.params.id).to.equal('58278'); + expect(request.data.ortb.imp[0].ext.mediaTypes).to.deep.equal(expectedMediaTypes); }); it('should attache params to the bannerWithHyperId request', function () { - const defaultUA = window.navigator.userAgent; - window.navigator.__defineGetter__('userAgent', function () { - return 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1'; - }); - const request = spec.buildRequests(bidRequests, bidderRequest)[2]; + const hyperIdParams = { + user: { + ext: { + eids: [ + { + source: 'novatiq.com', + uids: [ + { + 'id': 'xxxxxx' + } + ] + }, + ] + } + } + } + const request = spec.buildRequests(bidRequests, {...bidderRequest, ortb2: hyperIdParams})[2]; - window.navigator.__defineGetter__('userAgent', function () { - return defaultUA; - }); - expect(request.data).to.equal(data.bannerWithHyperId); + expect(request.data.ortb.imp[0].ext.novatiqSyncResponse).to.equal(1); + expect(request.data.ortb.user).to.deep.equal(hyperIdParams.user); }); it('should attache params to the bannerWithAdgextCriteoId request', function () { - const request = spec.buildRequests(bidRequests, bidderRequest)[3]; - expect(request.data).to.equal(data.bannerWithAdgextCriteoId); + const criteoParams = { + user: { + ext: { + eids: [ + { + source: 'criteo.com', + uids: [ + { + id: 'xxxxxxx', + atype: 1 + } + ] + }, + ] + } + } + } + const request = spec.buildRequests(bidRequests, {...bidderRequest, ortb2: criteoParams})[0]; + expect(request.data.ortb.user).to.deep.equal(criteoParams.user); }); it('should attache params to the bannerWithAdgextIds request', function () { - const request = spec.buildRequests(bidRequests, bidderRequest)[4]; - expect(request.data).to.equal(data.bannerWithAdgextIds); + const idparams = { + user: { + ext: { + eids: [ + { + source: 'id5-sync.com', + uids: [ + { + id: 'ID5*RCKp3flI7Jutz2TKfExBb6T2kY8KC6xJ5FAXIVuKo2_SDBBFN9x3KQf-FMHXA3Sv', + atype: 1, + ext: { + linkType: 1, + pba: 'L+L6bQ6WoA2INCSS31vtiawRuBYQQ5H6OioCAXUNkl8=', + abTestingControlGroup: false + } + } + ] + }, + { + source: 'intimatemerger.com', + uids: [ + { + id: 'h.c2bef39c502aef97', + atype: 1 + } + ] + }, + { + source: 'ppid.intimatemerger.com', + uids: [ + { + id: 'f11490d3c7903e7455ac4af887280a3f', + atype: 1 + } + ] + }, + ] + } + }, + device: { + sua: suaSample + }, + source: { + ext: { + schain: schainSmaple + } + }, + } + const request = spec.buildRequests(bidRequests, {...bidderRequest, ortb2: idparams})[4]; + expect(request.data.ortb.user).to.deep.equal(idparams.user); + + // gpid + expect(request.data.ortb.imp[0].ext.gpid).to.equal('/1111/homepage#300x250'); + // sua + expect(request.data.ortb.device.sua).to.deep.equal(suaSample); + // schain + expect(request.data.ortb.source.ext.schain).to.deep.equal(schainSmaple); }); it('allows setConfig to set bidder currency for JPY', function () { - config.setConfig({ - currency: { - adServerCurrency: 'JPY' - } + setCurrencyConfig({ adServerCurrency: 'JPY' }); + return addFPDToBidderRequest(bidderRequest).then(res => { + const bidRequest = spec.buildRequests(bidRequests, res)[0]; + expect(bidRequest.data.currency).to.equal('JPY'); + setCurrencyConfig({}); }); - const request = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(request.data).to.equal(data.banner); - config.resetConfig(); }); + it('allows setConfig to set bidder currency for USD', function () { - config.setConfig({ - currency: { - adServerCurrency: 'USD' - } + setCurrencyConfig({ adServerCurrency: 'USD' }); + return addFPDToBidderRequest(bidderRequest).then(res => { + const bidRequest = spec.buildRequests(bidRequests, res)[0]; + expect(bidRequest.data.currency).to.equal('USD'); + setCurrencyConfig({}); }); - const request = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(request.data).to.equal(data.bannerUSD); - config.resetConfig(); }); }); + describe('interpretResponse', function () { const bidRequests = { banner: { - bidRequest: { - bidder: 'adg', - params: { - id: '58278', // banner + bidderRequest: { + ortb2: {ext: {prebid: {adServerCurrency: 'JPY'}}} + }, + method: 'POST', + url: 'https://api-test.scaleout.jp/adgen/prebid?id=15415&posall=SSPLOC&sdktype=0', + data: { + currency: 'JPY', + pbver: prebid.version, + sdkname: 'prebidjs', + adapterver: ADGENE_PREBID_VERSION, + ortb: { + imp: [ + { + ext: { + gpid: '/1111/homepage-leftnav', + data: { + pbadslot: '/1111/homepage-leftnav' + }, + params: { + id: '15415', + debug: true + }, + mediaTypes: { + banner: { + sizes: [ + [ + 1, + 1 + ], + [ + 320, + 180 + ], + [ + 320, + 100 + ], + [ + 320, + 50 + ], + [ + 300, + 250 + ], + [ + 970, + 250 + ] + ] + } + } + }, + id: '2f6ac468a9c15e', + banner: { + topframe: 1, + format: [ + { + 'w': 1, + 'h': 1 + }, + { + 'w': 320, + 'h': 180 + }, + { + 'w': 320, + 'h': 100 + }, + { + 'w': 320, + 'h': 50 + }, + { + 'w': 300, + 'h': 250 + }, + { + 'w': 970, + 'h': 250 + } + ] + } + } + ], + source: {}, + '': { + ext: { + 'data': { + 'CxSegments': [ + 'xxxxxxx', + ] + }, + eids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'xxxxxxxx', + 'atype': 1 + } + ] + }, + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1, + 'ext': { + 'linkType': 1, + 'pba': 'xxxxxx', + 'abTestingControlGroup': false + } + } + ] + }, + { + 'source': 'intimatemerger.com', + 'uids': [ + { + 'id': 'xxxx', + 'atype': 1 + } + ] + }, + { + 'source': 'ppid.intimatemerger.com', + 'uids': [ + { + 'id': 'xxxxxxx', + 'atype': 1 + } + ] + } + ] + } + }, + 'site': { + 'domain': 'example.com', + 'publisher': { + 'domain': 'example.com' + }, + 'page': 'https://example.com/post/html/fi/test2.html?pbjs_debug=true' + }, + 'device': { + 'w': 1792, + 'h': 1120, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'language': 'ja', + 'ext': { + 'vpw': 616, + 'vph': 974 + }, + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Google Chrome', + 'version': [ + '129' + ] + }, + { + 'brand': 'Not=A?Brand', + 'version': [ + '8' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '129' + ] + } + ], + 'mobile': 0 + } + }, + 'id': 'f149e3b5-46af-414c-a93a-4bbca5503112', + 'test': 0, + 'tmax': 3000 }, - adUnitCode: 'adunit-code', - sizes: [[320, 100]], - bidId: '2f6ac468a9c15e', - bidderRequestId: '14a9f773e30243', - auctionId: '4aae9f05-18c6-4fcd-80cf-282708cd584a', - transactionTd: 'f76f6dfd-d64f-4645-a29f-682bac7f431a' + 'imark': 1 }, + 'options': { + 'withCredentials': true, + 'crossOrigin': true + } }, native: { - bidRequest: { - bidder: 'adg', - params: { - id: '58278', // banner - }, - mediaTypes: { - native: { - image: { - required: true - }, - title: { - required: true, - len: 80 - }, - sponsoredBy: { - required: true - }, - clickUrl: { - required: true + method: 'POST', + url: 'https://api-test.scaleout.jp/adgen/prebid?id=10697&posall=SSPLOC&sdktype=0', + data: { + 'currency': 'JPY', + 'pbver': prebid.version, + 'sdkname': 'prebidjs', + 'adapterver': ADGENE_PREBID_VERSION, + 'ortb': { + 'imp': [ + { + 'ext': { + 'gpid': '/1111/homepage-leftnav', + 'data': { + 'pbadslot': '/1111/homepage-leftnav' + }, + 'params': { + 'id': '10697', + 'debug': true + }, + 'mediaTypes': { + 'native': { + 'image': { + 'required': true + }, + 'title': { + 'required': true, + 'len': 80 + }, + 'sponsoredBy': { + 'required': true + }, + 'clickUrl': { + 'required': true + }, + 'body': { + 'required': true + }, + 'icon': { + 'required': true + }, + 'privacyLink': { + 'required': true, + 'sendId': false + } + } + }, + 'novatiqSyncResponse': 2 + }, + 'id': '2f6ac468a9c15e', + 'native': { + 'request': '{\'ver\':\'1.2\',\'assets\':[{\'id\':0,\'required\':1,\'img\':{\'type\':3}},{\'id\':1,\'required\':1,\'title\':{\'len\':80}},{\'id\':2,\'required\':1,\'data\':{\'type\':1}},{\'id\':3,\'required\':1,\'data\':{\'type\':2}},{\'id\':4,\'required\':1,\'img\':{\'type\':1}}],\'privacy\':1}', + 'ver': '1.2' + } + } + ], + 'source': {}, + 'user': { + 'ext': { + 'data': { + 'CxSegments': [ + 'xxxxxxxx', + 'xxxxxxxy', + ] + }, + 'eids': [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1 + } + ] + }, + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1, + 'ext': { + 'linkType': 1, + 'pba': 'xxxxxx', + 'abTestingControlGroup': false + } + } + ] + }, + { + 'source': 'intimatemerger.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1 + } + ] + }, + { + 'source': 'ppid.intimatemerger.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1 + } + ] + } + ] + } + }, + 'site': { + 'domain': 'example.com', + 'publisher': { + 'domain': 'example.com' }, - body: { - required: true + 'page': 'https://example.com/post/html/test3.html?pbjs_debug=true' + }, + 'device': { + 'w': 1792, + 'h': 1120, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'language': 'ja', + 'ext': { + 'vpw': 616, + 'vph': 974 }, - icon: { - required: true + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Google Chrome', + 'version': [ + '129' + ] + }, + { + 'brand': 'Not=A?Brand', + 'version': [ + '8' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '129' + ] + } + ], + 'mobile': 0 } - } - }, - adUnitCode: 'adunit-code', - sizes: [[1, 1]], - bidId: '2f6ac468a9c15e', - bidderRequestId: '14a9f773e30243', - auctionId: '4aae9f05-18c6-4fcd-80cf-282708cd584a', - transactionTd: 'f76f6dfd-d64f-4645-a29f-682bac7f431a' + }, + 'id': '8fa21cb7-d874-41e9-a735-9edf560b306c', + 'test': 0, + 'tmax': 20000 + } }, + options: { + withCredentials: true, + crossOrigin: true + } }, upperBillboard: { - bidRequest: { - bidder: 'adg', - params: { - id: '143038', // banner - marginTop: '50', + method: 'POST', + url: 'https://api-test.scaleout.jp/adgen/prebid?id=15410&posall=SSPLOC&sdktype=0', + data: { + 'currency': 'JPY', + 'pbver': prebid.version, + 'sdkname': 'prebidjs', + 'adapterver': ADGENE_PREBID_VERSION, + 'ortb': { + 'imp': [ + { + 'ext': { + 'params': { + 'id': '15410', + 'debug': true, + 'marginTop': '50' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 1, + 1 + ], + [ + 320, + 180 + ], + [ + 320, + 100 + ], + [ + 320, + 50 + ], + [ + 300, + 250 + ], + [ + 970, + 250 + ] + ] + } + }, + 'novatiqSyncResponse': 2 + }, + 'id': '2f6ac468a9c15e', + 'banner': { + 'topframe': 1, + 'format': [ + { + 'w': 1, + 'h': 1 + }, + { + 'w': 320, + 'h': 180 + }, + { + 'w': 320, + 'h': 100 + }, + { + 'w': 320, + 'h': 50 + }, + { + 'w': 300, + 'h': 250 + }, + { + 'w': 970, + 'h': 250 + } + ] + } + } + ], + 'source': {}, + 'user': { + 'ext': { + 'data': { + 'CxSegments': [ + 'xxxxxxxx', + 'xxxxxxxy', + ] + }, + 'eids': [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1 + } + ] + }, + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1, + 'ext': { + 'linkType': 1, + 'pba': 'xxxxxx', + 'abTestingControlGroup': false + } + } + ] + }, + { + 'source': 'intimatemerger.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1 + } + ] + }, + { + 'source': 'ppid.intimatemerger.com', + 'uids': [ + { + 'id': 'xxxxxx', + 'atype': 1 + } + ] + } + ] + } + }, + 'site': { + 'domain': 'example.com', + 'publisher': { + 'domain': 'example.com' + }, + 'page': 'https://example.com/post/html/fi/test1.html?pbjs_debug=true' + }, + 'device': { + 'w': 1792, + 'h': 1120, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + 'language': 'ja', + 'ext': { + 'vpw': 616, + 'vph': 974 + }, + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Google Chrome', + 'version': [ + '129' + ] + }, + { + 'brand': 'Not=A?Brand', + 'version': [ + '8' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '129' + ] + } + ], + 'mobile': 0 + } + }, + 'id': '24cb6de9-68ee-49df-8a90-27259215e059', + 'test': 0, + 'tmax': 3000 }, - adUnitCode: 'adunit-code', - sizes: [[320, 180]], - bidId: '2f6ac468a9c15e', - bidderRequestId: '14a9f773e30243', - auctionId: '4aae9f05-18c6-4fcd-80cf-282708cd584a', - transactionTd: 'f76f6dfd-d64f-4645-a29f-682bac7f431a' + 'imark': 1 }, - }, + } }; const serverResponse = { @@ -334,133 +982,127 @@ describe('AdgenerationAdapter', function () { }, normal: { banner: { - ad: '
', - beacon: '', - cpm: 36.0008, displaytype: '1', - ids: {}, - w: 320, - h: 100, location_params: null, - locationid: '58279', - rotation: '0', - scheduleid: '512603', - sdktype: '0', - creativeid: '1k2kv35vsa5r', - dealid: 'fd5sa5fa7f', - ttl: 1000, results: [ - {ad: '<\!DOCTYPE html>
'}, + { + ad: '
', + beacon: '', + cpm: 36.0008, + ids: {}, + w: 320, + h: 100, + locationid: '58279', + rotation: '0', + scheduleid: '512603', + sdktype: '0', + creativeid: '1k2kv35vsa5r', + dealid: 'fd5sa5fa7f', + ttl: 1000, + adomain: ['advertiserdomain.com'] + }, ], - adomain: ['advertiserdomain.com'] }, native: { - ad: '<\!DOCTYPE html>↵ ↵ ↵ ↵ ↵
↵ ', - beacon: '', - cpm: 36.0008, displaytype: '1', - ids: {}, location_params: null, locationid: '58279', - adomain: ['advertiserdomain.com'], - native_ad: { - assets: [ - { - data: { - label: 'accompanying_text', - value: 'AD' - }, - id: 501 - }, - { - data: { - label: 'optout_url', - value: 'https://supership.jp/optout/#' - }, - id: 502 - }, - { - data: { - ext: { - black_back: 'https://i.socdm.com/sdk/img/icon_adg_optout_26x26_white.png', + results: [ + { + ad: '
', + beacon: '', + cpm: 36.0008, + ids: {}, + adomain: ['advertiserdomain.com'], + scheduleid: '512603', + creativeid: '1k2kv35vsa5r', + dealid: 'fd5sa5fa7f', + ttl: 1000, + native: { + assets: [ + { + data: { + label: 'accompanying_text', + value: 'AD' + }, + id: 501 }, - label: 'information_icon_url', - value: 'https://i.socdm.com/sdk/img/icon_adg_optout_26x26_gray.png', - id: 503 - } - }, - { - id: 1, - required: 1, - title: {text: 'Title'} - }, - { - id: 2, - img: { - h: 250, - url: 'https://sdk-temp.s3-ap-northeast-1.amazonaws.com/adg-sample-ad/img/300x250.png', - w: 300 - }, - required: 1 - }, - { - id: 3, - img: { - h: 300, - url: 'https://placehold.jp/300x300.png', - w: 300 + { + data: { + label: 'optout_url', + value: 'https://example.com/optout/#' + }, + id: 502 + }, + { + data: { + ext: { + black_back: 'https://example.com/icon_adg_optout_26x26_white.png', + }, + label: 'information_icon_url', + value: 'https://example.com/icon_adg_optout_26x26_gray.png', + id: 503 + } + }, + { + id: 1, + required: 1, + title: {text: 'Title'} + }, + { + id: 2, + img: { + h: 250, + url: 'https://example.com/adg-sample-ad/img/300x250.png', + w: 300 + }, + required: 1 + }, + { + id: 3, + img: { + h: 300, + url: 'https://example.com/300x300.png', + w: 300 + }, + required: 1 + }, + { + data: {value: 'Description'}, + id: 5, + required: 0 + }, + { + data: {value: 'CTA'}, + id: 6, + required: 0 + }, + { + data: {value: 'Sponsored'}, + id: 4, + required: 0 + } + ], + imptrackers: ['https://example.com/1x1.gif'], + link: { + clicktrackers: [ + 'https://example.com/1x1_clicktracker_access.gif' + ], + url: 'https://example.com' }, - required: 1 - }, - { - data: {value: 'Description'}, - id: 5, - required: 0 }, - { - data: {value: 'CTA'}, - id: 6, - required: 0 - }, - { - data: {value: 'Sponsored'}, - id: 4, - required: 0 - } - ], - imptrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1.gif'], - link: { - clicktrackers: [ - 'https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1_clicktracker_access.gif' - ], - url: 'https://supership.jp' - }, - }, - results: [ - {ad: 'Creative<\/body>'} + } ], rotation: '0', - scheduleid: '512603', - sdktype: '0', - creativeid: '1k2kv35vsa5r', - dealid: 'fd5sa5fa7f', - ttl: 1000 }, upperBillboard: { - 'ad': '<\!DOCTYPE html>\n \n \n \n \n \n \n \n
\n \n
\n \n', - 'beacon': '', - 'beaconurl': 'https://tg.socdm.com/bc/v3?b=Y2hzbT0yOTQsNGZiM2NkNWVpZD0xNDMwMzgmcG9zPVNTUExPQyZhZD0xMjMzMzIzLzI2MTA2MS4yNjU3OTkuMTIzMzMyMy8yMTY2NDY2LzE1NDQxMC8xNDMwMzg6U1NQTE9DOiovaWR4PTA7ZHNwaWQ9MTtkaTI9MjEzNC0xMzI4NjRfbmV3Zm9ybWF0X3Rlc3Q7ZnR5cGU9Mztwcj15TXB3O3ByYj15UTtwcm89eVE7cHJvYz1KUFk7Y3JkMnk9MTExLjkyO2NyeTJkPTAuMDA4OTM0OTUzNTM4MjQxNjAxMztwcnY9aWp6QVZtWW9wbmJUV1B0cWhtZEN1ZWRXNDd0MjU1MEtmYjFWYmI3SzsmZXg9MTYzMzMyNzU4MyZjdD0xNjMzMzI3NTgzODAzJnNyPWh0dHA-&xuid=Xm8Q8cCo5r8AAHCCMg0AAAAA&ctsv=m-ad240&seqid=be38bdb4-74a7-14a7-3439-b7f1ea8b4f33&seqtime=1633327583803&seqctx=gat3by5hZy5mdHlwZaEz&t=.gif', - 'cpm': 80, - 'creative_params': {}, - 'creativeid': 'ScaleOut_2146187', - 'dealid': '2134-132864_newformat_test', 'displaytype': '1', 'h': 180, 'ids': { 'anid': '', 'diid': '', 'idfa': '', - 'soc': 'Xm8Q8cCo5r8AAHCCMg0AAAAA' + 'soc': 'yyyyyyyy' }, 'location_params': { 'option': { @@ -470,284 +1112,50 @@ describe('AdgenerationAdapter', function () { 'locationid': '143038', 'results': [ { - 'ad': '<\!DOCTYPE html>\n \n \n \n \n \n \n \n
\n \n
\n \n', - 'beacon': '', - 'beaconurl': 'https://tg.socdm.com/bc/v3?b=Y2hzbT0yOTQsNGZiM2NkNWVpZD0xNDMwMzgmcG9zPVNTUExPQyZhZD0xMjMzMzIzLzI2MTA2MS4yNjU3OTkuMTIzMzMyMy8yMTY2NDY2LzE1NDQxMC8xNDMwMzg6U1NQTE9DOiovaWR4PTA7ZHNwaWQ9MTtkaTI9MjEzNC0xMzI4NjRfbmV3Zm9ybWF0X3Rlc3Q7ZnR5cGU9Mztwcj15TXB3O3ByYj15UTtwcm89eVE7cHJvYz1KUFk7Y3JkMnk9MTExLjkyO2NyeTJkPTAuMDA4OTM0OTUzNTM4MjQxNjAxMztwcnY9aWp6QVZtWW9wbmJUV1B0cWhtZEN1ZWRXNDd0MjU1MEtmYjFWYmI3SzsmZXg9MTYzMzMyNzU4MyZjdD0xNjMzMzI3NTgzODAzJnNyPWh0dHA-&xuid=Xm8Q8cCo5r8AAHCCMg0AAAAA&ctsv=m-ad240&seqid=be38bdb4-74a7-14a7-3439-b7f1ea8b4f33&seqtime=1633327583803&seqctx=gat3by5hZy5mdHlwZaEz&t=.gif', + 'ad': '
', + 'beacon': '', + 'beaconurl': 'http://example.com', 'cpm': 80, 'creative_params': {}, 'creativeid': 'ScaleOut_2146187', 'dealid': '2134-132864_newformat_test', 'h': 180, - 'landing_url': 'https://supership.jp/', + 'landing_url': 'https://example.com/', 'rparams': {}, 'scheduleid': '1233323', 'trackers': { 'imp': [ - 'https://tg.socdm.com/bc/v3?b=Y2hzbT0yOTQsNGZiM2NkNWVpZD0xNDMwMzgmcG9zPVNTUExPQyZhZD0xMjMzMzIzLzI2MTA2MS4yNjU3OTkuMTIzMzMyMy8yMTY2NDY2LzE1NDQxMC8xNDMwMzg6U1NQTE9DOiovaWR4PTA7ZHNwaWQ9MTtkaTI9MjEzNC0xMzI4NjRfbmV3Zm9ybWF0X3Rlc3Q7ZnR5cGU9Mztwcj15TXB3O3ByYj15UTtwcm89eVE7cHJvYz1KUFk7Y3JkMnk9MTExLjkyO2NyeTJkPTAuMDA4OTM0OTUzNTM4MjQxNjAxMztwcnY9aWp6QVZtWW9wbmJUV1B0cWhtZEN1ZWRXNDd0MjU1MEtmYjFWYmI3SzsmZXg9MTYzMzMyNzU4MyZjdD0xNjMzMzI3NTgzODAzJnNyPWh0dHA-&xuid=Xm8Q8cCo5r8AAHCCMg0AAAAA&ctsv=m-ad240&seqid=be38bdb4-74a7-14a7-3439-b7f1ea8b4f33&seqtime=1633327583803&seqctx=gat3by5hZy5mdHlwZaEz&t=.gif' + 'https://example.com' ], 'viewable_imp': [ - 'https://tg.socdm.com/aux/inview?creative_id=2166466&ctsv=m-ad240&extra_field=idx%3D0%3Bdspid%3D1%3Bdi2%3D2134-132864_newformat_test%3Bftype%3D3%3Bprb%3D0%3Bpro%3D0%3Bproc%3DJPY%3Bcrd2y%3D111.92%3Bcry2d%3D0.0089349535382416013%3Bsspm%3D0%3Bsom%3D0.2%3Borgm%3D0%3Btechm%3D0%3Bssp_margin%3D0%3Bso_margin%3D0.2%3Borg_margin%3D0%3Btech_margin%3D0%3Bbs%3Dclassic%3B&family_id=1233323&id=143038&loglocation_id=154410&lookupname=143038%3ASSPLOC%3A*&pos=SSPLOC&schedule_id=261061.265799.1233323&seqid=be38bdb4-74a7-14a7-3439-b7f1ea8b4f33&seqtime=1633327583803&xuid=Xm8Q8cCo5r8AAHCCMg0AAAAA' + 'https://example.com' ], 'viewable_measured': [ - 'https://tg.socdm.com/aux/measured?creative_id=2166466&ctsv=m-ad240&extra_field=idx%3D0%3Bdspid%3D1%3Bdi2%3D2134-132864_newformat_test%3Bftype%3D3%3Bprb%3D0%3Bpro%3D0%3Bproc%3DJPY%3Bcrd2y%3D111.92%3Bcry2d%3D0.0089349535382416013%3Bsspm%3D0%3Bsom%3D0.2%3Borgm%3D0%3Btechm%3D0%3Bssp_margin%3D0%3Bso_margin%3D0.2%3Borg_margin%3D0%3Btech_margin%3D0%3Bbs%3Dclassic%3B&family_id=1233323&id=143038&loglocation_id=154410&lookupname=143038%3ASSPLOC%3A*&pos=SSPLOC&schedule_id=261061.265799.1233323&seqid=be38bdb4-74a7-14a7-3439-b7f1ea8b4f33&seqtime=1633327583803&xuid=Xm8Q8cCo5r8AAHCCMg0AAAAA' + 'https://example.com' ] }, 'ttl': 1000, - 'vastxml': '\n \n \n SOADS\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n https://i.socdm.com/a/2/2095/2091787/20210810043037-de3e74aec30f36.mp4\n https://i.socdm.com/a/2/2095/2091788/20210810043037-6dd368dc91d507.mp4\n https://i.socdm.com/a/2/2095/2091789/20210810043037-c8eb814ddd85c4.webm\n https://i.socdm.com/a/2/2095/2091790/20210810043037-0a7f74c40268ab.webm\n \n \n \n \n \n \n', + 'vastxml': '', 'vcpm': 0, 'w': 320, 'weight': 1 } ], 'rotation': '0', - 'scheduleid': '1233323', - 'sdktype': '0', - 'trackers': { - 'imp': [ - 'https://tg.socdm.com/bc/v3?b=Y2hzbT0yOTQsNGZiM2NkNWVpZD0xNDMwMzgmcG9zPVNTUExPQyZhZD0xMjMzMzIzLzI2MTA2MS4yNjU3OTkuMTIzMzMyMy8yMTY2NDY2LzE1NDQxMC8xNDMwMzg6U1NQTE9DOiovaWR4PTA7ZHNwaWQ9MTtkaTI9MjEzNC0xMzI4NjRfbmV3Zm9ybWF0X3Rlc3Q7ZnR5cGU9Mztwcj15TXB3O3ByYj15UTtwcm89eVE7cHJvYz1KUFk7Y3JkMnk9MTExLjkyO2NyeTJkPTAuMDA4OTM0OTUzNTM4MjQxNjAxMztwcnY9aWp6QVZtWW9wbmJUV1B0cWhtZEN1ZWRXNDd0MjU1MEtmYjFWYmI3SzsmZXg9MTYzMzMyNzU4MyZjdD0xNjMzMzI3NTgzODAzJnNyPWh0dHA-&xuid=Xm8Q8cCo5r8AAHCCMg0AAAAA&ctsv=m-ad240&seqid=be38bdb4-74a7-14a7-3439-b7f1ea8b4f33&seqtime=1633327583803&seqctx=gat3by5hZy5mdHlwZaEz&t=.gif' - ], - 'viewable_imp': [ - 'https://tg.socdm.com/aux/inview?creative_id=2166466&ctsv=m-ad240&extra_field=idx%3D0%3Bdspid%3D1%3Bdi2%3D2134-132864_newformat_test%3Bftype%3D3%3Bprb%3D0%3Bpro%3D0%3Bproc%3DJPY%3Bcrd2y%3D111.92%3Bcry2d%3D0.0089349535382416013%3Bsspm%3D0%3Bsom%3D0.2%3Borgm%3D0%3Btechm%3D0%3Bssp_margin%3D0%3Bso_margin%3D0.2%3Borg_margin%3D0%3Btech_margin%3D0%3Bbs%3Dclassic%3B&family_id=1233323&id=143038&loglocation_id=154410&lookupname=143038%3ASSPLOC%3A*&pos=SSPLOC&schedule_id=261061.265799.1233323&seqid=be38bdb4-74a7-14a7-3439-b7f1ea8b4f33&seqtime=1633327583803&xuid=Xm8Q8cCo5r8AAHCCMg0AAAAA' - ], - 'viewable_measured': [ - 'https://tg.socdm.com/aux/measured?creative_id=2166466&ctsv=m-ad240&extra_field=idx%3D0%3Bdspid%3D1%3Bdi2%3D2134-132864_newformat_test%3Bftype%3D3%3Bprb%3D0%3Bpro%3D0%3Bproc%3DJPY%3Bcrd2y%3D111.92%3Bcry2d%3D0.0089349535382416013%3Bsspm%3D0%3Bsom%3D0.2%3Borgm%3D0%3Btechm%3D0%3Bssp_margin%3D0%3Bso_margin%3D0.2%3Borg_margin%3D0%3Btech_margin%3D0%3Bbs%3Dclassic%3B&family_id=1233323&id=143038&loglocation_id=154410&lookupname=143038%3ASSPLOC%3A*&pos=SSPLOC&schedule_id=261061.265799.1233323&seqid=be38bdb4-74a7-14a7-3439-b7f1ea8b4f33&seqtime=1633327583803&xuid=Xm8Q8cCo5r8AAHCCMg0AAAAA' - ] - }, - 'ttl': 1000, - 'vastxml': '\n \n \n SOADS\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n https://i.socdm.com/a/2/2095/2091787/20210810043037-de3e74aec30f36.mp4\n https://i.socdm.com/a/2/2095/2091788/20210810043037-6dd368dc91d507.mp4\n https://i.socdm.com/a/2/2095/2091789/20210810043037-c8eb814ddd85c4.webm\n https://i.socdm.com/a/2/2095/2091790/20210810043037-0a7f74c40268ab.webm\n \n \n \n \n \n \n', - 'vcpm': 0, - 'w': 320, } }, - emptyAdomain: { - banner: { - ad: '
', - beacon: '', - cpm: 36.0008, - displaytype: '1', - ids: {}, - w: 320, - h: 100, - location_params: null, - locationid: '58279', - rotation: '0', - scheduleid: '512603', - sdktype: '0', - creativeid: '1k2kv35vsa5r', - dealid: 'fd5sa5fa7f', - ttl: 1000, - results: [ - {ad: '<\!DOCTYPE html>
'}, - ], - adomain: [] - }, - native: { - ad: '<\!DOCTYPE html>↵ ↵ ↵ ↵ ↵
↵ ', - beacon: '', - cpm: 36.0008, - displaytype: '1', - ids: {}, - location_params: null, - locationid: '58279', - adomain: [], - native_ad: { - assets: [ - { - data: { - label: 'accompanying_text', - value: 'AD' - }, - id: 501 - }, - { - data: { - label: 'optout_url', - value: 'https://supership.jp/optout/#' - }, - id: 502 - }, - { - data: { - ext: { - black_back: 'https://i.socdm.com/sdk/img/icon_adg_optout_26x26_white.png', - }, - label: 'information_icon_url', - value: 'https://i.socdm.com/sdk/img/icon_adg_optout_26x26_gray.png', - id: 503 - } - }, - { - id: 1, - required: 1, - title: {text: 'Title'} - }, - { - id: 2, - img: { - h: 250, - url: 'https://sdk-temp.s3-ap-northeast-1.amazonaws.com/adg-sample-ad/img/300x250.png', - w: 300 - }, - required: 1 - }, - { - id: 3, - img: { - h: 300, - url: 'https://placehold.jp/300x300.png', - w: 300 - }, - required: 1 - }, - { - data: {value: 'Description'}, - id: 5, - required: 0 - }, - { - data: {value: 'CTA'}, - id: 6, - required: 0 - }, - { - data: {value: 'Sponsored'}, - id: 4, - required: 0 - } - ], - imptrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1.gif'], - link: { - clicktrackers: [ - 'https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1_clicktracker_access.gif' - ], - url: 'https://supership.jp' - }, - }, - results: [ - {ad: 'Creative<\/body>'} - ], - rotation: '0', - scheduleid: '512603', - sdktype: '0', - creativeid: '1k2kv35vsa5r', - dealid: 'fd5sa5fa7f', - ttl: 1000 - } - }, - noAdomain: { - banner: { - ad: '
', - beacon: '', - cpm: 36.0008, - displaytype: '1', - ids: {}, - w: 320, - h: 100, - location_params: null, - locationid: '58279', - rotation: '0', - scheduleid: '512603', - sdktype: '0', - creativeid: '1k2kv35vsa5r', - dealid: 'fd5sa5fa7f', - ttl: 1000, - results: [ - {ad: '<\!DOCTYPE html>
'}, - ], - }, - native: { - ad: '<\!DOCTYPE html>↵ ↵ ↵ ↵ ↵
↵ ', - beacon: '', - cpm: 36.0008, - displaytype: '1', - ids: {}, - location_params: null, - locationid: '58279', - native_ad: { - assets: [ - { - data: { - label: 'accompanying_text', - value: 'AD' - }, - id: 501 - }, - { - data: { - label: 'optout_url', - value: 'https://supership.jp/optout/#' - }, - id: 502 - }, - { - data: { - ext: { - black_back: 'https://i.socdm.com/sdk/img/icon_adg_optout_26x26_white.png', - }, - label: 'information_icon_url', - value: 'https://i.socdm.com/sdk/img/icon_adg_optout_26x26_gray.png', - id: 503 - } - }, - { - id: 1, - required: 1, - title: {text: 'Title'} - }, - { - id: 2, - img: { - h: 250, - url: 'https://sdk-temp.s3-ap-northeast-1.amazonaws.com/adg-sample-ad/img/300x250.png', - w: 300 - }, - required: 1 - }, - { - id: 3, - img: { - h: 300, - url: 'https://placehold.jp/300x300.png', - w: 300 - }, - required: 1 - }, - { - data: {value: 'Description'}, - id: 5, - required: 0 - }, - { - data: {value: 'CTA'}, - id: 6, - required: 0 - }, - { - data: {value: 'Sponsored'}, - id: 4, - required: 0 - } - ], - imptrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1.gif'], - link: { - clicktrackers: [ - 'https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1_clicktracker_access.gif' - ], - url: 'https://supership.jp' - }, - }, - results: [ - {ad: 'Creative<\/body>'} - ], - rotation: '0', - scheduleid: '512603', - sdktype: '0', - creativeid: '1k2kv35vsa5r', - dealid: 'fd5sa5fa7f', - ttl: 1000 - } - } - }; + } + serverResponse.emptyAdomain = {}; + serverResponse.emptyAdomain.banner = JSON.parse(JSON.stringify(serverResponse.normal.banner)); + serverResponse.emptyAdomain.banner.results[0].adomain = []; + serverResponse.emptyAdomain.native = JSON.parse(JSON.stringify(serverResponse.normal.native)); + serverResponse.emptyAdomain.native.results[0].adomain = []; + + serverResponse.noAdomain = {}; + serverResponse.noAdomain.banner = JSON.parse(JSON.stringify(serverResponse.normal.banner)); + delete serverResponse.noAdomain.banner.results[0].adomain; + serverResponse.noAdomain.native = JSON.parse(JSON.stringify(serverResponse.normal.native)); + delete serverResponse.noAdomain.native.results[0].adomain; const bidResponses = { normal: { @@ -761,7 +1169,7 @@ describe('AdgenerationAdapter', function () { currency: 'JPY', netRevenue: true, ttl: 1000, - ad: '
', + ad: '
', adomain: ['advertiserdomain.com'] }, native: { @@ -775,26 +1183,26 @@ describe('AdgenerationAdapter', function () { netRevenue: true, ttl: 1000, adomain: ['advertiserdomain.com'], - ad: '↵
', + ad: '
', native: { title: 'Title', image: { - url: 'https://sdk-temp.s3-ap-northeast-1.amazonaws.com/adg-sample-ad/img/300x250.png', + url: 'https://example.com/adg-sample-ad/img/300x250.png', height: 250, width: 300 }, icon: { - url: 'https://placehold.jp/300x300.png', + url: 'https://example.com/300x300.png', height: 300, width: 300 }, sponsoredBy: 'Sponsored', body: 'Description', cta: 'CTA', - privacyLink: 'https://supership.jp/optout/#', - clickUrl: 'https://supership.jp', - clickTrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1_clicktracker_access.gif'], - impressionTrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1.gif'] + privacyLink: 'https://example.com/optout/#', + clickUrl: 'https://example.com', + clickTrackers: ['https://example.com/1x1_clicktracker_access.gif'], + impressionTrackers: ['https://example.com/1x1.gif'] }, mediaType: NATIVE }, @@ -805,109 +1213,13 @@ describe('AdgenerationAdapter', function () { height: 180, creativeId: 'ScaleOut_2146187', dealId: '2134-132864_newformat_test', - currency: 'JPY', + currency: 'USD', netRevenue: true, ttl: 1000, - ad: ``, + ad: ``, adomain: ['advertiserdomain.com'] }, - }, - emptyAdomain: { - banner: { - requestId: '2f6ac468a9c15e', - cpm: 36.0008, - width: 320, - height: 100, - creativeId: '1k2kv35vsa5r', - dealId: 'fd5sa5fa7f', - currency: 'JPY', - netRevenue: true, - ttl: 1000, - ad: '
', - adomain: [] - }, - native: { - requestId: '2f6ac468a9c15e', - cpm: 36.0008, - width: 1, - height: 1, - creativeId: '1k2kv35vsa5r', - dealId: 'fd5sa5fa7f', - currency: 'JPY', - netRevenue: true, - ttl: 1000, - adomain: [], - ad: '↵
', - native: { - title: 'Title', - image: { - url: 'https://sdk-temp.s3-ap-northeast-1.amazonaws.com/adg-sample-ad/img/300x250.png', - height: 250, - width: 300 - }, - icon: { - url: 'https://placehold.jp/300x300.png', - height: 300, - width: 300 - }, - sponsoredBy: 'Sponsored', - body: 'Description', - cta: 'CTA', - privacyLink: 'https://supership.jp/optout/#', - clickUrl: 'https://supership.jp', - clickTrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1_clicktracker_access.gif'], - impressionTrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1.gif'] - }, - mediaType: NATIVE - }, - }, - noAdomain: { - banner: { - requestId: '2f6ac468a9c15e', - cpm: 36.0008, - width: 320, - height: 100, - creativeId: '1k2kv35vsa5r', - dealId: 'fd5sa5fa7f', - currency: 'JPY', - netRevenue: true, - ttl: 1000, - ad: '
', - }, - native: { - requestId: '2f6ac468a9c15e', - cpm: 36.0008, - width: 1, - height: 1, - creativeId: '1k2kv35vsa5r', - dealId: 'fd5sa5fa7f', - currency: 'JPY', - netRevenue: true, - ttl: 1000, - ad: '↵
', - native: { - title: 'Title', - image: { - url: 'https://sdk-temp.s3-ap-northeast-1.amazonaws.com/adg-sample-ad/img/300x250.png', - height: 250, - width: 300 - }, - icon: { - url: 'https://placehold.jp/300x300.png', - height: 300, - width: 300 - }, - sponsoredBy: 'Sponsored', - body: 'Description', - cta: 'CTA', - privacyLink: 'https://supership.jp/optout/#', - clickUrl: 'https://supership.jp', - clickTrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1_clicktracker_access.gif'], - impressionTrackers: ['https://adg-dummy-dsp.s3-ap-northeast-1.amazonaws.com/1x1.gif'] - }, - mediaType: NATIVE - } - }, + } }; it('no bid responses', function () { @@ -916,107 +1228,115 @@ describe('AdgenerationAdapter', function () { }); it('handles ADGBrowserM responses', function () { - config.setConfig({ - currency: { - adServerCurrency: 'JPY' + setCurrencyConfig({ adServerCurrency: 'USD' }); + const bidderRequest = { + refererInfo: { + page: 'https://example.com' } + }; + return addFPDToBidderRequest(bidderRequest).then(res => { + const result = spec.interpretResponse({body: serverResponse.normal.upperBillboard}, { ...bidRequests.upperBillboard, bidderRequest: res })[0]; + expect(result.requestId).to.equal(bidResponses.normal.upperBillboard.requestId); + expect(result.width).to.equal(bidResponses.normal.upperBillboard.width); + expect(result.height).to.equal(bidResponses.normal.upperBillboard.height); + expect(result.creativeId).to.equal(bidResponses.normal.upperBillboard.creativeId); + expect(result.dealId).to.equal(bidResponses.normal.upperBillboard.dealId); + expect(result.currency).to.equal(bidResponses.normal.upperBillboard.currency); + expect(result.netRevenue).to.equal(bidResponses.normal.upperBillboard.netRevenue); + expect(result.ttl).to.equal(bidResponses.normal.upperBillboard.ttl); + expect(result.ad).to.equal(bidResponses.normal.upperBillboard.ad); + setCurrencyConfig({}); }); - const result = spec.interpretResponse({body: serverResponse.normal.upperBillboard}, bidRequests.upperBillboard)[0]; - expect(result.requestId).to.equal(bidResponses.normal.upperBillboard.requestId); - expect(result.width).to.equal(bidResponses.normal.upperBillboard.width); - expect(result.height).to.equal(bidResponses.normal.upperBillboard.height); - expect(result.creativeId).to.equal(bidResponses.normal.upperBillboard.creativeId); - expect(result.dealId).to.equal(bidResponses.normal.upperBillboard.dealId); - expect(result.currency).to.equal(bidResponses.normal.upperBillboard.currency); - expect(result.netRevenue).to.equal(bidResponses.normal.upperBillboard.netRevenue); - expect(result.ttl).to.equal(bidResponses.normal.upperBillboard.ttl); - expect(result.ad).to.equal(bidResponses.normal.upperBillboard.ad); }); it('handles banner responses for empty adomain', function () { const result = spec.interpretResponse({body: serverResponse.emptyAdomain.banner}, bidRequests.banner)[0]; - expect(result.requestId).to.equal(bidResponses.emptyAdomain.banner.requestId); - expect(result.width).to.equal(bidResponses.emptyAdomain.banner.width); - expect(result.height).to.equal(bidResponses.emptyAdomain.banner.height); - expect(result.creativeId).to.equal(bidResponses.emptyAdomain.banner.creativeId); - expect(result.dealId).to.equal(bidResponses.emptyAdomain.banner.dealId); - expect(result.currency).to.equal(bidResponses.emptyAdomain.banner.currency); - expect(result.netRevenue).to.equal(bidResponses.emptyAdomain.banner.netRevenue); - expect(result.ttl).to.equal(bidResponses.emptyAdomain.banner.ttl); - expect(result.ad).to.equal(bidResponses.emptyAdomain.banner.ad); + expect(result.requestId).to.equal(bidResponses.normal.banner.requestId); + expect(result.width).to.equal(bidResponses.normal.banner.width); + expect(result.height).to.equal(bidResponses.normal.banner.height); + expect(result.creativeId).to.equal(bidResponses.normal.banner.creativeId); + expect(result.dealId).to.equal(bidResponses.normal.banner.dealId); + expect(result.currency).to.equal(bidResponses.normal.banner.currency); + expect(result.netRevenue).to.equal(bidResponses.normal.banner.netRevenue); + expect(result.ttl).to.equal(bidResponses.normal.banner.ttl); + expect(result.ad).to.equal(bidResponses.normal.banner.ad); + // no adomian expect(result).to.not.have.any.keys('meta'); expect(result).to.not.have.any.keys('advertiserDomains'); }); it('handles native responses for empty adomain', function () { const result = spec.interpretResponse({body: serverResponse.emptyAdomain.native}, bidRequests.native)[0]; - expect(result.requestId).to.equal(bidResponses.emptyAdomain.native.requestId); - expect(result.width).to.equal(bidResponses.emptyAdomain.native.width); - expect(result.height).to.equal(bidResponses.emptyAdomain.native.height); - expect(result.creativeId).to.equal(bidResponses.emptyAdomain.native.creativeId); - expect(result.dealId).to.equal(bidResponses.emptyAdomain.native.dealId); - expect(result.currency).to.equal(bidResponses.emptyAdomain.native.currency); - expect(result.netRevenue).to.equal(bidResponses.emptyAdomain.native.netRevenue); - expect(result.ttl).to.equal(bidResponses.emptyAdomain.native.ttl); - expect(result.native.title).to.equal(bidResponses.emptyAdomain.native.native.title); - expect(result.native.image.url).to.equal(bidResponses.emptyAdomain.native.native.image.url); - expect(result.native.image.height).to.equal(bidResponses.emptyAdomain.native.native.image.height); - expect(result.native.image.width).to.equal(bidResponses.emptyAdomain.native.native.image.width); - expect(result.native.icon.url).to.equal(bidResponses.emptyAdomain.native.native.icon.url); - expect(result.native.icon.width).to.equal(bidResponses.emptyAdomain.native.native.icon.width); - expect(result.native.icon.height).to.equal(bidResponses.emptyAdomain.native.native.icon.height); - expect(result.native.sponsoredBy).to.equal(bidResponses.emptyAdomain.native.native.sponsoredBy); - expect(result.native.body).to.equal(bidResponses.emptyAdomain.native.native.body); - expect(result.native.cta).to.equal(bidResponses.emptyAdomain.native.native.cta); - expect(decodeURIComponent(result.native.privacyLink)).to.equal(bidResponses.emptyAdomain.native.native.privacyLink); - expect(result.native.clickUrl).to.equal(bidResponses.emptyAdomain.native.native.clickUrl); - expect(result.native.impressionTrackers[0]).to.equal(bidResponses.emptyAdomain.native.native.impressionTrackers[0]); - expect(result.native.clickTrackers[0]).to.equal(bidResponses.emptyAdomain.native.native.clickTrackers[0]); - expect(result.mediaType).to.equal(bidResponses.emptyAdomain.native.mediaType); + expect(result.requestId).to.equal(bidResponses.normal.native.requestId); + expect(result.width).to.equal(bidResponses.normal.native.width); + expect(result.height).to.equal(bidResponses.normal.native.height); + expect(result.creativeId).to.equal(bidResponses.normal.native.creativeId); + expect(result.dealId).to.equal(bidResponses.normal.native.dealId); + expect(result.currency).to.equal(bidResponses.normal.native.currency); + expect(result.netRevenue).to.equal(bidResponses.normal.native.netRevenue); + expect(result.ttl).to.equal(bidResponses.normal.native.ttl); + expect(result.native.title).to.equal(bidResponses.normal.native.native.title); + expect(result.native.image.url).to.equal(bidResponses.normal.native.native.image.url); + expect(result.native.image.height).to.equal(bidResponses.normal.native.native.image.height); + expect(result.native.image.width).to.equal(bidResponses.normal.native.native.image.width); + expect(result.native.icon.url).to.equal(bidResponses.normal.native.native.icon.url); + expect(result.native.icon.width).to.equal(bidResponses.normal.native.native.icon.width); + expect(result.native.icon.height).to.equal(bidResponses.normal.native.native.icon.height); + expect(result.native.sponsoredBy).to.equal(bidResponses.normal.native.native.sponsoredBy); + expect(result.native.body).to.equal(bidResponses.normal.native.native.body); + expect(result.native.cta).to.equal(bidResponses.normal.native.native.cta); + expect(decodeURIComponent(result.native.privacyLink)).to.equal(bidResponses.normal.native.native.privacyLink); + expect(result.native.clickUrl).to.equal(bidResponses.normal.native.native.clickUrl); + expect(result.native.impressionTrackers[0]).to.equal(bidResponses.normal.native.native.impressionTrackers[0]); + expect(result.native.clickTrackers[0]).to.equal(bidResponses.normal.native.native.clickTrackers[0]); + expect(result.mediaType).to.equal(bidResponses.normal.native.mediaType); + // no adomain expect(result).to.not.have.any.keys('meta'); expect(result).to.not.have.any.keys('advertiserDomains'); }); it('handles banner responses for no adomain', function () { const result = spec.interpretResponse({body: serverResponse.noAdomain.banner}, bidRequests.banner)[0]; - expect(result.requestId).to.equal(bidResponses.noAdomain.banner.requestId); - expect(result.width).to.equal(bidResponses.noAdomain.banner.width); - expect(result.height).to.equal(bidResponses.noAdomain.banner.height); - expect(result.creativeId).to.equal(bidResponses.noAdomain.banner.creativeId); - expect(result.dealId).to.equal(bidResponses.noAdomain.banner.dealId); - expect(result.currency).to.equal(bidResponses.noAdomain.banner.currency); - expect(result.netRevenue).to.equal(bidResponses.noAdomain.banner.netRevenue); - expect(result.ttl).to.equal(bidResponses.noAdomain.banner.ttl); - expect(result.ad).to.equal(bidResponses.noAdomain.banner.ad); + expect(result.requestId).to.equal(bidResponses.normal.banner.requestId); + expect(result.width).to.equal(bidResponses.normal.banner.width); + expect(result.height).to.equal(bidResponses.normal.banner.height); + expect(result.creativeId).to.equal(bidResponses.normal.banner.creativeId); + expect(result.dealId).to.equal(bidResponses.normal.banner.dealId); + expect(result.currency).to.equal(bidResponses.normal.banner.currency); + expect(result.netRevenue).to.equal(bidResponses.normal.banner.netRevenue); + expect(result.ttl).to.equal(bidResponses.normal.banner.ttl); + expect(result.ad).to.equal(bidResponses.normal.banner.ad); + // no adomain expect(result).to.not.have.any.keys('meta'); expect(result).to.not.have.any.keys('advertiserDomains'); }); it('handles native responses for no adomain', function () { const result = spec.interpretResponse({body: serverResponse.noAdomain.native}, bidRequests.native)[0]; - expect(result.requestId).to.equal(bidResponses.noAdomain.native.requestId); - expect(result.width).to.equal(bidResponses.noAdomain.native.width); - expect(result.height).to.equal(bidResponses.noAdomain.native.height); - expect(result.creativeId).to.equal(bidResponses.noAdomain.native.creativeId); - expect(result.dealId).to.equal(bidResponses.noAdomain.native.dealId); - expect(result.currency).to.equal(bidResponses.noAdomain.native.currency); - expect(result.netRevenue).to.equal(bidResponses.noAdomain.native.netRevenue); - expect(result.ttl).to.equal(bidResponses.noAdomain.native.ttl); - expect(result.native.title).to.equal(bidResponses.noAdomain.native.native.title); - expect(result.native.image.url).to.equal(bidResponses.noAdomain.native.native.image.url); - expect(result.native.image.height).to.equal(bidResponses.noAdomain.native.native.image.height); - expect(result.native.image.width).to.equal(bidResponses.noAdomain.native.native.image.width); - expect(result.native.icon.url).to.equal(bidResponses.noAdomain.native.native.icon.url); - expect(result.native.icon.width).to.equal(bidResponses.noAdomain.native.native.icon.width); - expect(result.native.icon.height).to.equal(bidResponses.noAdomain.native.native.icon.height); - expect(result.native.sponsoredBy).to.equal(bidResponses.noAdomain.native.native.sponsoredBy); - expect(result.native.body).to.equal(bidResponses.noAdomain.native.native.body); - expect(result.native.cta).to.equal(bidResponses.noAdomain.native.native.cta); - expect(decodeURIComponent(result.native.privacyLink)).to.equal(bidResponses.noAdomain.native.native.privacyLink); - expect(result.native.clickUrl).to.equal(bidResponses.noAdomain.native.native.clickUrl); - expect(result.native.impressionTrackers[0]).to.equal(bidResponses.noAdomain.native.native.impressionTrackers[0]); - expect(result.native.clickTrackers[0]).to.equal(bidResponses.noAdomain.native.native.clickTrackers[0]); - expect(result.mediaType).to.equal(bidResponses.noAdomain.native.mediaType); + expect(result.requestId).to.equal(bidResponses.normal.native.requestId); + expect(result.width).to.equal(bidResponses.normal.native.width); + expect(result.height).to.equal(bidResponses.normal.native.height); + expect(result.creativeId).to.equal(bidResponses.normal.native.creativeId); + expect(result.dealId).to.equal(bidResponses.normal.native.dealId); + expect(result.currency).to.equal(bidResponses.normal.native.currency); + expect(result.netRevenue).to.equal(bidResponses.normal.native.netRevenue); + expect(result.ttl).to.equal(bidResponses.normal.native.ttl); + expect(result.native.title).to.equal(bidResponses.normal.native.native.title); + expect(result.native.image.url).to.equal(bidResponses.normal.native.native.image.url); + expect(result.native.image.height).to.equal(bidResponses.normal.native.native.image.height); + expect(result.native.image.width).to.equal(bidResponses.normal.native.native.image.width); + expect(result.native.icon.url).to.equal(bidResponses.normal.native.native.icon.url); + expect(result.native.icon.width).to.equal(bidResponses.normal.native.native.icon.width); + expect(result.native.icon.height).to.equal(bidResponses.normal.native.native.icon.height); + expect(result.native.sponsoredBy).to.equal(bidResponses.normal.native.native.sponsoredBy); + expect(result.native.body).to.equal(bidResponses.normal.native.native.body); + expect(result.native.cta).to.equal(bidResponses.normal.native.native.cta); + expect(decodeURIComponent(result.native.privacyLink)).to.equal(bidResponses.normal.native.native.privacyLink); + expect(result.native.clickUrl).to.equal(bidResponses.normal.native.native.clickUrl); + expect(result.native.impressionTrackers[0]).to.equal(bidResponses.normal.native.native.impressionTrackers[0]); + expect(result.native.clickTrackers[0]).to.equal(bidResponses.normal.native.native.clickTrackers[0]); + expect(result.mediaType).to.equal(bidResponses.normal.native.mediaType); + // no adomain expect(result).to.not.have.any.keys('meta'); expect(result).to.not.have.any.keys('advertiserDomains'); }); diff --git a/test/spec/modules/adgridBidAdapter_spec.js b/test/spec/modules/adgridBidAdapter_spec.js new file mode 100644 index 00000000000..fbc71fc7948 --- /dev/null +++ b/test/spec/modules/adgridBidAdapter_spec.js @@ -0,0 +1,173 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/adgridBidAdapter.js' + +const globalConfig = { + method: 'POST', + endPoint: 'https://api-prebid.adgrid.io/api/v1/auction' +}; + +const userConfig = { + gdprConsent: { + gdprApplies: true, + consentString: 'COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, + uspConsent: '123456' +}; + +describe('AdGrid Bid Adapter', function () { + const bannerRequest = [{ + bidId: 123456, + auctionId: 98765, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + params: { + domainId: 12345, + placement: 'leaderboard' + } + }]; + + const videoRequest = [{ + bidId: 123456, + auctionId: 98765, + mediaTypes: { + video: { + playerSize: [ + [640, 480] + ], + context: 'instream' + } + }, + params: { + domainId: 12345, + placement: 'video1' + } + }]; + + describe('isBidRequestValid', function () { + it('Should return true when domainId and placement exist inside params object', function () { + const isBidValid = spec.isBidRequestValid(bannerRequest[0]); + expect(isBidValid).to.be.true; + }); + + it('Should return false when domainId and placement are not exist inside params object', function () { + const isBidNotValid = spec.isBidRequestValid(null); + expect(isBidNotValid).to.be.false; + }); + }); + + describe('buildRequests', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const requestVideo = spec.buildRequests(videoRequest, videoRequest[0]); + const payload = request.data; + const apiURL = request.url; + const method = request.method; + + it('Test the request is not empty', function () { + expect(request).to.not.be.empty; + }); + + it('Test the request payload is not empty', function () { + expect(payload).to.not.be.empty; + }); + + it('Test the API End Point', function () { + expect(apiURL).to.equal(globalConfig.endPoint); + }); + + it('should send the correct method', function () { + expect(method).to.equal(globalConfig.method); + }); + + it('should send the correct requestId', function () { + expect(request.data.bids[0].requestId).to.equal(bannerRequest[0].bidId); + expect(requestVideo.data.bids[0].requestId).to.equal(videoRequest[0].bidId); + }); + + it('should send the correct sizes array', function () { + expect(request.data.bids[0].sizes).to.be.an('array'); + }); + + it('should send the correct media type', function () { + expect(request.data.bids[0].mediaType).to.equal('banner') + expect(requestVideo.data.bids[0].mediaType).to.equal('video') + }); + }); + + describe('interpretResponse', function () { + const responseObj = { + bids: [ + { + bidId: '4b99f3428651c1', + cpm: 7.7, + ad: '
Ad Content
', + creativeId: '9004', + currency: 'USD', + mediaType: 'banner', + width: 320, + height: 50, + domainId: '2002', + marketplaceId: '703', + devices: 'desktop' + } + ] + }; + + it('Test the interpretResponse function', function () { + const receivedBid = responseObj.bids[0]; + const response = {}; + response.body = responseObj; + + const bidRequest = {}; + bidRequest.currency = 'USD'; + + const bidResponse = spec.interpretResponse(response, bidRequest); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.requestId).to.equal(receivedBid.bidId); + expect(bid.ad).to.equal(receivedBid.ad); + expect(bid.cpm).to.equal(receivedBid.cpm); + expect(bid.mediaType).to.equal(receivedBid.mediaType); + expect(bid.creativeId).to.equal(receivedBid.creativeId); + expect(bid.width).to.equal(receivedBid.width); + expect(bid.height).to.equal(receivedBid.height); + expect(bid.currency).to.equal(receivedBid.currency); + }); + }); + + describe('getUserSyncs', function () { + const response = { body: { cookies: [] } }; + + it('Validate the user sync without cookie', function () { + var syncs = spec.getUserSyncs({}, [response], userConfig.gdprConsent, userConfig.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); + + it('Validate the user sync with cookie', function () { + response.body.ext = { + cookies: [{ 'type': 'image', 'url': 'https://cookie-sync.org/' }] + }; + var syncs = spec.getUserSyncs({}, [response], userConfig.gdprConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0]).to.have.property('type').and.to.equal('image'); + expect(syncs[0]).to.have.property('url').and.to.equal('https://cookie-sync.org/'); + }); + + it('Validate the user sync with no bid', function () { + var syncs = spec.getUserSyncs({}, null, userConfig.gdprConsent, userConfig.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); + + it('Validate the user sync with no bid body', function () { + var syncs = spec.getUserSyncs({}, [], userConfig.gdprConsent, userConfig.uspConsent); + expect(syncs).to.have.lengthOf(0); + var syncs = spec.getUserSyncs({}, [{}], userConfig.gdprConsent, userConfig.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); + }); +}); diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js index cc643d6d2ab..f3b63a2359b 100644 --- a/test/spec/modules/adhashBidAdapter_spec.js +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -178,105 +178,105 @@ describe('adhashBidAdapter', function () { }); it('should return empty array when there are bad words (full)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text badword badword example badword text' + ' word'.repeat(993); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (full cyrillic)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text дума дума example дума text' + ' текст'.repeat(993); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (partial)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text partialbadwordb badwordb example badwordbtext' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (partial, compound phrase)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text partialbad wordb bad wordb example bad wordbtext' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (starts)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text startsWith starts text startsAgain' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (starts cyrillic)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text стартТекст старт text стартТекст' + ' дума'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (ends)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text wordEnds ends text anotherends' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (ends cyrillic)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text ДругКрай край text ощеединкрай' + ' дума'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (combo)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'queen of england dies, the queen dies' + ' word'.repeat(993); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return empty array when there are bad words (regexp)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text xxxayyy zzxxxAyyyzz text xxxbyyy' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(0); }); it('should return non-empty array when there are not enough bad words (full)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text badword badword example text' + ' word'.repeat(994); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are not enough bad words (partial)', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text partialbadwordb example' + ' word'.repeat(996); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are no-bad word matches', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text partialbadword example text' + ' word'.repeat(995); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there are bad words and good words', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return 'example text badword badword example badword goodWord goodWord ' + ' word'.repeat(992); }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); }); it('should return non-empty array when there is a problem with the brand-safety', function () { - bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() { + bodyStub = sinon.stub(window.top.document.body, 'textContent').get(function() { return null; }); expect(spec.interpretResponse(serverResponse, request).length).to.equal(1); diff --git a/test/spec/modules/adipoloBidAdapter_spec.js b/test/spec/modules/adipoloBidAdapter_spec.js new file mode 100644 index 00000000000..6764d7d20d8 --- /dev/null +++ b/test/spec/modules/adipoloBidAdapter_spec.js @@ -0,0 +1,439 @@ +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec} from 'modules/adipoloBidAdapter.js'; +import {deepClone} from 'src/utils'; +import {getBidFloor} from '../../../libraries/xeUtils/bidderUtils.js'; + +const ENDPOINT = 'https://prebid.adipolo.live'; + +const defaultRequest = { + tmax: 0, + adUnitCode: 'test', + bidId: '1', + requestId: 'qwerty', + ortb2: { + source: { + tid: 'auctionId' + } + }, + ortb2Imp: { + ext: { + tid: 'tr1', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'adipolo', + params: { + pid: '40', + ext: {} + }, + bidRequestsCount: 1 +}; + +const defaultRequestVideo = deepClone(defaultRequest); +defaultRequestVideo.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } +}; + +const videoBidderRequest = { + bidderCode: 'adipolo', + bids: [{mediaTypes: {video: {}}, bidId: 'qwerty'}] +}; + +const displayBidderRequest = { + bidderCode: 'adipolo', + bids: [{bidId: 'qwerty'}] +}; + +describe('adipoloBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required pid param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.pid; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); + + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT + '/bid'); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); + + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('tmax').and.to.equal(defaultRequest.tmax); + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprConsent').and.to.deep.equal({}); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + pid: '40' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); + }); + + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); + }); + + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); + + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); + }); + + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }); + }); + + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); + + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); + + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); + + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); + }); + }); + + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; + + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); + + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['adipolo'] + }, + ext: { + pixels: [ + ['iframe', 'surl1'], + ['image', 'surl2'], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: displayBidderRequest}); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({advertiserDomains: ['adipolo']}); + }); + + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: displayBidderRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); + + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: videoBidderRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); + }); + + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); + }); + + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = {getFloor: 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when getFloor doesnt return an object', () => { + const bid = {getFloor: () => 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({floor: 'string', currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'EUR'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); + }); +}); diff --git a/test/spec/modules/adlooxAdServerVideo_spec.js b/test/spec/modules/adlooxAdServerVideo_spec.js index 58277bc830d..c941b9dc710 100644 --- a/test/spec/modules/adlooxAdServerVideo_spec.js +++ b/test/spec/modules/adlooxAdServerVideo_spec.js @@ -34,7 +34,6 @@ describe('Adloox Ad Server Video', function () { }; const analyticsOptions = { - js: 'https://j.adlooxtracking.com/ads/js/tfav_adl_%%clientid%%.js', client: 'adlooxtest', clientid: 127, platformid: 0, diff --git a/test/spec/modules/adlooxAnalyticsAdapter_spec.js b/test/spec/modules/adlooxAnalyticsAdapter_spec.js index 450dd83f86d..964eb6650e9 100644 --- a/test/spec/modules/adlooxAnalyticsAdapter_spec.js +++ b/test/spec/modules/adlooxAnalyticsAdapter_spec.js @@ -45,6 +45,11 @@ describe('Adloox Analytics Adapter', function () { adapter: analyticsAdapter }); describe('enableAnalytics', function () { + afterEach(function () { + analyticsAdapter.disableAnalytics(); + expect(analyticsAdapter.context).is.null; + }); + describe('invalid options', function () { it('should require options', function (done) { adapterManager.enableAnalytics({ @@ -68,6 +73,32 @@ describe('Adloox Analytics Adapter', function () { done(); }); + it('should accept subdomains of adlooxtracking.com for options.js', function (done) { + const analyticsOptionsLocal = utils.deepClone(analyticsOptions); + analyticsOptionsLocal.js = 'https://test.adlooxtracking.com/test.js'; + + adapterManager.enableAnalytics({ + provider: analyticsAdapterName, + options: analyticsOptionsLocal + }); + expect(analyticsAdapter.context).is.not.null; + + done(); + }); + + it('should reject non-subdomains of adlooxtracking.com for options.js', function (done) { + const analyticsOptionsLocal = utils.deepClone(analyticsOptions); + analyticsOptionsLocal.js = 'https://example.com/test.js'; + + adapterManager.enableAnalytics({ + provider: analyticsAdapterName, + options: analyticsOptionsLocal + }); + expect(analyticsAdapter.context).is.null; + + done(); + }); + it('should reject non-function options.toselector', function (done) { const analyticsOptionsLocal = utils.deepClone(analyticsOptions); analyticsOptionsLocal.toselector = esplode; @@ -169,7 +200,7 @@ describe('Adloox Analytics Adapter', function () { events.emit(EVENTS.BID_WON, bid); - const [urlInserted, moduleCode] = loadExternalScriptStub.getCall(0).args; + const [urlInserted, _, moduleCode] = loadExternalScriptStub.getCall(0).args; expect(urlInserted.substr(0, url.length)).to.equal(url); expect(moduleCode).to.equal(analyticsAdapterName); diff --git a/test/spec/modules/admanBidAdapter_spec.js b/test/spec/modules/admanBidAdapter_spec.js index a9413860072..abae74b9749 100644 --- a/test/spec/modules/admanBidAdapter_spec.js +++ b/test/spec/modules/admanBidAdapter_spec.js @@ -1,343 +1,471 @@ -import {expect} from 'chai'; -import {spec} from '../../../modules/admanBidAdapter.js'; -import {deepClone} from '../../../src/utils' +import { expect } from 'chai'; +import { spec } from '../../../modules/admanBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; -describe('AdmanAdapter', function () { - let bidBanner = { - bidId: '2dd581a2b6281d', - bidder: 'adman', - bidderRequestId: '145e1d6a7837c9', - params: { - placementId: 0 - }, - placementCode: 'placementid_0', - auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', - mediaTypes: { - banner: { - sizes: [[300, 250]] +const bidder = 'adman'; + +describe('AdmanBidAdapter', 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 }, - transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', - schain: { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'example.com', - sid: '0', - hp: 1, - rid: 'bidrequestid', - // name: 'alladsallthetime', - domain: 'example.com' + { + 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 } - }; + ]; - let bidVideo = deepClone({ - ...bidBanner, - params: { - placementId: 0, - traffic: 'video' - }, + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, mediaTypes: { - video: { - playerSize: [300, 250] + [BANNER]: { + sizes: [[300, 250]] } + }, + params: { + } - }); + } - let bidderRequest = { - bidderCode: 'adman', - auctionId: 'fffffff-ffff-ffff-ffff-ffffffffffff', - bidderRequestId: 'ffffffffffffff', - start: 1472239426002, - auctionStart: 1472239426000, - timeout: 5000, - uspConsent: '1YN-', - gdprConsent: 'gdprConsent', + const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'http://www.example.com', - reachedTop: true, + referer: 'https://test.com', + page: 'https://test.com' }, - bids: [bidBanner, bidVideo] - } + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } + }, + timeout: 500 + }; describe('isBidRequestValid', function () { - it('Should return true when placementId can be cast to a number', function () { - expect(spec.isBidRequestValid(bidBanner)).to.be.true; + 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 when placementId is not a number', function () { - bidBanner.params.placementId = 'aaa'; - expect(spec.isBidRequestValid(bidBanner)).to.be.false; - bidBanner.params.placementId = 0; + 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([bidBanner], bidderRequest); + 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 valid URL', function () { expect(serverRequest.url).to.equal('https://pub.admanmedia.com/?c=o&m=multi'); }); - it('Should contain ccpa', function() { - expect(serverRequest.data.ccpa).to.be.an('string') - }) - it('Returns valid BANNER data if array of bids is valid', function () { - serverRequest = spec.buildRequests([bidBanner], bidderRequest); + it('Returns general data valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'ccpa', 'gdpr'); + 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'); - let placements = data['placements']; - for (let i = 0; i < placements.length; i++) { - let placement = placements[i]; - expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain', 'bidFloor', 'ext'); - expect(placement.schain).to.be.an('object') - expect(placement.ext).to.be.an('object') - expect(placement.ext).to.have.key('tid') - expect(placement.ext.tid).to.equal(bidBanner.transactionId); - expect(placement.placementId).to.be.a('number'); + 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.traffic).to.be.oneOf([BANNER, VIDEO, NATIVE]); expect(placement.bidId).to.be.a('string'); - expect(placement.traffic).to.be.a('string'); - expect(placement.sizes).to.be.an('array'); - expect(placement.bidFloor).to.be.an('number'); + 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.traffic === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.traffic) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.wPlayer).to.be.an('number'); + expect(placement.hPlayer).to.be.an('number'); + 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 VIDEO data if array of bids is valid', function () { - serverRequest = spec.buildRequests([bidVideo], bidderRequest); + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'ccpa', 'gdpr'); - 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'); - let placements = data['placements']; - for (let i = 0; i < placements.length; i++) { - let placement = placements[i]; - expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'schain', 'bidFloor', - 'playerSize', 'minduration', 'maxduration', 'mimes', 'protocols', 'startdelay', 'placement', 'skip', - 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity', 'ext'); - expect(placement.ext).to.be.an('object') - expect(placement.ext).to.have.key('tid') - expect(placement.ext.tid).to.equal(bidBanner.transactionId); - expect(placement.schain).to.be.an('object') - expect(placement.placementId).to.be.a('number'); - expect(placement.bidId).to.be.a('string'); - expect(placement.traffic).to.be.a('string'); - expect(placement.bidFloor).to.be.an('number'); - } + 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 empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; + 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('buildRequests with user ids', function () { - bidBanner.userId = {} - bidBanner.userId.uid2 = { id: 'uid2id123' }; - let serverRequest = spec.buildRequests([bidBanner], bidderRequest); - it('Returns valid data if array of bids is valid', function () { + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - let placements = data['placements']; expect(data).to.be.an('object'); - for (let i = 0; i < placements.length; i++) { - let placement = placements[i]; - expect(placement).to.have.property('eids') - expect(placement.eids).to.be.an('array') - expect(placement.eids.length).to.be.equal(1) - for (let index in placement.eids) { - let v = placement.eids[index]; - expect(v).to.have.all.keys('source', 'uids') - expect(v.source).to.be.oneOf(['uidapi.com']) - expect(v.uids).to.be.an('array'); - expect(v.uids.length).to.be.equal(1) - expect(v.uids[0]).to.have.property('id') - } - } - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { - it('(BANNER) Returns an array of valid server responses if response object is valid', function () { - const resBannerObject = { - body: [ { - requestId: '123', + it('Should interpret banner response', function () { + const banner = { + body: [{ mediaType: 'banner', - cpm: 0.3, - width: 320, - height: 50, - ad: '

Hello ad

', - ttl: 1000, - creativeId: '123asd', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', netRevenue: true, currency: 'USD', - adomain: ['example.com'], + dealId: '1', meta: { advertiserDomains: ['google.com'], advertiserId: 1234 } - } ] + }] }; - - const serverResponses = spec.interpretResponse(resBannerObject); - - expect(serverResponses).to.be.an('array').that.is.not.empty; - for (let i = 0; i < serverResponses.length; i++) { - let dataItem = serverResponses[i]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'mediaType', 'meta', 'adomain'); - expect(dataItem.requestId).to.be.a('string'); - expect(dataItem.cpm).to.be.a('number'); - expect(dataItem.width).to.be.a('number'); - expect(dataItem.height).to.be.a('number'); - expect(dataItem.ad).to.be.a('string'); - expect(dataItem.ttl).to.be.a('number'); - expect(dataItem.creativeId).to.be.a('string'); - expect(dataItem.netRevenue).to.be.a('boolean'); - expect(dataItem.currency).to.be.a('string'); - expect(dataItem.mediaType).to.be.a('string'); - expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); - } + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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('(VIDEO) Returns an array of valid server responses if response object is valid', function () { - const resVideoObject = { - body: [ { - requestId: '123', + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', mediaType: 'video', - cpm: 0.3, - width: 320, - height: 50, - vastUrl: 'https://', - ttl: 1000, - creativeId: '123asd', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', netRevenue: true, currency: 'USD', - adomain: ['example.com'], + dealId: '1', meta: { advertiserDomains: ['google.com'], advertiserId: 1234 } - } ] + }] }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; - const serverResponses = spec.interpretResponse(resVideoObject); - - expect(serverResponses).to.be.an('array').that.is.not.empty; - for (let i = 0; i < serverResponses.length; i++) { - let dataItem = serverResponses[i]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastUrl', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'mediaType', 'meta', 'adomain'); - expect(dataItem.requestId).to.be.a('string'); - expect(dataItem.cpm).to.be.a('number'); - expect(dataItem.width).to.be.a('number'); - expect(dataItem.height).to.be.a('number'); - expect(dataItem.vastUrl).to.be.a('string'); - expect(dataItem.ttl).to.be.a('number'); - expect(dataItem.creativeId).to.be.a('string'); - expect(dataItem.netRevenue).to.be.a('boolean'); - expect(dataItem.currency).to.be.a('string'); - expect(dataItem.mediaType).to.be.a('string'); - expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); - } + let 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('(NATIVE) Returns an array of valid server responses if response object is valid', function () { - const resNativeObject = { - body: [ { - requestId: '123', + it('Should interpret native response', function () { + const native = { + body: [{ mediaType: 'native', - cpm: 0.3, - width: 320, - height: 50, native: { - title: 'title', - image: 'image', - impressionTrackers: [ 'https://' ] + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], }, - ttl: 1000, - creativeId: '123asd', + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', netRevenue: true, currency: 'USD', - adomain: ['example.com'], meta: { advertiserDomains: ['google.com'], advertiserId: 1234 } - } ] + }] }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; - const serverResponses = spec.interpretResponse(resNativeObject); - - expect(serverResponses).to.be.an('array').that.is.not.empty; - for (let i = 0; i < serverResponses.length; i++) { - let dataItem = serverResponses[i]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'native', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'mediaType', 'meta', 'adomain'); - expect(dataItem.requestId).to.be.a('string'); - expect(dataItem.cpm).to.be.a('number'); - expect(dataItem.width).to.be.a('number'); - expect(dataItem.height).to.be.a('number'); - expect(dataItem.native).to.be.an('object'); - expect(dataItem.ttl).to.be.a('number'); - expect(dataItem.creativeId).to.be.a('string'); - expect(dataItem.netRevenue).to.be.a('boolean'); - expect(dataItem.currency).to.be.a('string'); - expect(dataItem.mediaType).to.be.a('string'); - expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); - } + let 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('Invalid mediaType in response', function () { - const resBadObject = { - body: [ { - mediaType: 'other', - requestId: '123', - cpm: 0.3, - ttl: 1000, - creativeId: '123asd', - currency: 'USD' - } ] + 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(resBadObject); - + let 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' + }] + }; + let 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', + }] + }; + let 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' + }] + }; + let serverResponses = spec.interpretResponse(invalid); expect(serverResponses).to.be.an('array').that.is.empty; }); }); - describe('getUserSyncs', function () { - const gdprConsent = { consentString: 'consentString', gdprApplies: 1 }; - const consentString = { consentString: 'consentString' } - let userSync = spec.getUserSyncs({}, {}, gdprConsent, consentString); - it('Returns valid URL and type', function () { - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.exist; - expect(userSync[0].url).to.exist; - expect(userSync[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://sync.admanmedia.com/image?pbjs=1&gdpr=0&gdpr_consent=consentString&ccpa_consent=consentString&coppa=0'); + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.admanmedia.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') }); }); }); diff --git a/test/spec/modules/admaruBidAdapter_spec.js b/test/spec/modules/admaruBidAdapter_spec.js index 813a4ed8b29..05ec9eca67f 100644 --- a/test/spec/modules/admaruBidAdapter_spec.js +++ b/test/spec/modules/admaruBidAdapter_spec.js @@ -39,12 +39,12 @@ describe('Admaru Adapter', function () { }); it('should return false when required params are not passed', () => { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { wrong: 'missing pub_id or adspace_id' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/admaticBidAdapter_spec.js b/test/spec/modules/admaticBidAdapter_spec.js index 8730f2c1a0d..da4329b6405 100644 --- a/test/spec/modules/admaticBidAdapter_spec.js +++ b/test/spec/modules/admaticBidAdapter_spec.js @@ -3,7 +3,7 @@ import { spec } from 'modules/admaticBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -const ENDPOINT = 'https://layer.serve.admatic.com.tr/pb'; +const ENDPOINT = 'https://layer.rtb.admatic.com.tr/pb'; describe('admaticBidAdapter', () => { const adapter = newBidder(spec); @@ -15,7 +15,7 @@ describe('admaticBidAdapter', () => { 'bidder': 'admatic', 'params': { 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' + 'host': 'layer.rtb.admatic.com.tr' }, 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, @@ -250,7 +250,7 @@ describe('admaticBidAdapter', () => { 'bidder': 'admatic', 'params': { 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' + 'host': 'layer.rtb.admatic.com.tr' }, 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, @@ -564,7 +564,7 @@ describe('admaticBidAdapter', () => { 'bidder': 'admatic', 'params': { 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' + 'host': 'layer.rtb.admatic.com.tr' }, 'adUnitCode': 'adunit-code', 'mediaType': 'banner', @@ -683,7 +683,10 @@ describe('admaticBidAdapter', () => { }] } ], - params: {} + params: { + networkId: 10433394, + host: 'layer.rtb.admatic.com.tr' + } }, ]; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -724,7 +727,7 @@ describe('admaticBidAdapter', () => { 'ortb2': { 'badv': ['admatic.com.tr'] }, 'params': { 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' + 'host': 'layer.rtb.admatic.com.tr' }, getFloor: inputParams => { if (inputParams.mediaType === VIDEO && inputParams.size[0] === 300 && inputParams.size[1] === 250) { @@ -766,7 +769,7 @@ describe('admaticBidAdapter', () => { 'ortb2': { 'badv': ['admatic.com.tr'] }, 'params': { 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' + 'host': 'layer.rtb.admatic.com.tr' }, getFloor: inputParams => { if (inputParams.mediaType === NATIVE) { @@ -802,6 +805,7 @@ describe('admaticBidAdapter', () => { 'price': 0.01, 'type': 'banner', 'bidder': 'admatic', + 'currency': 'TRY', 'mime_type': { 'name': 'backfill', 'force': false @@ -816,6 +820,7 @@ describe('admaticBidAdapter', () => { 'width': 300, 'height': 250, 'price': 0.01, + 'currency': 'TRY', 'type': 'video', 'mime_type': { 'name': 'backfill', @@ -832,6 +837,7 @@ describe('admaticBidAdapter', () => { 'width': 1, 'height': 1, 'price': 0.01, + 'currency': 'TRY', 'type': 'native', 'mime_type': { 'name': 'backfill', @@ -843,6 +849,7 @@ describe('admaticBidAdapter', () => { 'iurl': 'https://www.admatic.com.tr' } ], + 'cur': 'TRY', 'queryId': 'cdnbh24rlv0hhkpfpln0', 'status': true }}; @@ -853,9 +860,9 @@ describe('admaticBidAdapter', () => { cpm: 0.01, width: 300, height: 250, - currency: 'TRY', mediaType: 'banner', netRevenue: true, + currency: 'TRY', ad: '
', creativeId: '374', meta: { @@ -873,9 +880,9 @@ describe('admaticBidAdapter', () => { cpm: 0.01, width: 300, height: 250, - currency: 'TRY', mediaType: 'video', netRevenue: true, + currency: 'TRY', vastXml: '', creativeId: '3741', meta: { @@ -893,9 +900,9 @@ describe('admaticBidAdapter', () => { cpm: 0.01, width: 1, height: 1, - currency: 'TRY', mediaType: 'native', netRevenue: true, + currency: 'TRY', native: { 'clickUrl': 'https://www.admatic.com.tr', 'impressionTrackers': ['https://www.admatic.com.tr'], @@ -1144,7 +1151,8 @@ describe('admaticBidAdapter', () => { let bids = { body: { data: [], 'queryId': 'cdnbh24rlv0hhkpfpln0', - 'status': true + 'status': true, + 'cur': 'TRY' }}; let result = spec.interpretResponse(bids, {data: request}); diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index 85538efc957..1da6a58bea3 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -4,7 +4,7 @@ import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from '../../../src/config.js'; const BIDDER_CODE = 'admixer'; -const WL_BIDDER_CODE = 'admixerwl' +const RTB_BIDDER_CODE = 'rtbstack' const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.2.aspx'; const ENDPOINT_URL_CUSTOM = 'https://custom.admixer.net/prebid.aspx'; const ZONE_ID = '2eb6bd58-865c-47ce-af7f-a918108c3fd2'; @@ -37,11 +37,10 @@ describe('AdmixerAdapter', function () { auctionId: '1d1a030790a475', }; - let wlBid = { - bidder: WL_BIDDER_CODE, + let rtbBid = { + bidder: RTB_BIDDER_CODE, params: { - clientId: CLIENT_ID, - endpointId: ENDPOINT_ID, + tagId: ENDPOINT_ID, }, adUnitCode: 'adunit-code', sizes: [ @@ -56,25 +55,25 @@ describe('AdmixerAdapter', function () { it('should return true when required params found', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return true when params required by WL found', function () { - expect(spec.isBidRequestValid(wlBid)).to.equal(true); + it('should return true when params required by RTB found', function () { + expect(spec.isBidRequestValid(rtbBid)).to.equal(true); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { placementId: 0, }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); - it('should return false when params required by WL are not passed', function () { - let wlBid = Object.assign({}, wlBid); - delete wlBid.params; - wlBid.params = { + it('should return false when params required by RTB are not passed', function () { + let invalidBid = Object.assign({}, rtbBid); + delete invalidBid.params; + invalidBid.params = { clientId: 0, }; - expect(spec.isBidRequestValid(wlBid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -118,7 +117,7 @@ describe('AdmixerAdapter', function () { it('sends bid request to CUSTOM_ENDPOINT via GET', function () { config.setBidderConfig({ bidders: [BIDDER_CODE], // one or more bidders - config: { [BIDDER_CODE]: { endpoint_url: ENDPOINT_URL_CUSTOM } }, + config: { bidderURL: ENDPOINT_URL_CUSTOM }, }); const request = config.runWithBidder(BIDDER_CODE, () => spec.buildRequests(validRequest, bidderRequest) @@ -133,9 +132,8 @@ describe('AdmixerAdapter', function () { validRequest: [ { bidder: bidder, - params: bidder === 'admixerwl' ? { - clientId: CLIENT_ID, - endpointId: ENDPOINT_ID + params: bidder === 'rtbstack' ? { + tagId: ENDPOINT_ID } : { zone: ZONE_ID, }, @@ -175,12 +173,6 @@ describe('AdmixerAdapter', function () { expect(request.url).to.equal('https://inv-nets.admixer.net/prebid.1.2.aspx'); expect(request.method).to.equal('POST'); }); - it('build request for adsyield', function () { - const requestParams = requestParamsFor('adsyield'); - const request = spec.buildRequests(requestParams.validRequest, requestParams.bidderRequest); - expect(request.url).to.equal('https://ads.adsyield.com/prebid.1.2.aspx'); - expect(request.method).to.equal('POST'); - }); it('build request for futureads', function () { const requestParams = requestParamsFor('futureads'); const request = spec.buildRequests(requestParams.validRequest, requestParams.bidderRequest); @@ -199,10 +191,16 @@ describe('AdmixerAdapter', function () { expect(request.url).to.equal('https://inv-nets.admixer.net/adxprebid.1.2.aspx'); expect(request.method).to.equal('POST'); }); - it('build request for admixerwl', function () { - const requestParams = requestParamsFor('admixerwl'); - const request = spec.buildRequests(requestParams.validRequest, requestParams.bidderRequest); - expect(request.url).to.equal(`https://inv-nets-adxwl.admixer.com/adxwlprebid.aspx?client=${CLIENT_ID}`); + it('build request for rtbstack', function () { + const requestParams = requestParamsFor('rtbstack'); + config.setBidderConfig({ + bidders: ['rtbstack'], + config: { bidderURL: ENDPOINT_URL_CUSTOM }, + }); + const request = config.runWithBidder(BIDDER_CODE, () => + spec.buildRequests(requestParams.validRequest, requestParams.bidderRequest) + ); + expect(request.url).to.equal(ENDPOINT_URL_CUSTOM); expect(request.method).to.equal('POST'); }); }); @@ -228,12 +226,12 @@ describe('AdmixerAdapter', function () { }, }; it('gets floor', function () { - bidderRequest.getFloor = () => { + validRequest[0].getFloor = () => { return { floor: 0.6 }; }; const request = spec.buildRequests(validRequest, bidderRequest); const payload = request.data; - expect(payload.bidFloor).to.deep.equal(0.6); + expect(payload.imps[0].bidFloor).to.deep.equal(0.6); }); }); diff --git a/test/spec/modules/admixerIdSystem_spec.js b/test/spec/modules/admixerIdSystem_spec.js index 753b1e3c2d5..56c356a153e 100644 --- a/test/spec/modules/admixerIdSystem_spec.js +++ b/test/spec/modules/admixerIdSystem_spec.js @@ -30,7 +30,7 @@ describe('admixerId tests', function () { }); it('should NOT call the admixer id endpoint if gdpr applies but consent string is missing', function () { - let submoduleCallback = admixerIdSubmodule.getId(getIdParams, { gdprApplies: true }); + let submoduleCallback = admixerIdSubmodule.getId(getIdParams, {gdpr: { gdprApplies: true }}); expect(submoduleCallback).to.be.undefined; }); diff --git a/test/spec/modules/adnuntiusAnalyticsAdapter_spec.js b/test/spec/modules/adnuntiusAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..09bf55db146 --- /dev/null +++ b/test/spec/modules/adnuntiusAnalyticsAdapter_spec.js @@ -0,0 +1,534 @@ +import adnAnalyticsAdapter, { BID_WON_TIMEOUT } from 'modules/adnuntiusAnalyticsAdapter.js'; +import { AD_RENDER_FAILED_REASON, EVENTS, STATUS } from 'src/constants.js'; +import { config } from 'src/config.js'; +import { server } from 'test/mocks/xhr.js'; +import { setConfig } from 'modules/currency.js'; + +let events = require('src/events'); +let utils = require('src/utils'); +let adapterManager = require('src/adapterManager').default; + +const { + AUCTION_INIT, + AUCTION_END, + BID_REQUESTED, + BID_RESPONSE, + BIDDER_DONE, + BID_WON, + BID_TIMEOUT, + SET_TARGETING, + AD_RENDER_FAILED +} = EVENTS; + +const BID1 = { + width: 980, + height: 240, + cpm: 1.1, + originalCpm: 12.0, + currency: 'USD', + originalCurrency: 'AUX', + timeToRespond: 200, + bidId: '2ec0db240757', + requestId: '2ec240757', + adId: '2ec240757', + auctionId: '1234-4567-7890', + mediaType: 'banner', + meta: { + data: 'value1' + }, + dealId: 'dealid', + getStatusCode() { + return STATUS.GOOD; + } +}; + +const BID2 = Object.assign({}, BID1, { + width: 300, + height: 250, + cpm: 2.2, + originalCpm: 23.0, + currency: 'USD', + originalCurrency: 'AUX', + timeToRespond: 300, + bidId: '30db240757', + requestId: '30db240757', + adId: '30db240757', + meta: { + data: 'value2' + }, + dealId: undefined +}); + +const BID3 = { + bidId: '4ecff0db240757', + requestId: '4ecff0db240757', + adId: '4ecff0db240757', + auctionId: '1234-4567-7890', + mediaType: 'banner', + getStatusCode() { + return STATUS.GOOD; + } +}; + +const MOCK = { + AUCTION_INIT: { + 'auctionId': '1234-4567-7890', + }, + BID_REQUESTED: { + 'bidder': 'adnuntius', + 'auctionId': '1234-4567-7890', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'adnuntius', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ec240757', + }, + { + 'bidder': 'adnuntius', + 'adUnitCode': 'box_d_1', + 'bidId': '30db240757', + }, + { + 'bidder': 'adnuntius', + 'adUnitCode': 'box_d_2', + 'bidId': '4ecff0db240757', + } + ], + 'start': 1519149562216 + }, + BID_RESPONSE: [ + BID1, + BID2 + ], + AUCTION_END: { + }, + BID_WON: [ + Object.assign({}, BID1, { + 'status': 'rendered', + 'requestId': '2ec240757' + }), + Object.assign({}, BID2, { + 'status': 'rendered', + 'requestId': '30db240757' + }) + ], + BIDDER_DONE: { + 'bidderCode': 'adnuntius', + 'bids': [ + BID1, + BID2, + BID3 + ] + }, + BID_TIMEOUT: [ + { + 'bidId': '2ec240757', + 'auctionId': '1234-4567-7890' + } + ], + AD_RENDER_FAILED: [ + { + 'bidId': '2ec240757', + 'reason': AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + 'message': 'message', + 'bid': BID1 + } + ] +}; + +const ANALYTICS_MESSAGE = { + publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', + gdpr: [{}], + auctionIds: ['1234-4567-7890'], + bidAdUnits: [ + { + adUnit: 'panorama_d_1', + adUnitId: 'adunitid', + timeStamp: 1519149562216 + }, + { + adUnit: 'box_d_1', + adUnitId: 'adunitid', + timeStamp: 1519149562216 + } + ], + requests: [ + { + adUnit: 'panorama_d_1', + adUnitId: 'adunitid', + bidder: 'adnuntius', + timeStamp: 1519149562216, + gdpr: 0, + auctionId: 0 + }, + { + adUnit: 'box_d_1', + adUnitId: 'adunitid', + bidder: 'adnuntius', + timeStamp: 1519149562216, + gdpr: 0, + auctionId: 0 + }, + { + adUnit: 'box_d_2', + adUnitId: 'adunitid', + bidder: 'adnuntius', + timeStamp: 1519149562216, + gdpr: 0, + auctionId: 0 + } + ], + responses: [ + { + adUnit: 'panorama_d_1', + adUnitId: 'adunitid', + bidder: 'adnuntius', + timeStamp: 1519149562216, + width: 980, + height: 240, + cpm: 1.1, + currency: 'USD', + originalCpm: 12, + originalCurrency: 'AUX', + ttr: 200, + isBid: true, + mediaType: 1, + gdpr: 0, + auctionId: 0, + meta: { + data: 'value1' + } + }, + { + adUnit: 'box_d_1', + adUnitId: 'adunitid', + bidder: 'adnuntius', + timeStamp: 1519149562216, + width: 300, + height: 250, + cpm: 2.2, + currency: 'USD', + originalCpm: 23, + originalCurrency: 'AUX', + ttr: 300, + isBid: true, + mediaType: 1, + gdpr: 0, + auctionId: 0, + meta: { + data: 'value2' + } + } + ], + timeouts: [], + wins: [ + { + adUnit: 'panorama_d_1', + adUnitId: 'adunitid', + bidder: 'adnuntius', + timeStamp: 1519149562216, + width: 980, + height: 240, + cpm: 1.1, + currency: 'USD', + originalCpm: 12, + originalCurrency: 'AUX', + mediaType: 1, + gdpr: 0, + auctionId: 0, + meta: { + data: 'value1' + }, + dealId: 'dealid' + }, + { + adUnit: 'box_d_1', + adUnitId: 'adunitid', + bidder: 'adnuntius', + timeStamp: 1519149562216, + width: 300, + height: 250, + cpm: 2.2, + currency: 'USD', + originalCpm: 23, + originalCurrency: 'AUX', + mediaType: 1, + gdpr: 0, + auctionId: 0, + meta: { + data: 'value2' + } + } + ], + rf: [ + { + adUnit: 'panorama_d_1', + adUnitId: 'adunitid', + bidder: 'adnuntius', + timeStamp: 1519149562216, + auctionId: 0, + rsn: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + msg: 'message' + }, + ] +}; + +function performStandardAuction() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(AD_RENDER_FAILED, MOCK.AD_RENDER_FAILED[0]); +} + +describe('Adnuntius analytics adapter', function () { + let sandbox; + let clock; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + + let element = { + getAttribute: function() { + return 'adunitid'; + } + } + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'timestamp').returns(1519149562416); + sandbox.stub(document, 'getElementById').returns(element); + + clock = sandbox.useFakeTimers(1519767013781); + }); + + afterEach(function () { + sandbox.restore(); + config.resetConfig(); + }); + + describe('when handling events', function () { + adapterManager.registerAnalyticsAdapter({ + code: 'adnuntius', + adapter: adnAnalyticsAdapter + }); + + beforeEach(function () { + adapterManager.enableAnalytics({ + provider: 'adnuntius', + options: { + publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7' + } + }); + }); + + afterEach(function () { + adnAnalyticsAdapter.disableAnalytics(); + }); + + it('should build a batched message from prebid events', function () { + performStandardAuction(); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.equal('https://analytics.adnuntius.com/prebid'); + + const message = JSON.parse(request.requestBody); + expect(message).to.deep.equal(ANALYTICS_MESSAGE); + }); + + it('should send batched message without BID_WON AND AD_RENDER_FAILED if necessary and further BID_WON and AD_RENDER_FAILED events individually', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(BID_WON_TIMEOUT + 1000); + + events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(AD_RENDER_FAILED, MOCK.AD_RENDER_FAILED[0]); + + expect(server.requests.length).to.equal(3); + + let message = JSON.parse(server.requests[0].requestBody); + expect(message.wins.length).to.equal(1); + expect(message.requests).to.deep.equal(ANALYTICS_MESSAGE.requests); + expect(message.wins[0]).to.deep.equal(ANALYTICS_MESSAGE.wins[0]); + + message = JSON.parse(server.requests[1].requestBody); + expect(message.wins.length).to.equal(1); + expect(message.wins[0]).to.deep.equal(ANALYTICS_MESSAGE.wins[1]); + + message = JSON.parse(server.requests[2].requestBody); + expect(message.rf.length).to.equal(1); + expect(message.rf[0]).to.deep.equal(ANALYTICS_MESSAGE.rf[0]); + }); + + it('should properly mark bids as timed out', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + expect(message.timeouts.length).to.equal(1); + expect(message.timeouts[0].bidder).to.equal('adnuntius'); + expect(message.timeouts[0].adUnit).to.equal('panorama_d_1'); + }); + + it('should forward GDPR data', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, { + 'bidder': 'adnuntius', + 'auctionId': '1234-4567-7890', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'adnuntius', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ec240757', + }, + { + 'bidder': 'adnuntius', + 'adUnitCode': 'box_d_1', + 'bidId': '30db240757', + } + ], + 'start': 1519149562216, + 'gdprConsent': { + 'gdprApplies': true, + 'consentString': 'consentstring' + } + }, + ); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.gdpr.length).to.equal(1); + expect(message.gdpr[0].gdprApplies).to.equal(true); + expect(message.gdpr[0].gdprConsent).to.equal('consentstring'); + expect(message.requests.length).to.equal(2); + expect(message.requests[0].gdpr).to.equal(0); + expect(message.requests[1].gdpr).to.equal(0); + + expect(message.responses.length).to.equal(1); + expect(message.responses[0].gdpr).to.equal(0); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0].gdpr).to.equal(0); + }); + + it('should forward runner-up data as given by Adnuntius wrapper', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_WON, Object.assign({}, + MOCK.BID_WON[0], + { + 'rUp': 'rUpObject' + })); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0].rUp).to.equal('rUpObject'); + }); + }); + + describe('when given other endpoint', function () { + adapterManager.registerAnalyticsAdapter({ + code: 'adnuntius', + adapter: adnAnalyticsAdapter + }); + + beforeEach(function () { + adapterManager.enableAnalytics({ + provider: 'adnuntius', + options: { + publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', + endPoint: 'https://whitelabeled.com/analytics/10' + } + }); + }); + + afterEach(function () { + adnAnalyticsAdapter.disableAnalytics(); + }); + + it('should call the endpoint', function () { + performStandardAuction(); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.equal('https://whitelabeled.com/analytics/10'); + }); + }); + + describe('when given extended options', function () { + adapterManager.registerAnalyticsAdapter({ + code: 'adnuntius', + adapter: adnAnalyticsAdapter + }); + + beforeEach(function () { + adapterManager.enableAnalytics({ + provider: 'adnuntius', + options: { + publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', + ext: { + testparam: 123 + } + } + }); + }); + + afterEach(function () { + adnAnalyticsAdapter.disableAnalytics(); + }); + + it('should forward the extended options', function () { + performStandardAuction(); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.ext).to.not.equal(null); + expect(message.ext.testparam).to.equal(123); + }); + }); +}); diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index cad5e9ea1cf..436fe170aa3 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -1,18 +1,20 @@ // import or require modules necessary for the test, e.g.: -import {expect} from 'chai'; // may prefer 'assert' in place of 'expect' -import {misc, spec} from 'modules/adnuntiusBidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; -import {config} from 'src/config.js'; +import { expect } from 'chai'; // may prefer 'assert' in place of 'expect' +import { misc, spec } from 'modules/adnuntiusBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import {config, newConfig} from 'src/config.js'; import * as utils from 'src/utils.js'; -import {getStorageManager} from 'src/storageManager.js'; -import {getGlobal} from '../../../src/prebidGlobal'; +import { getStorageManager } from 'src/storageManager.js'; +import { getGlobal } from '../../../src/prebidGlobal'; +import {getUnixTimestampFromNow, getWindowTop} from 'src/utils.js'; +import { getWinDimensions } from '../../../src/utils'; -describe('adnuntiusBidAdapter', function() { +describe('adnuntiusBidAdapter', function () { const URL = 'https://ads.adnuntius.delivery/i?tzo='; const EURO_URL = 'https://europe.delivery.adnuntius.com/i?tzo='; const usi = utils.generateUUID() - const meta = [{key: 'valueless'}, {value: 'keyless'}, {key: 'voidAuIds'}, {key: 'voidAuIds', value: [{auId: '11118b6bc', exp: misc.getUnixTimestamp()}, {exp: misc.getUnixTimestamp(1)}]}, {key: 'valid-withnetwork', value: 'also-valid-network', network: 'the-network', exp: misc.getUnixTimestamp(1)}, {key: 'valid', value: 'also-valid', exp: misc.getUnixTimestamp(1)}, {key: 'expired', value: 'fwefew', exp: misc.getUnixTimestamp()}, {key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp(), network: 'adnuntius'}, {key: 'usi', value: usi, exp: misc.getUnixTimestamp(100), network: 'adnuntius'}, {key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp()}] + const meta = [{ key: 'valueless' }, { value: 'keyless' }, { key: 'voidAuIds' }, { key: 'voidAuIds', value: [{ auId: '11118b6bc', exp: getUnixTimestampFromNow() }, { exp: getUnixTimestampFromNow(1) }] }, { key: 'valid-withnetwork', value: 'also-valid-network', network: 'the-network', exp: getUnixTimestampFromNow(1) }, { key: 'valid', value: 'also-valid', exp: getUnixTimestampFromNow(1) }, { key: 'expired', value: 'fwefew', exp: getUnixTimestampFromNow() }, { key: 'usi', value: 'should be skipped because timestamp', exp: getUnixTimestampFromNow(), network: 'adnuntius' }, { key: 'usi', value: usi, exp: getUnixTimestampFromNow(100), network: 'adnuntius' }, { key: 'usi', value: 'should be skipped because timestamp', exp: getUnixTimestampFromNow() }] let storage; // need this to make the restore work correctly -- something to do with stubbing static prototype methods @@ -24,7 +26,7 @@ describe('adnuntiusBidAdapter', function() { storageAllowed: true } }; - storage = getStorageManager({bidderCode: 'adnuntius'}); + storage = getStorageManager({ bidderCode: 'adnuntius' }); }); beforeEach(() => { @@ -35,7 +37,7 @@ describe('adnuntiusBidAdapter', function() { getGlobal().bidderSettings = {}; }); - afterEach(function() { + afterEach(function () { config.resetConfig(); if (stub1.restore) { @@ -47,12 +49,15 @@ describe('adnuntiusBidAdapter', function() { }); const tzo = new Date().getTimezoneOffset(); - const ENDPOINT_URL_BASE = `${URL}${tzo}&format=prebid`; + const winDimensions = getWinDimensions(); + const screen = winDimensions.screen.availWidth + 'x' + winDimensions.screen.availHeight; + const viewport = winDimensions.innerWidth + 'x' + winDimensions.innerHeight; + const ENDPOINT_URL_BASE = `${URL}${tzo}&format=prebid&screen=${screen}&viewport=${viewport}`; const ENDPOINT_URL = `${ENDPOINT_URL_BASE}&userId=${usi}`; - const ENDPOINT_URL_VIDEO = `${ENDPOINT_URL_BASE}&userId=${usi}&tt=vast4`; + const LOCALHOST_URL = `http://localhost:8078/i?tzo=${tzo}&format=prebid&screen=${screen}&viewport=${viewport}&userId=${usi}`; const ENDPOINT_URL_NOCOOKIE = `${ENDPOINT_URL_BASE}&userId=${usi}&noCookies=true`; const ENDPOINT_URL_SEGMENTS = `${ENDPOINT_URL_BASE}&segments=segment1,segment2,segment3&userId=${usi}`; - const ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=prebid&consentString=consentString&gdpr=1&userId=${usi}`; + const ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=prebid&consentString=consentString&gdpr=1&screen=${screen}&viewport=${viewport}&userId=${usi}`; const adapter = newBidder(spec); const bidderRequests = [ @@ -101,7 +106,158 @@ describe('adnuntiusBidAdapter', function() { } }, } - ] + ]; + + const legacyNativeBidderRequest = {bid: [ + { + bidId: 'adn-0000000000000551', + bidder: 'adnuntius', + params: { + auId: '0000000000000551', + network: 'adnuntius', + }, + mediaTypes: { + native: { + sizes: [[200, 200], [300, 300]], + image: { + required: true, + sizes: [250, 250] + } + } + } + }]}; + + const nativeBidderRequest = {bid: [ + { + bidId: 'adn-0000000000000551', + bidder: 'adnuntius', + params: { + auId: '0000000000000551', + network: 'adnuntius', + }, + mediaTypes: { + native: { + sizes: [[200, 200], [300, 300]], + ortb: { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 250, + h: 250, + } + }] + } + } + }, + } + ]}; + + const multiBidAndFormatRequest = { + bid: [{ + bidder: 'adnuntius', + bidId: '3a602680158a85', + params: { + auId: '381535', + network: '1287', + bidType: 'netBid', + }, + mediaTypes: { + banner: { + sizes: [[200, 200]] + }, + video: { + playerSize: [200, 200], + context: 'instream' + } + } + }, + { + bidder: 'adnuntius', + bidId: 'fewwef', + params: { + auId: '381535', + network: '1287', + bidType: 'netBid', + targetId: 'fred', + }, + mediaTypes: { + banner: { + sizes: [[200, 200]] + }, + video: { + playerSize: [200, 200], + context: 'instream' + }, + native: { + ortb: { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }] + } + } + } + }, + { + bidder: 'adnuntius', + bidId: 'pol', + params: { + auId: '381535', + network: '1287', + bidType: 'netBid', + targetId: 'alt', + }, + mediaTypes: { + banner: { + sizes: [[200, 200]] + }, + video: { + playerSize: [200, 200], + context: 'instream' + }, + native: { + ortb: { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }] + } + } + } + }] + }; + + const multiBidderRequest = [ + { + bidId: 'adn-0000000000000551', + bidder: 'adnuntius', + params: { + auId: '0000000000000551', + network: 'adnuntius', + }, + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + }, + banner: { + sizes: [[1640, 1480], [1600, 1400]], + } + }, + } + ]; const singleBidRequest = { bid: [ @@ -110,7 +266,7 @@ describe('adnuntiusBidAdapter', function() { bidId: 'adn-0000000000000551', } ] - } + }; const videoBidRequest = { bid: videoBidderRequest, @@ -118,7 +274,7 @@ describe('adnuntiusBidAdapter', function() { params: { bidType: 'justsomestuff-error-handling' } - } + }; const deals = [ { @@ -139,6 +295,7 @@ describe('adnuntiusBidAdapter', function() { 'urlsEsc': { 'destination': 'https%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com' }, + 'advertiserDomains': ['fred.com', 'george.com'], 'destinationUrls': { 'destination': 'https://adnuntius.com' }, @@ -183,6 +340,236 @@ describe('adnuntiusBidAdapter', function() { } ]; + const nativeResponseBody = [ + { + 'auId': '0000000000000551', + 'targetId': 'adn-0000000000000551', + 'nativeJson': { + 'ortb': { + 'link': { + 'url': 'https://whatever.com' + }, + 'assets': [ + { + 'id': 1, + 'required': 1, + 'img': { + 'url': 'http://something.com/something.png' + } + }] + } + }, + 'matchedAdCount': 1, + 'responseId': '', + 'ads': [ + { + 'advertiserDomains': ['adnuntius.com'], + 'creativeWidth': 640, + 'creativeHeight': 640, + 'cpm': { + 'amount': 2000, + 'currency': 'NOK' + }, + 'bid': { + 'amount': 2, + 'currency': 'NOK' + }, + 'grossBid': { + 'amount': 2, + 'currency': 'NOK' + }, + 'netBid': { + 'amount': 2, + 'currency': 'NOK' + }, + 'cost': { + 'amount': 2, + 'currency': 'NOK' + }, + 'assets': { + 'img': { + 'cdnId': 'http://localhost:8079/cdn/9urJusYWpjFDLcpOwfejrkWlLP1heM3vWIJjuHk48BQ.mp4', + 'width': '1920', + 'height': '1080' + } + }, + } + ] + } + ]; + + const nativeResponse = { + body: { + 'adUnits': nativeResponseBody + } + }; + + const nativeMultiFormatResponseBody = JSON.parse(JSON.stringify(nativeResponseBody[0])); + nativeMultiFormatResponseBody.auId = '0000000000381535'; + nativeMultiFormatResponseBody.targetId = 'alt-native'; + + const multiFormatServerResponse = { + body: { + 'adUnits': [ + { + 'auId': '0000000000381535', + 'targetId': '3a602680158a85-video', + 'vastXml': '\n', + 'matchedAdCount': 1, + 'responseId': 'adn-rsp-453419729', + 'ads': [ + { + 'cpm': { + 'amount': 12500.0, + 'currency': 'NOK' + }, + 'bid': { + 'amount': 5, + 'currency': 'NOK' + }, + 'grossBid': { + 'amount': 5, + 'currency': 'NOK' + }, + 'netBid': { + 'amount': 5, + 'currency': 'NOK' + }, + 'cost': { + 'amount': 5, + 'currency': 'NOK' + }, + creativeWidth: 200, + creativeHeight: 240, + 'adId': 'adn-id-615465411', + 'vastXml': '' + } + ] + }, + { + 'auId': '0000000000381535', + 'targetId': 'fred-video', + 'vastXml': '', + 'matchedAdCount': 1, + 'responseId': 'adn-rsp--1809523040', + 'ads': [ + { + 'cpm': { + 'amount': 1500.0, + 'currency': 'NOK' + }, + 'bid': { + 'amount': 1.5, + 'currency': 'NOK' + }, + 'grossBid': { + 'amount': 1.5, + 'currency': 'NOK' + }, + 'netBid': { + 'amount': 1.5, + 'currency': 'NOK' + }, + 'cost': { + 'amount': 1.5, + 'currency': 'NOK' + }, + creativeWidth: 200, + creativeHeight: 240, + 'adId': 'adn-id-344789675', + 'selectedColumn': '0', + 'selectedColumnPosition': '0', + 'vastXml': '\n', + } + ] + }, + nativeMultiFormatResponseBody, + { + 'auId': '0000000000381535', + 'targetId': '3a602680158a85', + 'html': '\u003C!DOCTYPE html\u003E\n\n\u003C/html\u003E', + 'matchedAdCount': 0, + 'responseId': '', + 'ads': [] + }, + { + 'auId': '0000000000381535', + 'renderOption': 'DIV', + 'targetId': 'fred', + 'html': '\u003C!DOCTYPE html\u003E\n\u003C\u003E\n\u003C/html\u003E', + 'matchedAdCount': 1, + 'responseId': 'adn-rsp-1620340740', + 'ads': [ + { + 'destinationUrlEsc': '', + 'cpm': { + 'amount': 1250.0, + 'currency': 'NOK' + }, + creativeWidth: 200, + creativeHeight: 240, + 'bid': { + 'amount': 1.75, + 'currency': 'NOK' + }, + 'grossBid': { + 'amount': 1.75, + 'currency': 'NOK' + }, + 'netBid': { + 'amount': 1.75, + 'currency': 'NOK' + }, + 'cost': { + 'amount': 1.75, + 'currency': 'NOK' + }, + 'html': '\u003Ca \'\u003E\u003C/script\u003E', + } + ] + }, + { + 'auId': '0000000000381535', + 'renderOption': 'DIV', + 'targetId': 'alt', + 'html': '\u003C!DOCTYPE html\u003E\n\u003C\u003E\n\u003C/html\u003E', + 'matchedAdCount': 1, + 'responseId': 'adn-rsp-1620340740', + 'ads': [ + { + 'destinationUrlEsc': '', + 'cpm': { + 'amount': 1000.0, + 'currency': 'NOK' + }, + creativeWidth: 200, + creativeHeight: 240, + 'bid': { + 'amount': 1.75, + 'currency': 'NOK' + }, + 'grossBid': { + 'amount': 1.75, + 'currency': 'NOK' + }, + 'netBid': { + 'amount': 1.75, + 'currency': 'NOK' + }, + 'cost': { + 'amount': 1.75, + 'currency': 'NOK' + }, + 'html': '\u003Ca \'\u003E\u003C/script\u003E', + } + ] + } + ], + 'network': '1287', + 'keywords': [] + } + }; + const serverResponse = { body: { 'adUnits': [ @@ -453,20 +840,20 @@ describe('adnuntiusBidAdapter', function() { } } - describe('inherited functions', function() { - it('exists and is a function', function() { + describe('inherited functions', function () { + it('exists and is a function', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); }); - describe('isBidRequestValid', function() { - it('should return true when required params found', function() { + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { expect(spec.isBidRequestValid(bidderRequests[0])).to.equal(true); }); }); - describe('buildRequests', function() { - it('Test requests', function() { + describe('buildRequests', function () { + it('Test requests', function () { stub1 = sinon.stub(URLSearchParams.prototype, 'has').callsFake(() => { return true; }); @@ -474,18 +861,34 @@ describe('adnuntiusBidAdapter', function() { return 'overridden-value'; }); - const request = spec.buildRequests(bidderRequests, {}); + const request = spec.buildRequests(bidderRequests, { + refererInfo: { + canonicalUrl: 'https://canonical.com/page.html', + page: 'https://canonical.com/something-else.html' + } + }); expect(request.length).to.equal(1); expect(request[0]).to.have.property('bid'); const bid = request[0].bid[0] expect(bid).to.have.property('bidId'); expect(request[0]).to.have.property('url'); - expect(request[0].url).to.equal(ENDPOINT_URL.replace('format=prebid', 'format=prebid&so=overridden-value')); + expect(request[0].url).to.equal(ENDPOINT_URL.replace('&userId', '&so=overridden-value&userId')); expect(request[0]).to.have.property('data'); - expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]}]}'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]}],"context":"https://canonical.com/something-else.html","canonical":"https://canonical.com/page.html"}'); }); - it('Test requests with no local storage', function() { + it('should pass for different end points in config', function () { + config.setConfig({ + env: 'localhost', + protocol: 'http' + }) + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + expect(request[0].url).to.equal(LOCALHOST_URL); + }); + + it('Test requests with no local storage', function () { storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{}])); const request = spec.buildRequests(bidderRequests, {}); expect(request.length).to.equal(1); @@ -504,8 +907,8 @@ describe('adnuntiusBidAdapter', function() { expect(request2[0].url).to.equal(ENDPOINT_URL_BASE); }); - it('Test request changes for voided au ids', function() { - storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{key: 'voidAuIds', value: [{auId: '11118b6bc', exp: misc.getUnixTimestamp(1)}, {auId: '0000000000000023', exp: misc.getUnixTimestamp(1)}]}])); + it('Test request changes for voided au ids', function () { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{ key: 'voidAuIds', value: [{ auId: '11118b6bc', exp: getUnixTimestampFromNow(1) }, { auId: '0000000000000023', exp: getUnixTimestampFromNow(1) }] }])); const bRequests = bidderRequests.concat([{ bidId: 'adn-11118b6bc', bidder: 'adnuntius', @@ -556,22 +959,45 @@ describe('adnuntiusBidAdapter', function() { expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]},{"auId":"13","targetId":"adn-13","dimensions":[[164,140],[10,1400]]}]}'); }); - it('Test Video requests', function() { + it('Test Video requests', function () { const request = spec.buildRequests(videoBidderRequest, {}); expect(request.length).to.equal(1); + + const data = JSON.parse(request[0].data); + expect(data.adUnits.length).to.equal(1); + expect(data.adUnits[0].targetId).to.equal('adn-0000000000000551'); + expect(data.adUnits[0].adType).to.equal('VAST'); + expect(request[0]).to.have.property('bid'); const bid = request[0].bid[0] expect(bid).to.have.property('bidId'); expect(request[0]).to.have.property('url'); - expect(request[0].url).to.equal(ENDPOINT_URL_VIDEO); + expect(request[0].url).to.equal(ENDPOINT_URL); }); - it('should pass segments if available in config', function() { + it('Test multiformat requests', function () { + const request = spec.buildRequests(multiBidderRequest, {}); + expect(request.length).to.equal(1); + expect(request.data) + const data = JSON.parse(request[0].data); + expect(data.adUnits.length).to.equal(2); + expect(data.adUnits[0].targetId).to.equal('adn-0000000000000551'); + expect(data.adUnits[0]).not.to.have.property('adType'); + expect(data.adUnits[1].targetId).to.equal('adn-0000000000000551-video'); + expect(data.adUnits[1].adType).to.equal('VAST'); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL); + }); + + it('should pass segments if available in config and merge from targeting', function () { const ortb2 = { user: { data: [{ name: 'adnuntius', - segment: [{id: 'segment1'}, {id: 'segment2'}, {invalidSegment: 'invalid'}, {id: 123}, {id: ['3332']}] + segment: [{ id: 'segment1' }, { id: 'segment2' }, { invalidSegment: 'invalid' }, { id: 123 }, { id: ['3332'] }] }, { name: 'other', @@ -580,18 +1006,161 @@ describe('adnuntiusBidAdapter', function() { } }; - const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, {ortb2})); + bidderRequests[0].params.targeting = { + segments: ['merge-this', 'and-this'] + }; + + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') - expect(request[0].url).to.equal(ENDPOINT_URL_SEGMENTS); + expect(request[0].url).to.equal(ENDPOINT_URL_SEGMENTS.replace('segment3', 'segment3,merge-this,and-this')); + + delete bidderRequests[0].params.targeting; + }); + + function countMatches(actualArray, expectedValue) { + return actualArray.filter(val => { + return JSON.stringify(val) === JSON.stringify(expectedValue); + }).length; + } + + it('should pass site data ext as key values to ad server', function () { + const ortb2 = { + site: { + ext: { + data: { + '12345': 'true', + '45678': 'true', + '9090': 'should-be-retained' + } + } + } + }; + bidderRequests[0].params.targeting = { + kv: { + 'merge': ['this'], + '9090': ['take it over'] + } + }; + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + const data = JSON.parse(request[0].data); + expect(countMatches(data.adUnits[0].kv, {'9090': ['take it over']})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'merge': ['this']})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'9090': 'should-be-retained'})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'45678': 'true'})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'12345': 'true'})).to.equal(1); + expect(data.adUnits[0].kv.length).to.equal(5); + + delete bidderRequests[0].params.targeting; + }); + + it('should pass site.ext.data and user.ext.data as key values to ad server with targeting in different format', function () { + const ortb2 = { + user: { + ext: { + data: { + 'from': 'user', + '9090': 'from-user' + } + } + }, + site: { + ext: { + data: { + '12345': 'true', + '45678': 'true', + '9090': 'should-be-retained' + } + } + } + }; + bidderRequests[0].params.targeting = { + kv: [ + {'merge': ['this']}, + {'9090': ['take it over']} + ] + }; + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + const data = JSON.parse(request[0].data); + expect(countMatches(data.adUnits[0].kv, {'from': 'user'})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'9090': 'from-user'})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'9090': ['take it over']})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'merge': ['this']})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'9090': 'should-be-retained'})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'45678': 'true'})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'12345': 'true'})).to.equal(1); + expect(data.adUnits[0].kv.length).to.equal(7); + + delete bidderRequests[0].params.targeting; + }); + + it('should pass site data ext as key values to ad server even if no kv targeting specified in params.targeting', function () { + const ortb2 = { + site: { + ext: { + data: { + '12345': 'true', + '45678': 'true', + '9090': 'should-be-retained' + } + } + } + }; + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + const data = JSON.parse(request[0].data); + expect(countMatches(data.adUnits[0].kv, {'9090': 'should-be-retained'})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'45678': 'true'})).to.equal(1); + expect(countMatches(data.adUnits[0].kv, {'12345': 'true'})).to.equal(1); + expect(data.adUnits[0].kv.length).to.equal(3); + + delete bidderRequests[0].params.targeting; + }); + + it('should skip passing site ext if missing', function () { + const ortb2 = { + site: { + ext: { + } + } + }; + + delete bidderRequests[0].params.targeting; + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url'); + const data = JSON.parse(request[0].data); + expect(data.adUnits[0]).to.not.have.property('kv'); + }); + + it('should skip passing site ext data if missing', function () { + const ortb2 = { + site: { + ext: { + data: {} + } + } + }; + + delete bidderRequests[0].params.targeting; + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url'); + const data = JSON.parse(request[0].data); + expect(data.adUnits[0]).to.not.have.property('kv'); }); - it('should skip segments in config if not either id or array of strings', function() { + it('should skip segments in config if not either id or array of strings', function () { const ortb2 = { user: { data: [{ name: 'adnuntius', - segment: [{id: 'segment1'}, {id: 'segment2'}, {id: 'segment3'}] + segment: [{ id: 'segment1' }, { id: 'segment2' }, { id: 'segment3' }] }, { name: 'other', @@ -602,34 +1171,34 @@ describe('adnuntiusBidAdapter', function() { } }; - const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, {ortb2})); + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL_SEGMENTS); }); }); - describe('user privacy', function() { - it('should send GDPR Consent data if gdprApplies', function() { - let request = spec.buildRequests(bidderRequests, {gdprConsent: {gdprApplies: true, consentString: 'consentString'}}); + describe('user privacy', function () { + it('should send GDPR Consent data if gdprApplies', function () { + let request = spec.buildRequests(bidderRequests, { gdprConsent: { gdprApplies: true, consentString: 'consentString' } }); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL_CONSENT); }); - it('should not send GDPR Consent data if gdprApplies equals undefined', function() { - let request = spec.buildRequests(bidderRequests, {gdprConsent: {gdprApplies: undefined, consentString: 'consentString'}}); + it('should not send GDPR Consent data if gdprApplies equals undefined', function () { + let request = spec.buildRequests(bidderRequests, { gdprConsent: { gdprApplies: undefined, consentString: 'consentString' } }); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL); }); - it('should pass segments if available in config', function() { + it('should pass segments if available in config', function () { const ortb2 = { user: { data: [{ name: 'adnuntius', - segment: [{id: 'segment1'}, {id: 'segment2'}] + segment: [{ id: 'segment1' }, { id: 'segment2' }] }, { name: 'other', @@ -638,18 +1207,18 @@ describe('adnuntiusBidAdapter', function() { } } - const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, {ortb2})); + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL_SEGMENTS); }); - it('should skip segments in config if not either id or array of strings', function() { + it('should skip segments in config if not either id or array of strings', function () { const ortb2 = { user: { data: [{ name: 'adnuntius', - segment: [{id: 'segment1'}, {id: 'segment2'}, {id: 'segment3'}] + segment: [{ id: 'segment1' }, { id: 'segment2' }, { id: 'segment3' }] }, { name: 'other', @@ -660,20 +1229,20 @@ describe('adnuntiusBidAdapter', function() { } }; - const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, {ortb2})); + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL_SEGMENTS); }); - it('should user user ID if present in ortb2.user.id field', function() { + it('should user user ID if present in ortb2.user.id field', function () { const ortb2 = { user: { id: usi } }; - const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, {ortb2})); + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 })); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL); @@ -721,40 +1290,41 @@ describe('adnuntiusBidAdapter', function() { }); }); - describe('user privacy', function() { - it('should send GDPR Consent data if gdprApplies', function() { - let request = spec.buildRequests(bidderRequests, {gdprConsent: {gdprApplies: true, consentString: 'consentString'}}); + describe('user privacy', function () { + it('should send GDPR Consent data if gdprApplies', function () { + let request = spec.buildRequests(bidderRequests, { gdprConsent: { gdprApplies: true, consentString: 'consentString' } }); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL_CONSENT); }); - it('should not send GDPR Consent data if gdprApplies equals undefined', function() { - let request = spec.buildRequests(bidderRequests, {gdprConsent: {gdprApplies: undefined, consentString: 'consentString'}}); + it('should not send GDPR Consent data if gdprApplies equals undefined', function () { + let request = spec.buildRequests(bidderRequests, { gdprConsent: { gdprApplies: undefined, consentString: 'consentString' } }); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL); }); }); - describe('use cookie', function() { - it('should send noCookie in url if set to false.', function() { + describe('use cookie', function () { + it('should send noCookie in url if set to false.', function () { config.setBidderConfig({ bidders: ['adnuntius'], config: { - useCookie: false + useCookie: false, + advertiserTransparency: true } }); const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, {})); expect(request.length).to.equal(1); expect(request[0]).to.have.property('url') - expect(request[0].url).to.equal(ENDPOINT_URL_NOCOOKIE); + expect(request[0].url).to.equal(ENDPOINT_URL_NOCOOKIE + '&advertiserTransparency=true'); }); }); - describe('validate auId', function() { - it('should fail when auId is not hexadecimal', function() { + describe('validate auId', function () { + it('should fail when auId is not hexadecimal', function () { const invalidRequest = { bidId: 'adn-000000000008b6bc', bidder: 'adnuntius', @@ -766,7 +1336,7 @@ describe('adnuntiusBidAdapter', function() { expect(valid).to.equal(false); }); - it('should pass when auId is hexadecimal', function() { + it('should pass when auId is hexadecimal', function () { const invalidRequest = { bidId: 'adn-000000000008b6bc', bidder: 'adnuntius', @@ -779,8 +1349,8 @@ describe('adnuntiusBidAdapter', function() { }); }); - describe('request deals', function() { - it('Should set max deals.', function() { + describe('request deals', function () { + it('Should set max deals.', function () { config.setBidderConfig({ bidders: ['adnuntius'] }); @@ -797,7 +1367,7 @@ describe('adnuntiusBidAdapter', function() { expect(bidderRequests[1].params).to.not.have.property('maxBids'); expect(data.adUnits[1].maxDeals).to.equal(undefined); }); - it('Should allow a maximum of 5 deals.', function() { + it('Should allow a maximum of 5 deals.', function () { config.setBidderConfig({ bidders: ['adnuntius'], }); @@ -820,7 +1390,7 @@ describe('adnuntiusBidAdapter', function() { expect(data.adUnits.length).to.equal(1); expect(data.adUnits[0].maxDeals).to.equal(5); }); - it('Should allow a minumum of 0 deals.', function() { + it('Should allow a minimum of 0 deals.', function () { config.setBidderConfig({ bidders: ['adnuntius'], }); @@ -843,11 +1413,13 @@ describe('adnuntiusBidAdapter', function() { expect(data.adUnits.length).to.equal(1); expect(data.adUnits[0].maxDeals).to.equal(undefined); }); - it('Should set max deals using bidder config.', function() { + it('Should set max deals using bidder config.', function () { config.setBidderConfig({ bidders: ['adnuntius'], config: { - maxDeals: 2 + maxDeals: 2, + useCookie: 'ignore-this', + advertiserTransparency: 'ignore-this-as-well' } }); @@ -856,7 +1428,7 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL + '&ds=2'); }); - it('Should allow a maximum of 5 deals when using bidder config.', function() { + it('Should allow a maximum of 5 deals when using bidder config.', function () { config.setBidderConfig({ bidders: ['adnuntius'], config: { @@ -869,7 +1441,7 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL + '&ds=5'); }); - it('Should allow a minimum of 0 deals when using bidder config.', function() { + it('Should allow a minimum of 0 deals when using bidder config.', function () { config.setBidderConfig({ bidders: ['adnuntius'], config: { @@ -885,8 +1457,8 @@ describe('adnuntiusBidAdapter', function() { }); }); - describe('interpretResponse', function() { - it('should return valid response when passed valid server response', function() { + describe('interpretResponse', function () { + it('should return valid response when passed valid server response', function () { config.setBidderConfig({ bidders: ['adnuntius'], config: { @@ -907,8 +1479,9 @@ describe('adnuntiusBidAdapter', function() { expect(interpretedResponse[0].currency).to.equal(deal.bid.currency); expect(interpretedResponse[0].netRevenue).to.equal(false); expect(interpretedResponse[0].meta).to.have.property('advertiserDomains'); - expect(interpretedResponse[0].meta.advertiserDomains).to.have.lengthOf(1); - expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal('adnuntius.com'); + expect(interpretedResponse[0].meta.advertiserDomains).to.have.lengthOf(2); + expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal('fred.com'); + expect(interpretedResponse[0].meta.advertiserDomains[1]).to.equal('george.com'); expect(interpretedResponse[0].ad).to.equal(serverResponse.body.adUnits[0].deals[0].html); expect(interpretedResponse[0].ttl).to.equal(360); expect(interpretedResponse[0].dealId).to.equal('abc123xyz'); @@ -934,33 +1507,79 @@ describe('adnuntiusBidAdapter', function() { const usiEntry = results.find(entry => entry.key === 'usi' && entry.network === 'some-network-id'); expect(usiEntry.key).to.equal('usi'); expect(usiEntry.value).to.equal('from-api-server dude'); - expect(usiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90)); + expect(usiEntry.exp).to.be.greaterThan(getUnixTimestampFromNow(90)); expect(usiEntry.network).to.equal('some-network-id') const voidAuIdsEntry = results.find(entry => entry.key === 'voidAuIds'); expect(voidAuIdsEntry.key).to.equal('voidAuIds'); expect(voidAuIdsEntry.exp).to.equal(undefined); expect(voidAuIdsEntry.value[0].auId).to.equal('00000000000abcde'); - expect(voidAuIdsEntry.value[0].exp).to.be.greaterThan(misc.getUnixTimestamp()); - expect(voidAuIdsEntry.value[0].exp).to.be.lessThan(misc.getUnixTimestamp(2)); + expect(voidAuIdsEntry.value[0].exp).to.be.greaterThan(getUnixTimestampFromNow()); + expect(voidAuIdsEntry.value[0].exp).to.be.lessThan(getUnixTimestampFromNow(2)); expect(voidAuIdsEntry.value[1].auId).to.equal('00000000000fffff'); - expect(voidAuIdsEntry.value[1].exp).to.be.greaterThan(misc.getUnixTimestamp()); - expect(voidAuIdsEntry.value[1].exp).to.be.lessThan(misc.getUnixTimestamp(2)); + expect(voidAuIdsEntry.value[1].exp).to.be.greaterThan(getUnixTimestampFromNow()); + expect(voidAuIdsEntry.value[1].exp).to.be.lessThan(getUnixTimestampFromNow(2)); const validEntry = results.find(entry => entry.key === 'valid'); expect(validEntry.key).to.equal('valid'); expect(validEntry.value).to.equal('also-valid'); - expect(validEntry.exp).to.be.greaterThan(misc.getUnixTimestamp()); - expect(validEntry.exp).to.be.lessThan(misc.getUnixTimestamp(2)); + expect(validEntry.exp).to.be.greaterThan(getUnixTimestampFromNow()); + expect(validEntry.exp).to.be.lessThan(getUnixTimestampFromNow(2)); const randomApiEntry = results.find(entry => entry.key === 'randomApiKey'); expect(randomApiEntry.key).to.equal('randomApiKey'); expect(randomApiEntry.value).to.equal('randomApiValue'); expect(randomApiEntry.network).to.equal('some-network-id'); - expect(randomApiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90)); + expect(randomApiEntry.exp).to.be.greaterThan(getUnixTimestampFromNow(90)); + }); + + it('should return valid response when passed valid multiformat server response', function () { + config.setBidderConfig({ + bidders: ['adnuntius'], + config: { + bidType: 'netBid', + maxDeals: 0 + } + }); + + const interpretedResponse = config.runWithBidder('adnuntius', () => spec.interpretResponse(multiFormatServerResponse, multiBidAndFormatRequest)); + expect(interpretedResponse).to.have.lengthOf(3); + + let ad = multiFormatServerResponse.body.adUnits[0].ads[0]; + expect(interpretedResponse[0].bidderCode).to.equal('adnuntius'); + expect(interpretedResponse[0].cpm).to.equal(ad.netBid.amount * 1000); + expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[0].currency).to.equal(ad.bid.currency); + expect(interpretedResponse[0].netRevenue).to.equal(false); + expect(interpretedResponse[0].ad).to.equal(multiFormatServerResponse.body.adUnits[0].html); + expect(interpretedResponse[0].ttl).to.equal(360); + + ad = multiFormatServerResponse.body.adUnits[4].ads[0]; + expect(interpretedResponse[1].bidderCode).to.equal('adnuntius'); + expect(interpretedResponse[1].cpm).to.equal(ad.netBid.amount * 1000); + expect(interpretedResponse[1].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[1].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[1].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[1].currency).to.equal(ad.bid.currency); + expect(interpretedResponse[1].netRevenue).to.equal(false); + expect(interpretedResponse[1].ad).to.equal(multiFormatServerResponse.body.adUnits[4].html); + expect(interpretedResponse[1].ttl).to.equal(360); + + ad = multiFormatServerResponse.body.adUnits[2].ads[0]; + expect(interpretedResponse[2].native).to.not.be.undefined; + expect(interpretedResponse[2].bidderCode).to.equal('adnuntius'); + expect(interpretedResponse[2].cpm).to.equal(ad.netBid.amount * 1000); + expect(interpretedResponse[2].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[2].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[2].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[2].currency).to.equal(ad.bid.currency); + expect(interpretedResponse[2].netRevenue).to.equal(false); + expect(interpretedResponse[2].ttl).to.equal(360); }); - it('should not process valid response when passed alt bidder that is an adndeal', function() { + it('should not process valid response when passed alt bidder that is an adndeal', function () { const altBidder = { bid: [ { @@ -978,7 +1597,7 @@ describe('adnuntiusBidAdapter', function() { serverResponse.body.adUnits[0].deals = deals; }); - it('should return valid response when passed alt bidder', function() { + it('should return valid response when passed alt bidder', function () { const altBidder = { bid: [ { @@ -1015,8 +1634,8 @@ describe('adnuntiusBidAdapter', function() { }); }); - describe('interpretVideoResponse', function() { - it('should return valid response when passed valid server response', function() { + describe('interpretVideoResponse', function () { + it('should return valid response when passed valid server response', function () { const interpretedResponse = spec.interpretResponse(serverVideoResponse, videoBidRequest); const ad = serverVideoResponse.body.adUnits[0].ads[0] const deal = serverVideoResponse.body.adUnits[0].deals[0] @@ -1051,4 +1670,60 @@ describe('adnuntiusBidAdapter', function() { expect(interpretedResponse[1].dealCount).to.equal(0); }); }); + + describe('Native ads handling', function () { + it('should pass requests on correctly', function () { + const request = spec.buildRequests(nativeBidderRequest.bid, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"0000000000000551","targetId":"adn-0000000000000551","adType":"NATIVE","nativeRequest":{"ortb":{"assets":[{"id":1,"required":1,"img":{"type":3,"w":250,"h":250}}]}},"dimensions":[[200,200],[300,300]]}]}'); + }); + + it('should return valid response when passed valid server response', function () { + const interpretedResponse = spec.interpretResponse(nativeResponse, nativeBidderRequest); + const ad = nativeResponse.body.adUnits[0].ads[0] + expect(interpretedResponse).to.have.lengthOf(1); + + expect(interpretedResponse[0].bidderCode).to.equal('adnuntius'); + expect(interpretedResponse[0].cpm).to.equal(ad.bid.amount * 1000); + expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[0].currency).to.equal(ad.bid.currency); + expect(interpretedResponse[0].netRevenue).to.equal(false); + expect(interpretedResponse[0].meta).to.have.property('advertiserDomains'); + expect(interpretedResponse[0].meta.advertiserDomains).to.have.lengthOf(1); + expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal('adnuntius.com'); + expect(interpretedResponse[0].vastXml).to.equal(ad.vastXml); + expect(JSON.stringify(interpretedResponse[0].native.ortb)).to.equal('{"link":{"url":"https://whatever.com"},"assets":[{"id":1,"required":1,"img":{"url":"http://something.com/something.png"}}]}'); + }); + + it('should pass legacy requests on correctly', function () { + const request = spec.buildRequests(legacyNativeBidderRequest.bid, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"0000000000000551","targetId":"adn-0000000000000551","adType":"NATIVE","nativeRequest":{"ortb":{"ver":"1.2","assets":[{"id":0,"required":1,"img":{"type":3,"w":250,"h":250}}],"eventtrackers":[{"event":1,"methods":[1]},{"event":2,"methods":[1]}]}},"dimensions":[[200,200],[300,300]]}]}'); + }); + + it('should return valid legacy response when passed valid server response', function () { + const interpretedResponse = spec.interpretResponse(nativeResponse, legacyNativeBidderRequest); + const ad = nativeResponse.body.adUnits[0].ads[0] + expect(interpretedResponse).to.have.lengthOf(1); + + expect(interpretedResponse[0].bidderCode).to.equal('adnuntius'); + expect(interpretedResponse[0].cpm).to.equal(ad.bid.amount * 1000); + expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[0].currency).to.equal(ad.bid.currency); + expect(interpretedResponse[0].netRevenue).to.equal(false); + expect(interpretedResponse[0].meta).to.have.property('advertiserDomains'); + expect(interpretedResponse[0].meta.advertiserDomains).to.have.lengthOf(1); + expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal('adnuntius.com'); + expect(interpretedResponse[0].vastXml).to.equal(ad.vastXml); + expect(JSON.stringify(interpretedResponse[0].native)).to.equal('{"clickUrl":"https://whatever.com","icon":{"url":"http://something.com/something.png"},"impressionTrackers":[]}'); + }); + }); }); diff --git a/test/spec/modules/adoceanBidAdapter_spec.js b/test/spec/modules/adoceanBidAdapter_spec.js index 080b5bd5d1d..cff5e77d95b 100644 --- a/test/spec/modules/adoceanBidAdapter_spec.js +++ b/test/spec/modules/adoceanBidAdapter_spec.js @@ -36,13 +36,13 @@ describe('AdoceanAdapter', function () { }); it('should return false when required params are not passed', function () { - const bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + const invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'masterId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/adomikAnalyticsAdapter_spec.js b/test/spec/modules/adomikAnalyticsAdapter_spec.js deleted file mode 100644 index 703e6ed8992..00000000000 --- a/test/spec/modules/adomikAnalyticsAdapter_spec.js +++ /dev/null @@ -1,253 +0,0 @@ -import adomikAnalytics from 'modules/adomikAnalyticsAdapter.js'; -import { expect } from 'chai'; -import {EVENTS} from 'src/constants.js'; - -let events = require('src/events'); -let adapterManager = require('src/adapterManager').default; - -describe('Adomik Prebid Analytic', function () { - let sendEventStub; - let sendWonEventStub; - let clock; - - beforeEach(function () { - clock = sinon.useFakeTimers(); - sinon.spy(adomikAnalytics, 'track'); - sendEventStub = sinon.stub(adomikAnalytics, 'sendTypedEvent'); - sendWonEventStub = sinon.stub(adomikAnalytics, 'sendWonEvent'); - sinon.stub(events, 'getEvents').returns([]); - adomikAnalytics.currentContext = undefined; - - adapterManager.registerAnalyticsAdapter({ - code: 'adomik', - adapter: adomikAnalytics - }); - }); - - afterEach(function () { - adomikAnalytics.disableAnalytics(); - clock.restore(); - adomikAnalytics.track.restore(); - sendEventStub.restore(); - sendWonEventStub.restore(); - events.getEvents.restore(); - }); - - describe('adomikAnalytics.enableAnalytics', function () { - it('should catch all events', function (done) { - const initOptions = { - id: '123456', - url: 'testurl' - }; - - const bid = { - bidderCode: 'adomik_test_bid', - width: 10, - height: 10, - statusMessage: 'Bid available', - adId: '1234', - auctionId: '', - responseTimestamp: 1496410856397, - requestTimestamp: 1496410856295, - cpm: 0.1, - bidder: 'biddertest', - adUnitCode: '0000', - timeToRespond: 100, - placementCode: 'placementtest' - } - - // Step 1: Initialize adapter - adapterManager.enableAnalytics({ - provider: 'adomik', - options: initOptions - }); - expect(adomikAnalytics.currentContext).to.deep.equal({ - uid: '123456', - url: 'testurl', - sampling: undefined, - id: '', - timeouted: false - }); - - // Step 2: Send init auction event - events.emit(EVENTS.AUCTION_INIT, {config: initOptions, auctionId: 'test-test-test'}); - - expect(adomikAnalytics.currentContext).to.deep.equal({ - uid: '123456', - url: 'testurl', - sampling: undefined, - id: 'test-test-test', - timeouted: false - }); - - // Step 3: Send bid requested event - events.emit(EVENTS.BID_REQUESTED, { bids: [bid] }); - - expect(adomikAnalytics.bucketEvents.length).to.equal(1); - expect(adomikAnalytics.bucketEvents[0]).to.deep.equal({ - type: 'request', - event: { - bidder: 'BIDDERTEST', - placementCode: '0000', - } - }); - - // Step 4: Send bid response event - events.emit(EVENTS.BID_RESPONSE, bid); - - expect(adomikAnalytics.bucketEvents.length).to.equal(2); - expect(adomikAnalytics.bucketEvents[1]).to.deep.equal({ - type: 'response', - event: { - bidder: 'ADOMIK_TEST_BID', - placementCode: '0000', - id: '1234', - status: 'VALID', - cpm: 0.1, - size: { - width: 10, - height: 10 - }, - timeToRespond: 100, - afterTimeout: false, - } - }); - - // Step 5: Send bid won event - events.emit(EVENTS.BID_WON, bid); - - expect(adomikAnalytics.bucketEvents.length).to.equal(2); - - // Step 6: Send bid timeout event - events.emit(EVENTS.BID_TIMEOUT, {}); - - expect(adomikAnalytics.currentContext.timeouted).to.equal(true); - - // Step 7: Send auction end event - events.emit(EVENTS.AUCTION_END, {}); - - setTimeout(function() { - sinon.assert.callCount(sendEventStub, 1); - sinon.assert.callCount(sendWonEventStub, 1); - done(); - }, 3000); - - clock.tick(5000); - - sinon.assert.callCount(adomikAnalytics.track, 6); - }); - - describe('when sampling is undefined', function () { - beforeEach(function() { - adapterManager.enableAnalytics({ - provider: 'adomik', - options: { id: '123456', url: 'testurl' } - }); - }); - - it('is enabled', function () { - expect(adomikAnalytics.currentContext).is.not.null; - }); - }); - - describe('when sampling is 0', function () { - beforeEach(function() { - adapterManager.enableAnalytics({ - provider: 'adomik', - options: { id: '123456', url: 'testurl', sampling: 0 } - }); - }); - - it('is disabled', function () { - expect(adomikAnalytics.currentContext).to.equal(undefined); - }); - }); - - describe('when sampling is 1', function () { - beforeEach(function() { - adapterManager.enableAnalytics({ - provider: 'adomik', - options: { id: '123456', url: 'testurl', sampling: 1 } - }); - }); - - it('is enabled', function () { - expect(adomikAnalytics.currentContext).is.not.null; - }); - }); - - describe('when options is not defined', function () { - beforeEach(function() { - adapterManager.enableAnalytics({ provider: 'adomik' }); - }); - - it('is disabled', function () { - expect(adomikAnalytics.currentContext).to.equal(undefined); - }); - }); - - describe('when id is not defined in options', function () { - beforeEach(function() { - adapterManager.enableAnalytics({ provider: 'adomik', url: 'xxx' }); - }); - - it('is disabled', function () { - expect(adomikAnalytics.currentContext).to.equal(undefined); - }); - }); - - describe('when url is not defined in options', function () { - beforeEach(function() { - adapterManager.enableAnalytics({ provider: 'adomik', id: 'xxx' }); - }); - - it('is disabled', function () { - expect(adomikAnalytics.currentContext).to.equal(undefined); - }); - }); - }); - - describe('adomikAnalytics.getKeyValues', function () { - it('returns [undefined, undefined]', function () { - let [testId, testValue] = adomikAnalytics.getKeyValues() - expect(testId).to.equal(undefined); - expect(testValue).to.equal(undefined); - }); - - describe('when test is in scope', function () { - beforeEach(function () { - sessionStorage.setItem(window.location.hostname + '_AdomikTestInScope', true); - }); - - it('returns [undefined, undefined]', function () { - let [testId, testValue] = adomikAnalytics.getKeyValues() - expect(testId).to.equal(undefined); - expect(testValue).to.equal(undefined); - }); - - describe('when key values are defined', function () { - beforeEach(function () { - sessionStorage.setItem(window.location.hostname + '_AdomikTest', '{"testId":"12345","testOptionLabel":"1000"}'); - }); - - it('returns key values', function () { - let [testId, testValue] = adomikAnalytics.getKeyValues() - expect(testId).to.equal('12345'); - expect(testValue).to.equal('1000'); - }); - - describe('when preventTest is on', function () { - beforeEach(function () { - sessionStorage.setItem(window.location.hostname + '_NoAdomikTest', true); - }); - - it('returns [undefined, undefined]', function () { - let [testId, testValue] = adomikAnalytics.getKeyValues() - expect(testId).to.equal(undefined); - expect(testValue).to.equal(undefined); - }); - }); - }); - }); - }); -}); diff --git a/test/spec/modules/adotBidAdapter_spec.js b/test/spec/modules/adotBidAdapter_spec.js index 34252e00f9e..df628088bb0 100644 --- a/test/spec/modules/adotBidAdapter_spec.js +++ b/test/spec/modules/adotBidAdapter_spec.js @@ -124,7 +124,7 @@ describe('Adot Adapter', function () { it('should build request (video)', function () { const bidderRequestId = 'bidderRequestId'; - const validBidRequests = [{ bidderRequestId, mediaTypes: {} }, { bidderRequestId, bidId: 'bidId', mediaTypes: { video: { playerSize: [[300, 250]], minduration: 1, maxduration: 2, api: 'api', linearity: 'linearity', mimes: [], placement: 'placement', playbackmethod: 'playbackmethod', protocols: 'protocol', startdelay: 'startdelay' } }, params: { placementId: 'placementId', adUnitCode: 200 } }]; + const validBidRequests = [{ bidderRequestId, mediaTypes: {} }, { bidderRequestId, bidId: 'bidId', mediaTypes: { video: { playerSize: [[300, 250]], minduration: 1, maxduration: 2, api: 'api', linearity: 'linearity', mimes: [], plcmt: '1', playbackmethod: 'playbackmethod', protocols: 'protocol', startdelay: 'startdelay' } }, params: { placementId: 'placementId', adUnitCode: 200 } }]; const bidderRequest = { position: 2, refererInfo: { page: 'http://localhost.com', domain: 'localhost.com' }, gdprConsent: { consentString: 'consentString', gdprApplies: true }, userId: { pubProvidedId: 'userId' }, schain: { ver: '1.0' } }; const request = spec.buildRequests(validBidRequests, bidderRequest); @@ -144,7 +144,7 @@ describe('Adot Adapter', function () { maxduration: 2, mimes: [], minduration: 1, - placement: 'placement', + placement: '1', playbackmethod: 'playbackmethod', pos: 0, protocols: 'protocol', diff --git a/test/spec/modules/adpartnerBidAdapter_spec.js b/test/spec/modules/adpartnerBidAdapter_spec.js index d9f9b0d0074..597974acce8 100644 --- a/test/spec/modules/adpartnerBidAdapter_spec.js +++ b/test/spec/modules/adpartnerBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec, ENDPOINT_PROTOCOL, ENDPOINT_DOMAIN, ENDPOINT_PATH} from 'modules/adpartnerBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import * as miUtils from 'libraries/mediaImpactUtils/index.js'; const BIDDER_CODE = 'adpartner'; @@ -117,7 +118,7 @@ describe('AdpartnerAdapter', function () { describe('joinSizesToString', function () { it('success convert sizes list to string', function () { - const sizesStr = spec.joinSizesToString([[300, 250], [300, 600]]); + const sizesStr = miUtils.joinSizesToString([[300, 250], [300, 600]]); expect(sizesStr).to.equal('300x250|300x600'); }); }); @@ -238,7 +239,7 @@ describe('AdpartnerAdapter', function () { let ajaxStub; beforeEach(() => { - ajaxStub = sinon.stub(spec, 'postRequest') + ajaxStub = sinon.stub(miUtils, 'postRequest') }) afterEach(() => { diff --git a/test/spec/modules/adprimeBidAdapter_spec.js b/test/spec/modules/adprimeBidAdapter_spec.js index 5efed4ec5ab..c185ef3c1ad 100644 --- a/test/spec/modules/adprimeBidAdapter_spec.js +++ b/test/spec/modules/adprimeBidAdapter_spec.js @@ -1,131 +1,261 @@ -import {expect} from 'chai'; -import {spec} from '../../../modules/adprimeBidAdapter.js'; -import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import { expect } from 'chai'; +import { spec } from '../../../modules/adprimeBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; -describe('AdprimebBidAdapter', function () { - const bid = { - bidId: '23fhj33i987f', - bidder: 'adprime', +const bidder = 'adprime'; + +describe('AdprimeBidAdapter', 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]], + [BANNER]: { + sizes: [[300, 250]] } }, params: { - placementId: 'testBanner' + } - }; + } const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'test.com' - } + 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 placementId parameters present', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + 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 () { - delete bid.params.placementId; - expect(spec.isBidRequestValid(bid)).to.be.false; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; }); }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid], bidderRequest); + 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 valid URL', function () { expect(serverRequest.url).to.equal('https://delta.adprime.com/pbjs'); }); - it('Returns valid data if array of bids is valid', function () { + + it('Returns general data valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + 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.gdpr).to.not.exist; - expect(data.ccpa).to.not.exist; - let placement = data['placements'][0]; - expect(placement).to.have.keys('placementId', 'bidId', 'identeties', 'adFormat', 'sizes', 'hPlayer', 'wPlayer', 'schain', 'keywords', 'audiences', 'bidFloor'); - expect(placement.placementId).to.equal('testBanner'); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.adFormat).to.equal(BANNER); - expect(placement.schain).to.be.an('object'); + 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 data for mediatype video', function () { - const playerSize = [300, 300]; - bid.mediaTypes = {}; - bid.mediaTypes[VIDEO] = { - playerSize - }; - serverRequest = spec.buildRequests([bid], bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - let placement = data['placements'][0]; - expect(placement).to.be.an('object'); - expect(placement.adFormat).to.equal(VIDEO); - expect(placement.wPlayer).to.equal(playerSize[0]); - expect(placement.hPlayer).to.equal(playerSize[1]); + 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.wPlayer).to.be.an('number'); + expect(placement.hPlayer).to.be.an('number'); + expect(placement.keywords).to.be.an('array'); + expect(placement.audiences).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 () { - bidderRequest.gdprConsent = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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 = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); - }); - describe('buildRequests with user ids', function () { - bid.userId = {} - bid.userId.idl_env = 'idl_env123'; - let serverRequest = spec.buildRequests([bid], bidderRequest); - it('Return bids with user identeties', function () { + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - let placements = data['placements']; expect(data).to.be.an('object'); - for (let i = 0; i < placements.length; i++) { - let placement = placements[i]; - expect(placement).to.have.property('identeties') - expect(placement.identeties).to.be.an('object') - expect(placement.identeties).to.have.property('identityLink') - expect(placement.identeties.identityLink).to.be.equal('idl_env123') - } - }); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { @@ -141,7 +271,10 @@ describe('AdprimebBidAdapter', function () { netRevenue: true, currency: 'USD', dealId: '1', - meta: {} + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let bannerResponses = spec.interpretResponse(banner); @@ -149,15 +282,15 @@ describe('AdprimebBidAdapter', function () { let 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('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.width).to.equal(300); - expect(dataItem.height).to.equal(250); - expect(dataItem.ad).to.equal('Test'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); + 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('USD'); + 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 () { @@ -172,7 +305,10 @@ describe('AdprimebBidAdapter', function () { netRevenue: true, currency: 'USD', dealId: '1', - meta: {} + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let videoResponses = spec.interpretResponse(video); @@ -206,7 +342,10 @@ describe('AdprimebBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - meta: {} + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let nativeResponses = spec.interpretResponse(native); @@ -293,6 +432,7 @@ describe('AdprimebBidAdapter', function () { expect(serverResponses).to.be.an('array').that.is.empty; }); }); + describe('getUserSyncs', function() { it('Should return array of objects with proper sync config , include GDPR', function() { const syncData = spec.getUserSyncs({}, {}, { @@ -317,5 +457,17 @@ describe('AdprimebBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://sync.adprime.com/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.adprime.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/adqueryIdSystem_spec.js b/test/spec/modules/adqueryIdSystem_spec.js index 7952f23189e..9b7304d1984 100644 --- a/test/spec/modules/adqueryIdSystem_spec.js +++ b/test/spec/modules/adqueryIdSystem_spec.js @@ -1,6 +1,9 @@ import {adqueryIdSubmodule, storage} from 'modules/adqueryIdSystem.js'; import {server} from 'test/mocks/xhr.js'; import sinon from 'sinon'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; const config = { storage: { @@ -58,4 +61,23 @@ describe('AdqueryIdSystem', function () { expect(callbackSpy.lastCall.lastArg).to.deep.equal('testqid'); }); }); + describe('eid', () => { + before(() => { + attachIdSystem(adqueryIdSubmodule); + }); + it('qid', function() { + const userId = { + qid: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'adquery.io', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + }) }); diff --git a/test/spec/modules/adrelevantisBidAdapter_spec.js b/test/spec/modules/adrelevantisBidAdapter_spec.js index 7f24176e850..db637663f39 100644 --- a/test/spec/modules/adrelevantisBidAdapter_spec.js +++ b/test/spec/modules/adrelevantisBidAdapter_spec.js @@ -34,12 +34,12 @@ describe('AdrelevantisAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placementId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/ads_interactiveBidAdapter_spec.js b/test/spec/modules/ads_interactiveBidAdapter_spec.js new file mode 100644 index 00000000000..a0b8c67af93 --- /dev/null +++ b/test/spec/modules/ads_interactiveBidAdapter_spec.js @@ -0,0 +1,519 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/ads_interactiveBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'ads_interactive'; + +describe('AdsInteractiveBidAdapter', 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 valid URL', function () { + expect(serverRequest.url).to.equal('https://bntb.adsinteractive.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.include.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 + } + ]; + + let 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); + let 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); + let 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] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + + 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 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let 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 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let 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' + }] + }; + + let 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' + }] + }; + let 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', + }] + }; + let 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' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cstb.adsinteractive.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cstb.adsinteractive.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cstb.adsinteractive.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/adspiritBidAdapter_spec.js b/test/spec/modules/adspiritBidAdapter_spec.js index 022a26da60e..e41cf72abf7 100644 --- a/test/spec/modules/adspiritBidAdapter_spec.js +++ b/test/spec/modules/adspiritBidAdapter_spec.js @@ -3,14 +3,15 @@ import { spec } from 'modules/adspiritBidAdapter.js'; import * as utils from 'src/utils.js'; import { registerBidder } from 'src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from 'src/mediaTypes.js'; +const { getWinDimensions } = utils; const RTB_URL = '/rtb/getbid.php?rtbprovider=prebid'; const SCRIPT_URL = '/adasync.min.js'; -describe('Adspirit Bidder Spec', function () { +describe('Adspirit Bidder Spec', function () { // isBidRequestValid ---case describe('isBidRequestValid', function () { it('should return true if the bid request is valid', function () { - const validBid = { bidder: 'adspirit', params: { placementId: '57', host: 'test.adspirit.de' } }; + const validBid = { bidder: 'adspirit', params: { placementId: '99', host: 'test.adspirit.de' } }; const result = spec.isBidRequestValid(validBid); expect(result).to.be.true; }); @@ -41,252 +42,425 @@ describe('Adspirit Bidder Spec', function () { expect(result).to.be.null; }); }); + // getScriptUrl + describe('Adspirit Bid Adapter', function () { + describe('getScriptUrl', function () { + it('should return the correct script URL', function () { + expect(spec.getScriptUrl()).to.equal('/adasync.min.js'); + }); + }); + }); // Test cases for buildRequests - describe('buildRequests', function () { - const bidRequestWithGDPRAndSchain = [ - { - id: '26c1ee0038ac11', - bidder: 'adspirit', - params: { - placementId: '57' - }, - schain: { - ver: '1.0', - nodes: [ - { - asi: 'exchange1.com', - sid: '1234', - hp: 1, - rid: 'bidRequest123', - name: 'Publisher', - domain: 'publisher.com' - }, - { - asi: 'network1.com', - sid: '5678', - hp: 1, - rid: 'bidderRequest123', - name: 'Network', - domain: 'network1.com' - } - ] - } - } - ]; - - const mockBidderRequestWithGDPR = { - refererInfo: { - topmostLocation: 'test.adspirit.de' - }, - gdprConsent: { - gdprApplies: true, - consentString: 'consentString' - }, - schain: { - ver: '1.0', - nodes: [ - { - asi: 'network1.com', - sid: '5678', - hp: 1, - rid: 'bidderRequest123', - name: 'Network', - domain: 'network1.com' - } - ] - } - }; + describe('Adspirit Bidder Spec', function () { + let originalInnerWidth; + let originalInnerHeight; + let originalClientWidth; + let originalClientHeight; - it('should construct valid bid requests with GDPR consent and schain', function () { - const requests = spec.buildRequests(bidRequestWithGDPRAndSchain, mockBidderRequestWithGDPR); - expect(requests).to.be.an('array').that.is.not.empty; - const request = requests[0]; - expect(request.method).to.equal('GET'); - expect(request.url).to.include('test.adspirit.de'); - expect(request.url).to.include('pid=57'); - expect(request.data).to.have.property('schain'); - expect(request.data.schain).to.be.an('object'); - if (request.data.schain && Array.isArray(request.data.schain.nodes)) { - const nodeWithGdpr = request.data.schain.nodes.find(node => node.gdpr); - if (nodeWithGdpr) { - expect(nodeWithGdpr).to.have.property('gdpr'); - expect(nodeWithGdpr.gdpr).to.be.an('object'); - expect(nodeWithGdpr.gdpr).to.have.property('applies', true); - expect(nodeWithGdpr.gdpr).to.have.property('consent', 'consentString'); + beforeEach(() => { + originalInnerWidth = window.innerWidth; + originalInnerHeight = window.innerHeight; + originalClientWidth = document.documentElement.clientWidth; + originalClientHeight = document.documentElement.clientHeight; + + Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1024 }); + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 768 }); + Object.defineProperty(document.documentElement, 'clientWidth', { writable: true, configurable: true, value: 800 }); + Object.defineProperty(document.documentElement, 'clientHeight', { writable: true, configurable: true, value: 600 }); + }); + + afterEach(() => { + Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: originalInnerWidth }); + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: originalInnerHeight }); + Object.defineProperty(document.documentElement, 'clientWidth', { writable: true, configurable: true, value: originalClientWidth }); + Object.defineProperty(document.documentElement, 'clientHeight', { writable: true, configurable: true, value: originalClientHeight }); + }); + + it('should correctly capture window and document dimensions in payload', function () { + const bidRequest = [ + { + bidId: '26c1ee0038ac11', + bidder: 'adspirit', + params: { placementId: '99', host: 'test.adspirit.de' }, + adUnitCode: 'banner-div', + mediaTypes: { + banner: { sizes: [[300, 250]] } + } } - } + ]; + const mockBidderRequest = { refererInfo: { topmostLocation: 'https://test.adspirit.com' } }; + const requests = spec.buildRequests(bidRequest, mockBidderRequest); + const request = requests[0]; + const requestData = JSON.parse(request.data); }); - it('should construct valid bid requests without GDPR consent and schain', function () { - const bidRequestWithoutGDPR = [ + it('should correctly fall back to document dimensions if window dimensions are not available', function () { + const bidRequest = [ { - id: '26c1ee0038ac11', + bidId: '26c1ee0038ac11', bidder: 'adspirit', - params: { - placementId: '57' + params: { placementId: '99', host: 'test.adspirit.de' }, + adUnitCode: 'banner-div', + mediaTypes: { + banner: { sizes: [[300, 250]] } + } + } + ]; + const mockBidderRequest = { refererInfo: { topmostLocation: 'https://test.adspirit.com' } }; + delete global.window.innerWidth; + delete global.window.innerHeight; + const requests = spec.buildRequests(bidRequest, mockBidderRequest); + const request = requests[0]; + const requestData = JSON.parse(request.data); + }); + it('should correctly add GDPR consent parameters to the request', function () { + const bidRequest = [ + { + bidId: '26c1ee0038ac11', + bidder: 'adspirit', + params: { placementId: '99', host: 'test.adspirit.de' }, + adUnitCode: 'banner-div', + mediaTypes: { + banner: { sizes: [[300, 250]] } } } ]; - const mockBidderRequestWithoutGDPR = { - refererInfo: { - topmostLocation: 'test.adspirit.de' + const mockBidderRequest = { + refererInfo: { topmostLocation: 'https://test.adspirit.com' }, + gdprConsent: { + gdprApplies: true, + consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA' } }; - const requests = spec.buildRequests(bidRequestWithoutGDPR, mockBidderRequestWithoutGDPR); - expect(requests).to.be.an('array').that.is.not.empty; + const requests = spec.buildRequests(bidRequest, mockBidderRequest); + const request = requests[0]; + expect(request.url).to.include('&gdpr=1'); + expect(request.url).to.include('&gdpr_consent=BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA'); + const requestData = JSON.parse(request.data); + expect(requestData.regs.ext.gdpr).to.equal(1); + expect(requestData.regs.ext.gdpr_consent).to.equal(mockBidderRequest.gdprConsent.consentString); + }); + + it('should correctly include schain in the OpenRTB request if provided', function () { + const bidRequest = [ + { + bidId: '26c1ee0038ac11', + bidder: 'adspirit', + params: { placementId: '99', host: 'test.adspirit.de' }, + adUnitCode: 'banner-div', + mediaTypes: { + banner: { sizes: [[300, 250]] } + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'publisher.com', + sid: '1234', + hp: 1 + } + ] + } + } + ]; + + const mockBidderRequest = { refererInfo: { topmostLocation: 'https://test.adspirit.com' } }; + const requests = spec.buildRequests(bidRequest, mockBidderRequest); const request = requests[0]; - expect(request.method).to.equal('GET'); - expect(request.url).to.include('test.adspirit.de'); - expect(request.url).to.include('pid=57'); - expect(request.data).to.deep.equal({}); + const requestData = JSON.parse(request.data); + expect(requestData.source).to.exist; + expect(requestData.source.ext).to.exist; + expect(requestData.source.ext.schain).to.deep.equal(bidRequest[0].schain); + }); + it('should correctly handle bidfloor values (valid, missing, and non-numeric)', function () { + const bidRequest = [ + { + bidId: 'validBidfloor', + bidder: 'adspirit', + params: { placementId: '99', host: 'test.adspirit.de', bidfloor: '1.23' }, + adUnitCode: 'banner-div', + mediaTypes: { + banner: { sizes: [[300, 250]] } + } + }, + { + bidId: 'missingBidfloor', + bidder: 'adspirit', + params: { placementId: '100', host: 'test.adspirit.de' }, + adUnitCode: 'banner-div', + mediaTypes: { + banner: { sizes: [[300, 250]] } + } + }, + { + bidId: 'invalidBidfloor', + bidder: 'adspirit', + params: { placementId: '101', host: 'test.adspirit.de', bidfloor: 'abc' }, + adUnitCode: 'banner-div', + mediaTypes: { + banner: { sizes: [[300, 250]] } + } + } + ]; + + const mockBidderRequest = { refererInfo: { topmostLocation: 'https://test.adspirit.com' } }; + const requests = spec.buildRequests(bidRequest, mockBidderRequest); + const requestData = requests.map(req => JSON.parse(req.data)); + expect(requestData[0].imp[0].bidfloor).to.equal(1.23); + expect(requestData[1].imp[0].bidfloor).to.equal(0); + expect(requestData[2].imp[0].bidfloor || 0).to.equal(0); + }); + it('should correctly add and handle banner/native media types', function () { + const bidRequest = [ + { + bidId: 'validBannerNative', + bidder: 'adspirit', + params: { placementId: '99', host: 'test.adspirit.de' }, + adUnitCode: 'test-div', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + native: { + ortb: { + request: { + assets: [{ id: 1, required: 1, title: { len: 100 } }] + } + } + } + } + }, + { + bidId: 'noBanner', + bidder: 'adspirit', + params: { placementId: '100', host: 'test.adspirit.de' }, + adUnitCode: 'no-banner-div', + mediaTypes: { + banner: {} + } + }, + { + bidId: 'emptyNative', + bidder: 'adspirit', + params: { placementId: '101', host: 'test.adspirit.de' }, + adUnitCode: 'empty-native-div', + mediaTypes: { + native: { + ortb: { + request: { + assets: [] + } + } + } + } + } + ]; + + const mockBidderRequest = { refererInfo: { topmostLocation: 'https://test.adspirit.com' } }; + const requests = spec.buildRequests(bidRequest, mockBidderRequest); + const requestData = requests.map(req => JSON.parse(req.data)); + + expect(requestData[0].imp[0]).to.have.property('banner'); + expect(requestData[0].imp[0].banner.format).to.deep.equal([{ w: 300, h: 250 }]); + + expect(requestData[0].imp[0]).to.have.property('native'); + expect(JSON.parse(requestData[0].imp[0].native.request).assets).to.deep.equal([ + { id: 1, required: 1, title: { len: 100 } }, + { id: 2, required: 1, img: { type: 3, wmin: 1200, hmin: 627, mimes: ['image/png', 'image/gif', 'image/jpeg'] } }, + { id: 4, required: 1, data: { type: 2, len: 150 } }, + { id: 3, required: 0, data: { type: 12, len: 50 } }, + { id: 6, required: 0, data: { type: 1, len: 50 } }, + { id: 5, required: 0, img: { type: 1, wmin: 50, hmin: 50, mimes: ['image/png', 'image/gif', 'image/jpeg'] } } + ]); + + expect(requestData[1].imp[0]).to.not.have.property('banner'); + + expect(requestData[2].imp[0]).to.have.property('native'); + expect(JSON.parse(requestData[2].imp[0].native.request).assets).to.deep.equal([ + { id: 1, required: 1, title: { len: 100 } }, + { id: 2, required: 1, img: { type: 3, wmin: 1200, hmin: 627, mimes: ['image/png', 'image/gif', 'image/jpeg'] } }, + { id: 4, required: 1, data: { type: 2, len: 150 } }, + { id: 3, required: 0, data: { type: 12, len: 50 } }, + { id: 6, required: 0, data: { type: 1, len: 50 } }, + { id: 5, required: 0, img: { type: 1, wmin: 50, hmin: 50, mimes: ['image/png', 'image/gif', 'image/jpeg'] } } + ]); }); }); - // interpretResponse For Native + // interpretResponse describe('interpretResponse', function () { - const nativeBidRequestMock = { + const validBidRequestMock = { bidRequest: { bidId: '123456', + bidder: 'adspirit', params: { placementId: '57', adomain: ['test.adspirit.de'] - }, - mediaTypes: { - native: true } } }; - it('should handle native media type bids and missing cpm in the server response body', function () { - const serverResponse = { - body: { - w: 320, - h: 50, - title: 'Ad Title', - body: 'Ad Body', - cta: 'Click Here', - image: 'img_url', - click: 'click_url', - view: 'view_tracker_url' - } - }; + it('should return an empty array when serverResponse is missing', function () { + const result = spec.interpretResponse(null, validBidRequestMock); + expect(result).to.be.an('array').that.is.empty; + }); - const result = spec.interpretResponse(serverResponse, nativeBidRequestMock); - expect(result.length).to.equal(0); + it('should return an empty array when serverResponse.body is missing', function () { + const result = spec.interpretResponse({}, validBidRequestMock); + expect(result).to.be.an('array').that.is.empty; }); - it('should handle native media type bids', function () { + it('should correctly parse a valid banner ad response', function () { const serverResponse = { body: { - cpm: 1.0, - w: 320, - h: 50, - title: 'Ad Title', - body: 'Ad Body', - cta: 'Click Here', - image: 'img_url', - click: 'click_url', - view: 'view_tracker_url' + cpm: 2.0, + w: 728, + h: 90, + adm: '
Banner Ad Content
', + adomain: ['siva.adspirit.de'] } }; - const result = spec.interpretResponse(serverResponse, nativeBidRequestMock); + const result = spec.interpretResponse(serverResponse, validBidRequestMock); expect(result.length).to.equal(1); const bid = result[0]; expect(bid).to.include({ requestId: '123456', - cpm: 1.0, - width: 320, - height: 50, - creativeId: '57', + cpm: 2.0, + width: 728, + height: 90, currency: 'EUR', netRevenue: true, - ttl: 300, - mediaType: 'native' + ttl: 300 }); - expect(bid.native).to.deep.include({ - title: 'Ad Title', - body: 'Ad Body', - cta: 'Click Here', - image: { url: 'img_url' }, - clickUrl: 'click_url', - impressionTrackers: ['view_tracker_url'] - }); - }); - - const bannerBidRequestMock = { - bidRequest: { - bidId: '123456', - params: { - placementId: '57', - adomain: ['siva.adspirit.de'] - }, - mediaTypes: { - banner: true - } - } - }; - - // Test cases for various scenarios - it('should return empty array when serverResponse is missing', function () { - const result = spec.interpretResponse(null, { bidRequest: {} }); - expect(result).to.be.an('array').that.is.empty; - }); - - it('should return empty array when serverResponse.body is missing', function () { - const result = spec.interpretResponse({}, { bidRequest: {} }); - expect(result).to.be.an('array').that.is.empty; - }); - - it('should return empty array when bidObj is missing', function () { - const result = spec.interpretResponse({ body: { cpm: 1.0 } }, { bidRequest: null }); - expect(result).to.be.an('array').that.is.empty; - }); - it('should return empty array when all required parameters are missing', function () { - const result = spec.interpretResponse(null, { bidRequest: null }); - expect(result).to.be.an('array').that.is.empty; + expect(bid).to.have.property('mediaType', 'banner'); + expect(bid.ad).to.include(''); + expect(bid.ad).to.include('
Banner Ad Content
'); }); - it('should handle banner media type bids and missing cpm in the server response body', function () { - const serverResponseBanner = { + it('should return empty array if banner ad response has missing CPM', function () { + const serverResponse = { body: { w: 728, h: 90, adm: '
Ad Content
' } }; - const result = spec.interpretResponse(serverResponseBanner, bannerBidRequestMock); + const result = spec.interpretResponse(serverResponse, validBidRequestMock); expect(result.length).to.equal(0); }); - it('should handle banner media type bids', function () { + it('should correctly handle default values for width, height, creativeId, currency, and advertiserDomains', function () { const serverResponse = { body: { - cpm: 2.0, - w: 728, - h: 90, - adm: '
Ad Content
' + seatbid: [{ + bid: [{ + impid: '123456', + price: 1.8, + crid: undefined, + w: undefined, + h: undefined, + adomain: undefined + }] + }], + cur: undefined } }; - const result = spec.interpretResponse(serverResponse, bannerBidRequestMock); + + const validBidRequestMock = { + bidRequest: { + bidId: '987654', + params: { placementId: '57' } + } + }; + + const result = spec.interpretResponse(serverResponse, validBidRequestMock); expect(result.length).to.equal(1); + const bid = result[0]; - expect(bid).to.include({ - requestId: '123456', - cpm: 2.0, - width: 728, - height: 90, - creativeId: '57', - currency: 'EUR', - netRevenue: true, - ttl: 300, - mediaType: 'banner' + + expect(bid.width).to.equal(1); + expect(bid.height).to.equal(1); + + expect(bid.creativeId).to.equal('123456'); + expect(bid.currency).to.equal('EUR'); + expect(bid.meta.advertiserDomains).to.deep.equal([]); + }); + + it('should correctly parse a valid native ad response, ensuring all assets are loaded dynamically with extra fields', function () { + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + impid: '123456', + price: 1.5, + w: 320, + h: 50, + crid: 'creative789', + adomain: ['test.adspirit.de'], + adm: JSON.stringify({ + native: { + assets: [ + { id: 1, title: { text: 'Primary Title' } }, + { id: 4, data: { value: 'Main Description' } }, + { id: 4, data: { value: 'Extra Description' } }, + { id: 3, data: { value: 'Main CTA' } }, + { id: 3, data: { value: 'Additional CTA' } }, + { id: 2, img: { url: 'https://example.com/main-image.jpg', w: 100, h: 100 } }, + { id: 2, img: { url: 'https://example.com/extra-image.jpg', w: 200, h: 200 } }, + { id: 5, img: { url: 'https://example.com/icon-main.jpg', w: 50, h: 50 } }, + { id: 5, img: { url: 'https://example.com/icon-extra.jpg', w: 60, h: 60 } }, + { id: 6, data: { value: 'Main Sponsor' } }, + { id: 6, data: { value: 'Secondary Sponsor' } } + ], + link: { url: 'https://clickurl.com' }, + imptrackers: ['https://tracker.com/impression'] + } + }) + }] + }], + cur: 'EUR' + } + }; + + const validBidRequestMock = { + bidRequest: { + bidId: '987654', + params: { placementId: '57' } + } + }; + + const result = spec.interpretResponse(serverResponse, validBidRequestMock); + expect(result.length).to.equal(1); + + const bid = result[0]; + + expect(bid.native.title).to.equal('Primary Title'); + expect(bid.native.body).to.equal('Main Description'); + expect(bid.native['data_4_extra1']).to.equal('Extra Description'); + + expect(bid.native.cta).to.equal('Main CTA'); + expect(bid.native['data_3_extra1']).to.equal('Additional CTA'); + + expect(bid.native.sponsoredBy).to.equal('Main Sponsor'); + expect(bid.native['data_6_extra1']).to.equal('Secondary Sponsor'); + expect(bid.native.image.url).to.equal('https://example.com/main-image.jpg'); + expect(bid.native['image_2_extra1']).to.deep.equal({ + url: 'https://example.com/extra-image.jpg', + width: 200, + height: 200 + }); + + expect(bid.native.icon.url).to.equal('https://example.com/icon-main.jpg'); + expect(bid.native['image_5_extra1']).to.deep.equal({ + url: 'https://example.com/icon-extra.jpg', + width: 60, + height: 60 }); - expect(bid.ad).to.equal('
Ad Content
'); + expect(bid.native.impressionTrackers).to.deep.equal(['https://tracker.com/impression']); }); }); }); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 0acbaa06f5b..b12744d3a28 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -16,8 +16,8 @@ const aliasEP = { 'janet': 'https://ghb.bidder.jmgads.com/v2/auction/', 'ocm': 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', '9dotsmedia': 'https://ghb.platform.audiodots.com/v2/auction/', - 'copper6': 'https://ghb.app.copper6.com/v2/auction/', 'indicue': 'https://ghb.console.indicue.com/v2/auction/', + 'stellormedia': 'https://ghb.ads.stellormedia.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; diff --git a/test/spec/modules/adtrgtmeBidAdapter_spec.js b/test/spec/modules/adtrgtmeBidAdapter_spec.js index fce270b4ea7..925f5763c5d 100644 --- a/test/spec/modules/adtrgtmeBidAdapter_spec.js +++ b/test/spec/modules/adtrgtmeBidAdapter_spec.js @@ -1,249 +1,238 @@ import { expect } from 'chai'; import { config } from 'src/config.js'; -import { BANNER } from 'src/mediaTypes.js'; import { spec } from 'modules/adtrgtmeBidAdapter.js'; -const DEFAULT_SID = 1220291391; -const DEFAULT_ZID = 1836455615; -const DEFAULT_BID_ID = '84ab500420319d'; +const DEFAULT_SID = '1220291391'; +const DEFAULT_ZID = '1836455615'; +const DEFAULT_PIXEL_URL = 'https://cdn.adtarget.me/libs/1x1.gif'; +const DEFAULT_BANNER_URL = 'https://cdn.adtarget.me/libs/banner/300x250.jpg'; +const BIDDER_VERSION = '1.0.6'; +const PREBIDJS_VERSION = '$prebid.version$'; -const DEFAULT_AD_UNIT_CODE = '/1220291391/header-banner'; -const DEFAULT_AD_UNIT_TYPE = BANNER; -const DEFAULT_PARAMS_BID_OVERRIDE = {}; - -const ADAPTER_VERSION = '1.0.0'; -const PREBID_VERSION = '$prebid.version$'; -const INTEGRATION_METHOD = 'prebid.js'; - -// Utility functions -const generateBidRequest = ({bidId, adUnitCode, bidOverrideObject, zid, ortb2}) => { - const bidRequest = { +const createBidRequest = ({bidId, adUnitCode, bidOverride, zid, ortb2}) => { + const bR = { + auctionId: 'f3c594t-3o0ch1b0rm-ayn93c3o0ch1b0rm', adUnitCode, - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidId, - bidderRequestsCount: 1, bidder: 'adtrgtme', - bidderRequestId: '7101db09af0db2', - bidderWinsCount: 0, mediaTypes: {}, params: { - bidOverride: bidOverrideObject + sid: DEFAULT_SID, + bidOverride }, - src: 'client', transactionId: '5b17b67d-7704-4732-8cc9-5b1723e9bcf9', ortb2 }; - const bannerObj = { + bR.mediaTypes.banner = { sizes: [[300, 250]] }; + bR.sizes = [[300, 250]]; - bidRequest.mediaTypes.banner = bannerObj; - bidRequest.sizes = [[300, 250]]; - - bidRequest.params.sid = DEFAULT_SID; - if (typeof zid == 'number') { - bidRequest.params.zid = zid; + if (typeof zid == 'string') { + bR.params.zid = zid; } - - return bidRequest; + return bR; } -let generateBidderRequest = (bidRequestArray, adUnitCode, ortb2 = {}) => { - const bidderRequest = { - adUnitCode: adUnitCode || 'default-adUnitCode', +let createBidderRequest = (arr, code = 'default-code', ortb2 = {}) => { + return { + adUnitCode: code, auctionId: 'd4c83a3b-18e4-4208-b98b-63848449c7aa', - auctionStart: new Date().getTime(), bidderCode: 'adtrgtme', - bidderRequestId: '112f1c7c5d399a', - bids: bidRequestArray, + bids: arr, refererInfo: { - page: 'https://publisher-test.com', - reachedTop: true, - isAmp: false, - numIframes: 0, - stack: ['https://publisher-test.com'], + page: 'https://partner-site.com', + stack: ['https://partner-site.com'], }, gdprConsent: { consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', vendorData: {}, gdprApplies: true }, - start: new Date().getTime(), timeout: 1000, ortb2 }; - - return bidderRequest; }; -const generateBuildRequestMock = ({bidId, adUnitCode, adUnitType, zid, bidOverrideObject, pubIdMode, ortb2}) => { - const bidRequestConfig = { - bidId: bidId || DEFAULT_BID_ID, - adUnitCode: adUnitCode || DEFAULT_AD_UNIT_CODE, - adUnitType: adUnitType || DEFAULT_AD_UNIT_TYPE, +const createRequestMock = ({bidId, adUnitCode, type, zid, bidOverride, pubIdMode, ortb2}) => { + const bR = createBidRequest({ + bidId: bidId || '84ab500420319d', + adUnitCode: adUnitCode || '/1220291391/banner', + type: type || 'banner', zid: zid || DEFAULT_ZID, - bidOverrideObject: bidOverrideObject || DEFAULT_PARAMS_BID_OVERRIDE, - + bidOverride: bidOverride || {}, pubIdMode: pubIdMode || false, ortb2: ortb2 || {} - }; - const bidRequest = generateBidRequest(bidRequestConfig); - const validBidRequests = [bidRequest]; - const bidderRequest = generateBidderRequest(validBidRequests, adUnitCode, ortb2); - - return { bidRequest, validBidRequests, bidderRequest } + }); + return { bidRequest: bR, validBR: [bR], bidderRequest: createBidderRequest([bR], adUnitCode, ortb2) } }; -const generateAdmPayload = (admPayloadType) => { - let ADM_PAYLOAD; - switch (admPayloadType) { +const createAdm = (type) => { + let ADM; + switch (type) { case 'banner': - ADM_PAYLOAD = ''; // banner + ADM = ` + `; break; - default: ''; break; + default: 'Ad is here'; break; }; - - return ADM_PAYLOAD; + return ADM; }; -const generateResponseMock = (admPayloadType) => { - const bidResponse = { - id: 'fc0c35df-21fb-4f93-9ebd-88759dbe31f9', - impid: '274395c06a24e5', - adm: generateAdmPayload(admPayloadType), - price: 1, - w: 300, - h: 250, - crid: 'ssp-placement-name', - adomain: ['advertiser-domain.com'] - }; - - const serverResponse = { +const createResponseMock = (type) => { + const sR = { body: { - id: 'fc0c35df-21fb-4f93-9ebd-88759dbe31f9', - seatbid: [{ bid: [ bidResponse ], seat: 13107 }] + id: '5qtvluj7bk6jhzmqwu4zzulv', + seatbid: [{ + bid: [{ + id: '5qtvluj7bk6jhzmqwu4zzulv', + impid: 'y7v7iu0uljj94rbjcw9', + adm: createAdm(type), + price: 1, + w: 300, + h: 250, + crid: 'creativeid', + adomain: ['some-advertiser-domain.com'] + }], + seat: 12345 + }] } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: admPayloadType}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + const { validBR, bidderRequest } = createRequestMock({type}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; - return {serverResponse, data, bidderRequest}; + return {sR, data, bidderRequest}; } -// Unit tests -describe('adtrgtme Bid Adapter:', () => { +describe('Adtrgtme Bid Adapter:', () => { it('PLACEHOLDER TO PASS GULP', () => { - const obj = {}; - expect(obj).to.be.an('object'); + expect({}).to.be.an('object'); }); - describe('Validate basic properties', () => { - it('should define the correct bidder code', () => { + describe('check basic properties', () => { + it('should define bidder code', () => { expect(spec.code).to.equal('adtrgtme') }); }); - describe('getUserSyncs()', () => { - const IMAGE_PIXEL_URL = 'http://image-pixel.com/foo/bar?1234&baz=true'; - const IFRAME_ONE_URL = 'http://image-iframe.com/foo/bar?1234&baz=true'; - const IFRAME_TWO_URL = 'http://image-iframe-two.com/foo/bar?1234&baz=true'; + describe('getUserSyncs', () => { + const BAD_SYNC_URL = 'cdn.adtarget.me/libs/1x1.gif?image&rnd=5fr55r'; + const IMAGE_SYNC_URL = `${DEFAULT_PIXEL_URL}?image&rnd=5fr55r`; + const IFRAME_SYNC_ONE_URL = `${DEFAULT_PIXEL_URL}?iframe1&rnd=5fr55r`; + const IFRAME_SYNC_TWO_URL = `${DEFAULT_PIXEL_URL}?iframe2&rnd=5fr55r`; - let serverResponses = []; + let sRs = []; beforeEach(() => { - serverResponses[0] = { + sRs[0] = { body: { ext: { - pixels: `` + pixels: [ + ['image', BAD_SYNC_URL], + ['invalid', IMAGE_SYNC_URL], + ['image', IMAGE_SYNC_URL], + ['iframe', IFRAME_SYNC_ONE_URL], + ['iframe', IFRAME_SYNC_TWO_URL] + ] } } } }); after(() => { - serverResponses = undefined; + sRs = undefined; + }); + + it('sync check bad url and type in pixels', () => { + let opt = { + iframeEnabled: true, + pixelEnabled: true + }; + let pixels = spec.getUserSyncs(opt, sRs); + expect(pixels.length).to.equal(3); }); - it('for only iframe enabled syncs', () => { - let syncOptions = { + it('sync check for iframe only', () => { + let opt = { iframeEnabled: true, pixelEnabled: false }; - let pixelsObjects = spec.getUserSyncs(syncOptions, serverResponses); - expect(pixelsObjects.length).to.equal(2); - expect(pixelsObjects).to.deep.equal( + let pixels = spec.getUserSyncs(opt, sRs); + expect(pixels.length).to.equal(2); + expect(pixels).to.deep.equal( [ - {type: 'iframe', 'url': IFRAME_ONE_URL}, - {type: 'iframe', 'url': IFRAME_TWO_URL} + {type: 'iframe', 'url': IFRAME_SYNC_ONE_URL}, + {type: 'iframe', 'url': IFRAME_SYNC_TWO_URL} ] ) }); - it('for only pixel enabled syncs', () => { - let syncOptions = { + it('sync check for image only', () => { + let opt = { iframeEnabled: false, pixelEnabled: true }; - let pixelsObjects = spec.getUserSyncs(syncOptions, serverResponses); - expect(pixelsObjects.length).to.equal(1); - expect(pixelsObjects).to.deep.equal( + let pixels = spec.getUserSyncs(opt, sRs); + expect(pixels.length).to.equal(1); + expect(pixels).to.deep.equal( [ - {type: 'image', 'url': IMAGE_PIXEL_URL} + {type: 'image', 'url': IMAGE_SYNC_URL} ] ) }); - it('for both pixel and iframe enabled syncs', () => { - let syncOptions = { + it('Sync for iframe and image', () => { + let opt = { iframeEnabled: true, pixelEnabled: true }; - let pixelsObjects = spec.getUserSyncs(syncOptions, serverResponses); - expect(pixelsObjects.length).to.equal(3); - expect(pixelsObjects).to.deep.equal( + let pixels = spec.getUserSyncs(opt, sRs); + expect(pixels.length).to.equal(3); + expect(pixels).to.deep.equal( [ - {type: 'iframe', 'url': IFRAME_ONE_URL}, - {type: 'image', 'url': IMAGE_PIXEL_URL}, - {type: 'iframe', 'url': IFRAME_TWO_URL} + {type: 'image', 'url': IMAGE_SYNC_URL}, + {type: 'iframe', 'url': IFRAME_SYNC_ONE_URL}, + {type: 'iframe', 'url': IFRAME_SYNC_TWO_URL} ] ) }); }); - // Validate Bid Requests - describe('isBidRequestValid()', () => { - const INVALID_INPUT = [ + describe('Check if bid request is OK', () => { + const BAD_VALUE = [ {}, {params: {}}, - {params: {sid: '1234', zid: '4321'}}, - {params: {sid: '1220291391', zid: 4321}}, + {params: {sid: 1220291391, zid: '1836455615'}}, + {params: {sid: '1220291391', zid: 'A'}}, + {params: {sid: '', zid: '1836455615'}}, + {params: {sid: '', zid: 'A'}}, {params: {zid: ''}}, - {params: {sid: '', zid: ''}}, ]; - INVALID_INPUT.forEach(input => { - it(`should determine that the bid is INVALID for the input ${JSON.stringify(input)}`, () => { - expect(spec.isBidRequestValid(input)).to.be.false; + BAD_VALUE.forEach(value => { + it(`should determine bad bid for ${JSON.stringify(value)}`, () => { + expect(spec.isBidRequestValid(value)).to.be.false; }); }); - it('should determine that the bid is VALID if sid and zid are present on the params object', () => { - const validBid = { - params: { - sid: 1220291391, - zid: 1836455615 - } - }; - expect(spec.isBidRequestValid(validBid)).to.be.true; + const OK_VALUE = [ + {params: {sid: '1220291391'}}, + {params: {sid: '1220291391', zid: 1836455615}}, + {params: {sid: '1220291391', zid: '1836455615'}}, + {params: {sid: '1220291391', zid: '1836455615A'}}, + ]; + + OK_VALUE.forEach(value => { + it(`should determine OK bid for ${JSON.stringify(value)}`, () => { + expect(spec.isBidRequestValid(value)).to.be.true; + }); }); }); - describe('Price Floor module support:', () => { + describe('Bidfloor support:', () => { it('should get bidfloor from getFloor method', () => { - const { bidRequest, validBidRequests, bidderRequest } = generateBuildRequestMock({}); - bidRequest.params.bidOverride = {cur: 'EUR'}; + const { bidRequest, validBR, bidderRequest } = createRequestMock({}); + bidRequest.params.bidOverride = {cur: 'AUD'}; bidRequest.getFloor = (floorObj) => { return { floor: bidRequest.floors.values[floorObj.mediaType + '|300x250'], @@ -252,203 +241,167 @@ describe('adtrgtme Bid Adapter:', () => { } }; bidRequest.floors = { - currency: 'EUR', + currency: 'AUD', values: { - 'banner|300x250': 5.55 + 'banner|300x250': 1.111 } }; - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.cur).to.deep.equal(['EUR']); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.cur).to.deep.equal(['AUD']); expect(data.imp[0].bidfloor).is.a('number'); - expect(data.imp[0].bidfloor).to.equal(5.55); + expect(data.imp[0].bidfloor).to.equal(1.111); }); }); - describe('Schain module support:', () => { - it('should send Global or Bidder specific schain', function () { - const { bidRequest, validBidRequests, bidderRequest } = generateBuildRequestMock({}); + describe('Schain support:', () => { + it('should send schains', function () { + const { bidRequest, validBR, bidderRequest } = createRequestMock({}); const globalSchain = { ver: '1.0', complete: 1, nodes: [{ - asi: 'some-platform.com', - sid: '111111', + asi: 'adtarget-partner.com', + sid: '1234567890', rid: bidRequest.bidId, hp: 1 }] }; bidRequest.schain = globalSchain; - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + const data = spec.buildRequests(validBR, bidderRequest)[0].data; const schain = data.source.ext.schain; expect(schain.nodes.length).to.equal(1); expect(schain).to.equal(globalSchain); }); }); - describe('First party data module - "Site" support (ortb2):', () => { - // Should not allow invalid "site" data types - const INVALID_ORTB2_TYPES = [ null, [], 123, 'unsupportedKeyName', true, false, undefined ]; - INVALID_ORTB2_TYPES.forEach(param => { - it(`should not allow invalid site types to be added to bid-request: ${JSON.stringify(param)}`, () => { - const ortb2 = { site: param } - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.site[param]).to.be.undefined; + describe('Check Site obj support (ortb2):', () => { + const BAD_ORTB2_TYPES = [ null, [], 123, 'invalidID', true, false, undefined ]; + BAD_ORTB2_TYPES.forEach(key => { + it(`should remove bad site data: ${JSON.stringify(key)}`, () => { + const ortb2 = { site: key } + const { validBR, bidderRequest } = createRequestMock({ortb2}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.site[key]).to.be.undefined; }); }); - // Should add valid "site" params - const VALID_SITE_STRINGS = ['name', 'domain', 'page', 'ref', 'keywords']; - const VALID_SITE_ARRAYS = ['cat', 'sectioncat', 'pagecat']; + const OK_SITE_STR = ['id', 'name', 'domain', 'page', 'ref', 'keywords']; + const OK_SITE_ARR = ['cat', 'sectioncat', 'pagecat']; - VALID_SITE_STRINGS.forEach(param => { - it(`should allow supported site keys to be added bid-request: ${JSON.stringify(param)}`, () => { + OK_SITE_STR.forEach(key => { + it(`should allow supported site keys to be added bid request: ${JSON.stringify(key)}`, () => { const ortb2 = { site: { - [param]: 'something' + [key]: 'some value here' } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.site[param]).to.exist; - expect(data.site[param]).to.be.a('string'); - expect(data.site[param]).to.be.equal(ortb2.site[param]); + const { validBR, bidderRequest } = createRequestMock({ortb2}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.site[key]).to.exist; + expect(data.site[key]).to.be.a('string'); + expect(data.site[key]).to.be.equal(ortb2.site[key]); }); }); - VALID_SITE_ARRAYS.forEach(param => { - it(`should determine that the ortb2.site Array key is valid and append to the bid-request: ${JSON.stringify(param)}`, () => { + OK_SITE_ARR.forEach(key => { + it(`should determine valid keys of the ortb2 site and append to the bid request: ${JSON.stringify(key)}`, () => { const ortb2 = { site: { - [param]: ['something'] + [key]: ['some value here'] } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.site[param]).to.exist; - expect(data.site[param]).to.be.a('array'); - expect(data.site[param]).to.be.equal(ortb2.site[param]); + const { validBR, bidderRequest } = createRequestMock({ortb2}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.site[key]).to.exist; + expect(data.site[key]).to.be.a('array'); + expect(data.site[key]).to.be.equal(ortb2.site[key]); }); }); - // Should not allow invalid "site.content" data types - INVALID_ORTB2_TYPES.forEach(param => { - it(`should determine that the ortb2.site.content key is invalid and should not be added to bid-request: ${JSON.stringify(param)}`, () => { - const ortb2 = { - site: { - content: param - } - }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.site.content).to.be.undefined; - }); - }); - - // Should not allow invalid "site.content" keys - it(`should not allow invalid ortb2.site.content object keys to be added to bid-request: {custom object}`, () => { - const ortb2 = { - site: { - content: { - fake: 'news', - unreal: 'param', - counterfit: 'data' - } - } - }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.site.content).to.be.a('object'); - }); - - // Should append valid "site.content" keys - const VALID_CONTENT_STRINGS = ['id', 'title', 'language']; - VALID_CONTENT_STRINGS.forEach(param => { - it(`should determine that the ortb2.site String key is valid and append to the bid-request: ${JSON.stringify(param)}`, () => { + const OK_CONTENT_STR = ['id', 'title', 'language']; + OK_CONTENT_STR.forEach(key => { + it(`should determine that the ortb2.site String key is ok and append to the bid request: ${JSON.stringify(key)}`, () => { const ortb2 = { site: { content: { - [param]: 'something' + [key]: 'some value here' } } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.site.content[param]).to.exist; - expect(data.site.content[param]).to.be.a('string'); - expect(data.site.content[param]).to.be.equal(ortb2.site.content[param]); + const { validBR, bidderRequest } = createRequestMock({ortb2}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.site.content[key]).to.exist; + expect(data.site.content[key]).to.be.a('string'); + expect(data.site.content[key]).to.be.equal(ortb2.site.content[key]); }); }); - const VALID_CONTENT_ARRAYS = ['cat']; - VALID_CONTENT_ARRAYS.forEach(param => { - it(`should determine that the ortb2.site Array key is valid and append to the bid-request: ${JSON.stringify(param)}`, () => { + const OK_CONTENT_ARR = ['cat']; + OK_CONTENT_ARR.forEach(key => { + it(`should determine that the ortb2.site key is ok and append to the bid request: ${JSON.stringify(key)}`, () => { const ortb2 = { site: { content: { - [param]: ['something', 'something-else'] + [key]: ['some value here', 'something-else'] } } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.site.content[param]).to.be.a('array'); - expect(data.site.content[param]).to.be.equal(ortb2.site.content[param]); + const { validBR, bidderRequest } = createRequestMock({ortb2}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.site.content[key]).to.be.a('array'); + expect(data.site.content[key]).to.be.equal(ortb2.site.content[key]); }); }); }); - describe('First party data module - "User" support (ortb2):', () => { - // Global ortb2.user validations - // Should not allow invalid "user" data types - const INVALID_ORTB2_TYPES = [ null, [], 'unsupportedKeyName', true, false, undefined ]; - INVALID_ORTB2_TYPES.forEach(param => { - it(`should not allow invalid site types to be added to bid-request: ${JSON.stringify(param)}`, () => { - const ortb2 = { user: param } - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user[param]).to.be.undefined; + describe('Check ortb2 user support:', () => { + const BAD_ORTB2_TYPES = [ null, [], 'unsupportedKeyName', true, false, undefined ]; + BAD_ORTB2_TYPES.forEach(key => { + it(`should not allow bad site types to be added to bid request: ${JSON.stringify(key)}`, () => { + const ortb2 = { user: key } + const { validBR, bidderRequest } = createRequestMock({ortb2}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.user[key]).to.be.undefined; }); }); - // Should add valid "user" params - const VALID_USER_STRINGS = ['id', 'buyeruid', 'gender', 'keywords', 'customdata']; - VALID_USER_STRINGS.forEach(param => { - it(`should allow supported user string keys to be added bid-request: ${JSON.stringify(param)}`, () => { + const OK_USER_STR = ['id', 'buyeruid', 'gender', 'keywords', 'customdata']; + OK_USER_STR.forEach(key => { + it(`should allow valid keys of the user to be added to bid request: ${JSON.stringify(key)}`, () => { const ortb2 = { user: { - [param]: 'something' + [key]: 'some value here' } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user[param]).to.exist; - expect(data.user[param]).to.be.a('string'); - expect(data.user[param]).to.be.equal(ortb2.user[param]); + const { validBR, bidderRequest } = createRequestMock({ortb2}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.user[key]).to.exist; + expect(data.user[key]).to.be.a('string'); + expect(data.user[key]).to.be.equal(ortb2.user[key]); }); }); - const VALID_USER_OBJECTS = ['ext']; - VALID_USER_OBJECTS.forEach(param => { - it(`should allow supported user extObject keys to be added to the bid-request: ${JSON.stringify(param)}`, () => { + const OK_USER_OBJECTS = ['ext']; + OK_USER_OBJECTS.forEach(key => { + it(`should allow user ext to be added to the bid request: ${JSON.stringify(key)}`, () => { const ortb2 = { user: { - [param]: {a: '123', b: '456'} + [key]: {a: '123', b: '456'} } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user[param]).to.exist; - expect(data.user[param]).to.be.a('object'); - expect(data.user[param]).to.be.deep.include({[param]: {a: '123', b: '456'}}); + const { validBR, bidderRequest } = createRequestMock({ortb2}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.user[key]).to.exist; + expect(data.user[key]).to.be.a('object'); + expect(data.user[key].a).to.be.equal('123'); + expect(data.user[key].b).to.be.equal('456'); config.setConfig({ortb2: {}}); }); }); - // adUnit.ortb2Imp.ext.data - it(`should allow adUnit.ortb2Imp.ext.data object to be added to the bid-request`, () => { - let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) - validBidRequests[0].ortb2Imp = { + it(`should allow adUnit.ortb2Imp.ext.data object to be added to the bid request`, () => { + let { validBR, bidderRequest } = createRequestMock({}) + validBR[0].ortb2Imp = { ext: { data: { pbadslot: 'homepage-top-rect', @@ -456,43 +409,42 @@ describe('adtrgtme Bid Adapter:', () => { } } }; - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].ext.data).to.deep.equal(validBidRequests[0].ortb2Imp.ext.data); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.imp[0].ext.data).to.deep.equal(validBR[0].ortb2Imp.ext.data); }); - // adUnit.ortb2Imp.instl - it(`should allow adUnit.ortb2Imp.instl numeric boolean "1" to be added to the bid-request`, () => { - let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) - validBidRequests[0].ortb2Imp = { + it(`should allow adUnit.ortb2Imp.instl numeric boolean "1" to be added to the bid request`, () => { + let { validBR, bidderRequest } = createRequestMock({}) + validBR[0].ortb2Imp = { instl: 1 }; - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].instl).to.deep.equal(validBidRequests[0].ortb2Imp.instl); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; + expect(data.imp[0].instl).to.deep.equal(validBR[0].ortb2Imp.instl); }); - it(`should prevent adUnit.ortb2Imp.instl boolean "true" to be added to the bid-request`, () => { - let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) - validBidRequests[0].ortb2Imp = { + it(`should prevent adUnit.ortb2Imp.instl boolean "true" to be added to the bid request`, () => { + let { validBR, bidderRequest } = createRequestMock({}) + validBR[0].ortb2Imp = { instl: true }; - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + const data = spec.buildRequests(validBR, bidderRequest)[0].data; expect(data.imp[0].instl).to.not.exist; }); - it(`should prevent adUnit.ortb2Imp.instl boolean "false" to be added to the bid-request`, () => { - let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) - validBidRequests[0].ortb2Imp = { + it(`should prevent adUnit.ortb2Imp.instl boolean false to be added to the bid request`, () => { + let { validBR, bidderRequest } = createRequestMock({}) + validBR[0].ortb2Imp = { instl: false }; - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + const data = spec.buildRequests(validBR, bidderRequest)[0].data; expect(data.imp[0].instl).to.not.exist; }); }); - describe('GDPR & Consent:', () => { + describe('GDPR:', () => { it('should return request objects that do not send cookies if purpose 1 consent is not provided', () => { - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const { validBR, bidderRequest } = createRequestMock({}); bidderRequest.gdprConsent = { - consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + consentString: 'BOtmiBKO234234tmiBKABABAEN234AFAAAAACeAAA', apiVersion: 2, vendorData: { purpose: { @@ -503,22 +455,22 @@ describe('adtrgtme Bid Adapter:', () => { }, gdprApplies: true }; - const options = spec.buildRequests(validBidRequests, bidderRequest)[0].options; - expect(options.withCredentials).to.be.false; + const opt = spec.buildRequests(validBR, bidderRequest)[0].options; + expect(opt.withCredentials).to.be.false; }); }); - describe('Endpoint & Impression Request Mode:', () => { + describe('Endpoint & Impression request mode:', () => { it('should route request to config override endpoint', () => { - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const sid = validBidRequests[0].params.sid; - const testOverrideEndpoint = 'http://new_bidder_host.com/ssp?s='; + const { validBR, bidderRequest } = createRequestMock({}); + const sid = validBR[0].params.sid; + const testOverrideEndpoint = 'http://partner-adserv-domain.com/ssp?s='; config.setConfig({ adtrgtme: { endpoint: testOverrideEndpoint } }); - const response = spec.buildRequests(validBidRequests, bidderRequest)[0]; + const response = spec.buildRequests(validBR, bidderRequest)[0]; expect(response).to.deep.include( { method: 'POST', @@ -530,9 +482,9 @@ describe('adtrgtme Bid Adapter:', () => { config.setConfig({ adtrgtme: {} }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const sid = validBidRequests[0].params.sid; - const response = spec.buildRequests(validBidRequests, bidderRequest); + const { validBR, bidderRequest } = createRequestMock({}); + const sid = validBR[0].params.sid; + const response = spec.buildRequests(validBR, bidderRequest); expect(response[0]).to.deep.include({ method: 'POST', url: 'https://z.cdn.adtarget.market/ssp?prebid&s=' + sid @@ -540,13 +492,10 @@ describe('adtrgtme Bid Adapter:', () => { }); it('should return a single request object for single request mode', () => { - let { bidRequest, validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const BID_ID_2 = '84ab50xxxxx'; - const BID_ZID_2 = 98876543210; - const AD_UNIT_CODE_2 = 'test-ad-unit-code-123'; - const { bidRequest: bidRequest2 } = generateBuildRequestMock({bidId: BID_ID_2, zid: BID_ZID_2, adUnitCode: AD_UNIT_CODE_2}); - validBidRequests = [bidRequest, bidRequest2]; - bidderRequest.bids = validBidRequests; + let { bidRequest, validBR, bidderRequest } = createRequestMock({}); + const { bidRequest: mock } = createRequestMock({bidId: '6heos7ks8z0j', zid: '98876543210', adUnitCode: 'bidder-code'}); + validBR = [bidRequest, mock]; + bidderRequest.bids = validBR; config.setConfig({ adtrgtme: { @@ -554,60 +503,57 @@ describe('adtrgtme Bid Adapter:', () => { } }); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBR, bidderRequest).data; expect(data.imp).to.be.an('array').with.lengthOf(2); expect(data.imp[0]).to.deep.include({ - id: DEFAULT_BID_ID, + id: '84ab500420319d', ext: { - dfp_ad_unit_code: DEFAULT_AD_UNIT_CODE + dfp_ad_unit_code: '/1220291391/banner' } }); expect(data.imp[1]).to.deep.include({ - id: BID_ID_2, - tagid: BID_ZID_2, + id: '6heos7ks8z0j', + tagid: '98876543210', ext: { - dfp_ad_unit_code: AD_UNIT_CODE_2 + dfp_ad_unit_code: 'bidder-code' } }); }); }); - describe('Validate request filtering:', () => { - it('should not return request when no bids are present', function () { + describe('validate request filtering:', () => { + it('should return undefined when no bids', function () { let request = spec.buildRequests([]); expect(request).to.be.undefined; }); - it('buildRequests(): should return an array with the correct amount of request objects', () => { - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const response = spec.buildRequests(validBidRequests, bidderRequest).bidderRequest; + it('buildRequests should return correct amount of objects', () => { + const { validBR, bidderRequest } = createRequestMock({}); + const response = spec.buildRequests(validBR, bidderRequest).bidderRequest; expect(response.bids).to.be.an('array').to.have.lengthOf(1); }); }); - describe('Request Headers validation:', () => { - it('should return request objects with the relevant custom headers and content type declaration', () => { - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + describe('Validate request headers:', () => { + it('should return request objects with the custom headers and content type', () => { + const { validBR, bidderRequest } = createRequestMock({}); bidderRequest.gdprConsent.gdprApplies = false; - const options = spec.buildRequests(validBidRequests, bidderRequest).options; - expect(options).to.deep.equal( + const opt = spec.buildRequests(validBR, bidderRequest).options; + expect(opt).to.deep.equal( { contentType: 'application/json', - customHeaders: { - 'x-openrtb-version': '2.5' - }, withCredentials: true }); }); }); - describe('Request Payload oRTB bid validation:', () => { - it('should generate a valid openRTB bid-request object in the data field', () => { - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + describe('Request data oRTB bid validation:', () => { + it('should create valid oRTB bid request object in the data field', () => { + const { validBR, bidderRequest } = createRequestMock({}); + const data = spec.buildRequests(validBR, bidderRequest).data; expect(data.site).to.deep.include({ id: bidderRequest.bids[0].params.sid, page: bidderRequest.refererInfo.page @@ -626,68 +572,52 @@ describe('adtrgtme Bid Adapter:', () => { } }); - expect(data.source).to.deep.equal({ - ext: { - hb: 1, - adapterver: ADAPTER_VERSION, - prebidver: PREBID_VERSION, - integration: { - name: INTEGRATION_METHOD, - ver: PREBID_VERSION - } - }, - fd: 1 - }); - expect(data.cur).to.deep.equal(['USD']); }); - it('should generate a valid openRTB imp.ext object in the bid-request', () => { - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const bid = validBidRequests[0]; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + it('should create valid oRTB imp.ext in the bid request', () => { + const { validBR, bidderRequest } = createRequestMock({}); + const data = spec.buildRequests(validBR, bidderRequest).data; expect(data.imp[0].ext).to.deep.equal({ - dfp_ad_unit_code: DEFAULT_AD_UNIT_CODE + dfp_ad_unit_code: '/1220291391/banner' }); }); - it('should use siteId value as site.id in the outbound bid-request when using "pubId" integration mode', () => { - let { validBidRequests, bidderRequest } = generateBuildRequestMock({pubIdMode: true}); - validBidRequests[0].params.sid = 9876543210; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; - expect(data.site.id).to.equal(9876543210); + it('should use siteId value as site.id', () => { + let { validBR, bidderRequest } = createRequestMock({pubIdMode: true}); + validBR[0].params.sid = '9876543210'; + const data = spec.buildRequests(validBR, bidderRequest).data; + expect(data.site.id).to.equal('9876543210'); }); - it('should use placementId value as imp.tagid in the outbound bid-request when using "zid"', () => { - let { validBidRequests, bidderRequest } = generateBuildRequestMock({}), - TEST_ZID = 54321; - validBidRequests[0].params.zid = TEST_ZID; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + it('should use placementId value as imp.tagid when using "zid"', () => { + let { validBR, bidderRequest } = createRequestMock({}), + TEST_ZID = '54321'; + validBR[0].params.zid = TEST_ZID; + const data = spec.buildRequests(validBR, bidderRequest).data; expect(data.imp[0].tagid).to.deep.equal(TEST_ZID); }); }); - describe('Request Payload oRTB bid.imp validation:', () => { - // Validate Banner imp imp when adtrgtme.mode=undefined - it('should generate a valid "Banner" imp object', () => { + describe('Request oRTB bid.imp validation:', () => { + it('should create valid default Banner imp', () => { config.setConfig({ adtrgtme: {} }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + const { validBR, bidderRequest } = createRequestMock({}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; expect(data.imp[0].banner).to.deep.equal({ mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], format: [{w: 300, h: 250}] }); }); - // Validate Banner imp - it('should generate a valid "Banner" imp object', () => { + it('should create valid Banner imp', () => { config.setConfig({ adtrgtme: { mode: 'banner' } }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + const { validBR, bidderRequest } = createRequestMock({}); + const data = spec.buildRequests(validBR, bidderRequest)[0].data; expect(data.imp[0].banner).to.deep.equal({ mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], format: [{w: 300, h: 250}] @@ -697,104 +627,105 @@ describe('adtrgtme Bid Adapter:', () => { describe('interpretResponse()', () => { describe('for mediaTypes: "banner"', () => { - it('should insert banner payload into response[0].ad', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ad).to.equal(''); + it('should insert banner object into response[0].ad', () => { + const { sR, bidderRequest } = createResponseMock('banner'); + const response = spec.interpretResponse(sR, {bidderRequest}); + expect(response[0].ad).to.equal(` + `); expect(response[0].mediaType).to.equal('banner'); }) }); - describe('Support Advertiser domains', () => { + describe('Support adomains', () => { it('should append bid-response adomain to meta.advertiserDomains', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + const { sR, bidderRequest } = createResponseMock('banner'); + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].meta.advertiserDomains).to.be.a('array'); - expect(response[0].meta.advertiserDomains[0]).to.equal('advertiser-domain.com'); + expect(response[0].meta.advertiserDomains[0]).to.equal('some-advertiser-domain.com'); }) }); - describe('bid response Ad ID / Creative ID', () => { + describe('Check response Ad ID / Creative ID', () => { it('should use adId if it exists in the bid-response', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const { sR, bidderRequest } = createResponseMock('banner'); const adId = 'bid-response-adId'; - serverResponse.body.seatbid[0].bid[0].adId = adId; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + sR.body.seatbid[0].bid[0].adId = adId; + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].adId).to.equal(adId); }); it('should use impid if adId does not exist in the bid-response', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - const impid = '25b6c429c1f52f'; - serverResponse.body.seatbid[0].bid[0].impid = impid; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + const { sR, bidderRequest } = createResponseMock('banner'); + const impid = 'y7v7iu0uljj94rbjcw9'; + sR.body.seatbid[0].bid[0].impid = impid; + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].adId).to.equal(impid); }); it('should use crid if adId & impid do not exist in the bid-response', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const { sR, bidderRequest } = createResponseMock('banner'); const crid = 'passback-12579'; - serverResponse.body.seatbid[0].bid[0].impid = undefined; - serverResponse.body.seatbid[0].bid[0].crid = crid; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + sR.body.seatbid[0].bid[0].impid = undefined; + sR.body.seatbid[0].bid[0].crid = crid; + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].adId).to.equal(crid); }); }); describe('Time To Live (ttl)', () => { - const UNSUPPORTED_TTL_FORMATS = ['string', [1, 2, 3], true, false, null, undefined]; - UNSUPPORTED_TTL_FORMATS.forEach(param => { + const BAD_TTL_FORMATS = ['string', [1, 2, 3], true, false, null, undefined]; + BAD_TTL_FORMATS.forEach(key => { it('should not allow unsupported global adtrgtme.ttl formats and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const { sR, bidderRequest } = createResponseMock('banner'); config.setConfig({ - adtrgtme: { ttl: param } + adtrgtme: { ttl: key } }); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].ttl).to.equal(300); }); - it('should not allow unsupported params.ttl formats and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - bidderRequest.bids[0].params.ttl = param; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + it('should not set unsupported ttl formats and check default to 300', () => { + const { sR, bidderRequest } = createResponseMock('banner'); + bidderRequest.bids[0].params.ttl = key; + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].ttl).to.equal(300); }); }); - const UNSUPPORTED_TTL_VALUES = [-1, 3601]; - UNSUPPORTED_TTL_VALUES.forEach(param => { - it('should not allow invalid global adtrgtme.ttl values 3600 < ttl < 0 and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const BAD_TTL_VALUES = [-1, 12345]; + BAD_TTL_VALUES.forEach(key => { + it('should not set bad global adtrgtme.ttl and check default to 300', () => { + const { sR, bidderRequest } = createResponseMock('banner'); config.setConfig({ - adtrgtme: { ttl: param } + adtrgtme: { ttl: key } }); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].ttl).to.equal(300); }); - it('should not allow invalid params.ttl values 3600 < ttl < 0 and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - bidderRequest.bids[0].params.ttl = param; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + it('should not set bad keys.ttl values', () => { + const { sR, bidderRequest } = createResponseMock('banner'); + bidderRequest.bids[0].params.ttl = key; + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].ttl).to.equal(300); }); }); - it('should give presedence to Gloabl ttl over params.ttl ', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); + it('should set gloabl ttl over params.ttl if it presents', () => { + const { sR, bidderRequest } = createResponseMock('banner'); config.setConfig({ adtrgtme: { ttl: 500 } }); bidderRequest.bids[0].params.ttl = 400; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].ttl).to.equal(500); }); }); - describe('Aliasing support', () => { + describe('Alias support', () => { it('should return undefined as the bidder code value', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); + const { sR, bidderRequest } = createResponseMock('banner'); + const response = spec.interpretResponse(sR, {bidderRequest}); expect(response[0].bidderCode).to.be.undefined; }); }); diff --git a/test/spec/modules/advRedAnalyticsAdapter_spec.js b/test/spec/modules/advRedAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..c493710ab53 --- /dev/null +++ b/test/spec/modules/advRedAnalyticsAdapter_spec.js @@ -0,0 +1,114 @@ +import advRedAnalytics from 'modules/advRedAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +import {expectEvents} from '../../helpers/analytics.js'; +import { EVENTS } from 'src/constants.js'; +import sinon from 'sinon'; + +let events = require('src/events'); + +describe('AdvRed Analytics Adapter', function () { + let bidWonEvent = { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'adId': '1ebb82ec35375e', + 'mediaType': 'banner', + 'cpm': 0.5, + 'requestId': '1582271863760569973', + 'creative_id': '96846035', + 'creativeId': '96846035', + 'ttl': 60, + 'currency': 'USD', + 'netRevenue': true, + 'auctionId': '9c7b70b9-b6ab-4439-9e71-b7b382797c18', + 'responseTimestamp': 1537521629657, + 'requestTimestamp': 1537521629331, + 'bidder': 'appnexus', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'timeToRespond': 326, + 'size': '300x250', + 'status': 'rendered', + 'eventType': 'bidWon', + 'ad': 'some ad', + 'adUrl': 'ad url' + }; + + describe('AdvRed Analytic tests', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function () { + advRedAnalytics.disableAnalytics(); + events.getEvents.restore(); + }); + + it('support custom endpoint', function () { + let custom_endpoint = 'custom url'; + advRedAnalytics.enableAnalytics({ + provider: 'advRed', + options: { + url: custom_endpoint, + publisherId: '1234567890' + } + }); + + expect(advRedAnalytics.getOptions().url).to.equal(custom_endpoint); + }); + + it('bid won event', function() { + let publisherId = '1234567890'; + advRedAnalytics.enableAnalytics({ + provider: 'advRed', + options: { + publisherId: publisherId + } + }); + + events.emit(EVENTS.BID_WON, bidWonEvent); + advRedAnalytics.sendEvents(); + + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://api.adv.red/api/event'); + + const message = JSON.parse(server.requests[0].requestBody); + expect(message.pwId).to.exist; + expect(message.publisherId).to.equal(publisherId); + expect(message.events.length).to.equal(1); + expect(message.events[0].eventType).to.equal('bidWon'); + expect(message.events[0].ad).to.be.undefined; + expect(message.events[0].adUrl).to.be.undefined; + }); + + it('track event', function () { + sinon.spy(advRedAnalytics, 'track'); + + advRedAnalytics.enableAnalytics({ + provider: 'advRed', + options: { + publisherId: '1234567890' + } + }); + + expectEvents().to.beTrackedBy(advRedAnalytics.track); + }); + }); + + describe('pageUrl detection', function () { + afterEach(function () { + advRedAnalytics.disableAnalytics() + }); + it('check pageUrl property', function () { + advRedAnalytics.enableAnalytics({ + provider: 'advRed', + options: { + publisherId: '1234567890' + } + }); + + const message = JSON.parse(server.requests[0].requestBody); + expect(message.pageUrl).to.equal(window.top.location.href); + }); + }); +}); diff --git a/test/spec/modules/advangelistsBidAdapter_spec.js b/test/spec/modules/advangelistsBidAdapter_spec.js index 143d85a1ab6..57ad2d0e898 100755 --- a/test/spec/modules/advangelistsBidAdapter_spec.js +++ b/test/spec/modules/advangelistsBidAdapter_spec.js @@ -44,19 +44,19 @@ describe('advangelistsBidAdapter', function () { describe('spec.buildRequests', function () { it('should create a POST request for each bid', function () { const bidRequest = bidRequests[0]; - const requests = spec.buildRequests([ bidRequest ]); + const requests = spec.buildRequests([ bidRequest ], { timeout: 1000 }); expect(requests[0].method).to.equal('POST'); }); it('should create a POST request for each bid in video request', function () { const bidRequest = bidRequestsVid[0]; - const requests = spec.buildRequests([ bidRequest ]); + const requests = spec.buildRequests([ bidRequest ], { timeout: 1000 }); expect(requests[0].method).to.equal('POST'); }); it('should have domain in request', function () { const bidRequest = bidRequests[0]; - const requests = spec.buildRequests([ bidRequest ]); + const requests = spec.buildRequests([ bidRequest ], { timeout: 1000 }); expect(requests[0].data.site.domain).length !== 0; }); }); diff --git a/test/spec/modules/adverxoBidAdapter_spec.js b/test/spec/modules/adverxoBidAdapter_spec.js new file mode 100644 index 00000000000..e3b98d49f07 --- /dev/null +++ b/test/spec/modules/adverxoBidAdapter_spec.js @@ -0,0 +1,725 @@ +import {expect} from 'chai'; +import {spec} from 'modules/adverxoBidAdapter.js'; +import {config} from 'src/config'; + +describe('Adverxo Bid Adapter', () => { + function makeBidRequestWithParams(params) { + return { + bidId: '2e9f38ea93bb9e', + bidder: 'adverxo', + adUnitCode: 'adunit-code', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: params, + bidderRequestId: 'test-bidder-request-id' + }; + } + + const bannerBidRequests = [ + { + bidId: 'bid-banner', + bidder: 'adverxo', + adUnitCode: 'adunit-code', + userIdAsEids: [{ + 'source': 'pubcid.org', + 'uids': [{ + 'atype': 1, + 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' + }] + }], + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + host: 'bid.example.com', + adUnitId: 1, + auth: 'authExample', + }, + bidderRequestId: 'test-bidder-request-id', + }, + ]; + + const bannerBidderRequest = { + bidderCode: 'adverxo', + bidderRequestId: 'test-bidder-request-id', + bids: bannerBidRequests, + auctionId: 'new-auction-id' + }; + + const nativeOrtbRequest = { + assets: [ + { + id: 1, + required: 1, + img: {type: 3, w: 150, h: 50} + }, + { + id: 2, + required: 1, + title: {len: 80} + }, + { + id: 3, + required: 0, + data: {type: 1} + } + ] + }; + + const nativeBidRequests = [ + { + bidId: 'bid-native', + mediaTypes: { + native: { + ortb: nativeOrtbRequest + } + }, + nativeOrtbRequest, + params: { + host: 'bid.example.com', + adUnitId: 1, + auth: 'authExample' + } + }, + ]; + + const nativeBidderRequest = { + bidderCode: 'adverxo', + bidderRequestId: 'test-bidder-request-id', + bids: nativeBidRequests, + auctionId: 'new-auction-id' + }; + + const videoInstreamBidRequests = [ + { + bidId: 'bid-video', + mediaTypes: { + video: { + context: 'instream', + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + params: { + host: 'bid.example.com', + adUnitId: 1, + auth: 'authExample' + } + } + ]; + + const videoInstreamBidderRequest = { + bidderCode: 'adverxo', + bidderRequestId: 'test-bidder-request-id', + bids: videoInstreamBidRequests, + auctionId: 'new-auction-id' + }; + + const videoOutstreamBidRequests = [ + { + bidId: 'bid-video', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + params: { + host: 'bid.example.com', + adUnitId: 1, + auth: 'authExample' + } + } + ]; + + const videoOutstreamBidderRequest = { + bidderCode: 'adverxo', + bidderRequestId: 'test-bidder-request-id', + bids: videoOutstreamBidRequests, + auctionId: 'new-auction-id' + }; + + afterEach(function () { + config.resetConfig(); + }); + + describe('isBidRequestValid', function () { + it('should validate bid request with valid params', () => { + const validBid = makeBidRequestWithParams({ + adUnitId: 1, + auth: 'authExample', + host: 'www.bidExample.com' + }); + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.be.true; + }); + + it('should not validate bid request with empty params', () => { + const invalidBid = makeBidRequestWithParams({}); + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.false; + }); + + it('should not validate bid request with missing param(adUnitId)', () => { + const invalidBid = makeBidRequestWithParams({ + auth: 'authExample', + host: 'www.bidExample.com' + }); + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.false; + }); + + it('should not validate bid request with missing param(auth)', () => { + const invalidBid = makeBidRequestWithParams({ + adUnitId: 1, + host: 'www.bidExample.com' + }); + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.false; + }); + + it('should validate bid request with missing param(host)', () => { + const invalidBid = makeBidRequestWithParams({ + adUnitId: 1, + auth: 'authExample', + }); + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.true; + }); + }); + + describe('buildRequests', () => { + it('should add eids information to the request', function () { + const request = spec.buildRequests(bannerBidRequests, bannerBidderRequest)[0]; + + expect(request.data.user.ext.eids).to.exist; + expect(request.data.user.ext.eids).to.deep.equal(bannerBidRequests[0].userIdAsEids); + }); + + it('should use correct bidUrl for an alias', () => { + const bidRequests = [ + { + bidder: 'bidsmind', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + adUnitId: 1, + auth: 'authExample', + } + }, + ]; + + const bidderRequest = { + bidderCode: 'bidsmind', + bids: bidRequests, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://egrevirda.com/pickpbs?id=1&auth=authExample'); + }); + + it('should use correct default bidUrl', () => { + const bidRequests = [ + { + bidder: 'adverxo', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + adUnitId: 1, + auth: 'authExample', + } + }, + ]; + + const bidderRequest = { + bidderCode: 'adverxo', + bids: bidRequests + }; + + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://js.pbsadverxo.com/pickpbs?id=1&auth=authExample'); + }); + + it('should build post request for banner', () => { + const request = spec.buildRequests(bannerBidRequests, bannerBidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.example.com/pickpbs?id=1&auth=authExample'); + expect(request.data.device.ip).to.equal('caller'); + expect(request.data.ext.avx_add_vast_url).to.equal(1); + }); + + if (FEATURES.NATIVE) { + it('should build post request for native', () => { + const request = spec.buildRequests(nativeBidRequests, nativeBidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.example.com/pickpbs?id=1&auth=authExample'); + + const nativeRequest = JSON.parse(request.data.imp[0]['native'].request); + + expect(nativeRequest.assets).to.have.lengthOf(3); + + expect(nativeRequest.assets[0]).to.deep.equal({ + id: 1, + required: 1, + img: {w: 150, h: 50, type: 3} + }); + + expect(nativeRequest.assets[1]).to.deep.equal({ + id: 2, + required: 1, + title: {len: 80} + }); + + expect(nativeRequest.assets[2]).to.deep.equal({ + id: 3, + required: 0, + data: {type: 1} + }); + }); + } + + if (FEATURES.VIDEO) { + it('should build post request for video', function () { + const request = spec.buildRequests(videoInstreamBidRequests, videoInstreamBidderRequest)[0]; + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.example.com/pickpbs?id=1&auth=authExample'); + + const ortbRequest = request.data; + + expect(ortbRequest.imp).to.have.lengthOf(1); + + expect(ortbRequest.imp[0]).to.deep.equal({ + id: 'bid-video', + secure: 1, + video: { + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }); + }); + } + + it('should add bid floor to request', function () { + const bannerBidRequestWithFloor = { + ...bannerBidRequests[0], + getFloor: () => ({currency: 'USD', floor: 3}) + }; + + const request = spec.buildRequests([bannerBidRequestWithFloor], {})[0].data; + + expect(request.imp[0].bidfloor).to.equal(3); + expect(request.imp[0].bidfloorcur).to.equal('USD'); + }); + }); + + describe('interpretResponse', () => { + it('should return empty array if serverResponse is not defined', () => { + const bidRequest = spec.buildRequests(bannerBidRequests, bannerBidderRequest); + const bids = spec.interpretResponse(undefined, bidRequest); + + expect(bids.length).to.equal(0); + }); + + it('should interpret banner response', () => { + const bidResponse = { + body: { + id: 'bid-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-banner', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 1, + adomain: ['test.com'], + adm: '
' + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'banner', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + requestId: 'bid-banner', + ttl: 60, + width: 300, + ad: '
' + }, + ]; + + const request = spec.buildRequests(bannerBidRequests, bannerBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids).to.deep.equal(expectedBids); + }); + + it('should replace openrtb auction price macro', () => { + const bidResponse = { + body: { + id: 'bid-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-banner', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 1, + adomain: ['test.com'], + adm: '' + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const request = spec.buildRequests(bannerBidRequests, bannerBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids[0].ad).to.equal(''); + }); + + if (FEATURES.NATIVE) { + it('should interpret native response', () => { + const bidResponse = { + body: { + id: 'native-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-native', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 4, + adomain: ['test.com'], + adm: '{"native":{"assets":[{"id":2,"title":{"text":"Title"}},{"id":3,"data":{"value":"Description"}},{"id":1,"img":{"url":"http://example.com?img","w":150,"h":50}}],"link":{"url":"http://example.com?link"}}}' + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'native', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + requestId: 'bid-native', + ttl: 60, + width: 300, + native: { + ortb: { + assets: [ + {id: 2, title: {text: 'Title'}}, + {id: 3, data: {value: 'Description'}}, + {id: 1, img: {url: 'http://example.com?img', w: 150, h: 50}} + ], + link: {url: 'http://example.com?link'} + } + } + } + ]; + + const request = spec.buildRequests(nativeBidRequests, nativeBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids).to.deep.equal(expectedBids); + }); + } + + if (FEATURES.VIDEO) { + it('should interpret video instream response', () => { + const bidResponse = { + body: { + id: 'video-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-video', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 2, + adomain: ['test.com'], + adm: 'vastXml', + ext: { + avx_vast_url: 'vastUrl' + } + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'video', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + playerHeight: 300, + playerWidth: 400, + requestId: 'bid-video', + ttl: 60, + vastUrl: 'vastUrl', + vastXml: 'vastXml', + width: 300 + } + ]; + + const request = spec.buildRequests(videoInstreamBidRequests, videoInstreamBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids).to.deep.equal(expectedBids); + }); + + it('should interpret video outstream response', () => { + const bidResponse = { + body: { + id: 'video-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: 'bid-video', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 2, + adomain: ['test.com'], + adm: 'vastXml', + ext: { + avx_vast_url: 'vastUrl', + avx_video_renderer_url: 'videoRendererUrl', + } + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + avxVideoRendererUrl: 'videoRendererUrl', + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'video', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + playerHeight: 300, + playerWidth: 400, + requestId: 'bid-video', + ttl: 60, + vastUrl: 'vastUrl', + vastXml: 'vastXml', + width: 300 + } + ]; + + const request = spec.buildRequests(videoOutstreamBidRequests, videoOutstreamBidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids[0].renderer.url).to.equal('videoRendererUrl'); + + delete (bids[0].renderer); + expect(bids).to.deep.equal(expectedBids); + }); + } + }); + + describe('getUserSyncs', () => { + const exampleUrl = 'https://example.com/usync?id=5'; + const iframeConfig = {iframeEnabled: true}; + + const responses = [{ + body: {ext: {avx_usync: [exampleUrl]}} + }]; + + const responseWithoutQueryString = [{ + body: {ext: {avx_usync: ['https://example.com/usync/sf/5']}} + }]; + + it('should not return empty list if not allowed', function () { + expect(spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, responses, undefined, undefined, undefined)).to.be.empty; + }); + + it('should not return iframe if not allowed', function () { + expect(spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, responses, undefined, undefined, undefined)).to.deep.equal([{ + type: 'image', url: `${exampleUrl}&type=image` + }]); + }); + + it('should add query string to url when missing', function () { + expect(spec.getUserSyncs(iframeConfig, responseWithoutQueryString, undefined, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `https://example.com/usync/sf/5?type=iframe` + }]); + }); + + it('should not add parameters if not provided', function () { + expect(spec.getUserSyncs(iframeConfig, responses, undefined, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe` + }]); + }); + + it('should add GDPR parameters if provided', function () { + expect(spec.getUserSyncs(iframeConfig, responses, {gdprApplies: true}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gdpr=1&gdpr_consent=` + }]); + + expect(spec.getUserSyncs(iframeConfig, responses, + {gdprApplies: true, consentString: 'foo?'}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gdpr=1&gdpr_consent=foo%3F` + }]); + + expect(spec.getUserSyncs(iframeConfig, responses, + {gdprApplies: false, consentString: 'bar'}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gdpr=0&gdpr_consent=bar` + }]); + }); + + it('should add CCPA parameters if provided', function () { + expect(spec.getUserSyncs(iframeConfig, responses, undefined, 'foo?', undefined)).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&us_privacy=foo%3F` + }]); + }); + + it('should not apply if not gppConsent.gppString', function () { + const gppConsent = {gppString: '', applicableSections: [123]}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe` + }]); + }); + + it('should not apply if not gppConsent.applicableSections', function () { + const gppConsent = {gppString: '', applicableSections: undefined}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe` + }]); + }); + + it('should not apply if empty gppConsent.applicableSections', function () { + const gppConsent = {gppString: '', applicableSections: []}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe` + }]); + }); + + it('should apply if all above are available', function () { + const gppConsent = {gppString: 'foo?', applicableSections: [123]}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gpp=foo%3F&gpp_sid=123` + }]); + }); + + it('should support multiple sections', function () { + const gppConsent = {gppString: 'foo', applicableSections: [123, 456]}; + const result = spec.getUserSyncs(iframeConfig, responses, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${exampleUrl}&type=iframe&gpp=foo&gpp_sid=123%2C456` + }]); + }); + }); +}); diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index e07e3a6e5d4..14b399eaad4 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,13 +1,10 @@ // jshint esversion: 6, es3: false, node: true -import { assert } from 'chai'; -import { spec } from 'modules/adxcgBidAdapter.js'; -import { config } from 'src/config.js'; -import { createEidsArray } from 'modules/userId/eids.js'; /* eslint dot-notation:0, quote-props:0 */ -import { expect } from 'chai'; +import {assert, expect} from 'chai'; +import {spec} from 'modules/adxcgBidAdapter.js'; +import {config} from 'src/config.js'; -import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; -import { deepClone } from '../../../src/utils'; +import {addFPDToBidderRequest} from '../../helpers/fpd.js'; const utils = require('src/utils'); @@ -104,9 +101,6 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }, bidId: 'bid12345', params: { - cp: 'p10000', - ct: 't10000', - cf: '300x250', adzoneid: '77' } }, { @@ -118,9 +112,6 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }, bidId: 'bid23456', params: { - cp: 'p10000', - ct: 't20000', - cf: '728x90', adzoneid: '77' } }]; @@ -160,8 +151,7 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }, nativeOrtbRequest, params: { - cp: 'p10000', - ct: 't10000', + adzoneid: '77' } }]; @@ -182,8 +172,6 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { } }, params: { - cp: 'p10000', - ct: 't10000', adzoneid: '77' } }]; @@ -196,9 +184,6 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }, bidId: 'bid12345', params: { - cp: 'p10000', - ct: 't10000', - cf: '1x1', adzoneid: '77', extra_key1: 'extra_val1', extra_key2: 12345, @@ -219,9 +204,6 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }, bidId: 'bid12345', params: { - cp: 'p10000', - ct: 't10000', - cf: '1x1', adzoneid: '77', bcat: ['IAB-1', 'IAB-20'], battr: [1, 2, 3], @@ -244,39 +226,40 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }, }]; - const bidderRequest = { - refererInfo: { - page: 'https://publisher.com/home', - ref: 'https://referrer' - } - }; + let bidderRequest; + + beforeEach(() => { + return addFPDToBidderRequest({ + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + } + }).then(br => { bidderRequest = br }); + }) it('Verify build request', function () { - const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(slotConfigs, bidderRequest); expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; // site object expect(ortbRequest.site).to.not.equal(null); expect(ortbRequest.site.publisher).to.not.equal(null); - // expect(ortbRequest.site.publisher.id).to.equal('p10000'); expect(ortbRequest.site.page).to.equal('https://publisher.com/home'); expect(ortbRequest.imp).to.have.lengthOf(2); // device object expect(ortbRequest.device).to.not.equal(null); expect(ortbRequest.device.ua).to.equal(navigator.userAgent); // slot 1 - // expect(ortbRequest.imp[0].tagid).to.equal('t10000'); expect(ortbRequest.imp[0].banner).to.not.equal(null); expect(ortbRequest.imp[0].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }, { 'w': 160, 'h': 600 }]); // slot 2 - // expect(ortbRequest.imp[1].tagid).to.equal('t20000'); expect(ortbRequest.imp[1].banner).to.not.equal(null); expect(ortbRequest.imp[1].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }]); }); it('Verify parse response', function () { - const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(slotConfigs, bidderRequest); const ortbRequest = request.data; const ortbResponse = { seatbid: [{ @@ -318,7 +301,7 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { if (FEATURES.NATIVE) { it('Verify Native request', function () { - const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(nativeSlotConfig, bidderRequest); expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -354,7 +337,7 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }); it('Verify Native response', function () { - const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(nativeSlotConfig, bidderRequest); expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -426,7 +409,7 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { if (FEATURES.VIDEO) { it('Verify Video request', function () { - const request = spec.buildRequests(videoSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(videoSlotConfig, bidderRequest); expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -447,7 +430,7 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { } it('Verify extra parameters', function () { - let request = spec.buildRequests(additionalParamsConfig, syncAddFPDToBidderRequest(bidderRequest)); + let request = spec.buildRequests(additionalParamsConfig, bidderRequest); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.imp).to.have.lengthOf(1); @@ -477,7 +460,7 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }, gdprConsent: { gdprApplies: true, - consentString: 'serialized_gpdr_data' + consentString: 'serialized_gdpr_data' }, ortb2: { user: { @@ -492,7 +475,7 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { } } }; - let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let request = spec.buildRequests(slotConfigs, bidderRequest); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.user).to.not.equal(null); @@ -519,7 +502,7 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { } } }; - let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let request = spec.buildRequests(slotConfigs, bidderRequest); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.site).to.not.equal(null); @@ -536,7 +519,6 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { page: 'http://pub.com/news', ref: 'http://google.com', publisher: { - // id: 'p10000', domain: 'pub.com' } }); @@ -552,8 +534,6 @@ describe('adxcg v8 oRtbConverter Adapter Tests', function () { }, bidId: 'bid12345', params: { - cp: 'p10000', - ct: 't10000', adzoneid: '77', extra_key1: 'extra_val1', extra_key2: 12345 diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index bafa031cd25..367fc62c719 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -619,19 +619,19 @@ describe('Adyoulike Adapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.size; + let invalidBid = Object.assign({}, bid); + delete invalidBid.sizes; - expect(!!spec.isBidRequestValid(bid)).to.equal(false); + expect(!!spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placement': 0 }; - expect(!!spec.isBidRequestValid(bid)).to.equal(false); + expect(!!spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/agmaAnalyticsAdapter_spec.js b/test/spec/modules/agmaAnalyticsAdapter_spec.js index 227acacde12..2e50801b559 100644 --- a/test/spec/modules/agmaAnalyticsAdapter_spec.js +++ b/test/spec/modules/agmaAnalyticsAdapter_spec.js @@ -8,7 +8,7 @@ import { gdprDataHandler } from '../../../src/adapterManager.js'; import { expect } from 'chai'; import * as events from '../../../src/events.js'; import { EVENTS } from '../../../src/constants.js'; -import { generateUUID } from '../../../src/utils.js'; +import * as utils from 'src/utils.js'; import { server } from '../../mocks/xhr.js'; import { config } from 'src/config.js'; @@ -80,7 +80,7 @@ describe('AGMA Analytics Adapter', () => { describe('getPayload', () => { it('should use non extended payload with no consent info', () => { sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null) - const payload = getPayload([generateUUID()], { + const payload = getPayload([utils.generateUUID()], { code: 'test', }); @@ -97,7 +97,7 @@ describe('AGMA Analytics Adapter', () => { }, }, })); - const payload = getPayload([generateUUID()], { + const payload = getPayload([utils.generateUUID()], { code: 'test', }); expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); @@ -113,7 +113,7 @@ describe('AGMA Analytics Adapter', () => { }, }, })); - const payload = getPayload([generateUUID()], { + const payload = getPayload([utils.generateUUID()], { code: 'test', }); expect(payload).to.have.all.keys([...extendedKey, 'debug']); @@ -242,6 +242,35 @@ describe('AGMA Analytics Adapter', () => { }); }); + it('can be overwritten with a global agma variable', () => { + sandbox.stub(utils, 'getWindowSelf').returns({ + agma: { + ortb2: { + site: { + domain: 'overwritten.com', + }, + }, + }, + }); + + const ortb2 = { + site: { + domain: 'inital.com' + } + }; + + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal({ + user: undefined, + site: { + domain: 'overwritten.com', + } + }); + }); + describe('Event Payload', () => { beforeEach(() => { agmaAnalyticsAdapter.enableAnalytics({ @@ -278,26 +307,26 @@ describe('AGMA Analytics Adapter', () => { }, })); const auction = { - auctionId: generateUUID(), + auctionId: utils.generateUUID(), }; events.emit(EVENTS.AUCTION_INIT, { - auctionId: generateUUID('1'), + auctionId: utils.generateUUID('1'), auction, }); clock.tick(200); events.emit(EVENTS.AUCTION_INIT, { - auctionId: generateUUID('2'), + auctionId: utils.generateUUID('2'), auction, }); events.emit(EVENTS.AUCTION_INIT, { - auctionId: generateUUID('3'), + auctionId: utils.generateUUID('3'), auction, }); events.emit(EVENTS.AUCTION_INIT, { - auctionId: generateUUID('4'), + auctionId: utils.generateUUID('4'), auction, }); @@ -324,7 +353,7 @@ describe('AGMA Analytics Adapter', () => { }, })); const auction = { - auctionId: generateUUID(), + auctionId: utils.generateUUID(), }; events.emit(EVENTS.AUCTION_INIT, auction); @@ -348,7 +377,7 @@ describe('AGMA Analytics Adapter', () => { })); const auction = { - auctionId: generateUUID(), + auctionId: utils.generateUUID(), }; events.emit(EVENTS.AUCTION_INIT, auction); @@ -373,7 +402,7 @@ describe('AGMA Analytics Adapter', () => { }, }); const auction = { - auctionId: generateUUID(), + auctionId: utils.generateUUID(), }; events.emit(EVENTS.AUCTION_INIT, auction); diff --git a/test/spec/modules/aidemBidAdapter_spec.js b/test/spec/modules/aidemBidAdapter_spec.js index 3de348197b2..c07b999afe6 100644 --- a/test/spec/modules/aidemBidAdapter_spec.js +++ b/test/spec/modules/aidemBidAdapter_spec.js @@ -168,7 +168,7 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ }, params: { siteId: '1', - placementId: '13144370' + placementId: '13144370', }, src: 'client', transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' @@ -193,7 +193,7 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ }, params: { siteId: '1', - placementId: '13144370' + placementId: '13144370', }, src: 'client', transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' @@ -209,7 +209,50 @@ const VALID_BIDDER_REQUEST = { params: { placementId: '13144370', siteId: '23434', - publisherId: '7689670753' + publisherId: '7689670753', + }, + } + ], + refererInfo: { + page: 'test-page', + domain: 'test-domain', + ref: 'test-referer' + }, +} + +const VALID_GDPR_BIDDER_REQUEST = { + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + bidderCode: 'aidem', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: { + placementId: '13144370', + siteId: '23434', + publisherId: '7689670753', + }, + } + ], + refererInfo: { + page: 'test-page', + domain: 'test-domain', + ref: 'test-referer' + }, + gdprConsent: { + consentString: 'test-gdpr-string' + } +} + +const VALID_USP_BIDDER_REQUEST = { + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + bidderCode: 'aidem', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: { + placementId: '13144370', + siteId: '23434', + publisherId: '7689670753', }, } ], @@ -218,6 +261,7 @@ const VALID_BIDDER_REQUEST = { domain: 'test-domain', ref: 'test-referer' }, + uspConsent: '1YYY' } const SERVER_RESPONSE_BANNER = { @@ -411,7 +455,7 @@ describe('Aidem adapter', () => { expect(data.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_BANNER_REQUESTS.length) expect(data.imp[0]).to.be.a('object').that.has.all.keys( - 'banner', 'id', 'tagId' + 'banner', 'id', 'tagId', 'secure' ) expect(data.imp[0].banner).to.be.a('object').that.has.all.keys( 'format', 'topframe' @@ -427,7 +471,7 @@ describe('Aidem adapter', () => { expect(data.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_VIDEO_REQUESTS.length) expect(data.imp[0]).to.be.a('object').that.has.all.keys( - 'video', 'id', 'tagId' + 'video', 'id', 'tagId', 'secure' ) expect(data.imp[0].video).to.be.a('object').that.has.all.keys( 'mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h' @@ -601,47 +645,51 @@ describe('Aidem adapter', () => { }); it(`should set gdpr to true`, function () { - config.setConfig({ - consentManagement: { - gdpr: { - // any data here set gdpr to true - }, - } - }); - const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); + // config.setConfig({ + // consentManagement: { + // gdpr: { + // consentData: { + // getTCData: { + // tcString: 'test-gdpr-string' + // } + // } + // }, + // } + // }); + const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_GDPR_BIDDER_REQUEST); expect(data.regs.gdpr_applies).to.equal(true) }); it(`should set usp_consent string`, function () { - config.setConfig({ - consentManagement: { - usp: { - cmpApi: 'static', - consentData: { - getUSPData: { - uspString: '1YYY' - } - } - } - } - }); - const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); + // config.setConfig({ + // consentManagement: { + // usp: { + // cmpApi: 'static', + // consentData: { + // getUSPData: { + // uspString: '1YYY' + // } + // } + // } + // } + // }); + const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_USP_BIDDER_REQUEST); expect(data.regs.us_privacy).to.equal('1YYY') }); it(`should not set usp_consent string`, function () { - config.setConfig({ - consentManagement: { - usp: { - cmpApi: 'iab', - consentData: { - getUSPData: { - uspString: '1YYY' - } - } - } - } - }); + // config.setConfig({ + // consentManagement: { + // usp: { + // cmpApi: 'iab', + // consentData: { + // getUSPData: { + // uspString: '1YYY' + // } + // } + // } + // } + // }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); expect(data.regs.us_privacy).to.undefined }); diff --git a/test/spec/modules/ajaBidAdapter_spec.js b/test/spec/modules/ajaBidAdapter_spec.js index dbc72d113f4..bd2bdd3e407 100644 --- a/test/spec/modules/ajaBidAdapter_spec.js +++ b/test/spec/modules/ajaBidAdapter_spec.js @@ -24,12 +24,12 @@ describe('AjaAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'asi': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/akamaiDapRtdProvider_spec.js b/test/spec/modules/akamaiDapRtdProvider_spec.js index 337fcf57a33..0787f911591 100644 --- a/test/spec/modules/akamaiDapRtdProvider_spec.js +++ b/test/spec/modules/akamaiDapRtdProvider_spec.js @@ -44,7 +44,7 @@ describe('akamaiDapRtdProvider', function() { 'apiVersion': 'x1', 'domain': 'prebid.org', 'identityType': 'dap-signature:1.0.0', - 'segtax': 503 + 'segtax': 708 } } @@ -56,7 +56,7 @@ describe('akamaiDapRtdProvider', function() { 'apiVersion': 'x1', 'domain': 'prebid.org', 'identityType': 'dap-signature:1.0.0', - 'segtax': 504 + 'segtax': 710 } } @@ -64,7 +64,7 @@ describe('akamaiDapRtdProvider', function() { 'api_hostname': 'prebid.dap.akadns.net', 'api_version': 'x1', 'domain': 'prebid.org', - 'segtax': 503, + 'segtax': 708, 'identity': sampleIdentity } @@ -72,7 +72,7 @@ describe('akamaiDapRtdProvider', function() { 'api_hostname': 'prebid.dap.akadns.net', 'api_version': 'x1', 'domain': 'prebid.org', - 'segtax': 504, + 'segtax': 710, 'identity': sampleIdentity } let cacheExpiry = Math.round(Date.now() / 1000.0) + 300; // in seconds @@ -97,7 +97,7 @@ describe('akamaiDapRtdProvider', function() { const encRtdUserObj = { name: 'www.dataprovider3.com', ext: { - segtax: 504, + segtax: 710, taxonomyname: 'iab_audience_taxonomy' }, segment: [] @@ -262,13 +262,13 @@ describe('akamaiDapRtdProvider', function() { 'api_hostname': 'prebid.dap.akadns.net', 'api_version': 1, 'domain': '', - 'segtax': 503 + 'segtax': 708 }; const encConfig = { 'api_hostname': 'prebid.dap.akadns.net', 'api_version': 1, 'domain': '', - 'segtax': 504 + 'segtax': 710 }; let identity = { type: 'dap-signature:1.0.0' @@ -396,7 +396,7 @@ describe('akamaiDapRtdProvider', function() { apiHostname: 'prebid.dap.akadns.net', apiVersion: 'x1', domain: 'prebid.org', - segtax: 503 + segtax: 708 }; expect(dapUtils.dapRefreshMembership(ortb2, config, 'token', onDone)).to.equal(undefined) const membership = {cohorts: ['1', '5', '7']} @@ -405,11 +405,11 @@ describe('akamaiDapRtdProvider', function() { }); describe('checkAndAddRealtimeData test', function () { - it('add realtime data for segtax 503 and 504', function () { - dapUtils.checkAndAddRealtimeData(ortb2, cachedEncRtd, 504); - dapUtils.checkAndAddRealtimeData(ortb2, cachedEncRtd, 504); + it('add realtime data for segtax 708 and 710', function () { + dapUtils.checkAndAddRealtimeData(ortb2, cachedEncRtd, 710); + dapUtils.checkAndAddRealtimeData(ortb2, cachedEncRtd, 710); expect(ortb2.user.data).to.deep.include.members([encRtdUserObj]); - dapUtils.checkAndAddRealtimeData(ortb2, cachedRtd, 503); + dapUtils.checkAndAddRealtimeData(ortb2, cachedRtd, 708); expect(ortb2.user.data).to.deep.include.members([rtdUserObj]); }); }); @@ -466,7 +466,7 @@ describe('akamaiDapRtdProvider', function() { let request = server.requests[0]; responseHeader['Akamai-DAP-Token'] = encMembership; request.respond(200, responseHeader, encMembership); - let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 504) + let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 710) expect(ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); expect(JSON.parse(storage.getDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(expiry); }); @@ -477,7 +477,7 @@ describe('akamaiDapRtdProvider', function() { let request = server.requests[0]; responseHeader['Akamai-DAP-Token'] = encMembership; request.respond(200, responseHeader, encMembership); - let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 504) + let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 710) expect(ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); expect(JSON.parse(storage.getDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(1643830630); }); @@ -508,7 +508,7 @@ describe('akamaiDapRtdProvider', function() { dapUtils.dapRefreshMembership(ortb2, sampleConfig, sampleCachedToken.token, onDone); let request = server.requests[0]; request.respond(200, responseHeader, JSON.stringify(membership)); - let rtdObj = dapUtils.dapGetRtdObj(membership, 503); + let rtdObj = dapUtils.dapGetRtdObj(membership, 708); expect(ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); }); @@ -517,7 +517,7 @@ describe('akamaiDapRtdProvider', function() { dapUtils.dapRefreshMembership(ortb2, sampleConfig, sampleCachedToken.token, onDone); let request = server.requests[0]; request.respond(200, responseHeader, JSON.stringify(membership)); - let rtdObj = dapUtils.dapGetRtdObj(membership, 503) + let rtdObj = dapUtils.dapGetRtdObj(membership, 708) expect(ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); expect(JSON.parse(storage.getDataFromLocalStorage(DAP_MEMBERSHIP)).expires_at).to.be.equal(1647971548); }); diff --git a/test/spec/modules/akceloBidAdapter_spec.js b/test/spec/modules/akceloBidAdapter_spec.js new file mode 100644 index 00000000000..5c519ea9834 --- /dev/null +++ b/test/spec/modules/akceloBidAdapter_spec.js @@ -0,0 +1,263 @@ +import { converter, spec } from 'modules/akceloBidAdapter.js'; +import * as utils from '../../../src/utils.js'; +import { deepClone } from '../../../src/utils.js'; +import sinon from 'sinon'; + +describe('Akcelo bid adapter tests', () => { + let sandBox; + + beforeEach(() => { + sandBox = sinon.createSandbox(); + sandBox.stub(utils, 'logError'); + }); + + afterEach(() => sandBox.restore()); + + const DEFAULT_BANNER_BID_REQUESTS = [ + { + adUnitCode: 'div-banner-id', + bidId: 'bid-123', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bidder: 'akcelo', + params: { + siteId: 123, + adUnitId: 456, + }, + requestId: 'request-123', + } + ]; + + const DEFAULT_BANNER_BIDDER_REQUEST = { + bidderCode: 'akcelo', + 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: 0.6565, + adm: '

AD

', + adomain: ['abc.com'], + cid: '1242512', + crid: '535231', + w: 300, + h: 600, + mtype: 1, + ext: { + prebid: { + type: 'banner', + } + } + }, + ], + seat: '4212', + }, + ], + cur: 'EUR', + }, + }; + + describe('isBidRequestValid', () => { + it('should return true if params.siteId and params.adUnitId are set', () => { + const bidRequest = { + params: { + siteId: 123, + adUnitId: 456, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false if params.siteId is missing', () => { + const bidRequest = { + params: { + adUnitId: 456, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false if params.adUnitId is missing', () => { + const bidRequest = { + params: { + siteId: 123, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + it('should build correct requests using ORTB converter', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + ); + const dataFromConverter = converter.toORTB({ + bidderRequest: DEFAULT_BANNER_BIDDER_REQUEST, + bidRequests: DEFAULT_BANNER_BID_REQUESTS, + }); + expect(request[0]).to.deep.equal({ + data: { ...dataFromConverter, id: request[0].data.id }, + method: 'POST', + url: 'https://s2s.sportslocalmedia.com/openrtb2/auction', + }); + }); + + it('should add site.publisher.ext.prebid.parentAccount to request object when siteId is defined', () => { + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + expect(request.data.site.publisher.ext.prebid.parentAccount).to.equal(123); + }); + + it('should not add site.publisher.ext.prebid.parentAccount to request object when siteId is not defined', () => { + const bidRequests = [ + { ...DEFAULT_BANNER_BID_REQUESTS[0], params: { adUnitId: 456 } }, + ]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.site.publisher.ext.prebid.parentAccount).to.be.undefined; + }); + + it('should add ext.akcelo to imp object when siteId and adUnitId are defined', () => { + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + expect(request.data.imp[0].ext.akcelo).to.deep.equal({ + siteId: 123, + adUnitId: 456, + }); + }); + + it('should not add ext.akcelo.siteId to imp object when siteId is not defined', () => { + const bidRequests = [ + { ...DEFAULT_BANNER_BID_REQUESTS[0], params: { adUnitId: 456 } }, + ]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.imp[0].ext.akcelo.siteId).to.be.undefined; + expect(utils.logError.calledOnce).to.equal(true); + expect(utils.logError.args[0][0]).to.equal('Missing parameter : siteId') + }); + + it('should not add ext.akcelo.adUnitId to imp object when adUnitId is not defined', () => { + const bidRequests = [ + { ...DEFAULT_BANNER_BID_REQUESTS[0], params: { siteId: 123 } }, + ]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.imp[0].ext.akcelo.adUnitId).to.be.undefined; + expect(utils.logError.calledOnce).to.equal(true); + expect(utils.logError.args[0][0]).to.equal('Missing parameter : adUnitId') + }); + + it('should add ext.akcelo.test=1 to imp object when param akcelo_demo is true', () => { + sandBox.stub(utils, 'getParameterByName').callsFake(() => 'true'); + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + expect(request.data.imp[0].ext.akcelo.test).to.equal(1); + }); + + it('should not add ext.akcelo.test to imp object when param akcelo_demo is not true', () => { + sandBox.stub(utils, 'getParameterByName').callsFake(() => 'something_else'); + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + expect(request.data.imp[0].ext.akcelo.test).to.be.undefined; + }); + + it('should not add ext.akcelo.test to imp object when param akcelo_demo is not defined', () => { + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + expect(request.data.imp[0].ext.akcelo.test).to.be.undefined; + }); + }); + + describe('interpretResponse', () => { + it('should return data returned by ORTB converter', () => { + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + const bids = spec.interpretResponse(SAMPLE_RESPONSE, request); + const responseFromConverter = converter.fromORTB({ + request: request.data, + response: SAMPLE_RESPONSE.body, + }); + expect(bids).to.deep.equal(responseFromConverter.bids); + }); + + it('should find the media type from bid.mtype if possible', () => { + const serverResponse = deepClone(SAMPLE_RESPONSE); + serverResponse.body.seatbid[0].bid[0].mtype = 2; + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids[0].mediaType).to.equal('video'); + }); + + it('should find the media type from bid.ext.prebid.type if mtype is not defined', () => { + const serverResponse = deepClone(SAMPLE_RESPONSE); + delete serverResponse.body.seatbid[0].bid[0].mtype; + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids[0].mediaType).to.equal('banner'); + }); + + it('should skip the bid if bid.mtype and bid.ext.prebid.type are not defined', () => { + const serverResponse = deepClone(SAMPLE_RESPONSE); + delete serverResponse.body.seatbid[0].bid[0].mtype; + delete serverResponse.body.seatbid[0].bid[0].ext.prebid.type; + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.empty; + }); + }); + + describe('getUserSyncs', () => { + it('should return an empty array if iframe sync is not enabled', () => { + const syncs = spec.getUserSyncs({}, [SAMPLE_RESPONSE], null, null); + expect(syncs).to.deep.equal([]); + }); + + it('should return an array with iframe url', () => { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [SAMPLE_RESPONSE], null, null); + expect(syncs).to.deep.equal([{ + type: 'iframe', + url: 'https://ads.sportslocalmedia.com/load-cookie.html?endpoint=akcelo' + }]); + }); + + it('should return an array with iframe URL and GDPR parameters', () => { + const gdprConsent = { gdprApplies: true, consentString: 'the_gdpr_consent' }; + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [SAMPLE_RESPONSE], gdprConsent, null); + expect(syncs[0].url).to.contain('?endpoint=akcelo&gdpr=1&gdpr_consent=the_gdpr_consent'); + }); + + it('should return an array with iframe URL containing empty GDPR parameters when GDPR does not apply', () => { + const gdprConsent = { gdprApplies: false }; + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [SAMPLE_RESPONSE], gdprConsent, null); + expect(syncs[0].url).to.contain('?endpoint=akcelo&gdpr=0&gdpr_consent='); + }); + + it('should URI encode the GDPR consent string', () => { + const gdprConsent = { gdprApplies: true, consentString: 'the_gdpr_consent==' }; + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [SAMPLE_RESPONSE], gdprConsent, null); + expect(syncs[0].url).to.contain('?endpoint=akcelo&gdpr=1&gdpr_consent=the_gdpr_consent%3D%3D'); + }); + + it('should return an array with iframe URL containing USP parameters when USP is defined', () => { + const uspConsent = 'the_usp_consent'; + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [SAMPLE_RESPONSE], null, uspConsent); + expect(syncs[0].url).to.contain('?endpoint=akcelo&us_privacy=the_usp_consent'); + }); + + it('should URI encode the USP consent string', () => { + const uspConsent = 'the_usp_consent=='; + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [SAMPLE_RESPONSE], null, uspConsent); + expect(syncs[0].url).to.contain('?endpoint=akcelo&us_privacy=the_usp_consent%3D%3D'); + }); + }); +}); diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index e551d98fa07..d642afd2b26 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -7,6 +7,12 @@ const REQUEST = { 'bidder': 'alkimi', 'sizes': [[300, 250]], 'adUnitCode': 'bannerAdUnitCode', + 'ortb2Imp': { + 'ext': { + 'gpid': '/111/banner#300x250', + 'tid': 'e64782a4-8e68-4c38-965b-80ccf115d46a' + } + }, 'mediaTypes': { 'banner': { 'sizes': [[300, 250]] @@ -148,7 +154,7 @@ 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.contains({ 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 }) + 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.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 21fa2e2617c..680579aa299 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -5,6 +5,7 @@ import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; import * as utils from 'src/utils.js'; +import { getGlobal } from '../../../src/prebidGlobal'; const sampleRequestId = '82c91e127a9b93e'; const sampleDisplayAd = ``; @@ -442,7 +443,6 @@ describe('AmxBidAdapter', () => { it('will collect & forward RTI user IDs', () => { const randomRTI = `greatRTI${Math.floor(Math.random() * 100)}`; const userId = { - britepoolid: 'sample-britepool', criteoId: 'sample-criteo', digitrustid: { data: { id: 'sample-digitrust' } }, id5id: { uid: 'sample-id5' }, @@ -583,6 +583,89 @@ describe('AmxBidAdapter', () => { expect(parsed).to.eql([]); }); + const cases = [ + [ + 'pbjs.bidderSettings', + (conf) => { + const before = getGlobal().bidderSettings; + getGlobal().bidderSettings = conf; + return before; + }, + (before) => { + getGlobal().bidderSettings = before; + }, + ], + [ + 'setConfig / bidderSettings (legacy)', + (conf) => { + const before = config.getConfig(); + config.setConfig({ + bidderSettings: conf, + }); + + return before; + }, + (before) => { + config.setConfig(before); + }, + ], + ]; + + cases.forEach(([name, setup, teardown]) => { + it(`will read an bidderCode override from bid.ext.prebid.meta, set with ${name}`, () => { + const currentConfig = setup({ + amx: { + allowAlternateBidderCodes: true, + }, + }); + const parsed = spec.interpretResponse( + { + body: { + ...sampleServerResponse, + r: { + [sampleRequestId]: [ + { + ...sampleServerResponse.r[sampleRequestId][0], + b: [ + { + ...sampleServerResponse.r[sampleRequestId][0].b[0], + ext: { + bc: 'amx-pmp', + ds: 'example', + dsp: 'example-dsp', + }, + }, + ], + }, + ], + }, + }, + }, + baseRequest + ); + + teardown(currentConfig); + expect(parsed.length).to.equal(1); // we removed one + + // we should have display, video, display + expect(parsed[0]).to.deep.equal({ + ...baseBidResponse, + meta: { + ...baseBidResponse.meta, + mediaType: BANNER, + demandSource: 'example', + networkId: 'example-dsp', + }, + mediaType: BANNER, + bidderCode: 'amx-pmp', + width: 300, + height: 600, // from the bid itself + ttl: 90, + ad: sampleDisplayAd, + }); + }); + }); + it('can parse a display ad', () => { const parsed = spec.interpretResponse( { body: sampleServerResponse }, @@ -710,14 +793,14 @@ describe('AmxBidAdapter', () => { ]); const [request] = server.requests; - request.respond(204, {'Content-Type': 'text/html'}, null); + request.respond(204, { 'Content-Type': 'text/html' }, null); expect(request.url).to.equal('https://1x1.a-mo.net/e'); if (typeof Request !== 'undefined' && 'keepalive' in Request.prototype) { expect(request.fetch.request.keepalive).to.equal(true); } - const {c: common, e: events} = JSON.parse(request.requestBody) + const { c: common, e: events } = JSON.parse(request.requestBody); expect(common).to.deep.equal({ V: '$prebid.version$', vg: '$$PREBID_GLOBAL$$', @@ -727,7 +810,7 @@ describe('AmxBidAdapter', () => { expect(events.length).to.equal(1); const [event] = events; - expect(event.n).to.equal('g_pbto') + expect(event.n).to.equal('g_pbto'); expect(event.A).to.equal('example'); expect(event.mid).to.equal('tag-id'); expect(event.cn).to.equal(300); diff --git a/test/spec/modules/amxIdSystem_spec.js b/test/spec/modules/amxIdSystem_spec.js index c1ae2c791d5..b509ffe608b 100644 --- a/test/spec/modules/amxIdSystem_spec.js +++ b/test/spec/modules/amxIdSystem_spec.js @@ -1,6 +1,9 @@ import { amxIdSubmodule, storage } from 'modules/amxIdSystem.js'; import { server } from 'test/mocks/xhr.js'; import * as utils from 'src/utils.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; const TEST_ID = '51b561e3-0d82-4aea-8487-093fffca4a3a'; const ERROR_CODES = [404, 501, 500, 403]; @@ -13,177 +16,198 @@ const config = { type: 'html5', }, }; +describe('AMX ID', () => { + describe('amxid submodule', () => { + it('should expose a "name" property containing amxId', () => { + expect(amxIdSubmodule.name).to.equal('amxId'); + }); -describe('amxid submodule', () => { - it('should expose a "name" property containing amxId', () => { - expect(amxIdSubmodule.name).to.equal('amxId'); - }); - - it('should expose a "gvlid" property containing the GVL ID 737', () => { - expect(amxIdSubmodule.gvlid).to.equal(737); + it('should expose a "gvlid" property containing the GVL ID 737', () => { + expect(amxIdSubmodule.gvlid).to.equal(737); + }); }); -}); -describe('decode', () => { - it('should respond with an object with "amxId" key containing the value', () => { - expect(amxIdSubmodule.decode(TEST_ID)).to.deep.equal({ - amxId: TEST_ID + describe('decode', () => { + it('should respond with an object with "amxId" key containing the value', () => { + expect(amxIdSubmodule.decode(TEST_ID)).to.deep.equal({ + amxId: TEST_ID + }); }); - }); - it('should respond with undefined if the value is not a string', () => { - [1, null, undefined, NaN, [], {}].forEach((value) => { - expect(amxIdSubmodule.decode(value)).to.equal(undefined); + it('should respond with undefined if the value is not a string', () => { + [1, null, undefined, NaN, [], {}].forEach((value) => { + expect(amxIdSubmodule.decode(value)).to.equal(undefined); + }); }); }); -}); -describe('validateConfig', () => { - let logErrorSpy; + describe('validateConfig', () => { + let logErrorSpy; - beforeEach(() => { - logErrorSpy = sinon.spy(utils, 'logError'); - }); - afterEach(() => { - logErrorSpy.restore(); - }); + beforeEach(() => { + logErrorSpy = sinon.spy(utils, 'logError'); + }); + afterEach(() => { + logErrorSpy.restore(); + }); - it('should allow configuration with no storage', () => { - expect( - amxIdSubmodule.getId( - { - ...config, - storage: undefined - }, - null, - null - ) - ).to.not.equal(undefined); - }); + it('should allow configuration with no storage', () => { + expect( + amxIdSubmodule.getId( + { + ...config, + storage: undefined + }, + null, + null + ) + ).to.not.equal(undefined); + }); - it('should return undefined if expires > 30', () => { - const expires = Math.floor(Math.random() * 90) + 30.01; - expect( - amxIdSubmodule.getId( - { - ...config, - storage: { - type: 'html5', - expires, + it('should return undefined if expires > 30', () => { + const expires = Math.floor(Math.random() * 90) + 30.01; + expect( + amxIdSubmodule.getId( + { + ...config, + storage: { + type: 'html5', + expires, + }, }, - }, - null, - null - ) - ).to.equal(undefined); - - expect(logErrorSpy.calledOnce).to.be.true; - expect(logErrorSpy.lastCall.lastArg).to.contain(expires); + null, + null + ) + ).to.equal(undefined); + + expect(logErrorSpy.calledOnce).to.be.true; + expect(logErrorSpy.lastCall.lastArg).to.contain(expires); + }); }); -}); -describe('getId', () => { - const spy = sinon.spy(); + describe('getId', () => { + const spy = sinon.spy(); - beforeEach(() => { - spy.resetHistory(); - }); + beforeEach(() => { + spy.resetHistory(); + }); - it('should call the sync endpoint and accept a valid response', () => { - storage.setDataInLocalStorage('__amuidpb', TEST_ID); + it('should call the sync endpoint and accept a valid response', () => { + storage.setDataInLocalStorage('__amuidpb', TEST_ID); - const { callback } = amxIdSubmodule.getId(config, null, null); - callback(spy); + const { callback } = amxIdSubmodule.getId(config, null, null); + callback(spy); - const [request] = server.requests; - expect(request.withCredentials).to.be.true - expect(request.requestHeaders['Content-Type']).to.match(/text\/plain/) + const [request] = server.requests; + expect(request.withCredentials).to.be.true + expect(request.requestHeaders['Content-Type']).to.match(/text\/plain/) - const { search } = utils.parseUrl(request.url); - expect(search.av).to.equal(amxIdSubmodule.version); - expect(search.am).to.equal(TEST_ID); - expect(request.method).to.equal('GET'); + const { search } = utils.parseUrl(request.url); + expect(search.av).to.equal(amxIdSubmodule.version); + expect(search.am).to.equal(TEST_ID); + expect(request.method).to.equal('GET'); - request.respond( - 200, - {}, - JSON.stringify({ - id: TEST_ID, - v: '1.0a', - }) - ); + request.respond( + 200, + {}, + JSON.stringify({ + id: TEST_ID, + v: '1.0a', + }) + ); - expect(spy.calledOnce).to.be.true; - expect(spy.lastCall.lastArg).to.equal(TEST_ID); - }); + expect(spy.calledOnce).to.be.true; + expect(spy.lastCall.lastArg).to.equal(TEST_ID); + }); - it('should return undefined if the server has an error status code', () => { - const { callback } = amxIdSubmodule.getId(config, null, null); - callback(spy); + it('should return undefined if the server has an error status code', () => { + const { callback } = amxIdSubmodule.getId(config, null, null); + callback(spy); - const [request] = server.requests; - const responseCode = - ERROR_CODES[Math.floor(Math.random() * ERROR_CODES.length)]; - request.respond(responseCode, {}, ''); + const [request] = server.requests; + const responseCode = + ERROR_CODES[Math.floor(Math.random() * ERROR_CODES.length)]; + request.respond(responseCode, {}, ''); - expect(spy.calledOnce).to.be.true; - expect(spy.lastCall.lastArg).to.equal(undefined); - }); + expect(spy.calledOnce).to.be.true; + expect(spy.lastCall.lastArg).to.equal(undefined); + }); - it('should return undefined if the response has invalid keys', () => { - const { callback } = amxIdSubmodule.getId(config, null, null); - callback(spy); - - const [request] = server.requests; - request.respond( - 200, - {}, - JSON.stringify({ - test: TEST_ID, - }) - ); - - expect(spy.calledOnce).to.be.true; - expect(spy.lastCall.lastArg).to.equal(undefined); - }); + it('should return undefined if the response has invalid keys', () => { + const { callback } = amxIdSubmodule.getId(config, null, null); + callback(spy); + + const [request] = server.requests; + request.respond( + 200, + {}, + JSON.stringify({ + test: TEST_ID, + }) + ); + + expect(spy.calledOnce).to.be.true; + expect(spy.lastCall.lastArg).to.equal(undefined); + }); - it('should returned undefined if the server JSON is invalid', () => { - const { callback } = amxIdSubmodule.getId(config, null, null); - callback(spy); + it('should returned undefined if the server JSON is invalid', () => { + const { callback } = amxIdSubmodule.getId(config, null, null); + callback(spy); - const [request] = server.requests; - request.respond(200, {}, '{,,}'); + const [request] = server.requests; + request.respond(200, {}, '{,,}'); - expect(spy.calledOnce).to.be.true; - expect(spy.lastCall.lastArg).to.equal(undefined); - }); + expect(spy.calledOnce).to.be.true; + expect(spy.lastCall.lastArg).to.equal(undefined); + }); - it('should use the intermediate value for the sync server', () => { - const { callback } = amxIdSubmodule.getId(config, null, null); - callback(spy); - - const [request] = server.requests; - const intermediateValue = 'https://example-publisher.com/api/sync'; - - request.respond( - 200, - {}, - JSON.stringify({ - u: intermediateValue, - }) - ); - - const [, secondRequest] = server.requests; - expect(secondRequest.url).to.match(new RegExp(`^${intermediateValue}\?`)); - secondRequest.respond( - 200, - {}, - JSON.stringify({ - id: TEST_ID, - }) - ); - - expect(spy.calledOnce).to.be.true; - expect(spy.lastCall.lastArg).to.equal(TEST_ID); + it('should use the intermediate value for the sync server', () => { + const { callback } = amxIdSubmodule.getId(config, null, null); + callback(spy); + + const [request] = server.requests; + const intermediateValue = 'https://example-publisher.com/api/sync'; + + request.respond( + 200, + {}, + JSON.stringify({ + u: intermediateValue, + }) + ); + + const [, secondRequest] = server.requests; + expect(secondRequest.url).to.match(new RegExp(`^${intermediateValue}\?`)); + secondRequest.respond( + 200, + {}, + JSON.stringify({ + id: TEST_ID, + }) + ); + + expect(spy.calledOnce).to.be.true; + expect(spy.lastCall.lastArg).to.equal(TEST_ID); + }); }); -}); + describe('eid', () => { + before(() => { + attachIdSystem(amxIdSubmodule); + }); + it('amxId', () => { + const id = 'c4bcadb0-124f-4468-a91a-d3d44cf311c5' + const userId = { + amxId: id + }; + + const [eid] = createEidsArray(userId); + expect(eid).to.deep.equal({ + source: 'amxdt.net', + uids: [{ + atype: 1, + id, + }] + }); + }); + }) +}) diff --git a/test/spec/modules/anPspParamsConverter_spec.js b/test/spec/modules/anPspParamsConverter_spec.js new file mode 100644 index 00000000000..a96730088b8 --- /dev/null +++ b/test/spec/modules/anPspParamsConverter_spec.js @@ -0,0 +1,133 @@ +import { expect } from 'chai'; + +import {convertAnParams} from '../../../modules/anPspParamsConverter'; +import { config } from '../../../src/config.js'; +import { deepClone } from '../../../src/utils'; +import adapterManager from '../../../src/adapterManager.js'; + +describe('anPspParamsConverter', function () { + let configStub; + let resolveAliasStub; + let didHookRun = false; + + const bidderRequests = [{ + bidderCode: 'appnexus', + bids: [{ + bidder: 'appnexus', + src: 's2s', + params: { + member: 958, + invCode: 12345, + placementId: '10001', + keywords: { + music: 'rock', + genre: ['80s', '90s'] + }, + publisherId: '111', + use_payment_rule: true + } + }] + }]; + + beforeEach(function () { + configStub = sinon.stub(config, 'getConfig'); + resolveAliasStub = sinon.stub(adapterManager, 'resolveAlias').callsFake(function (tarBidder) { + return (tarBidder === 'rubicon') ? 'rubicon' : 'appnexus'; + }); + }); + + afterEach(function () { + didHookRun = false; + configStub.restore(); + resolveAliasStub.restore(); + }); + + it('does not modify params when appnexus is not in s2sconfig', function () { + configStub.callsFake(function () { + return { + bidders: ['rubicon'] + }; + }); + + const testBidderRequests = deepClone(bidderRequests); + + convertAnParams(function () { + didHookRun = true; + }, testBidderRequests); + + expect(didHookRun).to.equal(true); + const resultParams = testBidderRequests[0].bids[0].params; + expect(resultParams.member).to.equal(958); + expect(resultParams.invCode).to.equal(12345); + expect(resultParams.placementId).to.equal('10001'); + expect(resultParams.keywords).to.deep.equal({ + music: 'rock', + genre: ['80s', '90s'] + }); + expect(resultParams.publisherId).to.equal('111'); + expect(resultParams.use_payment_rule).to.equal(true); + }); + + const tests = [{ + testName: 'modifies params when appnexus is the bidder', + fakeConfigFn: function () { + return { + bidders: ['appnexus'] + }; + }, + applyBidderRequestChanges: function () { + const testBidderRequests = deepClone(bidderRequests); + + return testBidderRequests; + } + }, { + testName: 'modifies params when a registered appnexus alias is used', + fakeConfigFn: function () { + return { + bidders: ['beintoo'] + }; + }, + applyBidderRequestChanges: function () { + const testBidderRequests = deepClone(bidderRequests); + testBidderRequests.bidderCode = 'beintoo'; + testBidderRequests[0].bids[0].bidder = 'beintoo'; + + return testBidderRequests; + } + }, { + testName: 'modifies params when pbjs.aliasBidder alias is used', + fakeConfigFn: function () { + return { + bidders: ['aliasBidderTest'], + }; + }, + applyBidderRequestChanges: function () { + const testBidderRequests = deepClone(bidderRequests); + testBidderRequests.bidderCode = 'aliasBidderTest'; + testBidderRequests[0].bids[0].bidder = 'aliasBidderTest'; + + return testBidderRequests; + } + }]; + + tests.forEach((testCfg) => { + it(testCfg.testName, function () { + configStub.callsFake(testCfg.fakeConfigFn); + + const testBidderRequests = testCfg.applyBidderRequestChanges(); + + convertAnParams(function () { + didHookRun = true; + }, testBidderRequests); + + expect(didHookRun).to.equal(true); + const resultParams = testBidderRequests[0].bids[0].params; + expect(resultParams.member).to.equal('958'); + expect(resultParams.inv_code).to.equal('12345'); + expect(resultParams.placement_id).to.equal(10001); + expect(resultParams.keywords).to.equal('music=rock,genre=80s,genre=90s'); + expect(resultParams.publisher_id).to.equal(111); + expect(resultParams.use_pmt_rule).to.equal(true); + }); + }); +}); diff --git a/test/spec/modules/aniviewBidAdapter_spec.js b/test/spec/modules/aniviewBidAdapter_spec.js index a9498af046c..2c90eede1b4 100644 --- a/test/spec/modules/aniviewBidAdapter_spec.js +++ b/test/spec/modules/aniviewBidAdapter_spec.js @@ -2,267 +2,412 @@ import { spec } from 'modules/aniviewBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; const { expect } = require('chai'); -describe('ANIVIEW Bid Adapter Test', function () { +const PUBLISHER_ID_1 = 'publisher_id_1'; +const CHANNEL_ID_1 = 'channel_id_1'; +const PUBLISHER_ID_2 = 'publisher_id_2'; +const CHANNEL_ID_2 = 'channel_id_2'; +const BID_ID_1 = 'bid_id_1'; +const BID_ID_2 = 'bid_id_2'; +const BIDDER_REQUEST_ID = 'bidder_request_id'; +const CUSTOM_DOMAIN = 'example.com'; + +const BASE_URL = 'https://' + CUSTOM_DOMAIN + '/track' + + '?rtbbp=10' + + '&cpm=${AUCTION_PRICE}' + + '&aucid=${AUCTION_ID}' + + '&aucbid=${AUCTION_BID_ID}' + + '&limid=${AUCTION_IMP_ID}' + + '&aucseid=${AUCTION_SEAT_ID}' + + '&aucadid=${AUCTION_AD_ID}'; +const LURL = `${BASE_URL}&e=AV_M40&rcd=\${AUCTION_LOSS}`; +const NURL = `${BASE_URL}&e=AV_M4`; + +const VIDEO_VAST = `` +const BANNER_VAST = VIDEO_VAST; +const BANNER_HTML = '

HTML BANNER

'; + +const CURRENCY = 'USD'; +const PRICE = 10; +const FLOOR_PRICE = PRICE * 0.5; +const TTL = 600; + +const VIDEO_SIZE = { width: 640, height: 360 }; +const BANNER_SIZE = { width: 250, height: 250 }; + +const CUSTOM_RENDERER_URL = `https://${CUSTOM_DOMAIN}/script/6.1/prebidRenderer.js`; +const DEFAULT_RENDERER_URL = `https://player.aniview.com/script/6.1/prebidRenderer.js`; + +const REPLACEMENT_1 = '12345'; + +const MOCK = { + bidRequest: () => ({ + bidderCode: 'aniview', + auctionId: null, + bidderRequestId: BIDDER_REQUEST_ID, + bids: [ + { + bidder: 'aniview', + params: { + AV_PUBLISHERID: PUBLISHER_ID_1, + AV_CHANNELID: CHANNEL_ID_1, + playerDomain: CUSTOM_DOMAIN, + }, + mediaTypes: { + video: { + playerSize: [[VIDEO_SIZE.width, VIDEO_SIZE.height]], + context: 'outstream', + mimes: ['video/mpeg', 'video/mp4', 'application/javascript'], + } + }, + bidId: BID_ID_1, + bidderRequestId: BIDDER_REQUEST_ID, + sizes: [[VIDEO_SIZE.width, VIDEO_SIZE.height]], + }, + { + bidder: 'aniview', + params: { + AV_PUBLISHERID: PUBLISHER_ID_2, + AV_CHANNELID: CHANNEL_ID_2, + playerDomain: CUSTOM_DOMAIN, + replacements: { + AV_CDIM1: REPLACEMENT_1, + }, + }, + mediaTypes: { + video: { + playerSize: [[VIDEO_SIZE.width, VIDEO_SIZE.height]], + context: 'outstream', + mimes: ['video/mpeg', 'video/mp4', 'application/javascript'], + } + }, + bidId: BID_ID_2, + bidderRequestId: BIDDER_REQUEST_ID, + sizes: [[VIDEO_SIZE.width, VIDEO_SIZE.height]], + floorData: { + currency: CURRENCY, + floor: FLOOR_PRICE, + }, + getFloor: _ => ({ + currency: CURRENCY, + floor: FLOOR_PRICE, + }), + }, + ], + auctionStart: 1722343584268, + timeout: 1_000, + start: 1722343584269, + ortb2: { + source: {}, + site: { + page: 'http://localhost:8080/', + ref: 'http://localhost:8080/', + domain: 'http://localhost:8080', + publisher: { + domain: 'http://localhost:8080', + } + }, + device: { + w: 1800, + h: 1169, + language: 'en', + }, + }, + }), + + bidderResponse: () => ({ + body: { + id: 'bidder_response_id', + bidid: 'bidder_response_bid_id', + cur: CURRENCY, + ext: { + aniview: { + sync: [ + { url: 'https://iframe-1.example.com/sync', e: 'sync', pr: '14', t: 3 }, + { url: 'https://iframe-2.example.com/sync', e: 'sync', pr: '28', t: 3 }, + { url: 'https://image.example.com/sync', e: 'sync', pr: 'abc12', t: 1 } + ] + } + }, + seatbid: [ + { + seat: '', + bid: [ + { + adm: VIDEO_VAST, + adomain: [''], + id: 'seatbid_bid_id_1', + impid: BID_ID_1, + lurl: LURL, + nurl: NURL, + price: PRICE, + } + ] + } + ] + }, + }) +} + +describe('Aniview Bid Adapter', function () { const adapter = newBidder(spec); - describe('inherited functions', function () { + describe('Inherited function', function () { it('exists and is a function', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); }); describe('isBidRequestValid', function () { - let bid = { - 'bidder': 'aniview', - 'params': { - 'AV_PUBLISHERID': '123456', - 'AV_CHANNELID': '123456' - }, - 'adUnitCode': 'video1', - 'sizes': [[300, 250], [640, 480]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'requestId': 'a09c66c3-53e3-4428-b296-38fc08e7cd2a', - 'transactionId': 'd6f6b392-54a9-454c-85fb-a2fd882c4a2d', - }; - - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(bid)).to.equal(true); + let videoBidRequest; + + beforeEach(() => { + videoBidRequest = MOCK.bidRequest(); }); - it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { - something: 'is wrong' - }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + it('should return `true` when required params found', function () { + expect(spec.isBidRequestValid(videoBidRequest.bids[0])).to.be.true; + }); + + it('should return `false` when required params are wrong', function () { + videoBidRequest.bids[0].params = { something: 'is wrong' }; + + expect(spec.isBidRequestValid(videoBidRequest.bids[0])).to.be.false; }); }); describe('buildRequests', function () { - let bid2Requests = [ - { - 'bidder': 'aniview', - 'params': { - 'AV_PUBLISHERID': '123456', - 'AV_CHANNELID': '123456' - }, - 'adUnitCode': 'test1', - 'sizes': [[300, 250], [640, 480]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'requestId': 'a09c66c3-53e3-4428-b296-38fc08e7cd2a', - 'transactionId': 'd6f6b392-54a9-454c-85fb-a2fd882c4a2d', - } - ]; - let bid1Request = [ - { - 'bidder': 'aniview', - 'params': { - 'AV_PUBLISHERID': '123456', - 'AV_CHANNELID': '123456' - }, - 'adUnitCode': 'test1', - 'sizes': [640, 480], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'requestId': 'a09c66c3-53e3-4428-b296-38fc08e7cd2a', - 'transactionId': 'd6f6b392-54a9-454c-85fb-a2fd882c4a2d', - } - ]; - - it('Test 2 requests', function () { - const requests = spec.buildRequests(bid2Requests); - expect(requests.length).to.equal(2); - const r1 = requests[0]; - const d1 = requests[0].data; - expect(d1).to.have.property('AV_PUBLISHERID'); - expect(d1.AV_PUBLISHERID).to.equal('123456'); - expect(d1).to.have.property('AV_CHANNELID'); - expect(d1.AV_CHANNELID).to.equal('123456'); - expect(d1).to.have.property('AV_WIDTH'); - expect(d1.AV_WIDTH).to.equal(300); - expect(d1).to.have.property('AV_HEIGHT'); - expect(d1.AV_HEIGHT).to.equal(250); - expect(d1).to.have.property('AV_URL'); - expect(d1).to.have.property('cb'); - expect(d1).to.have.property('s2s'); - expect(d1.s2s).to.equal('1'); - expect(d1).to.have.property('pbjs'); - expect(d1.pbjs).to.equal(1); - expect(r1).to.have.property('url'); - expect(r1.url).to.contain('https://gov.aniview.com/api/adserver/vast3/'); - const r2 = requests[1]; - const d2 = requests[1].data; - expect(d2).to.have.property('AV_PUBLISHERID'); - expect(d2.AV_PUBLISHERID).to.equal('123456'); - expect(d2).to.have.property('AV_CHANNELID'); - expect(d2.AV_CHANNELID).to.equal('123456'); - expect(d2).to.have.property('AV_WIDTH'); - expect(d2.AV_WIDTH).to.equal(640); - expect(d2).to.have.property('AV_HEIGHT'); - expect(d2.AV_HEIGHT).to.equal(480); - expect(d2).to.have.property('AV_URL'); - expect(d2).to.have.property('cb'); - expect(d2).to.have.property('s2s'); - expect(d2.s2s).to.equal('1'); - expect(d2).to.have.property('pbjs'); - expect(d2.pbjs).to.equal(1); - expect(r2).to.have.property('url'); - expect(r2.url).to.contain('https://gov.aniview.com/api/adserver/vast3/'); + let videoBidRequest; + + beforeEach(() => { + videoBidRequest = MOCK.bidRequest(); }); - it('Test 1 request', function () { - const requests = spec.buildRequests(bid1Request); - expect(requests.length).to.equal(1); - const r = requests[0]; - const d = requests[0].data; - expect(d).to.have.property('AV_PUBLISHERID'); - expect(d.AV_PUBLISHERID).to.equal('123456'); - expect(d).to.have.property('AV_CHANNELID'); - expect(d.AV_CHANNELID).to.equal('123456'); - expect(d).to.have.property('AV_WIDTH'); - expect(d.AV_WIDTH).to.equal(640); - expect(d).to.have.property('AV_HEIGHT'); - expect(d.AV_HEIGHT).to.equal(480); - expect(d).to.have.property('AV_URL'); - expect(d).to.have.property('cb'); - expect(d).to.have.property('s2s'); - expect(d.s2s).to.equal('1'); - expect(d).to.have.property('pbjs'); - expect(d.pbjs).to.equal(1); - expect(r).to.have.property('url'); - expect(r.url).to.contain('https://gov.aniview.com/api/adserver/vast3/'); + it('should return expected request object', function () { + const bidRequest = spec.buildRequests(videoBidRequest.bids, videoBidRequest); + + expect(bidRequest).to.exist.and.to.be.a('array').and.to.have.lengthOf(2); + + const { url, method, data } = bidRequest[0]; + const { ext, imp } = data; + + expect(url).equal('https://rtb.aniview.com/sspRTB2'); + expect(method).equal('POST'); + expect(imp[0].tagid).equal(CHANNEL_ID_1); + expect(imp[0].id).equal(videoBidRequest.bids[0].bidId); + expect(ext.aniview.pbjs).equal(1); }); - }); - describe('interpretResponse', function () { - let bidRequest = { - 'url': 'https://gov.aniview.com/api/adserver/vast3/', - 'data': { - 'bidId': '253dcb69fb2577', - AV_PUBLISHERID: '55b78633181f4603178b4568', - AV_CHANNELID: '55b7904d181f46410f8b4568', - } - }; - let serverResponse = {}; - serverResponse.body = 'FORDFORD00:00:15'; - - it('Check bid interpretResponse', function () { - const BIDDER_CODE = 'aniview'; - let bidResponses = spec.interpretResponse(serverResponse, bidRequest); - expect(bidResponses.length).to.equal(1); - let bidResponse = bidResponses[0]; - expect(bidResponse.requestId).to.equal(bidRequest.data.bidId); - expect(bidResponse.cpm).to.equal('2'); - expect(bidResponse.ttl).to.equal(600); - expect(bidResponse.currency).to.equal('USD'); - expect(bidResponse.netRevenue).to.equal(true); - expect(bidResponse.mediaType).to.equal('video'); - expect(bidResponse.meta.advertiserDomains).to.be.an('array').that.is.empty; + it('should have floor data inside imp', function () { + const bidRequest = spec.buildRequests(videoBidRequest.bids, videoBidRequest); + const imp = bidRequest[1].data.imp[0]; + + expect(imp.bidfloor).equal(FLOOR_PRICE); + expect(imp.bidfloorcur).equal(CURRENCY); }); - it('safely handles XML parsing failure from invalid bid response', function () { - let invalidServerResponse = {}; - invalidServerResponse.body = ''; + it('should have replacements in request', function () { + const bidRequest = spec.buildRequests(videoBidRequest.bids, videoBidRequest); + const { replacements } = bidRequest[1].data.ext.aniview; - let result = spec.interpretResponse(invalidServerResponse, bidRequest); - expect(result.length).to.equal(0); + expect(replacements.AV_CDIM1).equal(REPLACEMENT_1); }); - it('handles nobid responses', function () { - let nobidResponse = {}; - nobidResponse.body = ''; + it('should not have floor data in imp if getFloor returns empty object', function () { + videoBidRequest.bids[1].getFloor = () => ({}); - let result = spec.interpretResponse(nobidResponse, bidRequest); - expect(result.length).to.equal(0); + const bidRequest = spec.buildRequests(videoBidRequest.bids, videoBidRequest); + const imp = bidRequest[1].data.imp[0]; + + expect(imp.bidfloor).not.exist; + expect(imp.bidfloorcur).not.exist; }); - it('should add renderer if outstream context', function () { - const bidRequest = spec.buildRequests([ - { - bidId: '253dcb69fb2577', - params: { - playerDomain: 'example.com', - AV_PUBLISHERID: '55b78633181f4603178b4568', - AV_CHANNELID: '55b7904d181f46410f8b4568' - }, - mediaTypes: { - video: { - playerSize: [[640, 480]], - context: 'outstream' - } - } - } - ])[0] - const bidResponse = spec.interpretResponse(serverResponse, bidRequest)[0] - - expect(bidResponse.renderer.url).to.equal('https://example.com/script/6.1/prebidRenderer.js') - expect(bidResponse.renderer.config.AV_PUBLISHERID).to.equal('55b78633181f4603178b4568') - expect(bidResponse.renderer.config.AV_CHANNELID).to.equal('55b7904d181f46410f8b4568') - expect(bidResponse.renderer.loaded).to.equal(false) - expect(bidResponse.width).to.equal(640) - expect(bidResponse.height).to.equal(480) + it('should use dev environment', function () { + const DEV_ENDPOINT = 'https://dev.aniview.com/sspRTB2'; + videoBidRequest.bids[0].params.dev = { endpoint: DEV_ENDPOINT }; + + const bidRequest = spec.buildRequests(videoBidRequest.bids, videoBidRequest); + + expect(bidRequest[0].url).to.equal(DEV_ENDPOINT); }); + }); - it('Support banner format', function () { - const bidRequest = spec.buildRequests([ - { - bidId: '253dcb69fb2577', - params: { - playerDomain: 'example.com', - AV_PUBLISHERID: '55b78633181f4603178b4568', - AV_CHANNELID: '55b7904d181f46410f8b4568' - }, - mediaTypes: { - banner: { - sizes: [[640, 480]], - } - } - } - ])[0] - const bidResponse = spec.interpretResponse(serverResponse, bidRequest)[0] + describe('interpretResponse', function () { + describe('Video format', function () { + let bidRequests, bidderResponse; + + beforeEach(function() { + const videoBidRequest = MOCK.bidRequest(); + + bidRequests = spec.buildRequests(videoBidRequest.bids, videoBidRequest); + bidderResponse = MOCK.bidderResponse(); + }); + + it('should return empty bids array for empty response', function () { + const emptyResponse = bidderResponse.body = {}; + const bids = spec.interpretResponse(emptyResponse, bidRequests[0]); + + expect(bids).to.be.empty; + }); + + it('should return valid bids array', function () { + const bids = spec.interpretResponse(bidderResponse, bidRequests[0]); + const bid = bids[0]; + + expect(bids.length).to.greaterThan(0); + expect(bid.vastXml).to.exist.and.to.not.have.string('${AUCTION_PRICE}'); + expect(bid.vastXml).to.have.string('cpm=' + PRICE); + expect(bid.vastUrl).to.exist.and.to.not.have.string('${AUCTION_PRICE}'); + expect(bid.vastUrl).to.have.string('cpm=' + PRICE); + expect(bid.requestId).to.equal(bidRequests[0].data.imp[0].id); + expect(bid.cpm).to.equal(PRICE); + expect(bid.ttl).to.equal(TTL); + expect(bid.currency).to.equal(CURRENCY); + expect(bid.netRevenue).to.equal(true); + expect(bid.mediaType).to.equal('video'); + expect(bid.meta.advertiserDomains).to.be.an('array'); + expect(bid.creativeId).to.exist; + expect(bid.width).to.exist; + expect(bid.height).to.exist; + }); + + it('should return bid without required properties if cpm less or equal 0', function () { + bidderResponse.body.seatbid[0].bid[0].price = 0; + + const bids = spec.interpretResponse(bidderResponse, bidRequests[0]); + const bid = bids[0]; + + expect(bid.renderer).to.not.exist; + expect(bid.ad).to.not.exist; + }); + + it('should return empty bids array if no bids in response', function () { + bidderResponse.body.seatbid[0].bid = []; + + const bids = spec.interpretResponse(bidderResponse, bidRequests[0]); + + expect(bids).to.exist.and.to.be.a('array').and.to.have.lengthOf(0); + }); + + it('should add renderer if outstream context', function () { + const bid = spec.interpretResponse(bidderResponse, bidRequests[0])[0]; + + expect(bid.renderer.url).to.equal(CUSTOM_RENDERER_URL); + expect(bid.renderer.config.AV_PUBLISHERID).to.equal(PUBLISHER_ID_1); + expect(bid.renderer.config.AV_CHANNELID).to.equal(CHANNEL_ID_1); + expect(bid.renderer.loaded).to.equal(false); + expect(bid.width).to.equal(VIDEO_SIZE.width); + expect(bid.height).to.equal(VIDEO_SIZE.height); + }); + + it('should use default renderer domain', function () { + delete bidRequests[0].bids[0].params.playerDomain; + + const bid = spec.interpretResponse(bidderResponse, bidRequests[0])[0]; + + expect(bid.renderer.url).to.equal(DEFAULT_RENDERER_URL); + }); + + it('should not add renderer if context is not outstream', function () { + bidRequests[0].bids[0].mediaTypes.video.context = 'instream'; + + const bid = spec.interpretResponse(bidderResponse, bidRequests[0])[0]; + + expect(bidRequests[0].bids[0].mediaTypes.video.context).to.be.not.equal('outstream'); + expect(bid.renderer).to.not.exist; + }); + }); - expect(bidResponse.ad).to.have.string('https://example.com/script/6.1/prebidRenderer.js'); - expect(bidResponse.width).to.equal(640) - expect(bidResponse.height).to.equal(480) - }) + describe('Banner format', function () { + let bidRequests, bidderResponse; + + beforeEach(function() { + const bannerBidRequest = MOCK.bidRequest(); + + // Converting video bid request to banner bid request + + delete bannerBidRequest.bids[0].mediaTypes.video; + + bannerBidRequest.bids[0].sizes = [[BANNER_SIZE.width, BANNER_SIZE.height]]; + bannerBidRequest.bids[0].mediaTypes.banner = { + sizes: [ + [BANNER_SIZE.width, BANNER_SIZE.height], + [BANNER_SIZE.width * 2, BANNER_SIZE.height], + ], + }; + + bidRequests = spec.buildRequests(bannerBidRequest.bids, bannerBidRequest); + bidderResponse = MOCK.bidderResponse(); + }); + + it('should return valid banner bids (HTML)', function () { + bidderResponse.body.seatbid[0].bid[0].adm = BANNER_HTML; + + const bid = spec.interpretResponse(bidderResponse, bidRequests[0])[0]; + + expect(bid.ad).to.exist; + expect(bid.cpm).to.equal(PRICE); + expect(bid.width).to.equal(BANNER_SIZE.width); + expect(bid.height).to.equal(BANNER_SIZE.height); + expect(bid.renderer).to.not.exist; + }); + + it('should return valid banner bids (VAST) with renderer', function () { + bidderResponse.body.seatbid[0].bid[0].adm = BANNER_VAST; + + const bid = spec.interpretResponse(bidderResponse, bidRequests[0])[0]; + + expect(bid.ad).to.exist; + expect(bid.ad).to.not.have.string('${AUCTION_PRICE}'); + expect(bid.ad).to.have.string('cpm=' + PRICE); + expect(bid.cpm).to.equal(PRICE); + expect(bid.width).to.equal(BANNER_SIZE.width); + expect(bid.height).to.equal(BANNER_SIZE.height); + expect(bid.renderer.url).to.equal(CUSTOM_RENDERER_URL); + expect(bid.renderer.config.AV_PUBLISHERID).to.equal(PUBLISHER_ID_1); + expect(bid.renderer.config.AV_CHANNELID).to.equal(CHANNEL_ID_1); + expect(bid.renderer.loaded).to.equal(false); + }); + }); }); describe('getUserSyncs', function () { - let pixelUrl = 'https://sync.pixel.url/sync'; - function createBidResponse (pixelEvent, pixelType) { - let pixelStr = '{"url":"' + pixelUrl + '", "e":"' + pixelEvent + '", "t":' + pixelType + '}'; - return 'FORDFORD00:00:15'; - } - - it('Check get iframe sync pixels from response on inventory', function () { - let pixelEvent = 'inventory'; - let pixelType = '3'; - let bidResponse = createBidResponse(pixelEvent, pixelType); - let serverResponse = [ - {body: bidResponse} - ]; - let syncPixels = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse); - expect(syncPixels.length).to.equal(1); - let pixel = syncPixels[0]; - expect(pixel.url).to.equal(pixelUrl); - expect(pixel.type).to.equal('iframe'); + let bidRequest, bidderResponse; + + beforeEach(function() { + const videoBidRequest = MOCK.bidRequest(); + + bidRequest = spec.buildRequests(videoBidRequest.bids, videoBidRequest); + bidderResponse = MOCK.bidderResponse(); + }); + + it('should get syncs from response', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, [bidderResponse]); + + expect(syncs).to.be.a('array').and.to.have.lengthOf(3); + }); + + it('should get only pixel syncs from response', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }, [bidderResponse]); + + expect(syncs).to.be.a('array').and.to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); }); - it('Check get image sync pixels from response on sync', function () { - let pixelEvent = 'sync'; - let pixelType = '1'; - let bidResponse = createBidResponse(pixelEvent, pixelType); - let serverResponse = [ - {body: bidResponse} - ]; - let syncPixels = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse); - expect(syncPixels.length).to.equal(1); - let pixel = syncPixels[0]; - expect(pixel.url).to.equal(pixelUrl); - expect(pixel.type).to.equal('image'); + it('should return empty array of syncs if no syncs in response', function () { + delete bidderResponse.body.ext.aniview.sync + + const syncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: false }, [bidderResponse]); + + expect(syncs).to.be.a('array').and.to.have.lengthOf(0); + }); + + it('should return empty array of syncs if no body in response', function () { + delete bidderResponse.body + + const syncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, [bidderResponse]); + + expect(syncs).to.be.a('array').and.to.have.lengthOf(0); }); }); }); diff --git a/test/spec/modules/anonymisedRtdProvider_spec.js b/test/spec/modules/anonymisedRtdProvider_spec.js index 89115e5e740..a2ddfbc7b3c 100644 --- a/test/spec/modules/anonymisedRtdProvider_spec.js +++ b/test/spec/modules/anonymisedRtdProvider_spec.js @@ -1,5 +1,6 @@ import {config} from 'src/config.js'; import {getRealTimeData, anonymisedRtdSubmodule, storage} from 'modules/anonymisedRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; describe('anonymisedRtdProvider', function() { let getDataFromLocalStorageStub; @@ -34,6 +35,115 @@ describe('anonymisedRtdProvider', function() { it('successfully instantiates', function () { expect(anonymisedRtdSubmodule.init()).to.equal(true); }); + it('should load external script when params.tagConfig.clientId is set', function () { + const rtdConfig = { + params: { + tagConfig: { + clientId: 'testId' + } + } + }; + anonymisedRtdSubmodule.init(rtdConfig, {}); + expect(loadExternalScript.called).to.be.true; + }); + it('should not load external script when params.tagConfig.clientId is not set', function () { + const rtdConfig = { + params: { + tagConfig: {} + } + }; + anonymisedRtdSubmodule.init(rtdConfig, {}); + expect(loadExternalScript.called).to.be.false; + }); + it('should not load external script when params.tagConfig is not defined', function () { + const rtdConfig = { + params: {} + }; + anonymisedRtdSubmodule.init(rtdConfig, {}); + expect(loadExternalScript.called).to.be.false; + }); + it('should not load external script when params.tagConfig.clientId is empty string', function () { + const rtdConfig = { + params: { + tagConfig: { + clientId: ' ' + } + } + }; + anonymisedRtdSubmodule.init(rtdConfig, {}); + expect(loadExternalScript.called).to.be.false; + }); + it('should not load external script when params.tagConfig.clientId is not a string', function () { + const rtdConfig = { + params: { + tagConfig: { + clientId: 123 + } + } + }; + anonymisedRtdSubmodule.init(rtdConfig, {}); + expect(loadExternalScript.called).to.be.false; + }); + it('should load external script with correct attributes', function () { + const rtdConfig = { + params: { + tagConfig: { + clientId: 'testId' + } + } + }; + anonymisedRtdSubmodule.init(rtdConfig, {}); + const expected = 'https://static.anonymised.io/light/loader.js?ref=prebid'; + const expectedTagConfig = { + idw_client_id: 'testId' + }; + + expect(loadExternalScript.args[0][0]).to.deep.equal(expected); + expect(loadExternalScript.args[0][5]).to.deep.equal(expectedTagConfig); + }); + it('should not load external script when it is already loaded', function () { + const rtdConfig = { + params: { + tagConfig: { + clientId: 'testId' + } + } + }; + const script = document.createElement('script'); + script.src = 'https://static.anonymised.io/light/loader.js?random=quary'; + document.body.appendChild(script); + anonymisedRtdSubmodule.init(rtdConfig, {}); + expect(loadExternalScript.called).to.be.false; + }); + it('should load external script from tagUrl when it is set', function () { + const rtdConfig = { + params: { + tagUrl: 'https://example.io/loader.js', + tagConfig: { + clientId: 'testId' + } + } + }; + anonymisedRtdSubmodule.init(rtdConfig, {}); + const expected = 'https://example.io/loader.js'; + + expect(loadExternalScript.args[0][0]).to.deep.equal(expected); + }); + it('should not load external script from tagUrl when it is already loaded', function () { + const rtdConfig = { + params: { + tagUrl: 'https://example.io/loader.js', + tagConfig: { + clientId: 'testId' + } + } + }; + const script = document.createElement('script'); + script.src = 'https://example.io/loader.js'; + document.body.appendChild(script); + anonymisedRtdSubmodule.init(rtdConfig, {}); + expect(loadExternalScript.called).to.be.false; + }); }); describe('Get Real-Time Data', function() { diff --git a/test/spec/modules/anyclipBidAdapter_spec.js b/test/spec/modules/anyclipBidAdapter_spec.js index 3de36f9fe06..3ec81d60546 100644 --- a/test/spec/modules/anyclipBidAdapter_spec.js +++ b/test/spec/modules/anyclipBidAdapter_spec.js @@ -1,160 +1,447 @@ -import { expect } from 'chai'; -import { spec } from 'modules/anyclipBidAdapter.js'; +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec} from 'modules/anyclipBidAdapter.js'; +import {deepClone} from 'src/utils'; +import {getBidFloor} from '../../../libraries/xeUtils/bidderUtils.js'; -describe('anyclipBidAdapter', function () { - afterEach(function () { - global._anyclip = undefined; - }); +const ENDPOINT = 'https://prebid.anyclip.com'; - let bid; - - function mockBidRequest() { - return { - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [728, 90], - [468, 60] - ] - } - }, - bidder: 'anyclip', - params: { - publisherId: '12345', - supplyTagId: '-mptNo0BycUG4oCDgGrU' - } - }; - }; +const defaultRequest = { + tmax: 0, + adUnitCode: 'test', + bidId: '1', + requestId: 'qwerty', + ortb2: { + source: { + tid: 'auctionId' + } + }, + ortb2Imp: { + ext: { + tid: 'tr1', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'anyclip', + params: { + publisherId: 'anyclip', + supplyTagId: '40', + ext: {} + }, + bidRequestsCount: 1 +}; + +const defaultRequestVideo = deepClone(defaultRequest); +defaultRequestVideo.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } +}; +const videoBidderRequest = { + bidderCode: 'anyclip', + bids: [{mediaTypes: {video: {}}, bidId: 'qwerty'}] +}; + +const displayBidderRequest = { + bidderCode: 'anyclip', + bids: [{bidId: 'qwerty'}] +}; + +describe('anyclipBidAdapter', () => { describe('isBidRequestValid', function () { - this.beforeEach(function () { - bid = mockBidRequest(); + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); }); - it('should return true if all required fields are present', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + it('should return false when required publisherId param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.publisherId; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); }); - it('should return false if bidder does not correspond', function () { - bid.bidder = 'abc'; - expect(spec.isBidRequestValid(bid)).to.be.false; + + it('should return false when required supplyTagId param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.supplyTagId; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); }); - it('should return false if params object is missing', function () { - delete bid.params; - expect(spec.isBidRequestValid(bid)).to.be.false; + + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); }); - it('should return false if publisherId is missing from params', function () { - delete bid.params.publisherId; - expect(spec.isBidRequestValid(bid)).to.be.false; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); }); - it('should return false if supplyTagId is missing from params', function () { - delete bid.params.supplyTagId; - expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); }); - it('should return false if mediaTypes is missing', function () { - delete bid.mediaTypes; - expect(spec.isBidRequestValid(bid)).to.be.false; + + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT + '/bid'); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); }); - it('should return false if banner is missing from mediaTypes ', function () { - delete bid.mediaTypes.banner; - expect(spec.isBidRequestValid(bid)).to.be.false; + + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('tmax').and.to.equal(defaultRequest.tmax); + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprConsent').and.to.deep.equal({}); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + publisherId: 'anyclip', + supplyTagId: '40', + floor: null + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); }); - it('should return false if sizes is missing from banner object', function () { - delete bid.mediaTypes.banner.sizes; - expect(spec.isBidRequestValid(bid)).to.be.false; + + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); }); - it('should return false if sizes is not an array', function () { - bid.mediaTypes.banner.sizes = 'test'; - expect(spec.isBidRequestValid(bid)).to.be.false; + + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); }); - it('should return false if sizes is an empty array', function () { - bid.mediaTypes.banner.sizes = []; - expect(spec.isBidRequestValid(bid)).to.be.false; + + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); }); - }); - describe('buildRequests', function () { - let bidderRequest = { - refererInfo: { - page: 'http://example.com', - domain: 'example.com', - }, - timeout: 3000 - }; - - this.beforeEach(function () { - bid = mockBidRequest(); - Object.assign(bid, { - adUnitCode: '1', - transactionId: '123', - sizes: bid.mediaTypes.banner.sizes + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } }); }); - it('when pubtag is not available, return undefined', function () { - expect(spec.buildRequests([bid], bidderRequest)).to.undefined; + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0].env; + expect(request).to.have.property('floor').and.to.equal(5); }); - it('when pubtag is available, creates a ServerRequest object with method, URL and data', function() { - global._anyclip = { - PubTag: function() {}, - pubTag: { - requestBids: function() {} - } + + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' }; - expect(spec.buildRequests([bid], bidderRequest)).to.exist; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); + + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); + + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); }); }); - describe('interpretResponse', function() { - it('should return an empty array when parsing a no bid response', function () { - const response = {}; - const request = {}; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(0); - }); - it('should return bids array', function() { - const response = {}; - const request = { - bidRequest: { - bidId: 'test-bidId', - transactionId: '123' + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; + + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); + + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['anyclip'] + }, + ext: { + pixels: [ + ['iframe', 'surl1'], + ['image', 'surl2'], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: displayBidderRequest}); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({advertiserDomains: ['anyclip']}); + }); + + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: displayBidderRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); + + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] } }; - global._anyclip = { - PubTag: function() {}, - pubTag: { - getBids: function(transactionId) { - return { - adServer: { - bid: { - ad: 'test-ad', - creativeId: 'test-crId', - meta: { - advertiserDomains: ['anyclip.com'] - }, - width: 300, - height: 250, - ttl: 300 - } - }, - cpm: 1.23, + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: videoBidderRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); + }); + + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] } - } + }] } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); + }); + + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = {getFloor: 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when getFloor doesnt return an object', () => { + const bid = {getFloor: () => 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({floor: 'string', currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'EUR'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'USD'}) }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].requestId).to.equal('test-bidId'); - expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].currency).to.equal('USD'); - expect(bids[0].width).to.equal(300); - expect(bids[0].height).to.equal(250); - expect(bids[0].ad).to.equal('test-ad'); - expect(bids[0].ttl).to.equal(300); - expect(bids[0].creativeId).to.equal('test-crId'); - expect(bids[0].netRevenue).to.false; - expect(bids[0].meta.advertiserDomains[0]).to.equal('anyclip.com'); + const result = getBidFloor(bid); + expect(result).to.equal(5); }); }); }); diff --git a/test/spec/modules/apacdexBidAdapter_spec.js b/test/spec/modules/apacdexBidAdapter_spec.js index 98d07575ee7..d005934d062 100644 --- a/test/spec/modules/apacdexBidAdapter_spec.js +++ b/test/spec/modules/apacdexBidAdapter_spec.js @@ -201,8 +201,7 @@ describe('ApacdexBidAdapter', function () { }, 'bidder': 'apacdex', 'params': { - 'siteId': '1a2b3c4d5e6f1a2b3c4d', - 'geo': { 'lat': 123.13123456, 'lon': 54.23467311, 'accuracy': 60 } + 'siteId': '1a2b3c4d5e6f1a2b3c4d' }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], @@ -321,10 +320,6 @@ describe('ApacdexBidAdapter', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests); expect(bidRequests.data.eids).to.deep.equal(bidRequest[0].userIdAsEids) }); - it('should fail to return a properly formatted request with geo defined', function () { - const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(bidRequests.data.geo).to.not.deep.equal(bidRequest[0].params.geo) - }); it('should return a properly formatted request with us_privacy included', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests); expect(bidRequests.data.us_privacy).to.equal('someCCPAString'); diff --git a/test/spec/modules/appierBidAdapter_spec.js b/test/spec/modules/appierBidAdapter_spec.js index 8b6ad5c2f6f..0ad14b1ec61 100644 --- a/test/spec/modules/appierBidAdapter_spec.js +++ b/test/spec/modules/appierBidAdapter_spec.js @@ -30,17 +30,17 @@ describe('AppierAdapter', function () { }); it('should return false when required param zoneId is missing', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when required param zoneId has wrong type', function () { - let bid = Object.assign({}, bid); - bid.params = { + let invalidBid = Object.assign({}, bid); + invalidBid.params = { 'hzid': null }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index c2da2f36223..4f384eefdda 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { spec } from 'modules/appnexusBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import * as bidderFactory from 'src/adapters/bidderFactory.js'; import { auctionManager } from 'src/auctionManager.js'; import { deepClone } from 'src/utils.js'; import * as utils from 'src/utils.js'; @@ -76,21 +75,21 @@ describe('AppNexusAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placementId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placement_id': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -432,6 +431,69 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) }); + it('should include ORTB video values when video params is empty - case 1', function () { + let bidRequest = deepClone(bidRequests[0]); + bidRequest.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'outstream', + mimes: ['video/mp4'], + startdelay: 0, + skip: 0, + minduration: 5, + api: [1, 5, 6], + playbackmethod: [2, 4] + } + }; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].video).to.deep.equal({ + minduration: 5, + playback_method: 2, + skippable: false, + context: 1 + }); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) + }); + + it('should convert and include ORTB2 device data when available', function () { + const bidRequest = deepClone(bidRequests[0]); + const bidderRequest = { + ortb2: { + device: { + 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', + }, + }, + }; + + const expectedDeviceResult = { + useragent: bidderRequest.ortb2.device.ua, + devicetype: 'Mobile/Tablet - General', + make: bidderRequest.ortb2.device.make, + model: bidderRequest.ortb2.device.model, + os: bidderRequest.ortb2.device.os, + os_version: bidderRequest.ortb2.device.osv, + w: bidderRequest.ortb2.device.w, + h: bidderRequest.ortb2.device.h, + }; + + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.device).to.deep.equal(expectedDeviceResult); + }); + it('should add video property when adUnit includes a renderer', function () { const videoData = { mediaTypes: { @@ -1996,45 +2058,47 @@ describe('AppNexusAdapter', function () { } if (FEATURES.NATIVE) { + const BASE_NATIVE = { + 'title': 'Native Creative', + 'desc': 'Cool description great stuff', + 'desc2': 'Additional body text', + 'ctatext': 'Do it', + 'sponsored': 'AppNexus', + 'icon': { + 'width': 0, + 'height': 0, + 'url': 'https://cdn.adnxs.com/icon.png' + }, + 'main_img': { + 'width': 2352, + 'height': 1516, + 'url': 'https://cdn.adnxs.com/img.png' + }, + 'link': { + 'url': 'https://www.appnexus.com', + 'fallback_url': '', + 'click_trackers': ['https://nym1-ib.adnxs.com/click'] + }, + 'impression_trackers': ['https://example.com'], + 'rating': '5', + 'displayurl': 'https://AppNexus.com/?url=display_url', + 'likes': '38908320', + 'downloads': '874983', + 'price': '9.99', + 'saleprice': 'FREE', + 'phone': '1234567890', + 'address': '28 W 23rd St, New York, NY 10010', + 'privacy_link': 'https://appnexus.com/?url=privacy_url', + 'javascriptTrackers': '', + 'video': { + 'content': '' + } + }; + it('handles native responses', function () { let response1 = deepClone(response); response1.tags[0].ads[0].ad_type = 'native'; - response1.tags[0].ads[0].rtb.native = { - 'title': 'Native Creative', - 'desc': 'Cool description great stuff', - 'desc2': 'Additional body text', - 'ctatext': 'Do it', - 'sponsored': 'AppNexus', - 'icon': { - 'width': 0, - 'height': 0, - 'url': 'https://cdn.adnxs.com/icon.png' - }, - 'main_img': { - 'width': 2352, - 'height': 1516, - 'url': 'https://cdn.adnxs.com/img.png' - }, - 'link': { - 'url': 'https://www.appnexus.com', - 'fallback_url': '', - 'click_trackers': ['https://nym1-ib.adnxs.com/click'] - }, - 'impression_trackers': ['https://example.com'], - 'rating': '5', - 'displayurl': 'https://AppNexus.com/?url=display_url', - 'likes': '38908320', - 'downloads': '874983', - 'price': '9.99', - 'saleprice': 'FREE', - 'phone': '1234567890', - 'address': '28 W 23rd St, New York, NY 10010', - 'privacy_link': 'https://appnexus.com/?url=privacy_url', - 'javascriptTrackers': '', - 'video': { - 'content': '' - } - }; + response1.tags[0].ads[0].rtb.native = BASE_NATIVE; let bidderRequest = { bids: [{ bidId: '3db3773286ee59', @@ -2045,10 +2109,236 @@ describe('AppNexusAdapter', function () { let result = spec.interpretResponse({ body: response1 }, { bidderRequest }); expect(result[0].native.title).to.equal('Native Creative'); expect(result[0].native.body).to.equal('Cool description great stuff'); + expect(result[0].native.body2).to.equal('Additional body text'); expect(result[0].native.cta).to.equal('Do it'); expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + // Video is technically not a base Prebid native field, so it should be included as part of the ext + // But it's also included here for backwards compatibility if people read the bid directly expect(result[0].native.video.content).to.equal(''); }); + + it('handles custom native fields as ext', function () { + let response1 = deepClone(response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { + ...BASE_NATIVE, + // 'video' is included in base native + 'title1': 'Custom Title 1', + 'title2': 'Custom Title 2', + 'title3': 'Custom Title 3', + 'title4': 'Custom Title 4', + 'title5': 'Custom Title 5', + // Not to be confused with Prebid's base native body & body2 + 'body1': 'Custom Body 1', + 'body2': 'Custom Body 2', + 'body3': 'Custom Body 3', + 'body4': 'Custom Body 4', + 'body5': 'Custom Body 5', + 'image1': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_1.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'image2': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_2.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'image3': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_3.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'image4': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_4.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'image5': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_5.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'icon1': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'icon2': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'icon3': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'icon4': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'icon5': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'socialicon1': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'socialicon2': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'socialicon3': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'socialicon4': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'socialicon5': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'socialurl1': 'https://www.xandr.com/platform/monetize/#socialUrl1', + 'socialurl2': 'https://www.xandr.com/platform/monetize/#socialUrl2', + 'socialurl3': 'https://www.xandr.com/platform/monetize/#socialUrl3', + 'socialurl4': 'https://www.xandr.com/platform/monetize/#socialUrl4', + 'socialurl5': 'https://www.xandr.com/platform/monetize/#socialUrl5', + 'displayurl1': 'https://www.xandr.com/platform/monetize/#displayUrl1', + 'displayurl2': 'https://www.xandr.com/platform/monetize/#displayUrl2', + 'displayurl3': 'https://www.xandr.com/platform/monetize/#displayUrl3', + 'displayurl4': 'https://www.xandr.com/platform/monetize/#displayUrl4', + 'displayurl5': 'https://www.xandr.com/platform/monetize/#displayUrl5', + 'ctatext1': 'Custom CTA 1', + 'ctatext2': 'Custom CTA 2', + 'ctatext3': 'Custom CTA 3', + 'ctatext4': 'Custom CTA 4', + 'ctatext5': 'Custom CTA 5', + }; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + + let result = spec.interpretResponse({ body: response1 }, { bidderRequest }); + expect(result[0].native.ext).to.deep.equal({ + 'video': { + 'content': '' + }, + 'customTitle1': 'Custom Title 1', + 'customTitle2': 'Custom Title 2', + 'customTitle3': 'Custom Title 3', + 'customTitle4': 'Custom Title 4', + 'customTitle5': 'Custom Title 5', + 'customBody1': 'Custom Body 1', + 'customBody2': 'Custom Body 2', + 'customBody3': 'Custom Body 3', + 'customBody4': 'Custom Body 4', + 'customBody5': 'Custom Body 5', + 'customImage1': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_1.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'customImage2': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_2.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'customImage3': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_3.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'customImage4': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_4.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'customImage5': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/custom_image_5.jpg?[fullhash]', + 'height': 627, + 'width': 1200, + }, + 'customIcon1': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customIcon2': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customIcon3': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customIcon4': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customIcon5': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customSocialIcon1': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customSocialIcon2': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customSocialIcon3': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customSocialIcon4': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customSocialIcon5': { + 'url': 'https://monetize.xandr.com/creative-ui/assets/logo.jpg?[fullhash]', + 'height': 128, + 'width': 128, + }, + 'customSocialUrl1': 'https://www.xandr.com/platform/monetize/#socialUrl1', + 'customSocialUrl2': 'https://www.xandr.com/platform/monetize/#socialUrl2', + 'customSocialUrl3': 'https://www.xandr.com/platform/monetize/#socialUrl3', + 'customSocialUrl4': 'https://www.xandr.com/platform/monetize/#socialUrl4', + 'customSocialUrl5': 'https://www.xandr.com/platform/monetize/#socialUrl5', + 'customDisplayUrl1': 'https://www.xandr.com/platform/monetize/#displayUrl1', + 'customDisplayUrl2': 'https://www.xandr.com/platform/monetize/#displayUrl2', + 'customDisplayUrl3': 'https://www.xandr.com/platform/monetize/#displayUrl3', + 'customDisplayUrl4': 'https://www.xandr.com/platform/monetize/#displayUrl4', + 'customDisplayUrl5': 'https://www.xandr.com/platform/monetize/#displayUrl5', + 'customCta1': 'Custom CTA 1', + 'customCta2': 'Custom CTA 2', + 'customCta3': 'Custom CTA 3', + 'customCta4': 'Custom CTA 4', + 'customCta5': 'Custom CTA 5', + }); + }); } if (FEATURES.VIDEO) { @@ -2151,54 +2441,73 @@ describe('AppNexusAdapter', function () { }); }); - describe('transformBidParams', function () { - let gcStub; - let adUnit = { bids: [{ bidder: 'appnexus' }] }; ; - - before(function () { - gcStub = sinon.stub(config, 'getConfig'); + describe('getUserSyncs', function() { + let syncOptions, gdprConsent; + + beforeEach(() => { + gdprConsent = { + gdprApplies: true, + consentString: 'CPJl4C8PJl4C8OoAAAENAwCMAP_AAH_AAAAAAPgAAAAIAPgAAAAIAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', + vendorData: { + purpose: { + consents: { + '1': true + } + } + } + } }); - after(function () { - gcStub.restore(); + describe('pixel', function () { + beforeEach(() => { + syncOptions = { pixelEnabled: true }; + }); + + it('pixelEnabled on', function () { + const result = spec.getUserSyncs(syncOptions, [], gdprConsent, null); + expect(result).to.have.length(1); + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.equal('https://px.ads.linkedin.com/setuid?partner=appNexus'); + }); + + it('pixelEnabled off', function () { + syncOptions.pixelEnabled = false; + const result = spec.getUserSyncs(syncOptions, [], gdprConsent, null); + expect(result).to.be.undefined; + }); }); - it('convert keywords param differently for psp endpoint with single s2sConfig', function () { - gcStub.withArgs('s2sConfig').returns({ - bidders: ['appnexus'], - endpoint: { - p1Consent: 'https://ib.adnxs.com/openrtb2/prebid' - } + describe('iframe', function () { + beforeEach(() => { + syncOptions = { iframeEnabled: true }; }); - const oldParams = { - keywords: { - genre: ['rock', 'pop'], - pets: 'dog' - } - }; + it('iframeEnabled on with gdpr purpose 1 on', function () { + const result = spec.getUserSyncs(syncOptions, [], gdprConsent, null); + expect(result).to.have.length(1); + expect(result[0].type).to.equal('iframe'); + expect(result[0].url).to.equal('https://acdn.adnxs.com/dmp/async_usersync.html'); + }); - const newParams = spec.transformBidParams(oldParams, true, adUnit); - expect(newParams.keywords).to.equal('genre=rock,genre=pop,pets=dog'); - }); + it('iframeEnabled on with gdpr purpose1 off', function () { + gdprConsent.vendorData.purpose.consents['1'] = false - it('convert keywords param differently for psp endpoint with array s2sConfig', function () { - gcStub.withArgs('s2sConfig').returns([{ - bidders: ['appnexus'], - endpoint: { - p1Consent: 'https://ib.adnxs.com/openrtb2/prebid' - } - }]); + const result = spec.getUserSyncs(syncOptions, [], gdprConsent, null); + expect(result).to.be.undefined; + }); - const oldParams = { - keywords: { - genre: ['rock', 'pop'], - pets: 'dog' - } - }; + it('iframeEnabled on without gdpr', function () { + const result = spec.getUserSyncs(syncOptions, [], null, null); + expect(result).to.have.length(1); + expect(result[0].type).to.equal('iframe'); + expect(result[0].url).to.equal('https://acdn.adnxs.com/dmp/async_usersync.html'); + }); - const newParams = spec.transformBidParams(oldParams, true, adUnit); - expect(newParams.keywords).to.equal('genre=rock,genre=pop,pets=dog'); + it('iframeEnabled off', function () { + syncOptions.iframeEnabled = false; + const result = spec.getUserSyncs(syncOptions, [], gdprConsent, null); + expect(result).to.be.undefined; + }); }); }); }); diff --git a/test/spec/modules/asealBidAdapter_spec.js b/test/spec/modules/asealBidAdapter_spec.js index 2dc1b47b7d0..900bda11390 100644 --- a/test/spec/modules/asealBidAdapter_spec.js +++ b/test/spec/modules/asealBidAdapter_spec.js @@ -87,10 +87,10 @@ describe('asealBidAdapter', () => { }); it('should return false when required params are not passed', () => { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/asoBidAdapter_spec.js b/test/spec/modules/asoBidAdapter_spec.js index e317a8828e7..c7ab69bca80 100644 --- a/test/spec/modules/asoBidAdapter_spec.js +++ b/test/spec/modules/asoBidAdapter_spec.js @@ -1,12 +1,12 @@ import {expect} from 'chai'; import {spec} from 'modules/asoBidAdapter.js'; -import {BANNER, VIDEO, NATIVE} from 'src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from 'src/mediaTypes.js'; import {OUTSTREAM} from 'src/video.js'; -import {syncAddFPDToBidderRequest} from '../../helpers/fpd'; +import {addFPDToBidderRequest} from '../../helpers/fpd'; import {parseUrl} from '../../../src/utils'; import 'modules/priceFloors.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'modules/consentManagementUsp.js'; describe('Adserver.Online bidding adapter', function () { @@ -89,19 +89,6 @@ describe('Adserver.Online bidding adapter', function () { nativeOrtbRequest }; - const bidderRequest = { - refererInfo: { - page: 'https://example.com/page.html', - topmostLocation: 'https://example.com/page.html', - reachedTop: true, - numIframes: 1, - stack: [ - 'https://example.com/page.html', - 'https://example.com/iframe1.html' - ] - } - }; - const gdprConsent = { gdprApplies: true, consentString: 'consentString', @@ -124,6 +111,23 @@ describe('Adserver.Online bidding adapter', function () { } }; + let bidderRequest; + + beforeEach(() => { + return addFPDToBidderRequest({ + refererInfo: { + page: 'https://example.com/page.html', + topmostLocation: 'https://example.com/page.html', + reachedTop: true, + numIframes: 1, + stack: [ + 'https://example.com/page.html', + 'https://example.com/iframe1.html' + ] + } + }).then(br => { bidderRequest = br }); + }) + const uspConsent = 'usp_consent'; describe('isBidRequestValid', function () { @@ -187,7 +191,7 @@ describe('Adserver.Online bidding adapter', function () { }); it('creates a valid banner request', function () { - const requests = spec.buildRequests([bannerRequest], syncAddFPDToBidderRequest(bidderRequest)); + const requests = spec.buildRequests([bannerRequest], bidderRequest); expect(requests).to.have.lengthOf(1); const request = requests[0]; @@ -200,8 +204,8 @@ describe('Adserver.Online bidding adapter', function () { expect(payload.site.page).to.equal('https://example.com/page.html'); expect(payload.device).to.exist; - expect(payload.device.w).to.equal(window.innerWidth); - expect(payload.device.h).to.equal(window.innerHeight); + expect(payload.device.w).to.equal(window.screen.width); + expect(payload.device.h).to.equal(window.screen.height); expect(payload.imp).to.have.lengthOf(1); @@ -216,7 +220,7 @@ describe('Adserver.Online bidding adapter', function () { if (FEATURES.VIDEO) { it('creates a valid video request', function () { - const requests = spec.buildRequests([videoRequest], syncAddFPDToBidderRequest(bidderRequest)); + const requests = spec.buildRequests([videoRequest], bidderRequest); expect(requests).to.have.lengthOf(1); const request = requests[0]; @@ -229,8 +233,8 @@ describe('Adserver.Online bidding adapter', function () { expect(payload.site.page).to.equal('https://example.com/page.html'); expect(payload.device).to.exist; - expect(payload.device.w).to.equal(window.innerWidth); - expect(payload.device.h).to.equal(window.innerHeight); + expect(payload.device.w).to.equal(window.screen.width); + expect(payload.device.h).to.equal(window.screen.height); expect(payload.imp).to.have.lengthOf(1); @@ -245,7 +249,7 @@ describe('Adserver.Online bidding adapter', function () { if (FEATURES.NATIVE) { it('creates a valid native request', function () { - const requests = spec.buildRequests([nativeRequest], syncAddFPDToBidderRequest(bidderRequest)); + const requests = spec.buildRequests([nativeRequest], bidderRequest); expect(requests).to.have.lengthOf(1); const request = requests[0]; @@ -258,8 +262,8 @@ describe('Adserver.Online bidding adapter', function () { expect(payload.site.page).to.equal('https://example.com/page.html'); expect(payload.device).to.exist; - expect(payload.device.w).to.equal(window.innerWidth); - expect(payload.device.h).to.equal(window.innerHeight); + expect(payload.device.w).to.equal(window.screen.width); + expect(payload.device.h).to.equal(window.screen.height); expect(payload.imp).to.have.lengthOf(1); @@ -275,34 +279,38 @@ describe('Adserver.Online bidding adapter', function () { bidderRequest.gdprConsent = gdprConsent; bidderRequest.uspConsent = uspConsent; - const requests = spec.buildRequests([bannerRequest], syncAddFPDToBidderRequest(bidderRequest)); - expect(requests).to.have.lengthOf(1); - const request = requests[0]; + return addFPDToBidderRequest(bidderRequest).then(bidderRequest => { + const requests = spec.buildRequests([bannerRequest], bidderRequest); + expect(requests).to.have.lengthOf(1); + const request = requests[0]; - expect(request.data).to.not.be.empty; + expect(request.data).to.not.be.empty; - const payload = request.data; + const payload = request.data; - expect(payload.user.ext.consent).to.equal('consentString'); - expect(payload.regs.ext.us_privacy).to.equal(uspConsent); - expect(payload.regs.ext.gdpr).to.equal(1); + expect(payload.user.ext.consent).to.equal('consentString'); + expect(payload.regs.ext.us_privacy).to.equal(uspConsent); + expect(payload.regs.ext.gdpr).to.equal(1); + }) }); it('should not send GDPR/USP consent data if it does not apply', function () { bidderRequest.gdprConsent = null; bidderRequest.uspConsent = null; - const requests = spec.buildRequests([bannerRequest], syncAddFPDToBidderRequest(bidderRequest)); - expect(requests).to.have.lengthOf(1); - const request = requests[0]; + return addFPDToBidderRequest(bidderRequest).then(bidderRequest => { + const requests = spec.buildRequests([bannerRequest], bidderRequest); + expect(requests).to.have.lengthOf(1); + const request = requests[0]; - expect(request.data).to.not.be.empty; + expect(request.data).to.not.be.empty; - const payload = request.data; + const payload = request.data; - expect(payload).to.not.have.nested.property('regs.ext.gdpr'); - expect(payload).to.not.have.nested.property('user.ext.consent'); - expect(payload).to.not.have.nested.property('regs.ext.us_privacy'); + expect(payload).to.not.have.nested.property('regs.ext.gdpr'); + expect(payload).to.not.have.nested.property('user.ext.consent'); + expect(payload).to.not.have.nested.property('regs.ext.us_privacy'); + }); }); }); diff --git a/test/spec/modules/audiencerunBidAdapter_spec.js b/test/spec/modules/audiencerunBidAdapter_spec.js index 5c736345068..65349409e5e 100644 --- a/test/spec/modules/audiencerunBidAdapter_spec.js +++ b/test/spec/modules/audiencerunBidAdapter_spec.js @@ -60,22 +60,22 @@ describe('AudienceRun bid adapter tests', function () { }); it('should return true when zoneId is valid', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { zoneId: '12345abcde', }; - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(spec.isBidRequestValid(invalidBid)).to.equal(true); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; - bid.params = {}; + invalidBid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/axisBidAdapter_spec.js b/test/spec/modules/axisBidAdapter_spec.js index 083f05f5c0a..65f1b9cbd94 100644 --- a/test/spec/modules/axisBidAdapter_spec.js +++ b/test/spec/modules/axisBidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/axisBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'axis' +const bidder = 'axis'; describe('AxisBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -19,7 +29,8 @@ describe('AxisBidAdapter', function () { params: { integration: '000000', token: '000000' - } + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -35,7 +46,8 @@ describe('AxisBidAdapter', function () { params: { integration: '000000', token: '000000' - } + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -59,7 +71,8 @@ describe('AxisBidAdapter', function () { params: { integration: '000000', token: '000000' - } + }, + userIdAsEids } ]; @@ -78,15 +91,25 @@ describe('AxisBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' }, ortb2: { site: { cat: ['IAB24'] + }, + device: { + w: 1512, + h: 982, + language: 'en-UK' } - } + }, + timeout: 500 }; describe('isBidRequestValid', function () { @@ -121,6 +144,7 @@ describe('AxisBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -130,7 +154,11 @@ describe('AxisBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -139,7 +167,7 @@ describe('AxisBidAdapter', function () { 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('string'); + expect(data.gdpr).to.be.a('object'); expect(data.ccpa).to.be.a('string'); expect(data.tmax).to.be.a('number'); expect(data.iabCat).to.have.lengthOf(1); @@ -156,6 +184,7 @@ describe('AxisBidAdapter', function () { expect(placement.token).to.be.a('string'); expect(placement.schain).to.be.an('object'); expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); if (placement.adFormat === BANNER) { expect(placement.sizes).to.be.an('array'); @@ -183,8 +212,10 @@ describe('AxisBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -199,12 +230,38 @@ describe('AxisBidAdapter', function () { expect(data.ccpa).to.equal(bidderRequest.uspConsent); expect(data.gdpr).to.not.exist; }); + }); - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -410,5 +467,17 @@ describe('AxisBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&ccpa=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/azerionedgeRtdProvider_spec.js b/test/spec/modules/azerionedgeRtdProvider_spec.js index f08aaebdf55..0eef82a2512 100644 --- a/test/spec/modules/azerionedgeRtdProvider_spec.js +++ b/test/spec/modules/azerionedgeRtdProvider_spec.js @@ -8,11 +8,17 @@ describe('Azerion Edge RTD submodule', function () { { id: '1', visits: 123 }, { id: '2', visits: 456 }, ]; - + const IMPROVEDIGITAL_GVLID = '253'; const key = 'publisher123'; const bidders = ['appnexus', 'improvedigital']; const process = { key: 'value' }; const dataProvider = { name: 'azerionedge', waitForIt: true }; + const userConsent = {gdpr: {gdprApplies: 'gdpr-applies', consentString: 'consent-string'}, usp: 'usp'}; + + const resetAll = () => { + window.azerionPublisherAudiences.resetHistory(); + loadExternalScript.resetHistory(); + } let reqBidsConfigObj; let storageStub; @@ -33,7 +39,11 @@ describe('Azerion Edge RTD submodule', function () { let returned; beforeEach(function () { - returned = azerionedgeRTD.azerionedgeSubmodule.init(dataProvider); + returned = azerionedgeRTD.azerionedgeSubmodule.init(dataProvider, userConsent); + }); + + it('should have the correct gvlid', () => { + expect(azerionedgeRTD.azerionedgeSubmodule.gvlid).to.equal(IMPROVEDIGITAL_GVLID); }); it('should return true', function () { @@ -49,18 +59,21 @@ describe('Azerion Edge RTD submodule', function () { expect(loadExternalScript.args[0][0]).to.deep.equal(expected); }); - it('should call azerionPublisherAudiencesStub with empty configuration', function () { - expect(window.azerionPublisherAudiences.args[0][0]).to.deep.equal({}); + [ + ['gdprApplies', userConsent.gdpr.gdprApplies], + ['gdprConsent', userConsent.gdpr.consentString], + ['uspConsent', userConsent.usp], + ].forEach(([key, value]) => { + it(`should call azerionPublisherAudiencesStub with ${key}:${value}`, function () { + expect(window.azerionPublisherAudiences.args[0][0]).to.include({[key]: value}); + }); }); describe('with key', function () { beforeEach(function () { - window.azerionPublisherAudiences.resetHistory(); - loadExternalScript.resetHistory(); - returned = azerionedgeRTD.azerionedgeSubmodule.init({ - ...dataProvider, - params: { key }, - }); + resetAll(); + const config = { ...dataProvider, params: { key } }; + returned = azerionedgeRTD.azerionedgeSubmodule.init(config, userConsent); }); it('should return true', function () { @@ -75,22 +88,24 @@ describe('Azerion Edge RTD submodule', function () { describe('with process configuration', function () { beforeEach(function () { - window.azerionPublisherAudiences.resetHistory(); - loadExternalScript.resetHistory(); - returned = azerionedgeRTD.azerionedgeSubmodule.init({ - ...dataProvider, - params: { process }, - }); + resetAll(); + const config = { ...dataProvider, params: { process } }; + returned = azerionedgeRTD.azerionedgeSubmodule.init(config, userConsent); }); it('should return true', function () { expect(returned).to.equal(true); }); - it('should call azerionPublisherAudiencesStub with process configuration', function () { - expect(window.azerionPublisherAudiences.args[0][0]).to.deep.equal( - process - ); + [ + ['gdprApplies', userConsent.gdpr.gdprApplies], + ['gdprConsent', userConsent.gdpr.consentString], + ['uspConsent', userConsent.usp], + ...Object.entries(process), + ].forEach(([key, value]) => { + it(`should call azerionPublisherAudiencesStub with ${key}:${value}`, function () { + expect(window.azerionPublisherAudiences.args[0][0]).to.include({[key]: value}); + }); }); }); }); @@ -111,7 +126,7 @@ describe('Azerion Edge RTD submodule', function () { ); }); - it('does not run apply audiences to bidders', function () { + it('does not apply audiences to bidders', function () { expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({}); }); diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index c0994985aae..2e766487951 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -245,14 +245,14 @@ describe('BeachfrontAdapter', function () { const mimes = ['video/webm']; const playbackmethod = 2; const maxduration = 30; - const placement = 4; + const plcmt = 4; const skip = 1; bidRequest.mediaTypes = { - video: { mimes, playbackmethod, maxduration, placement, skip } + video: { mimes, playbackmethod, maxduration, plcmt, skip } }; const requests = spec.buildRequests([ bidRequest ], {}); const data = requests[0].data; - expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, plcmt, skip }); }); it('must override video params from the bidder object', function () { @@ -260,13 +260,13 @@ describe('BeachfrontAdapter', function () { const mimes = ['video/webm']; const playbackmethod = 2; const maxduration = 30; - const placement = 4; + const plcmt = 4; const skip = 1; - bidRequest.mediaTypes = { video: { placement: 3, skip: 0 } }; - bidRequest.params.video = { mimes, playbackmethod, maxduration, placement, skip }; + bidRequest.mediaTypes = { video: { plcmt: 3, skip: 0 } }; + bidRequest.params.video = { mimes, playbackmethod, maxduration, plcmt, skip }; const requests = spec.buildRequests([ bidRequest ], {}); const data = requests[0].data; - expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, plcmt, skip }); }); it('must add US privacy data to the request', function () { diff --git a/test/spec/modules/bedigitechBidAdapter_spec.js b/test/spec/modules/bedigitechBidAdapter_spec.js index 20d4e86e0c4..336559e2812 100644 --- a/test/spec/modules/bedigitechBidAdapter_spec.js +++ b/test/spec/modules/bedigitechBidAdapter_spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/bedigitechBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import {BANNER} from 'src/mediaTypes.js'; +import { BANNER } from 'src/mediaTypes.js'; describe('BedigitechAdapter', function () { const adapter = newBidder(spec); @@ -34,13 +34,13 @@ describe('BedigitechAdapter', function () { }); it('should return false when required params are not passed', function () { - const bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + const invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'masterId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -126,7 +126,9 @@ describe('BedigitechAdapter', function () { } else if (k === 'meta') { expect(result[0][k]).to.deep.equal(expectedResponse[0][k]); } else { - expect(result[0][k]).to.equal(expectedResponse[0][k]); + if (k !== 'requestId') { + expect(result[0][k]).to.equal(expectedResponse[0][k]); + } } }); }); diff --git a/test/spec/modules/beopBidAdapter_spec.js b/test/spec/modules/beopBidAdapter_spec.js index 663d622e505..e0acde0aa21 100644 --- a/test/spec/modules/beopBidAdapter_spec.js +++ b/test/spec/modules/beopBidAdapter_spec.js @@ -2,9 +2,11 @@ import { expect } from 'chai'; import { spec } from 'modules/beopBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; const utils = require('src/utils'); -const ENDPOINT = 'https://hb.beop.io/bid'; +const ENDPOINT = 'https://hb.collectiveaudience.co/bid'; let validBid = { 'bidder': 'beop', @@ -92,18 +94,27 @@ describe('BeOp Bid Adapter tests', () => { bidRequests.push(validBid); it('should build the request', function () { - config.setConfig({'currency': {'adServerCurrency': 'USD'}}); - const request = spec.buildRequests(bidRequests, {}); - const payload = JSON.parse(request.data); - const url = request.url; - expect(url).to.equal(ENDPOINT); - expect(payload.pid).to.exist; - expect(payload.pid).to.equal('5a8af500c9e77c00017e4cad'); - expect(payload.gdpr_applies).to.exist; - expect(payload.gdpr_applies).to.equals(false); - expect(payload.slts[0].name).to.exist; - expect(payload.slts[0].name).to.equal('bellow-article'); - expect(payload.slts[0].flr).to.equal(10); + const bidderRequest = { + refererInfo: { + page: 'https://example.com' + } + }; + setCurrencyConfig({ adServerCurrency: 'USD' }) + + return addFPDToBidderRequest(bidderRequest).then(res => { + const request = spec.buildRequests(bidRequests, res); + const payload = JSON.parse(request.data); + const url = request.url; + expect(url).to.equal(ENDPOINT); + expect(payload.pid).to.exist; + expect(payload.pid).to.equal('5a8af500c9e77c00017e4cad'); + expect(payload.gdpr_applies).to.exist; + expect(payload.gdpr_applies).to.equals(false); + expect(payload.slts[0].name).to.exist; + expect(payload.slts[0].name).to.equal('bellow-article'); + expect(payload.slts[0].flr).to.equal(10); + setCurrencyConfig({}); + }); }); it('should call the endpoint with GDPR consent and pageURL info if found', function () { @@ -132,7 +143,7 @@ describe('BeOp Bid Adapter tests', () => { expect(payload.url).to.equal('http://test.te'); }); - it('should call the endpoint with psegs and bpsegs (stringified) data if any or [] if none', function () { + it('should call the endpoint with bpsegs (stringified) data if any or [] if none', function () { let bidderRequest = { 'ortb2': { @@ -149,15 +160,14 @@ describe('BeOp Bid Adapter tests', () => { const request = spec.buildRequests(bidRequests, bidderRequest); const payload = JSON.parse(request.data); - expect(payload.psegs).to.exist; - expect(payload.psegs).to.include(1234); - expect(payload.psegs).to.include(5678); - expect(payload.psegs).to.include(910); - expect(payload.psegs).to.not.include(1); expect(payload.bpsegs).to.exist; expect(payload.bpsegs).to.include('axed'); expect(payload.bpsegs).to.include('axec'); expect(payload.bpsegs).to.include('1234'); + expect(payload.bpsegs).to.include('1234'); + expect(payload.bpsegs).to.include('5678'); + expect(payload.bpsegs).to.include('910'); + expect(payload.bpsegs).to.not.include('1'); let bidderRequest2 = { @@ -166,8 +176,6 @@ describe('BeOp Bid Adapter tests', () => { const request2 = spec.buildRequests(bidRequests, bidderRequest2); const payload2 = JSON.parse(request2.data); - expect(payload2.psegs).to.exist; - expect(payload2.psegs).to.be.empty; expect(payload2.bpsegs).to.exist; expect(payload2.bpsegs).to.be.empty; }); @@ -231,7 +239,7 @@ describe('BeOp Bid Adapter tests', () => { expect(triggerPixelStub.getCall(0)).to.be.null; spec.onTimeout({params: {accountId: '5a8af500c9e77c00017e4cad'}, timeout: 2000}); expect(triggerPixelStub.getCall(0)).to.not.be.null; - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.beop.io'); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.collectiveaudience.co'); expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ca=bid'); expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ac=timeout'); expect(triggerPixelStub.getCall(0).args[0]).to.include('pid=5a8af500c9e77c00017e4cad'); @@ -243,7 +251,7 @@ describe('BeOp Bid Adapter tests', () => { expect(triggerPixelStub.getCall(0)).to.be.null; spec.onBidWon({params: {accountId: '5a8af500c9e77c00017e4cad'}, cpm: 1.2}); expect(triggerPixelStub.getCall(0)).to.not.be.null; - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.beop.io'); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.collectiveaudience.co'); expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ca=bid'); expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ac=won'); expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('pid=5a8af500c9e77c00017e4cad'); @@ -254,7 +262,7 @@ describe('BeOp Bid Adapter tests', () => { expect(triggerPixelStub.getCall(0)).to.be.null; spec.onBidWon({params: [{accountId: '5a8af500c9e77c00017e4cad'}], cpm: 1.2}); expect(triggerPixelStub.getCall(0)).to.not.be.null; - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.beop.io'); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.collectiveaudience.co'); expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ca=bid'); expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ac=won'); expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('pid=5a8af500c9e77c00017e4cad'); @@ -330,4 +338,76 @@ describe('BeOp Bid Adapter tests', () => { expect(payload.eids[0].source).to.equal('provider.com'); }); }) + + describe('Ensure first party cookie is well managed', function () { + let bidRequests = []; + + it(`should generate a new uuid`, function () { + let bid = Object.assign({}, validBid); + bidRequests.push(bid); + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + expect(payload.fg).to.exist; + }) + }) + describe('getUserSyncs', function () { + it('should return iframe sync when iframeEnabled and syncFrame provided', function () { + const syncOptions = { iframeEnabled: true, pixelEnabled: false }; + const serverResponses = [{ body: { sync_frames: ['https://example.com/sync_frame', 'https://example2.com/sync_second'] } }]; + + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + + expect(syncs).to.have.length(2); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://example.com/sync_frame'); + }); + + it('should return pixel syncs when pixelEnabled and syncPixels provided', function () { + const syncOptions = { iframeEnabled: false, pixelEnabled: true }; + const serverResponses = [{ + body: { + sync_pixels: [ + 'https://example.com/pixel1', + 'https://example.com/pixel2' + ] + } + }]; + + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + + expect(syncs).to.have.length(2); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://example.com/pixel1'); + expect(syncs[1].url).to.equal('https://example.com/pixel2'); + }); + + it('should return both iframe and pixel syncs when both options are enabled', function () { + const syncOptions = { iframeEnabled: true, pixelEnabled: true }; + const serverResponses = [{ + body: { + sync_frames: ['https://example.com/sync_frame'], + sync_pixels: ['https://example.com/pixel1'] + } + }]; + + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + + expect(syncs).to.have.length(2); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[1].type).to.equal('image'); + }); + + it('should return empty array when no serverResponses', function () { + const syncOptions = { iframeEnabled: true, pixelEnabled: true }; + const syncs = spec.getUserSyncs(syncOptions, []); + expect(syncs).to.be.an('array').that.is.empty; + }); + + it('should return empty array when no syncFrame or syncPixels provided', function () { + const syncOptions = { iframeEnabled: true, pixelEnabled: true }; + const serverResponses = [{ body: {} }]; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).to.be.an('array').that.is.empty; + }); + }); }); diff --git a/test/spec/modules/beyondmediaBidAdapter_spec.js b/test/spec/modules/beyondmediaBidAdapter_spec.js index 751b3ae1098..b117b2c4972 100644 --- a/test/spec/modules/beyondmediaBidAdapter_spec.js +++ b/test/spec/modules/beyondmediaBidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/beyondmediaBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'beyondmedia' +const bidder = 'beyondmedia'; describe('AndBeyondMediaBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('AndBeyondMediaBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('AndBeyondMediaBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('AndBeyondMediaBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -73,9 +86,20 @@ describe('AndBeyondMediaBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -83,8 +107,6 @@ describe('AndBeyondMediaBidAdapter', function () { 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; - expect(spec.isBidRequestValid(bids[1])).to.be.true; - expect(spec.isBidRequestValid(bids[2])).to.be.true; }); it('Should return false if at least one of parameters is not present', function () { expect(spec.isBidRequestValid(invalidBid)).to.be.false; @@ -114,6 +136,7 @@ describe('AndBeyondMediaBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -122,7 +145,11 @@ describe('AndBeyondMediaBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -131,7 +158,7 @@ describe('AndBeyondMediaBidAdapter', function () { 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('string'); + 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); @@ -172,8 +199,10 @@ describe('AndBeyondMediaBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -188,12 +217,38 @@ describe('AndBeyondMediaBidAdapter', function () { expect(data.ccpa).to.equal(bidderRequest.uspConsent); expect(data.gdpr).to.not.exist; }); + }); - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -397,5 +452,17 @@ describe('AndBeyondMediaBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://cookies.andbeyond.media/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cookies.andbeyond.media/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/bidResponseFilter_spec.js b/test/spec/modules/bidResponseFilter_spec.js new file mode 100644 index 00000000000..c4b4a776243 --- /dev/null +++ b/test/spec/modules/bidResponseFilter_spec.js @@ -0,0 +1,176 @@ +import { + addBidResponseHook, + BID_ADV_DOMAINS_REJECTION_REASON, + BID_ATTR_REJECTION_REASON, + BID_CATEGORY_REJECTION_REASON, + init, + MODULE_NAME + , reset} from '../../../modules/bidResponseFilter'; +import {config} from '../../../src/config'; +import {addBidResponse} from '../../../src/auction.js'; + +describe('bidResponseFilter', () => { + let mockAuctionIndex + beforeEach(() => { + mockAuctionIndex = { + getBidRequest: () => { + }, + getAdUnit: () => { + } + }; + }); + afterEach(() => { + config.resetConfig(); + reset(); + }) + + describe('enable/disable', () => { + let reject, dispatch; + + beforeEach(() => { + reject = sinon.stub(); + dispatch = sinon.stub(); + }); + + it('should not run if not configured', () => { + reset(); + addBidResponse.call({dispatch}, 'au', {}, reject); + sinon.assert.notCalled(reject); + sinon.assert.called(dispatch); + }); + + it('should run if configured', () => { + config.setConfig({ + bidResponseFilter: {} + }); + addBidResponse.call({dispatch}, 'au', {}, reject); + sinon.assert.called(reject); + sinon.assert.notCalled(dispatch); + }) + }); + + it('should pass the bid after successful ortb2 rules validation', () => { + const call = sinon.stub(); + + mockAuctionIndex.getOrtb2 = () => ({ + badv: [], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + const bid = { + meta: { + advertiserDomains: ['domain1.com', 'domain2.com'], + primaryCatId: 'EXAMPLE-CAT-ID', + attr: 'attr' + } + }; + + addBidResponseHook(call, 'adcode', bid, () => { + }, mockAuctionIndex); + sinon.assert.calledOnce(call); + }); + + it('should reject the bid after failed ortb2 cat rule validation', () => { + const reject = sinon.stub(); + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['domain1.com', 'domain2.com'], + primaryCatId: 'BANNED_CAT1', + attr: 'attr' + } + }; + mockAuctionIndex.getOrtb2 = () => ({ + badv: [], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + addBidResponseHook(call, 'adcode', bid, reject, mockAuctionIndex); + sinon.assert.calledWith(reject, BID_CATEGORY_REJECTION_REASON); + }); + + it('should reject the bid after failed ortb2 adv domains rule validation', () => { + const rejection = sinon.stub(); + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['domain1.com', 'domain2.com'], + primaryCatId: 'VALID_CAT', + attr: 'attr' + } + }; + mockAuctionIndex.getOrtb2 = () => ({ + badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + addBidResponseHook(call, 'adcode', bid, rejection, mockAuctionIndex); + sinon.assert.calledWith(rejection, BID_ADV_DOMAINS_REJECTION_REASON); + }); + + it('should reject the bid after failed ortb2 attr rule validation', () => { + const reject = sinon.stub(); + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['validdomain1.com', 'validdomain2.com'], + primaryCatId: 'VALID_CAT', + attr: 'BANNED_ATTR' + }, + mediaType: 'video' + }; + mockAuctionIndex.getOrtb2 = () => ({ + badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + mockAuctionIndex.getBidRequest = () => ({ + ortb2Imp: { + video: { + battr: 'BANNED_ATTR' + } + } + }) + + addBidResponseHook(call, 'adcode', bid, reject, mockAuctionIndex); + sinon.assert.calledWith(reject, BID_ATTR_REJECTION_REASON); + }); + + it('should omit the validation if the flag is set to false', () => { + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['validdomain1.com', 'validdomain2.com'], + primaryCatId: 'BANNED_CAT1', + attr: 'valid_attr' + } + }; + + mockAuctionIndex.getOrtb2 = () => ({ + badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + config.setConfig({[MODULE_NAME]: {cat: {enforce: false}}}); + + addBidResponseHook(call, 'adcode', bid, () => { + }, mockAuctionIndex); + sinon.assert.calledOnce(call); + }); + + it('should allow bid for unknown flag set to false', () => { + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['validdomain1.com', 'validdomain2.com'], + primaryCatId: undefined, + attr: 'valid_attr' + } + }; + + mockAuctionIndex.getOrtb2 = () => ({ + badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] + }); + + config.setConfig({[MODULE_NAME]: {cat: {blockUnknown: false}}}); + + addBidResponseHook(call, 'adcode', bid, () => { + }); + sinon.assert.calledOnce(call); + }); +}) diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index 1df9aecf73a..5dbf7d84c3c 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -245,7 +245,7 @@ describe('#bidViewability', function() { let logWinningBidNotFoundSpy; let callBidViewableBidderSpy; let winningBidsArray; - let callBidBillableBidderSpy; + let triggerBillingSpy; let adUnits = [ { 'code': 'abc123', @@ -262,7 +262,7 @@ describe('#bidViewability', function() { triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); eventsEmitSpy = sandbox.spy(events, ['emit']); callBidViewableBidderSpy = sandbox.spy(adapterManager, ['callBidViewableBidder']); - callBidBillableBidderSpy = sandbox.spy(adapterManager, ['callBidBillableBidder']); + triggerBillingSpy = sandbox.spy(adapterManager, ['triggerBilling']); // mocking winningBidsArray winningBidsArray = []; sandbox.stub(prebidGlobal, 'getGlobal').returns({ @@ -307,22 +307,16 @@ describe('#bidViewability', function() { expect(eventsEmitSpy.callCount).to.equal(0); }); - it('should call the callBidBillableBidder function if the viewable bid is associated with an ad unit with deferBilling set to true', function() { + it('should call the triggerBilling function if the viewable bid has deferBilling set to true', function() { let moduleConfig = {}; - const deferredBillingAdUnit = { - 'code': '/harshad/Jan/2021/', - 'deferBilling': true, - 'bids': [ - { - 'bidder': 'pubmatic' - } - ] - }; - adUnits.push(deferredBillingAdUnit); - winningBidsArray.push(PBJS_WINNING_BID); + const bid = { + ...PBJS_WINNING_BID, + deferBilling: true + } + winningBidsArray.push(bid); bidViewability.impressionViewableHandler(moduleConfig, GPT_SLOT, null); - expect(callBidBillableBidderSpy.callCount).to.equal(1); - sinon.assert.calledWith(callBidBillableBidderSpy, PBJS_WINNING_BID); + expect(triggerBillingSpy.callCount).to.equal(1); + sinon.assert.calledWith(triggerBillingSpy, bid); }); }); }); diff --git a/test/spec/modules/bidglassAdapter_spec.js b/test/spec/modules/bidglassAdapter_spec.js index 7b007f7cc1f..e0f85364933 100644 --- a/test/spec/modules/bidglassAdapter_spec.js +++ b/test/spec/modules/bidglassAdapter_spec.js @@ -23,10 +23,10 @@ describe('Bid Glass Adapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/bidmaticBidAdapter_spec.js b/test/spec/modules/bidmaticBidAdapter_spec.js new file mode 100644 index 00000000000..225c2a6dce6 --- /dev/null +++ b/test/spec/modules/bidmaticBidAdapter_spec.js @@ -0,0 +1,309 @@ +import { expect } from 'chai'; +import { END_POINT, SYNC_URL, spec, createUserSyncs } from 'modules/bidmaticBidAdapter.js'; +import { deepClone, deepSetValue, mergeDeep } from '../../../src/utils'; + +const expectedImp = { + 'secure': 1, + 'id': '2eb89f0f062afe', + 'banner': { + 'topframe': 0, + 'format': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 300, + 'h': 600 + } + ] + }, + 'bidfloor': 0, + 'bidfloorcur': 'USD', + 'tagid': 'div-gpt-ad-1460505748561-0' +} + +describe('Bidmatic Bid Adapter', () => { + const GPID_RTB_EXT = { + 'ortb2Imp': { + 'ext': { + 'gpid': 'gpId', + } + }, + } + const FLOOR_RTB_EXT = { + 'ortb2Imp': { + bidfloor: 1 + }, + } + const DEFAULT_BID_REQUEST = { + 'id': '10bb57ee-712f-43e9-9769-b26d03df8a39', + 'bidder': 'bidmatic', + 'params': { + 'source': 886409, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '7d79850b-70aa-4c0f-af95-c1def0452825', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '2eb89f0f062afe', + 'bidderRequestId': '1ae6c8e18f8462', + 'auctionId': '1286637c-51bc-4fdd-8e35-2435ec11775a', + 'ortb2': {} + }; + + describe('adapter interface', () => { + const bidRequest = deepClone(DEFAULT_BID_REQUEST); + + it('should validate params', () => { + expect(spec.isBidRequestValid({ + params: { + source: 1 + } + })).to.equal(true, 'source param must be a number'); + + expect(spec.isBidRequestValid({ + params: { + source: '1' + } + })).to.equal(false, 'source param must be a number'); + + expect(spec.isBidRequestValid({})).to.equal(false, 'source param must be a number'); + }); + + it('should build hb request', () => { + const [ortbRequest] = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }); + + expect(ortbRequest.data.imp[0]).to.deep.equal(expectedImp); + expect(ortbRequest.data.cur).to.deep.equal(['USD']); + }); + + it('should request with source in url', () => { + const [ortbRequest] = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }); + expect(ortbRequest.url).to.equal(`${END_POINT}?source=886409`); + }); + + it('should split http reqs by sources', () => { + const bidRequest2 = mergeDeep(deepClone(DEFAULT_BID_REQUEST), { + params: { + source: 1111 + } + }); + const [ortbRequest1, ortbRequest2] = spec.buildRequests([bidRequest2, bidRequest, bidRequest2], { + bids: [bidRequest2, bidRequest, bidRequest2] + }); + expect(ortbRequest1.url).to.equal(`${END_POINT}?source=1111`); + expect(ortbRequest1.data.imp.length).to.eq(2) + expect(ortbRequest2.url).to.equal(`${END_POINT}?source=886409`); + expect(ortbRequest2.data.imp.length).to.eq(1) + }); + + it('should grab bid floor info', () => { + const [ortbRequest] = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }); + + expect(ortbRequest.data.imp[0].bidfloor).eq(0) + expect(ortbRequest.data.imp[0].bidfloorcur).eq('USD') + }); + + it('should grab bid floor info from exts', () => { + const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), FLOOR_RTB_EXT); + const [ortbRequest] = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }); + + expect(ortbRequest.data.imp[0].bidfloor).eq(1) + }); + + it('should grab bid floor info from params', () => { + const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), { + params: { + bidfloor: 2 + } + }); + const [ortbRequest] = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }); + + expect(ortbRequest.data.imp[0].bidfloor).eq(2) + }); + + it('should set gpid as tagid', () => { + const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), GPID_RTB_EXT); + const [ortbRequest] = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }); + + expect(ortbRequest.data.imp[0].tagid).eq(GPID_RTB_EXT.ortb2Imp.ext.gpid) + }); + }) + + describe('syncs creation', () => { + const syncOptions = { iframeEnabled: true }; + + it('should not operate without syncs enabled', () => { + const syncs = createUserSyncs({}); + expect(syncs).to.eq(undefined); + }); + + it('should call uniq and unused sources only', () => { + const sources = { 111: 0, 222: 0, 333: 1 } + const syncs = createUserSyncs(sources, syncOptions); + + expect(syncs.length).to.eq(2); + + expect(syncs[0].type).to.eq('iframe'); + expect(syncs[0].url).to.eq(`${SYNC_URL}?aid=111`); + expect(syncs[1].type).to.eq('iframe'); + expect(syncs[1].url).to.eq(`${SYNC_URL}?aid=222`); + + expect(sources[111]).to.eq(1); + expect(sources[222]).to.eq(1); + + const syncs2 = createUserSyncs(sources, syncOptions); + expect(syncs2.length).to.eq(0); + }); + + it('should add consent info', () => { + const [{ url: syncUrl }] = createUserSyncs( + { 111: 0 }, + syncOptions, + { gdprApplies: true, consentString: '111' }, + 'yyy', + { gppString: '222', applicableSections: [1, 2] }); + + expect(syncUrl.includes('gdpr=1&gdpr_consent=111')).to.eq(true); + expect(syncUrl.includes('usp=yyy')).to.eq(true); + expect(syncUrl.includes('gpp=222&gpp_sid=1,2')).to.eq(true); + }); + }) + + describe('response interpreter', () => { + const SERVER_RESPONSE = { + 'body': { + 'id': '10bb57ee-712f-43e9-9769-b26d03df8a39', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'c5BsBD5QHHgx4aS8', + 'impid': '2eb89f0f062afe', + 'price': 1, + 'adid': 'BDhclfXLcGzRMeV', + 'adm': '123', + 'adomain': [ + 'https://test.com' + ], + 'crid': 'display_300x250', + 'w': 300, + 'h': 250, + } + ], + 'seat': '1' + } + ], + 'cur': 'USD' + }, + 'headers': {} + } + + it('should return empty results', () => { + const [req] = spec.buildRequests([deepClone(DEFAULT_BID_REQUEST)], { + bids: [deepClone(DEFAULT_BID_REQUEST)] + }) + const result = spec.interpretResponse(null, { + data: req.data + }) + + expect(result.length).to.eq(0); + }); + it('should detect media type based on adm', () => { + const [req] = spec.buildRequests([deepClone(DEFAULT_BID_REQUEST)], { + bids: [deepClone(DEFAULT_BID_REQUEST)] + }) + const result = spec.interpretResponse(SERVER_RESPONSE, { + data: req.data + }) + + expect(result.length).to.eq(1); + expect(result[0].mediaType).to.eq('banner') + }); + it('should detect video adm', () => { + const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), { + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + }, + video: { + playerSize: [640, 480] + } + } + }) + const bannerResponse = deepClone(SERVER_RESPONSE); + const [ortbReq] = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }) + deepSetValue(bannerResponse, 'body.seatbid.0.bid.0.adm', ''); + const result = spec.interpretResponse(bannerResponse, { + data: ortbReq.data + }) + + expect(result.length).to.eq(1); + expect(result[0].mediaType).to.eq('video') + }); + + it('should detect banner adm', () => { + const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), { + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + }, + video: { + playerSize: [640, 480] + } + } + }) + const bannerResponse = deepClone(SERVER_RESPONSE); + const [ortbReq] = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }) + const result = spec.interpretResponse(bannerResponse, { + data: ortbReq.data + }) + + expect(result.length).to.eq(1); + expect(result[0].mediaType).to.eq('banner') + }); + }) +}) diff --git a/test/spec/modules/bidtheatreBidAdapter_spec.js b/test/spec/modules/bidtheatreBidAdapter_spec.js new file mode 100644 index 00000000000..4842c43d1f0 --- /dev/null +++ b/test/spec/modules/bidtheatreBidAdapter_spec.js @@ -0,0 +1,266 @@ +import { expect } from 'chai'; +import { spec, ENDPOINT_URL, BIDDER_CODE, DEFAULT_CURRENCY } from 'modules/bidtheatreBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; +import { deepClone } from 'src/utils.js'; + +const VALID_PUBLISHER_ID = '73b20b3a-12a0-4869-b54e-8d42b55786ee'; +const STATIC_IMP_ID = '3263e5dec855c5'; +const BID_PRICE = 5.112871170043945; +const AUCTION_PRICE_MACRO = '${AUCTION_PRICE}'; + +const BANNER_BID_REQUEST = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'publisherId': VALID_PUBLISHER_ID + }, + 'bidId': STATIC_IMP_ID, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 980, + 240 + ] + ] + } + }, + 'sizes': [ + [ + 980, + 240 + ] + ] + } +]; + +const BANNER_BIDDER_REQUEST = {'bidderCode': BIDDER_CODE, 'bids': BANNER_BID_REQUEST}; + +const BANNER_BID_RESPONSE = { + 'cur': 'USD', + 'seatbid': [ + { + 'seat': '5', + 'bid': [ + { + 'ext': { + 'usersync_urls': [ + 'https://match.adsby.bidtheatre.com/usersync?gdpr=1&gdpr_consent=CONSENT_STRING' + ] + }, + 'crid': '1915538', + 'h': 240, + 'adm': "", + 'mtype': 1, + 'adid': '1915538', + 'adomain': [ + 'bidtheatre.com' + ], + 'price': BID_PRICE, + 'cat': [ + 'IAB3-1' + ], + 'w': 980, + 'id': STATIC_IMP_ID, + 'impid': STATIC_IMP_ID, + 'cid': 'c154375' + } + ] + } + ] +}; + +const VIDEO_BID_REQUEST = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'publisherId': VALID_PUBLISHER_ID + }, + 'bidId': STATIC_IMP_ID, + 'mediaTypes': { + 'video': { + 'playerSize': [ + [ + 1280, + 720 + ] + ], + 'context': 'instream' + } + }, + 'sizes': [[1280, 720]] + } +]; + +const VIDEO_BIDDER_REQUEST = {'bidderCode': BIDDER_CODE, 'bids': VIDEO_BID_REQUEST}; + +const VIDEO_BID_RESPONSE = { + 'cur': 'USD', + 'seatbid': [ + { + 'seat': '5', + 'bid': [ + { + 'ext': { + 'usersync_urls': [ + 'https://match.adsby.bidtheatre.com/usersync?gdpr=0&gdpr_consent=' + ] + }, + 'crid': '1922926', + 'h': 720, + 'mtype': 2, + 'nurl': 'https://adsby.bidtheatre.com/video?z=27025;a=1922926;ex=36;es=http%3A%2F%2F127.0.0.1%3A8080;eb=3672319;xs=940400838;so=1;tag=unspec_1280_720;kuid=05914b22-88cb-4c5d-9f7c-f133fdf9669a;wp=${AUCTION_PRICE};su=127.0.0.1%3A8080;iab=vast2;dealId=;ma=eyJjZCI6ZmFsc2UsInN0IjozLCJtbGF0Ijo1OS4yNywibW9yZyI6InRlbGlhIG5ldHdvcmsgc2VydmljZXMiLCJtbHNjb3JlIjowLjg2MDcwMDU0NzY5NTE1OTksIm16aXAiOiIxMjggMzUiLCJiaXAiOiI4MS4yMjcuODIuMjgiLCJhZ2lkIjozNTYyNzAyLCJtbG1vZGVsIjoibWFzdGVyX21sX2Nsa181MzYiLCJ1YSI6Ik1vemlsbGFcLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC8xMzAuMC4wLjAgU2FmYXJpXC81MzcuMzYiLCJtbG9uIjoxOC4xMywibXJlZ2lvbiI6ImFiIiwiZHQiOjEsIm1jaXR5Ijoic2thcnBuYWNrIiwicGFnZXVybCI6Imh0dHA6XC9cLzEyNy4wLjAuMTo4MDgwXC92aWRlby5odG1sP3BianNfZGVidWc9dHJ1ZSIsImltcGlkIjoieDM2X2FzeC1iLXMyXzQxNDMzMTA0MTIyMzUyNTU3NDgiLCJtY291bnRyeSI6InN3ZSIsInRzIjoxNzMxNTA3NTY5Njg3fQ%3D%3D;cd=0;cb0=;impId=x36_asx-b-s2_4143310412235255748;gdpr=1;gdpr_consent=CP-S4UAP-S4UACGABBENAzEsAP_gAEPgAAAAKKtV_H__bW1r8X73aft0eY1P9_j77sQxBhfJE-4FzLvW_JwXx2ExNA36tqIKmRIEu3bBIQNlHJDUTVCgaogVryDMakWcoTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr993dx2D-t_dv83dzyz4VHn3a5_2e0WJCdA58tDfv9bROb-9IPd_58v4v0_F_rE2_eT1l_tevp7D9-ct87_XW-9_fff79Ll9-goqAWYaFRAHWBISEGgYRQIAVBWEBFAgAAABIGiAgBMGBTsDAJdYSIAQAoABggBAACjIAEAAAEACEQAQAFAgAAgECgABAAgEAgAIGAAEAFgIBAACA6BCmBBAoFgAkZkRCmBCFAkEBLZUIJAECCuEIRZ4AEAiJgoAAAAACsAAQFgsDiSQEqEggS4g2gAAIAEAghAqEEnJgACBI2WoPBE2jK0gDQ04SAAAAA.YAAAAAAAAAAA', + 'adid': '1922926', + 'adomain': [ + 'bidtheatre.com' + ], + 'price': BID_PRICE, + 'cat': [ + 'IAB3-1' + ], + 'w': 1280, + 'id': STATIC_IMP_ID, + 'impid': STATIC_IMP_ID, + 'cid': 'c154375' + } + ] + } + ] +} + +describe('BidtheatreAdapter', function () { + describe('isBidRequestValid', function () { + let bid = { + 'bidder': BIDDER_CODE, + 'params': { + 'publisherId': VALID_PUBLISHER_ID + }, + 'sizes': [[980, 240]] + }; + + it('should return true when required param found and of correct type', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required param is not passed', function () { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { + + }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param of incorrect data type', function () { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { + 'publisherId': 12345 + }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param of incorrect length', function () { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { + 'publisherId': '73b20b3a-12a0-4869-b54e-8d42b55786e' + }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('should include correct http method, correct url and existing data', function () { + const request = spec.buildRequests(BANNER_BID_REQUEST, BANNER_BIDDER_REQUEST); + expect(request[0].method).to.equal('POST'); + expect(request[0].url).to.equal(ENDPOINT_URL); + expect(request[0].data).to.exist; + }); + + it('should include required bid param in request', function () { + const request = spec.buildRequests(BANNER_BID_REQUEST, BANNER_BIDDER_REQUEST); + const data = request[0].data; + expect(data.imp[0].ext.bidder.publisherId).to.equal(VALID_PUBLISHER_ID); + }); + + it('should include imp array in request', function () { + const request = spec.buildRequests(BANNER_BID_REQUEST, BANNER_BIDDER_REQUEST); + const data = request[0].data; + expect(data).to.have.property('imp').that.is.an('array').with.lengthOf.at.least(1); + expect(data.imp[0]).to.be.an('object'); + }); + }); + + describe('interpretResponse', function () { + it('should have exactly one bid in banner response', function () { + const request = spec.buildRequests(BANNER_BID_REQUEST, BANNER_BIDDER_REQUEST); + const bids = spec.interpretResponse({body: BANNER_BID_RESPONSE}, request[0]); + expect(bids).to.be.an('array').with.lengthOf(1); + expect(bids[0]).to.be.an('object'); + }); + + it('should have currency set to USD in banner response', function () { + const request = spec.buildRequests(BANNER_BID_REQUEST, BANNER_BIDDER_REQUEST); + const bids = spec.interpretResponse({body: BANNER_BID_RESPONSE}, request[0]); + expect(bids[0].currency).to.be.a('string').and.to.equal(DEFAULT_CURRENCY); + }); + + it('should have ad in response and auction price macros replaced in banner response', function () { + const request = spec.buildRequests(BANNER_BID_REQUEST, BANNER_BIDDER_REQUEST); + const bids = spec.interpretResponse({body: BANNER_BID_RESPONSE}, request[0]); + const ad = bids[0].ad; + expect(ad).to.exist; + expect(ad).to.be.a('string'); + expect(ad).to.include('&wp=' + BID_PRICE + '&'); + expect(ad).to.not.include(AUCTION_PRICE_MACRO); + }); + + if (FEATURES.VIDEO) { + it('should have exactly one bid in video response', function () { + const request = spec.buildRequests(VIDEO_BID_REQUEST, VIDEO_BIDDER_REQUEST); + const bids = spec.interpretResponse({body: VIDEO_BID_RESPONSE}, request[0]); + expect(bids).to.be.an('array').with.lengthOf(1); + expect(bids[0]).to.be.an('object'); + }); + + it('should have currency set to USD in video response', function () { + const request = spec.buildRequests(VIDEO_BID_REQUEST, VIDEO_BIDDER_REQUEST); + const bids = spec.interpretResponse({body: VIDEO_BID_RESPONSE}, request[0]); + expect(bids[0].currency).to.be.a('string').and.to.equal(DEFAULT_CURRENCY); + }); + + it('should have vastUrl in response and auction price macros replaced in video response', function () { + const request = spec.buildRequests(VIDEO_BID_REQUEST, VIDEO_BIDDER_REQUEST); + const bids = spec.interpretResponse({body: VIDEO_BID_RESPONSE}, request[0]); + const vastUrl = bids[0].vastUrl; + expect(vastUrl).to.exist; + expect(vastUrl).to.be.a('string'); + expect(vastUrl).to.include(';wp=' + BID_PRICE + ';'); + expect(vastUrl).to.not.include(AUCTION_PRICE_MACRO); + }); + } + }); + + describe('getUserSyncs', function () { + const bidResponse = deepClone(BANNER_BID_RESPONSE); + const bidResponseSyncURL = bidResponse.seatbid[0].bid[0].ext.usersync_urls[0]; + + const gdprConsent = { + gdprApplies: true, + consentString: 'CONSENT_STRING' + }; + + it('should return empty when pixel is disallowed', function () { + expect(spec.getUserSyncs({ pixelEnabled: false }, bidResponse, gdprConsent)).to.be.empty; + }); + + it('should return empty when no server response is present', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, [], gdprConsent)).to.be.empty; + }); + + it('should return usersync url when pixel is allowed and present in bid response', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, [{body: bidResponse}], gdprConsent)).to.deep.equal([{ type: 'image', url: bidResponseSyncURL }]); + }); + }); +}); diff --git a/test/spec/modules/big-richmediaBidAdapter_spec.js b/test/spec/modules/big-richmediaBidAdapter_spec.js index c3a9a8ef6c1..f37975c3bc1 100644 --- a/test/spec/modules/big-richmediaBidAdapter_spec.js +++ b/test/spec/modules/big-richmediaBidAdapter_spec.js @@ -271,31 +271,6 @@ describe('bigRichMediaAdapterTests', function () { }); }); - describe('transformBidParams', function() { - it('cast placementId to number', function() { - const adUnit = { - code: 'adunit-code', - params: { - placementId: '456' - } - }; - const bid = { - params: { - placementId: '456' - }, - sizes: [[300, 250]], - mediaTypes: { - banner: { sizes: [[300, 250]] } - } - }; - - const params = spec.transformBidParams({ placementId: '456' }, true, adUnit, [{ bidderCode: 'bigRichmedia', auctionId: bid.auctionId, bids: [bid] }]); - - expect(params.placement_id).to.exist; - expect(params.placement_id).to.be.a('number'); - }); - }); - describe('onBidWon', function() { it('Should not have any error', function() { const result = spec.onBidWon({}); diff --git a/test/spec/modules/bitmediaBidAdapter_spec.js b/test/spec/modules/bitmediaBidAdapter_spec.js new file mode 100644 index 00000000000..be454690235 --- /dev/null +++ b/test/spec/modules/bitmediaBidAdapter_spec.js @@ -0,0 +1,499 @@ +import {expect} from 'chai'; +import {spec, STORAGE, ENDPOINT_URL} from 'modules/bitmediaBidAdapter.js'; +import * as utils from 'src/utils.js'; +import {config} from 'src/config.js'; +import {BANNER} from '../../../src/mediaTypes'; + +describe('Bitmedia Bid Adapter', function () { + const createBidRequest = (sandbox, overrides = {}) => { + return Object.assign({ + bidder: 'bitmedia', + params: { + adUnitID: 'test-ad-unit-id', + currency: 'USD' + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'bid123', + auctionId: 'auction123', + transactionId: 'transaction123', + adUnitCode: 'adunit123', + sizes: [[300, 250], [300, 600]], + getFloor: sandbox.stub().returns({ + currency: 'USD', + floor: 0.4 + }) + }, overrides); + } + + const createBidderRequest = (overrides = {}) => { + return Object.assign({ + refererInfo: { + page: 'https://example.com/page.html', + domain: 'example.com', + referer: 'https://google.com' + }, + timeout: 2000, + bidderCode: 'bitmedia', + auctionId: 'auction123', + ortb2: { + site: { + domain: 'publisher.com', + page: 'https://publisher.com/homepage.html', + publisher: { + domain: 'publisher.com' + } + }, + device: { + ua: 'custom-user-agent', + language: 'fr' + } + } + }, overrides); + } + // Helper function to stub storage for user ID + const stubStorage = (sandbox, userIdInLocalStorage = null, userIdInCookies = null) => { + sandbox.stub(STORAGE, 'hasLocalStorage').returns(true); + sandbox.stub(STORAGE, 'getDataFromLocalStorage') + .withArgs('bitmedia_fid') + .returns(userIdInLocalStorage); + + sandbox.stub(STORAGE, 'cookiesAreEnabled').returns(true); + sandbox.stub(STORAGE, 'getCookie') + .withArgs('bitmedia_fid') + .returns(userIdInCookies); + + if (userIdInLocalStorage || userIdInCookies) { + const encodedFid = userIdInLocalStorage || userIdInCookies; + sandbox.stub(window, 'atob') + .withArgs(encodedFid) + .returns(`{"fid":"user123"}`); + } + } + + describe('isBidRequestValid', function () { + let bid; + beforeEach(function () { + bid = { + bidder: 'bitmedia', + params: { + adUnitID: 'test-ad-unit-id', + currency: 'USD' + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + } + }; + }); + + it('should return true when required params found and sizes are valid', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if adUnitID is missing', function () { + delete bid.params.adUnitID; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if currency is invalid', function () { + bid.params.currency = 'EUR'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if no valid sizes provided', function () { + bid.mediaTypes.banner.sizes = [[123, 456], [789, 101]]; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if mediaTypes.banner is missing', function () { + delete bid.mediaTypes.banner; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if banner sizes are not an array', function () { + bid.mediaTypes.banner.sizes = '300x250'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if bid params are missing', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + + // Stub generateUUID to return a fixed value for testing + sandbox.stub(utils, 'generateUUID').returns('test-generated-uuid'); + + // Stub config.getConfig for bidderTimeout + sandbox.stub(config, 'getConfig').withArgs('bidderTimeout').returns(30); + }); + + afterEach(function () { + sandbox.restore(); + }); + describe('when building the request', function () { + let bidRequests; + let bidderRequest; + let requests; + let request; + let data; + + beforeEach(function () { + bidRequests = [createBidRequest(sandbox,)]; + bidderRequest = createBidderRequest(); + stubStorage(sandbox, null, 'encodedFidString'); // User ID in cookies + + requests = spec.buildRequests(bidRequests, bidderRequest); + request = requests[0]; + data = request.data; + }); + + it('should return an array with one request', function () { + expect(requests).to.be.an('array').with.lengthOf(1); + }); + + it('should have method POST and the correct URL', function () { + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(`${ENDPOINT_URL}${bidRequests[0].params.adUnitID}`); + }); + + it('should have the correct request options', function () { + expect(request.options).to.deep.equal({ + withCredentials: false, + crossOrigin: true + }); + }); + + it('should have the correct request data structure', function () { + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('id', 'imp', 'site', 'device', 'cur', 'tmax', 'ext', 'user'); + }); + + it('should include the generated UUID in the request data', function () { + expect(data.id).to.equal('test-generated-uuid'); + }); + + it('should include the correct impressions in the request data', function () { + expect(data.imp).to.be.an('array').with.lengthOf(2); + + const expectedImp1 = { + id: 'bid123', + tagid: 'test-ad-unit-id', + banner: { + w: 300, + h: 250 + }, + bidfloor: 0.4, + bidfloorcur: 'USD' + }; + + const expectedImp2 = { + id: 'bid123', + tagid: 'test-ad-unit-id', + banner: { + w: 300, + h: 600 + }, + bidfloor: 0.4, + bidfloorcur: 'USD' + }; + + expect(data.imp[0]).to.deep.equal(expectedImp1); + expect(data.imp[1]).to.deep.equal(expectedImp2); + }); + + it('should include the correct site information', function () { + expect(data.site).to.deep.equal({ + domain: 'publisher.com', + page: 'https://publisher.com/homepage.html', + publisher: { + domain: 'publisher.com' + } + }); + }); + + it('should include the correct device information', function () { + expect(data.device).to.deep.equal({ + ua: 'custom-user-agent', + language: 'fr' + }); + }); + + it('should include the default currency', function () { + expect(data.cur).to.deep.equal(['USD']); + }); + + it('should include the correct timeout (tmax)', function () { + expect(data.tmax).to.equal(2000); + }); + + it('should include the ext field with adapter_version and prebid_version as strings', function () { + expect(data.ext.adapter_version).to.be.a('string'); + expect(data.ext.prebid_version).to.be.a('string'); + }); + + it('should include the user ID when present in cookies', function () { + expect(data.user).to.deep.equal({ + id: 'user123' + }); + }); + }); + + describe('when some invalid sizes are provided', function () { + let bidRequests; + let bidderRequest; + let requests; + let request; + let data; + + beforeEach(function () { + bidRequests = [createBidRequest(sandbox, { + mediaTypes: { + banner: { + sizes: [[300, 600], [888, 888]], + }, + }, + sizes: [[300, 600], [888, 888]], + })]; + bidderRequest = createBidderRequest(); + stubStorage(sandbox, null, null); + + requests = spec.buildRequests(bidRequests, bidderRequest); + request = requests[0]; + data = request.data; + }); + + it('should not build imp with invalid size', function () { + expect(data.imp).to.be.an('array').with.lengthOf(1); + }); + }); + + describe('when user ID is absent', function () { + let bidRequests; + let bidderRequest; + let requests; + let data; + + beforeEach(function () { + bidRequests = [createBidRequest(sandbox,)]; + bidderRequest = createBidderRequest(); + stubStorage(sandbox, null, null); + + requests = spec.buildRequests(bidRequests, bidderRequest); + data = requests[0].data; + }); + + it('should not include user ID in the request data', function () { + expect(data.user).to.be.undefined; + }); + }); + + describe('when getFloor is not available', function () { + let bidRequests; + let bidderRequest; + let requests; + let imp; + + beforeEach(function () { + bidRequests = [createBidRequest(sandbox, { + getFloor: undefined + })]; + bidderRequest = createBidderRequest(); + + requests = spec.buildRequests(bidRequests, bidderRequest); + imp = requests[0].data.imp; + }); + + it('should not include bidfloor in imp objects', function () { + imp.forEach(impression => { + expect(impression).to.not.have.property('bidfloor'); + expect(impression).to.not.have.property('bidfloorcur'); + }); + }); + }); + + describe('when different bid floors are provided per size', function () { + let bidRequests; + let bidderRequest; + let requests; + let imp; + + beforeEach(function () { + const getFloorStub = sinon.stub(); + getFloorStub.withArgs({currency: 'USD', mediaType: 'banner', size: [300, 250]}).returns({ + currency: 'USD', + floor: 0.5 + }); + getFloorStub.withArgs({currency: 'USD', mediaType: 'banner', size: [300, 600]}).returns({ + currency: 'USD', + floor: 0.7 + }); + + bidRequests = [createBidRequest(sandbox, { + getFloor: getFloorStub + })]; + bidderRequest = createBidderRequest(); + + requests = spec.buildRequests(bidRequests, bidderRequest); + imp = requests[0].data.imp; + }); + + it('should include the correct bidfloor per impression', function () { + expect(imp[0].bidfloor).to.equal(0.5); + expect(imp[0].banner).to.deep.equal({w: 300, h: 250}); + expect(imp[1].bidfloor).to.equal(0.7); + expect(imp[1].banner).to.deep.equal({w: 300, h: 600}); + }); + }); + + describe('when bid floor data is invalid', function () { + let bidRequests; + let bidderRequest; + let requests; + let imp; + + beforeEach(function () { + const invalidGetFloor = sinon.stub().returns({ + currency: 'USD', + floor: 'invalid' + }); + bidRequests = [createBidRequest(sandbox, { + getFloor: invalidGetFloor + })]; + bidderRequest = createBidderRequest(); + + requests = spec.buildRequests(bidRequests, bidderRequest); + imp = requests[0].data.imp; + }); + + it('should not include bidfloor when floor value is invalid', function () { + imp.forEach(impression => { + expect(impression).to.not.have.property('bidfloor'); + expect(impression).to.not.have.property('bidfloorcur'); + }); + }); + }); + }); + + describe('interpretResponse', function () { + let sandbox; + let bidRequests; + let bidderRequest; + let requests; + beforeEach(function () { + sandbox = sinon.createSandbox(); + sandbox.stub(utils, 'generateUUID').returns('test-generated-uuid'); + bidRequests = [createBidRequest(sandbox,)]; + bidderRequest = createBidderRequest(); + stubStorage(sandbox, null, null); // No user ID for simplicity + + requests = spec.buildRequests(bidRequests, bidderRequest); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should return bids with all required keys when server response has valid bids', function () { + const request = requests[0]; + + const serverResponse = { + body: { + id: request.data.id, + seatbid: [ + { + bid: [ + { + impid: request.data.imp[0].id, + price: 1.5, + w: request.data.imp[0].banner.w, + h: request.data.imp[0].banner.h, + adm: '
Ad Content
', + crid: 'creative123', + adomain: ['example.com'], + nurl: 'https://example.com/win', + exp: 360, + }, + ], + }, + ], + cur: 'USD', + }, + }; + + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.an('array').with.lengthOf(1); + const bid = bids[0]; + expect(bid).to.include.all.keys( + 'requestId', + 'cpm', + 'currency', + 'width', + 'height', + 'ad', + 'ttl', + 'creativeId', + 'netRevenue', + 'meta', + 'nurl', + 'mediaType' + ); + expect(bid.mediaType).to.equal(BANNER); + expect(bid.ttl).to.equal(360); + expect(bid.nurl).to.equal('https://example.com/win'); + }); + + it('should return an empty array when server response is empty', function () { + const serverResponse = {body: {}}; + const bidRequest = {}; + + const bids = spec.interpretResponse(serverResponse, bidRequest); + + expect(bids).to.be.an('array').that.is.empty; + }); + }); + + describe('onBidWon', function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + + it('should return nothing and trigger a pixel with nurl', function () { + const bid = { + nurl: 'https://example.com/win', + }; + + const triggerPixelSpy = sandbox.spy(utils, 'triggerPixel'); + + const response = spec.onBidWon(bid); + + expect(response).to.be.undefined; + + expect(triggerPixelSpy.calledOnce).to.equal(true); + + expect(triggerPixelSpy.calledWith(bid.nurl)).to.equal(true); + }); + }); +}); diff --git a/test/spec/modules/bizzclickBidAdapter_spec.js b/test/spec/modules/blastoBidAdapter_spec.js similarity index 81% rename from test/spec/modules/bizzclickBidAdapter_spec.js rename to test/spec/modules/blastoBidAdapter_spec.js index f8e66caf657..2efdd5ad286 100644 --- a/test/spec/modules/bizzclickBidAdapter_spec.js +++ b/test/spec/modules/blastoBidAdapter_spec.js @@ -1,9 +1,9 @@ import { expect } from 'chai'; -import { spec } from 'modules/bizzclickBidAdapter'; +import { spec } from 'modules/blastoBidAdapter'; import 'modules/priceFloors.js'; import { newBidder } from 'src/adapters/bidderFactory'; import { config } from '../../../src/config.js'; -import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; +import { addFPDToBidderRequest } from '../../helpers/fpd.js'; // load modules that register ORTB processors import 'src/prebid.js'; @@ -11,12 +11,12 @@ import 'modules/currency.js'; import 'modules/userId/index.js'; import 'modules/multibid/index.js'; import 'modules/priceFloors.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; const SIMPLE_BID_REQUEST = { - bidder: 'bizzclick', + bidder: 'blasto', params: { accountId: 'testAccountId', sourceId: 'testSourceId', @@ -46,7 +46,7 @@ const SIMPLE_BID_REQUEST = { } const BANNER_BID_REQUEST = { - bidder: 'bizzclick', + bidder: 'blasto', params: { accountId: 'testAccountId', sourceId: 'testSourceId', @@ -85,7 +85,7 @@ const VIDEO_BID_REQUEST = { protocols: [1, 2, 4] } }, - bidder: 'bizzclick', + bidder: 'blasto', params: { accountId: '123', sourceId: '123', @@ -128,7 +128,7 @@ const NATIVE_BID_REQUEST = { } } }, - bidder: 'bizzclick', + bidder: 'blasto', params: { accountId: 'testAccountId', sourceId: 'testSourceId', @@ -158,7 +158,7 @@ const gdprConsent = { addtlConsent: '1~1.35.41.101', } -describe('bizzclickAdapter', function () { +describe('blastoAdapter', function () { const adapter = newBidder(spec); describe('inherited functions', function () { it('exists and is a function', function () { @@ -167,23 +167,26 @@ describe('bizzclickAdapter', function () { }); describe('with user privacy regulations', function () { - it('should send the Coppa "required" flag set to "1" in the request', function () { + it('should send the Coppa "required" flag set to "1" in the request', async function () { sinon.stub(config, 'getConfig') .withArgs('coppa') .returns(true); - const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); expect(serverRequest.data.regs.coppa).to.equal(1); config.getConfig.restore(); }); - it('should send the GDPR Consent data in the request', function () { - const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({ ...bidderRequest, gdprConsent })); + it('should send the GDPR Consent data in the request', async function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], await addFPDToBidderRequest({ + ...bidderRequest, + gdprConsent + })); expect(serverRequest.data.regs.ext.gdpr).to.exist.and.to.equal(1); expect(serverRequest.data.user.ext.consent).to.equal('CONSENT'); }); - it('should send the CCPA data in the request', function () { - const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); + it('should send the CCPA data in the request', async function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], await addFPDToBidderRequest({...bidderRequest, ...{uspConsent: '1YYY'}})); expect(serverRequest.data.regs.ext.us_privacy).to.equal('1YYY'); }); }); @@ -202,14 +205,14 @@ describe('bizzclickAdapter', function () { }); describe('build request', function () { - it('should return an empty array when no bid requests', function () { - const bidRequest = spec.buildRequests([], syncAddFPDToBidderRequest(bidderRequest)); + it('should return an empty array when no bid requests', async function () { + const bidRequest = spec.buildRequests([], await addFPDToBidderRequest(bidderRequest)); expect(bidRequest).to.be.an('array'); expect(bidRequest.length).to.equal(0); }); - it('should return a valid bid request object', function () { - const request = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + it('should return a valid bid request object', async function () { + const request = spec.buildRequests([SIMPLE_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); expect(request).to.not.equal('array'); expect(request.data).to.be.an('object'); expect(request.method).to.equal('POST'); @@ -224,24 +227,24 @@ describe('bizzclickAdapter', function () { expect(request.data).to.have.property('device'); }); - it('should return a valid bid BANNER request object', function () { - const request = spec.buildRequests([BANNER_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + it('should return a valid bid BANNER request object', async function () { + const request = spec.buildRequests([BANNER_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); expect(request.data.imp[0].banner).to.exist; expect(request.data.imp[0].banner.format[0].w).to.be.an('number'); expect(request.data.imp[0].banner.format[0].h).to.be.an('number'); }); if (FEATURES.VIDEO) { - it('should return a valid bid VIDEO request object', function () { - const request = spec.buildRequests([VIDEO_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + it('should return a valid bid VIDEO request object', async function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); expect(request.data.imp[0].video).to.exist; expect(request.data.imp[0].video.w).to.be.an('number'); expect(request.data.imp[0].video.h).to.be.an('number'); }); } - it('should return a valid bid NATIVE request object', function () { - const request = spec.buildRequests([NATIVE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + it('should return a valid bid NATIVE request object', async function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); expect(request.data.imp[0]).to.be.an('object'); }); }) @@ -251,7 +254,7 @@ describe('bizzclickAdapter', function () { beforeEach(function () { bidRequests = [{ 'bidId': '28ffdk2B952532', - 'bidder': 'bizzclick', + 'bidder': 'blasto', 'userId': { 'freepassId': { 'userIp': '172.21.0.1', @@ -290,8 +293,8 @@ describe('bizzclickAdapter', function () { }] } }; - it('should interpret server response', function () { - const bidRequest = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + it('should interpret server response', async function () { + const bidRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); const bids = spec.interpretResponse(serverResponse, bidRequest); expect(bids).to.be.an('array'); const bid = bids[0]; diff --git a/test/spec/modules/bliinkBidAdapter_spec.js b/test/spec/modules/bliinkBidAdapter_spec.js index 3db97a17d88..ff48d8579a7 100644 --- a/test/spec/modules/bliinkBidAdapter_spec.js +++ b/test/spec/modules/bliinkBidAdapter_spec.js @@ -7,9 +7,14 @@ import { BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME, getEffectiveConnectionType, getUserIds, - getDomLoadingDuration, GVL_ID, } from 'modules/bliinkBidAdapter.js'; +import { + canAccessWindowTop, + getDomLoadingDuration, + getWindowSelf, + getWindowTop +} from 'src/utils.js'; import { config } from 'src/config.js'; /** @@ -32,8 +37,9 @@ import { config } from 'src/config.js'; * ortb2Imp: {ext: {data: {pbadslot: string}}}}} */ +const w = (canAccessWindowTop()) ? getWindowTop() : getWindowSelf(); const connectionType = getEffectiveConnectionType(); -const domLoadingDuration = getDomLoadingDuration().toString(); +const domLoadingDuration = getDomLoadingDuration(w).toString(); const getConfigBid = (placement) => { return { adUnitCode: '/19968336/test', diff --git a/test/spec/modules/blueBidAdapter_spec.js b/test/spec/modules/blueBidAdapter_spec.js new file mode 100755 index 00000000000..f3f6f435f20 --- /dev/null +++ b/test/spec/modules/blueBidAdapter_spec.js @@ -0,0 +1,91 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { spec, storage } from 'modules/blueBidAdapter.js'; + +const BIDDER_CODE = 'blue'; +const ENDPOINT_URL = 'https://bidder-us-east-1.getblue.io/engine/?src=prebid'; +const GVLID = 620; +const COOKIE_NAME = 'ckid'; +const CURRENCY = 'USD'; + +describe('blueBidAdapter:', function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('isBidRequestValid:', function () { + it('should return true for valid bid requests', function () { + const validBid = { + params: { + placementId: '12345', + publisherId: '67890', + }, + }; + expect(spec.isBidRequestValid(validBid)).to.be.true; + }); + + it('should return false for invalid bid requests', function () { + const invalidBid = { + params: { + placementId: '12345', + }, + }; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests:', function () { + let validBidRequests; + let bidderRequest; + + beforeEach(function () { + validBidRequests = [ + { + bidId: 'bid1', + params: { + placementId: '12345', + publisherId: '67890', + }, + getFloor: () => ({ currency: CURRENCY, floor: 1.5 }), + }, + ]; + + bidderRequest = { + refererInfo: { + page: 'https://example.com', + }, + }; + + sandbox.stub(storage, 'getDataFromLocalStorage').returns('testBuyerId'); + }); + + it('should build a valid OpenRTB request', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT_URL); + expect(request.options.contentType).to.equal('text/plain'); + + const ortbRequest = JSON.parse(request.data); + expect(ortbRequest.ext.gvlid).to.equal(GVLID); + expect(ortbRequest.user.ext.buyerid).to.equal('testBuyerId'); + expect(ortbRequest.imp[0].bidfloor).to.equal(1.5); + expect(ortbRequest.imp[0].bidfloorcur).to.equal(CURRENCY); + }); + + it('should omit bidfloor if getFloor is not implemented', function () { + validBidRequests[0].getFloor = undefined; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const ortbRequest = JSON.parse(request.data); + + expect(ortbRequest.imp[0].bidfloor).to.be.undefined; + }); + }); +}); diff --git a/test/spec/modules/bluebillywigBidAdapter_spec.js b/test/spec/modules/bluebillywigBidAdapter_spec.js deleted file mode 100644 index 4b58e3507db..00000000000 --- a/test/spec/modules/bluebillywigBidAdapter_spec.js +++ /dev/null @@ -1,1094 +0,0 @@ -import {expect} from 'chai'; -import {spec} from 'modules/bluebillywigBidAdapter.js'; -import {deepAccess, deepClone} from 'src/utils.js'; -import {config} from 'src/config.js'; -import {VIDEO} from 'src/mediaTypes.js'; - -const BB_CONSTANTS = { - BIDDER_CODE: 'bluebillywig', - AUCTION_URL: '$$URL_STARTpbs.bluebillywig.com/openrtb2/auction?pub=$$PUBLICATION', - SYNC_URL: '$$URL_STARTpbs.bluebillywig.com/static/cookie-sync.html?pub=$$PUBLICATION', - RENDERER_URL: 'https://$$PUBLICATION.bbvms.com/r/$$RENDERER.js', - DEFAULT_TIMEOUT: 5000, - DEFAULT_TTL: 300, - DEFAULT_WIDTH: 768, - DEFAULT_HEIGHT: 432, - DEFAULT_NET_REVENUE: true -}; - -describe('BlueBillywigAdapter', () => { - describe('isBidRequestValid', () => { - const baseValidBid = { - bidder: BB_CONSTANTS.BIDDER_CODE, - params: { - accountId: 123, - publicationName: 'bbprebid.dev', - rendererCode: 'glorious_renderer', - connections: [ BB_CONSTANTS.BIDDER_CODE ], - bluebillywig: {} - }, - mediaTypes: { - video: { - context: 'outstream' - } - } - }; - - it('should return true when required params found', () => { - expect(spec.isBidRequestValid(baseValidBid)).to.equal(true); - }); - - it('should return false when params missing', () => { - const bid = deepClone(baseValidBid); - delete bid.params; - - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when publicationName is missing', () => { - const bid = deepClone(baseValidBid); - delete bid.params.publicationName; - - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when publicationName is not a string', () => { - const bid = deepClone(baseValidBid); - - bid.params.publicationName = 123; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.publicationName = false; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.publicationName = void (0); - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.publicationName = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when publicationName is formatted poorly', () => { - const bid = deepClone(baseValidBid); - - bid.params.publicationName = 'bb.'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.publicationName = 'bb-test'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.publicationName = '?'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when renderer is not specified', () => { - const bid = deepClone(baseValidBid); - - delete bid.params.rendererCode; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when renderer is not a string', () => { - const bid = deepClone(baseValidBid); - - bid.params.rendererCode = 123; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererCode = false; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererCode = void (0); - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererCode = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when renderer is formatted poorly', () => { - const bid = deepClone(baseValidBid); - - bid.params.rendererCode = 'bb.'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererCode = 'bb-test'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererCode = '?'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when accountId is not specified', () => { - const bid = deepClone(baseValidBid); - - delete bid.params.accountId; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when connections is not specified', () => { - const bid = deepClone(baseValidBid); - - delete bid.params.connections; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when connections is not an array', () => { - const bid = deepClone(baseValidBid); - - bid.params.connections = 123; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.connections = false; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.connections = void (0); - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.connections = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.connections = 'string'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when a connection is missing', () => { - const bid = deepClone(baseValidBid); - - bid.params.connections.push('potatoes'); - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.connections.pop(); - - delete bid.params[BB_CONSTANTS.BIDDER_CODE]; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail if bid has no mediaTypes', () => { - const bid = deepClone(baseValidBid); - - delete bid.mediaTypes; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail if bid has no mediaTypes.video', () => { - const bid = deepClone(baseValidBid); - - delete bid.mediaTypes[VIDEO]; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail if bid has no mediaTypes.video.context', () => { - const bid = deepClone(baseValidBid); - - delete bid.mediaTypes[VIDEO].context; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail if mediaTypes.video.context is not "outstream"', () => { - const bid = deepClone(baseValidBid); - - bid.mediaTypes[VIDEO].context = 'instream'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail if video is specified but is not an object', () => { - const bid = deepClone(baseValidBid); - - bid.params.video = null; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.video = 'string'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.video = 123; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.video = false; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.video = void (0); - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail if rendererSettings is specified but is not an object', () => { - const bid = deepClone(baseValidBid); - - bid.params.rendererSettings = null; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererSettings = 'string'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererSettings = 123; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererSettings = false; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - bid.params.rendererSettings = void (0); - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - }); - - describe('buildRequests', () => { - const publicationName = 'bbprebid.dev'; - const rendererCode = 'glorious_renderer'; - - const baseValidBid = { - bidder: BB_CONSTANTS.BIDDER_CODE, - params: { - accountId: 123, - publicationName: publicationName, - rendererCode: rendererCode, - connections: [ BB_CONSTANTS.BIDDER_CODE ], - bluebillywig: {} - }, - mediaTypes: { - video: { - context: 'outstream' - } - } - }; - - const baseValidBidRequests = [baseValidBid]; - - const validBidderRequest = { - ortb2: { - source: { - tid: '12abc345-67d8-9012-e345-6f78901a2b34', - } - }, - auctionStart: 1585918458868, - bidderCode: BB_CONSTANTS.BIDDER_CODE, - bidderRequestId: '1a2345b67c8d9e0', - bids: [{ - adUnitCode: 'ad-unit-test', - auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', - bidId: '1234ab567c89de0', - bidRequestsCount: 1, - bidder: BB_CONSTANTS.BIDDER_CODE, - bidderRequestId: '1a2345b67c8d9e0', - params: baseValidBid.params, - sizes: [[768, 432], [640, 480], [630, 360]], - transactionId: '2b34c5de-f67a-8901-bcd2-34567efabc89' - }], - start: 11585918458869, - timeout: 3000 - }; - - it('sends bid request to AUCTION_URL via POST', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - expect(request.url).to.equal(`https://pbs.bluebillywig.com/openrtb2/auction?pub=${publicationName}`); - expect(request.method).to.equal('POST'); - }); - - it('sends data as a string', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - expect(request.data).to.be.a('string'); - }); - - it('sends all bid parameters', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); - }); - - it('builds the base request properly', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload.id).to.exist; - expect(payload.source).to.be.an('object'); - expect(payload.source.tid).to.equal(validBidderRequest.ortb2.source.tid); - expect(payload.tmax).to.equal(BB_CONSTANTS.DEFAULT_TIMEOUT); - expect(payload.imp).to.be.an('array'); - expect(payload.test).to.be.a('number'); - expect(payload).to.have.nested.property('ext.prebid.targeting'); - expect(payload.ext.prebid.targeting).to.be.an('object'); - expect(payload.ext.prebid.targeting.includewinners).to.equal(true); - expect(payload.ext.prebid.targeting.includebidderkeys).to.equal(false); - }); - - it('adds an impression to the payload', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload.imp.length).to.equal(1); - }); - - it('adds connections to ext', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload.imp[0].ext).to.have.all.keys(['bluebillywig']); - }); - - it('adds gdpr when present', () => { - const newValidBidderRequest = deepClone(validBidderRequest); - newValidBidderRequest.gdprConsent = { - consentString: 'BOh7mtYOh7mtYAcABBENCU-AAAAncgPIXJiiAoao0PxBFkgCAC8ACIAAQAQQAAIAAAIAAAhBGAAAQAQAEQgAAAAAAABAAAAAAAAAAAAAAACAAAAAAAACgAAAAABAAAAQAAAAAAA', - gdprApplies: true - }; - - const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('regs.ext.gdpr'); - expect(payload.regs.ext.gdpr).to.be.a('number'); - expect(payload.regs.ext.gdpr).to.equal(1); - expect(payload).to.have.nested.property('user.ext.consent'); - expect(payload.user.ext.consent).to.equal(newValidBidderRequest.gdprConsent.consentString); - }); - - it('sets gdpr to 0 when explicitly gdprApplies: false', () => { - const newValidBidderRequest = deepClone(validBidderRequest); - newValidBidderRequest.gdprConsent = { - gdprApplies: false - }; - - const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('regs.ext.gdpr'); - expect(payload.regs.ext.gdpr).to.be.a('number'); - expect(payload.regs.ext.gdpr).to.equal(0); - }); - - it('adds usp_consent when present', () => { - const newValidBidderRequest = deepClone(validBidderRequest); - newValidBidderRequest.uspConsent = '1YYY'; - - const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('regs.ext.us_privacy'); - expect(payload.regs.ext.us_privacy).to.equal(newValidBidderRequest.uspConsent); - }); - - it('sets coppa to 1 when specified in config', () => { - config.setConfig({'coppa': true}); - - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('regs.coppa'); - expect(payload.regs.coppa).to.equal(1); - - config.resetConfig(); - }); - - it('does not set coppa when disabled in the config', () => { - config.setConfig({'coppa': false}); - - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(deepAccess(payload, 'regs.coppa')).to.be.undefined; - - config.resetConfig(); - }); - - it('does not set coppa when not specified in config', () => { - config.resetConfig(); - - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(deepAccess(payload, 'regs.coppa')).to.be.undefined; - }); - - it('should add window size to request by default', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('device.w'); - expect(payload).to.have.nested.property('device.h'); - expect(payload.device.w).to.be.a('number'); - expect(payload.device.h).to.be.a('number'); - }); - - it('should add site when specified in config', () => { - config.setConfig({ site: { name: 'Blue Billywig', domain: 'bluebillywig.com', page: 'https://bluebillywig.com/', publisher: { id: 'abc', name: 'Blue Billywig', domain: 'bluebillywig.com' } } }); - - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.property('site'); - expect(payload).to.have.nested.property('site.name'); - expect(payload).to.have.nested.property('site.domain'); - expect(payload).to.have.nested.property('site.page'); - expect(payload).to.have.nested.property('site.publisher'); - expect(payload).to.have.nested.property('site.publisher.id'); - expect(payload).to.have.nested.property('site.publisher.name'); - expect(payload).to.have.nested.property('site.publisher.domain'); - - config.resetConfig(); - }); - - it('should add app when specified in config', () => { - config.setConfig({ app: { bundle: 'org.prebid.mobile.demoapp', domain: 'prebid.org' } }); - - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.property('app'); - expect(payload).to.have.nested.property('app.bundle'); - expect(payload).to.have.nested.property('app.domain'); - expect(payload.app.bundle).to.equal('org.prebid.mobile.demoapp'); - expect(payload.app.domain).to.equal('prebid.org'); - - config.resetConfig(); - }); - - it('should add referrerInfo as site when no app is set', () => { - const newValidBidderRequest = deepClone(validBidderRequest); - - newValidBidderRequest.refererInfo = { page: 'https://www.bluebillywig.com' }; - - const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('site.page'); - expect(payload.site.page).to.equal('https://www.bluebillywig.com'); - }); - - it('should not add referrerInfo as site when app is set', () => { - config.setConfig({ app: { bundle: 'org.prebid.mobile.demoapp', domain: 'prebid.org' } }); - - const newValidBidderRequest = deepClone(validBidderRequest); - newValidBidderRequest.refererInfo = { referer: 'https://www.bluebillywig.com' }; - - const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload.site).to.be.undefined; - config.resetConfig(); - }); - - it('should add device size to request when specified in config', () => { - config.setConfig({ device: { w: 1, h: 1 } }); - - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('device.w'); - expect(payload).to.have.nested.property('device.h'); - expect(payload.device.w).to.be.a('number'); - expect(payload.device.h).to.be.a('number'); - expect(payload.device.w).to.equal(1); - expect(payload.device.h).to.equal(1); - - config.resetConfig(); - }); - - it('should set schain on the request when set on config', () => { - const schain = { - validation: 'lax', - config: { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'indirectseller.com', - sid: '00001', - hp: 1 - } - ] - } - }; - - const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].schain = schain; - - const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('source.ext.schain'); - expect(payload.source.ext.schain).to.deep.equal(schain); - }); - - it('should add currency when specified on the config', () => { - config.setConfig({ currency: { adServerCurrency: 'USD' } }); - - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.property('cur'); - expect(payload.cur).to.eql(['USD']); // NB not equal, eql to check for same array because [1] === [1] fails normally - - config.resetConfig(); - }); - - it('should also take in array for currency on the config', () => { - config.setConfig({ currency: { adServerCurrency: ['USD', 'PHP'] } }); - - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.property('cur'); - expect(payload.cur).to.eql(['USD']); // NB not equal, eql to check for same array because [1] === [1] fails normally - - config.resetConfig(); - }); - - it('should not set cur when currency is not specified on the config', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload.cur).to.be.undefined; - }); - - it('should set user ids when present', () => { - const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].userIdAsEids = [ {} ]; - - const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(payload).to.have.nested.property('user.ext.eids'); - expect(payload.user.ext.eids).to.be.an('array'); - expect(payload.user.ext.eids.length).to.equal(1); - }); - - it('should not set user ids when none present', () => { - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(deepAccess(payload, 'user.ext.eids')).to.be.undefined; - }); - - it('should set imp.0.video.[w|h|placement] by default', () => { - const newBaseValidBidRequests = deepClone(baseValidBidRequests); - - const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(deepAccess(payload, 'imp.0.video.w')).to.equal(768); - expect(deepAccess(payload, 'imp.0.video.h')).to.equal(432); - expect(deepAccess(payload, 'imp.0.video.placement')).to.equal(3); - }); - - it('should update imp0.video.[w|h] when present in config', () => { - const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].mediaTypes.video.playerSize = [1, 1]; - - const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(deepAccess(payload, 'imp.0.video.w')).to.equal(1); - expect(deepAccess(payload, 'imp.0.video.h')).to.equal(1); - }); - - it('should allow overriding any imp0.video key through params.video', () => { - const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].params.video = { - w: 2, - h: 2, - placement: 1, - minduration: 15, - maxduration: 30 - }; - - const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(deepAccess(payload, 'imp.0.video.w')).to.equal(2); - expect(deepAccess(payload, 'imp.0.video.h')).to.equal(2); - expect(deepAccess(payload, 'imp.0.video.placement')).to.equal(1); - expect(deepAccess(payload, 'imp.0.video.minduration')).to.equal(15); - expect(deepAccess(payload, 'imp.0.video.maxduration')).to.equal(30); - }); - - it('should not allow placing any non-OpenRTB 2.5 keys on imp.0.video through params.video', () => { - const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].params.video = { - 'true': true, - 'testing': 'some', - 123: {}, - '': 'values' - }; - - const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - - expect(deepAccess(request, 'imp.0.video.true')).to.be.undefined; - expect(deepAccess(payload, 'imp.0.video.testing')).to.be.undefined; - expect(deepAccess(payload, 'imp.0.video.123')).to.be.undefined; - expect(deepAccess(payload, 'imp.0.video.')).to.be.undefined; - }); - }); - describe('interpretResponse', () => { - const publicationName = 'bbprebid.dev'; - const rendererCode = 'glorious_renderer'; - - const baseValidBid = { - bidder: BB_CONSTANTS.BIDDER_CODE, - params: { - accountId: 123, - publicationName: publicationName, - rendererCode: rendererCode, - connections: [ BB_CONSTANTS.BIDDER_CODE ], - bluebillywig: {} - }, - mediaTypes: { - video: { - context: 'outstream' - } - } - }; - - const baseValidBidRequests = [baseValidBid]; - - const validBidderRequest = { - auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', - auctionStart: 1585918458868, - bidderCode: BB_CONSTANTS.BIDDER_CODE, - bidderRequestId: '1a2345b67c8d9e0', - bids: [{ - adUnitCode: 'ad-unit-test', - auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', - bidId: '1234ab567c89de0', - bidRequestsCount: 1, - bidder: BB_CONSTANTS.BIDDER_CODE, - bidderRequestId: '1a2345b67c8d9e0', - params: baseValidBid.params, - sizes: [[640, 480], [630, 360]], - transactionId: '2b34c5de-f67a-8901-bcd2-34567efabc89' - }], - start: 11585918458869, - timeout: 3000 - }; - - const validResponse = { - id: 'a12abc345-67d8-9012-e345-6f78901a2b34', - seatbid: [ - { - bid: [ - { - id: '1', - impid: '1234ab567c89de0', - price: 1, - adm: '\r\nBB Adserver00:00:51', - adid: '67069817', - adomain: [ - 'bluebillywig.com' - ], - cid: '3535', - crid: '67069817', - w: 1, - h: 1, - publicationName: 'bbprebid', - accountId: 123, - ext: { - prebid: { - targeting: { - hb_bidder: 'bluebillywig', - hb_pb: '1.00', - hb_size: '1x1' - }, - type: 'video' - }, - bidder: { - prebid: { - targeting: { - hb_bidder: 'bluebillywig', - hb_pb: '10.00', - hb_size: '1x1' - }, - type: 'video', - video: { - duration: 51, - primary_category: '' - } - }, - bidder: { - bluebillywig: { - brand_id: 1, - auction_id: 1, - bid_ad_type: 1, - creative_info: { - video: { - duration: 51, - mimes: [ - 'video/x-flv', - 'video/mp4', - 'video/webm' - ] - } - } - } - } - } - } - } - ], - seat: 'bluebillywig' - } - ], - cur: 'USD', - ext: { - responsetimemillis: { - bluebillywig: 0 - }, - tmaxrequest: 5000 - } - }; - - const serverResponse = { body: validResponse }; - - it('should build bid array', () => { - const response = deepClone(serverResponse); - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(result.length).to.equal(1); - }); - - it('should have all relevant fields', () => { - const response = deepClone(serverResponse); - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - const bid = result[0]; - - // BB_HELPERS.transformRTBToPrebidProps - expect(bid.cpm).to.equal(serverResponse.body.seatbid[0].bid[0].price); - expect(bid.bidId).to.equal(serverResponse.body.seatbid[0].bid[0].impid); - expect(bid.requestId).to.equal(serverResponse.body.seatbid[0].bid[0].impid); - expect(bid.width).to.equal(serverResponse.body.seatbid[0].bid[0].w || BB_CONSTANTS.DEFAULT_WIDTH); - expect(bid.height).to.equal(serverResponse.body.seatbid[0].bid[0].h || BB_CONSTANTS.DEFAULT_HEIGHT); - expect(bid.ad).to.equal(serverResponse.body.seatbid[0].bid[0].adm); - expect(bid.netRevenue).to.equal(BB_CONSTANTS.DEFAULT_NET_REVENUE); - expect(bid.creativeId).to.equal(serverResponse.body.seatbid[0].bid[0].crid); - expect(bid.currency).to.equal(serverResponse.body.cur); - expect(bid.ttl).to.equal(BB_CONSTANTS.DEFAULT_TTL); - - expect(bid).to.have.property('meta'); - expect(bid.meta).to.have.property('advertiserDomains'); - expect(bid.meta.advertiserDomains[0]).to.equal('bluebillywig.com'); - - expect(bid.publicationName).to.equal(validBidderRequest.bids[0].params.publicationName); - expect(bid.rendererCode).to.equal(validBidderRequest.bids[0].params.rendererCode); - expect(bid.accountId).to.equal(validBidderRequest.bids[0].params.accountId); - }); - - it('should not give anything when seatbid is an empty array', () => { - const seatbidEmptyArray = deepClone(serverResponse); - seatbidEmptyArray.body.seatbid = []; - - const response = seatbidEmptyArray; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(result.length).to.equal(0); - }); - - it('should not give anything when seatbid is missing', () => { - const seatbidMissing = deepClone(serverResponse); - delete seatbidMissing.body.seatbid; - - const response = seatbidMissing; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(result.length).to.equal(0); - }); - - const seatbidNotArrayResponse = deepClone(serverResponse); - it('should not give anything when seatbid is not an array', () => { - const invalidValues = [ false, null, {}, void (0), 123, 'string' ]; - - for (const invalidValue of invalidValues) { - seatbidNotArrayResponse.body.seatbid = invalidValue - const response = deepClone(seatbidNotArrayResponse); // interpretResponse is destructive - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(result.length).to.equal(0); - } - }); - - it('should not give anything when seatbid.bid is an empty array', () => { - const seatbidBidEmpty = deepClone(serverResponse); - seatbidBidEmpty.body.seatbid[0].bid = []; - - const response = seatbidBidEmpty; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(result.length).to.equal(0); - }); - - it('should not give anything when seatbid.bid is missing', () => { - const seatbidBidMissing = deepClone(serverResponse); - delete seatbidBidMissing.body.seatbid[0].bid; - - const response = seatbidBidMissing; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(result.length).to.equal(0); - }); - - it('should not give anything when seatbid.bid is not an array', () => { - const seatbidBidNotArray = deepClone(serverResponse); - - const invalidValues = [ false, null, {}, void (0), 123, 'string' ]; - - for (const invalidValue of invalidValues) { - seatbidBidNotArray.body.seatbid[0].bid = invalidValue; - - const response = deepClone(seatbidBidNotArray); // interpretResponse is destructive - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(result.length).to.equal(0); - } - }); - - it('should take default width and height when w/h not present', () => { - const bidSizesMissing = deepClone(serverResponse); - - delete bidSizesMissing.body.seatbid[0].bid[0].w; - delete bidSizesMissing.body.seatbid[0].bid[0].h; - - const response = bidSizesMissing; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(deepAccess(result, '0.width')).to.equal(768); - expect(deepAccess(result, '0.height')).to.equal(432); - }); - - it('should take nurl value when adm not present', () => { - const bidAdmMissing = deepClone(serverResponse); - - delete bidAdmMissing.body.seatbid[0].bid[0].adm; - bidAdmMissing.body.seatbid[0].bid[0].nurl = 'https://bluebillywig.com'; - - const response = bidAdmMissing; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(deepAccess(result, '0.vastXml')).to.be.undefined; - expect(deepAccess(result, '0.vastUrl')).to.equal('https://bluebillywig.com'); - }); - - it('should not take nurl value when adm present', () => { - const bidAdmNurlPresent = deepClone(serverResponse); - - bidAdmNurlPresent.body.seatbid[0].bid[0].nurl = 'https://bluebillywig.com'; - - const response = bidAdmNurlPresent; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(deepAccess(result, '0.vastXml')).to.equal(bidAdmNurlPresent.body.seatbid[0].bid[0].adm); - expect(deepAccess(result, '0.vastUrl')).to.be.undefined; - }); - - it('should take ext.prebid.cache data when present, ignore ext.prebid.targeting and nurl', () => { - const bidExtPrebidCache = deepClone(serverResponse); - - delete bidExtPrebidCache.body.seatbid[0].bid[0].adm; - bidExtPrebidCache.body.seatbid[0].bid[0].nurl = 'https://notnurl.com'; - - bidExtPrebidCache.body.seatbid[0].bid[0].ext = { - prebid: { - cache: { - vastXml: { - url: 'https://bluebillywig.com', - cacheId: '12345' - } - }, - targeting: { - hb_uuid: '23456', - hb_cache_host: 'bluebillywig.com', - hb_cache_path: '/cache' - } - } - }; - - const response = bidExtPrebidCache; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(deepAccess(result, '0.vastUrl')).to.equal('https://bluebillywig.com'); - expect(deepAccess(result, '0.videoCacheKey')).to.equal('12345'); - }); - - it('should take ext.prebid.targeting data when ext.prebid.cache not present, and ignore nurl', () => { - const bidExtPrebidTargeting = deepClone(serverResponse); - - delete bidExtPrebidTargeting.body.seatbid[0].bid[0].adm; - bidExtPrebidTargeting.body.seatbid[0].bid[0].nurl = 'https://notnurl.com'; - - bidExtPrebidTargeting.body.seatbid[0].bid[0].ext = { - prebid: { - targeting: { - hb_uuid: '34567', - hb_cache_host: 'bluebillywig.com', - hb_cache_path: '/cache' - } - } - }; - - const response = bidExtPrebidTargeting; - const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); - const result = spec.interpretResponse(response, request); - - expect(deepAccess(result, '0.vastUrl')).to.equal('https://bluebillywig.com/cache?uuid=34567'); - expect(deepAccess(result, '0.videoCacheKey')).to.equal('34567'); - }); - }); - describe('getUserSyncs', () => { - const publicationName = 'bbprebid.dev'; - const rendererCode = 'glorious_renderer'; - - const baseValidBid = { - bidder: BB_CONSTANTS.BIDDER_CODE, - params: { - accountId: 123, - publicationName: publicationName, - rendererCode: rendererCode, - connections: [ BB_CONSTANTS.BIDDER_CODE ], - bluebillywig: {} - }, - mediaTypes: { - video: { - context: 'outstream' - } - } - }; - - const validBidRequests = [baseValidBid]; - - const validBidderRequest = { - auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', - auctionStart: 1585918458868, - bidderCode: BB_CONSTANTS.BIDDER_CODE, - bidderRequestId: '1a2345b67c8d9e0', - bids: [{ - adUnitCode: 'ad-unit-test', - auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', - bidId: '1234ab567c89de0', - bidRequestsCount: 1, - bidder: BB_CONSTANTS.BIDDER_CODE, - bidderRequestId: '1a2345b67c8d9e0', - params: baseValidBid.params, - sizes: [[768, 432], [640, 480], [630, 360]], - transactionId: '2b34c5de-f67a-8901-bcd2-34567efabc89' - }], - start: 11585918458869, - timeout: 3000 - }; - const validResponse = { - id: 'a12abc345-67d8-9012-e345-6f78901a2b34', - seatbid: [ - { - bid: [ - { - id: '1', - impid: '1234ab567c89de0', - price: 1, - adm: '\r\nBB Adserver00:00:51', - adid: '67069817', - adomain: [ - 'bluebillywig.com' - ], - cid: '3535', - crid: '67069817', - w: 1, - h: 1, - publicationName: 'bbprebid', - accountId: 123, - ext: { - prebid: { - targeting: { - hb_bidder: 'bluebillywig', - hb_pb: '1.00', - hb_size: '1x1' - }, - type: 'video' - }, - bidder: { - prebid: { - targeting: { - hb_bidder: 'bluebillywig', - hb_pb: '10.00', - hb_size: '1x1' - }, - type: 'video', - video: { - duration: 51, - primary_category: '' - } - }, - bidder: { - bluebillywig: { - brand_id: 1, - auction_id: 1, - bid_ad_type: 1, - creative_info: { - video: { - duration: 51, - mimes: [ - 'video/x-flv', - 'video/mp4', - 'video/webm' - ] - } - } - } - } - } - } - } - ], - seat: 'bluebillywig' - } - ], - cur: 'USD', - ext: { - responsetimemillis: { - bluebillywig: 0 - }, - tmaxrequest: 5000 - } - }; - - const serverResponse = { body: validResponse }; - - const gdpr = { - consentString: 'BOh7mtYOh7mtYAcABBENCU-AAAAncgPIXJiiAoao0PxBFkgCAC8ACIAAQAQQAAIAAAIAAAhBGAAAQAQAEQgAAAAAAABAAAAAAAAA AAAAAACAAAAAAAACgAAAAABAAAAQAAAAAAA', - gdprApplies: true - }; - - it('should return empty if no server response', function () { - const result = spec.getUserSyncs({}, false, gdpr); - expect(result).to.be.empty; - }); - - it('should return empty if server response is empty', function () { - const result = spec.getUserSyncs({}, [], gdpr); - expect(result).to.be.empty; - }); - - it('should return empty if iframeEnabled is not true', () => { - const result = spec.getUserSyncs({iframeEnabled: false}, [serverResponse], gdpr); - expect(result).to.be.empty; - }); - - it('should append the various values if they exist', function() { - // push data to syncStore - spec.buildRequests(validBidRequests, validBidderRequest); - - const result = spec.getUserSyncs({iframeEnabled: true}, [serverResponse], gdpr); - - expect(result).to.not.be.empty; - - expect(result[0].url).to.include('gdpr=1'); - expect(result[0].url).to.include(gdpr.consentString); - expect(result[0].url).to.include('accountId=123'); - expect(result[0].url).to.include(`bidders=${btoa(JSON.stringify(validBidRequests[0].params.connections))}`); - expect(result[0].url).to.include('cb='); - }); - }); -}); diff --git a/test/spec/modules/bmsBidAdapter_spec.js b/test/spec/modules/bmsBidAdapter_spec.js new file mode 100755 index 00000000000..44112032def --- /dev/null +++ b/test/spec/modules/bmsBidAdapter_spec.js @@ -0,0 +1,165 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { spec, storage } from 'modules/bmsBidAdapter.js'; + +const BIDDER_CODE = 'bms'; +const ENDPOINT_URL = + 'https://api.prebid.int.us-east-1.bluems.com/v1/bid?exchangeId=prebid'; +const GVLID = 1105; +const CURRENCY = 'USD'; + +describe('bmsBidAdapter:', function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('isBidRequestValid:', function () { + it('should return true for valid bid requests', function () { + const validBid = { + params: { + placementId: '12345', + publisherId: '67890', + }, + }; + expect(spec.isBidRequestValid(validBid)).to.be.true; + }); + + it('should return false for invalid bid requests', function () { + const invalidBid = { + params: { + placementId: '12345', + }, + }; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests:', function () { + let validBidRequests; + let bidderRequest; + + beforeEach(function () { + validBidRequests = [ + { + bidId: 'bid1', + params: { + placementId: '12345', + publisherId: '67890', + }, + getFloor: () => ({ currency: CURRENCY, floor: 1.5 }), + }, + ]; + + bidderRequest = { + refererInfo: { + page: 'https://example.com', + }, + }; + + sandbox.stub(storage, 'getDataFromLocalStorage').returns('testBuyerId'); + }); + + it('should build a valid OpenRTB request', function () { + const [request] = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT_URL); + expect(request.options.contentType).to.equal('text/plain'); + const ortbRequest = JSON.parse(request.data); + + expect(ortbRequest.ext.gvlid).to.equal(GVLID); + expect(ortbRequest.imp[0].bidfloor).to.equal(1.5); + expect(ortbRequest.imp[0].bidfloorcur).to.equal(CURRENCY); + }); + + it('should omit bidfloor if getFloor is not implemented', function () { + validBidRequests[0].getFloor = undefined; + + const [request] = spec.buildRequests(validBidRequests, bidderRequest); + const ortbRequest = JSON.parse(request.data); + + expect(ortbRequest.imp[0].bidfloor).to.be.undefined; + }); + + it('should convert from fromORTB', function () { + const response = { + id: 'response-id-123456', + cur: 'USD', + bidid: '2rgRKcbHfDyX6ZU4zuPuf38h000', + seatbid: [ + { + bid: [ + { + id: '2rgRKcbHfDyX6ZU4zuPuf521444:0', + impid: '3b948a96652621', + price: 2, + adomain: ['example.com'], + adid: '0', + adm: '', + iurl: 'https://ads.bluemsusercontent.com/v1/ad-container?acc=306850905425&ad=2pMGbaJioMDwMIESvwlCUekrdNA', + h: 600, + w: 300, + nurl: 'https://bid-notice.rtb.bluems.com/v1/bid:won?winPrice=${AUCTION_PRICE}&accountId=306850905425&adId=2pMGbaJioMDwMIESvwlCUekrdNA&campaignId=2Xzb0pyfcOibtp9A5XsUSkzj2IT&exchangeId=prebid&tagId=13144370&impressionId=3b948a96652621&bidId=2rgRKcbHfDyX6ZU4zuPuf38hDB8%3A0&bidPrice=2&bidFloor=2&height=600&width=300®ion=us-east-1&targetId=2rgH24MVckzSou4IpTyUa5l3Ija¤cy=USD&campaignCurrency=USD&campaignCurrencyConversionFactor=1&publisherId=13144370&domain=d3fef36u6k6muh.cloudfront.net', + lurl: 'https://bid-notice.rtb.bluems.com/v1/bid:lost?winPrice=${AUCTION_PRICE}&marketBidRatio=${AUCTION_MBR}&lossReasonCode=${AUCTION_LOSS}&accountId=306850905425&adId=2pMGbaJioMDwMIESvwlCUekrdNA&campaignId=2Xzb0pyfcOibtp9A5XsUSkzj2IT&exchangeId=prebid&tagId=13144370&impressionId=3b948a96652621&bidId=2rgRKcbHfDyX6ZU4zuPuf38hDB8%3A0&bidPrice=2&bidFloor=2&height=600&width=300®ion=us-east-1&targetId=2rgH24MVckzSou4IpTyUa5l3Ija¤cy=USD&campaignCurrency=USD&campaignCurrencyConversionFactor=1&publisherId=13144370&domain=d3fef36u6k6muh.cloudfront.net', + burl: 'https://bid-notice.rtb.bluems.com/v1/bid:charged?winPrice=${AUCTION_PRICE}&accountId=306850905425&adId=2pMGbaJioMDwMIESvwlCUekrdNA&campaignId=2Xzb0pyfcOibtp9A5XsUSkzj2IT&exchangeId=prebid&tagId=13144370&impressionId=3b948a96652621&bidId=2rgRKcbHfDyX6ZU4zuPuf38hDB8%3A0&bidPrice=2&bidFloor=2&height=600&width=300®ion=us-east-1&targetId=2rgH24MVckzSou4IpTyUa5l3Ija¤cy=USD&campaignCurrency=USD&campaignCurrencyConversionFactor=1&publisherId=13144370&domain=d3fef36u6k6muh.cloudfront.net', + exp: 60, + ext: { + bms: { + accountId: '306850900000', + campaignId: '2Xzb0pyfcOibtp9A5X8546254528', + adId: '2pMGbaJioMDwMIESvwlCUlkojug', + region: 'us-east-1', + targetId: '2rgH24MVckzSou4IpTyUalakush', + }, + }, + }, + ], + seat: '1', + }, + ], + }; + const request = { + id: '10bb57ee-712f-43e9-9769-b26d03lklkih', + bidder: BIDDER_CODE, + params: { + source: 886409, + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1460505iosakju-0', + transactionId: '7d79850b-70aa-4c0f-af95-c1d524sskjkjh', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '2eb89f0f062afe', + bidderRequestId: '1ae6c8e18f8462', + auctionId: '1286637c-51bc-4fdd-8e35-2435elklklka', + ortb2: {}, + }; + + const [ortbReq] = spec.buildRequests([request], { + bids: [request], + }); + + const ortbResponse = spec.interpretResponse( + { body: response }, + { data: ortbReq.data } + ); + + expect(ortbResponse.length).to.eq(1); + expect(ortbResponse[0].mediaType).to.eq('banner'); + }); + }); +}); diff --git a/test/spec/modules/boldwinBidAdapter_spec.js b/test/spec/modules/boldwinBidAdapter_spec.js index 9a7b16c0914..1435ba2c28b 100644 --- a/test/spec/modules/boldwinBidAdapter_spec.js +++ b/test/spec/modules/boldwinBidAdapter_spec.js @@ -1,115 +1,225 @@ -import {expect} from 'chai'; -import {spec} from '../../../modules/boldwinBidAdapter.js'; -import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import { expect } from 'chai'; +import { spec } from '../../../modules/boldwinBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'boldwin'; describe('BoldwinBidAdapter', function () { - const bid = { - bidId: '23fhj33i987f', - bidder: 'boldwin', + 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], [320, 50] ], + [BANNER]: { + sizes: [[300, 250]] } }, params: { - placementId: 'testBanner', + } - }; + } const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'test.com' + referer: 'https://test.com', + page: 'https://test.com' }, - ortb2: {} + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } + }, + timeout: 500 }; describe('isBidRequestValid', function () { - it('Should return true if there are bidId, params and placementId parameters present', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + 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 () { - delete bid.params.placementId; - expect(spec.isBidRequestValid(bid)).to.be.false; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; }); }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid], bidderRequest); + 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 valid URL', function () { expect(serverRequest.url).to.equal('https://ssp.videowalldirect.com/pbjs'); }); - it('Returns valid data if array of bids is valid', function () { + + it('Returns general data valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + 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.gdpr).to.not.exist; - expect(data.ccpa).to.not.exist; - let placement = data['placements'][0]; - expect(placement).to.have.keys('placementId', 'bidId', 'adFormat', 'sizes', 'hPlayer', 'wPlayer', 'schain', 'bidFloor', 'type'); - expect(placement.placementId).to.equal('testBanner'); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.adFormat).to.equal(BANNER); - expect(placement.schain).to.be.an('object'); - expect(placement.type).to.exist.and.to.equal('publisher'); + 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 data for mediatype video', function () { - const playerSize = [300, 300]; - bid.mediaTypes = {}; - bid.mediaTypes[VIDEO] = { - playerSize - }; - serverRequest = spec.buildRequests([bid], bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - let placement = data['placements'][0]; - expect(placement).to.be.an('object'); - expect(placement.adFormat).to.equal(VIDEO); - expect(placement.wPlayer).to.equal(playerSize[0]); - expect(placement.hPlayer).to.equal(playerSize[1]); + 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.wPlayer).to.be.an('number'); + expect(placement.hPlayer).to.be.an('number'); + 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 () { - bidderRequest.gdprConsent = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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 = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let 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; }); - - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); - let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); }); describe('gpp consent', function () { @@ -119,7 +229,7 @@ describe('BoldwinBidAdapter', function () { applicableSections: [8] }; - let serverRequest = spec.buildRequests([bid], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data).to.be.an('object'); expect(data).to.have.property('gpp'); @@ -129,15 +239,18 @@ describe('BoldwinBidAdapter', function () { }) 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]; - let serverRequest = spec.buildRequests([bid], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data).to.be.an('object'); expect(data).to.have.property('gpp'); expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; }) }); @@ -155,7 +268,11 @@ describe('BoldwinBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - dealId: '1' + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let bannerResponses = spec.interpretResponse(banner); @@ -163,18 +280,17 @@ describe('BoldwinBidAdapter', function () { let 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('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.width).to.equal(300); - expect(dataItem.height).to.equal(250); - expect(dataItem.ad).to.equal('Test'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); + 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('USD'); + 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: [{ @@ -186,7 +302,11 @@ describe('BoldwinBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - dealId: '1' + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let videoResponses = spec.interpretResponse(video); @@ -220,6 +340,10 @@ describe('BoldwinBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let nativeResponses = spec.interpretResponse(native); @@ -307,14 +431,41 @@ describe('BoldwinBidAdapter', function () { }); }); - describe('getUserSyncs', function () { - let userSync = spec.getUserSyncs(); - it('Returns valid URL and type', function () { - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.exist; - expect(userSync[0].url).to.exist; - expect(userSync[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://sync.videowalldirect.com'); + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.videowalldirect.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.videowalldirect.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.videowalldirect.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') }); }); }); diff --git a/test/spec/modules/brainxBidAdapter_spec.js b/test/spec/modules/brainxBidAdapter_spec.js new file mode 100644 index 00000000000..1d01e2cc642 --- /dev/null +++ b/test/spec/modules/brainxBidAdapter_spec.js @@ -0,0 +1,132 @@ +// import or require modules necessary for the test, e.g.: +import { expect } from 'chai'; // may prefer 'assert' in place of 'expect' +import { spec } from 'modules/brainxBidAdapter.js'; +import utils, { deepClone } from '../../../src/utils'; +// import adapter from 'src/adapters/'; +import { BANNER, NATIVE, VIDEO } from 'src/mediaTypes.js'; + +describe('Brain-X Aapater', function () { + describe('isBidRequestValid', function () { + it('undefined bid should return false', function () { + expect(spec.isBidRequestValid()).to.be.false; + }); + + it('null bid should return false', function () { + expect(spec.isBidRequestValid(null)).to.be.false; + }); + + it('bid.params should be set', function () { + expect(spec.isBidRequestValid({})).to.be.false; + }); + + it('bid.params.pubId should be set', function () { + expect(spec.isBidRequestValid({ + params: { pubId: 'F7B53DBC-85C1-4685-9A06-9CF4B6261FA3', endpoint: 'http://adx-engine-gray.tec-do.cn/bid' } + })).to.be.false; + }); + }) + + // describe('isBidRequestValid', function () { + // it('Test the banner request processing function', function () { + // const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + // expect(request).to.not.be.empty; + // const payload = request.data; + // expect(payload).to.not.be.empty; + // }); + // it('Test the video request processing function', function () { + // const request = spec.buildRequests(videoRequest, videoRequest[0]); + // expect(request).to.not.be.empty; + // const payload = request.data; + // expect(payload).to.not.be.empty; + // }); + // it('Test the param', function () { + // const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + // const payload = JSON.parse(request.data); + // expect(payload.imp[0].tagid).to.eql(videoRequest[0].params.tagid); + // expect(payload.imp[0].bidfloor).to.eql(videoRequest[0].params.bidfloor); + // }); + // }) + + describe('interpretResponse', function () { + it('Test banner interpretResponse', function () { + const serverResponse = { + body: { + 'bidid': 'a82042c055b04e539ec6876112c10ced1729663902983', + 'cur': 'USD', + 'id': '28f8f1f525372a', + 'seatbid': [ + { + 'bid': [ + { + 'adid': '76797', + 'adm': "
\n \n
\n
\n \n \n
\n
\n\n", + 'adomain': [ + 'taobao.com' + ], + 'bundle': 'com.taobao', + 'burl': 'https://adx-event-server.bidtrail.top/billing?s=Eid0ZWMxNDk4NDQzODk3MjcwOTU1MzAwNzM3YjczMTQwMTA4ODk5ODQaDjI4ZjhmMWY1MjUzNzJhIKkCKgbmtYvor5U6DmxvY2FsaG9zdDo5OTk5Qgl1bmRlZmluZWRKA0hLR1ADYAFoAXADeAOAAQKKAQpjb20udGFvYmFvlQEX2U49nQEX2U49pQEX2U49ugEqCO4HEiUI7gcSCFRlY2RvRFNQGA01F9lOPTgBQBVKC1RlY2RvRFNQX1NHwAHuB8gBjcqCwKsy0AEC-gGbAmh0dHBzOi8vbm90aWNlLXNnLmJpZHRyYWlsLnRvcC93aW4_YmlkX2lkPWE4MjA0MmMwNTViMDRlNTM5ZWM2ODc2MTEyYzEwY2VkMTcyOTY2MzkwMjk4MyZjYW1wYWlnbl9pZD00MjgmYWRfZ3JvdXBfaWQ9OTc2MSZhZF9pZD03Njc5NyZjcmVhdGl2ZV9pZD02NTI2JmFmZmlsaWF0ZV9pZD0yOTcmYnVuZGxlPSZmaXJzdF9zc3A9YnJhaW54LnRlY2gmcHVibGlzaF9pZD0mb3M9QW5kcm9pZCZ0YWdfaWQ9dW5kZWZpbmVkJmJpZF9zdWNjZXNzX3ByaWNlPTAuMDUwNSZzaWduPWUzODQyNjhiYzAzYzhmYmOSAgp0YW9iYW8uY29togILYnJhaW54LnRlY2ioAgGyAgdhbmRyb2lkwAICygICc2fQAgHYAo3hlcWrMuACjeGVxasy&v=P-1b7JJWs-uUQ68A37V4xDLplU0&auction_price=${AUCTION_PRICE}', + 'cat': [ + 'IAB18-5' + ], + 'cid': '428', + 'crid': 'D06g1RVGMEnC+9Le4SZMJw==', + 'h': 480, + 'id': 'a82042c055b04e539ec6876112c10ced1729663902983', + 'impid': '3c1dd9e1700358', + 'iurl': 'https://creative.bidtrail.top/png/2/20f24c10f21e/091b422e3014033e57acffcf2a5c71dbb17383ec15ac9421', + 'lurl': 'https://notice-sg.bidtrail.top/loss?bid_id=a82042c055b04e539ec6876112c10ced1729663902983&sign=e384268bc03c8fbc&campaign_id=428&ad_group_id=9761&ad_id=76797&creative_id=6526&affiliate_id=297&loss_code=${AUCTION_LOSS}', + 'nurl': 'https://adx-event-server.bidtrail.top/winnotice?s=Eid0ZWMxNDk4NDQzODk3MjcwOTU1MzAwNzM3YjczMTQwMTA4ODk5ODQaDjI4ZjhmMWY1MjUzNzJhIKkCKgbmtYvor5U6DmxvY2FsaG9zdDo5OTk5Qgl1bmRlZmluZWRKA0hLR1ADYAFoAXADeAOAAQKKAQpjb20udGFvYmFvlQEX2U49nQEX2U49pQEX2U49ugEqCO4HEiUI7gcSCFRlY2RvRFNQGA01F9lOPTgBQBVKC1RlY2RvRFNQX1NHwAHuB8gBjcqCwKsy0AEB-gGbAmh0dHBzOi8vbm90aWNlLXNnLmJpZHRyYWlsLnRvcC93aW4_YmlkX2lkPWE4MjA0MmMwNTViMDRlNTM5ZWM2ODc2MTEyYzEwY2VkMTcyOTY2MzkwMjk4MyZjYW1wYWlnbl9pZD00MjgmYWRfZ3JvdXBfaWQ9OTc2MSZhZF9pZD03Njc5NyZjcmVhdGl2ZV9pZD02NTI2JmFmZmlsaWF0ZV9pZD0yOTcmYnVuZGxlPSZmaXJzdF9zc3A9YnJhaW54LnRlY2gmcHVibGlzaF9pZD0mb3M9QW5kcm9pZCZ0YWdfaWQ9dW5kZWZpbmVkJmJpZF9zdWNjZXNzX3ByaWNlPTAuMDUwNSZzaWduPWUzODQyNjhiYzAzYzhmYmOSAgp0YW9iYW8uY29togILYnJhaW54LnRlY2ioAgGyAgdhbmRyb2lkwAICygICc2fQAgHYAo3hlcWrMuACjeGVxasy&v=jFUg_b6F14d50JL-M6UE5Jc8VuA&auction_price=${AUCTION_PRICE}', + 'price': 0.0505, + 'w': 320 + } + ], + 'group': 0, + 'seat': 'agency' + } + ] + } + }; + + const bidResponses = spec.interpretResponse(serverResponse, { + originalBidRequest: { + auctionId: '3eedbf83-7d1d-423c-be27-39e4af687040', + auctionStart: 1729663900819, + adUnitCode: 'dev-1', + bidId: '28f8f1f525372a', + bidder: 'brainx', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + params: { + pubId: 'F7B53DBC-85C1-4685-9A06-9CF4B6261FA3', + endpoint: 'http://adx-engine-gray.tec-do.cn/bid' + }, + src: 'client' + } + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + + const bid = serverResponse.body.seatbid[0].bid[0]; + const bidResponse = bidResponses[0]; + + expect(bidResponse.mediaType).to.equal(BANNER); + expect(bidResponse.requestId).to.equal(bid.impid); + expect(bidResponse.cpm).to.equal(parseFloat(bid.price).toFixed(2)) + expect(bidResponse.currency).to.equal(serverResponse.body.cur); + expect(bidResponse.creativeId).to.equal(bid.crid || bid.id); + expect(bidResponse.netRevenue).to.be.true; + expect(bidResponse.nurl).to.equal(bid.nurl); + expect(bidResponse.lurl).to.equal(bid.lurl); + + expect(bidResponse.meta).to.be.an('object'); + expect(bidResponse.meta.mediaType).to.equal(BANNER); + expect(bidResponse.meta.primaryCatId).to.equal('IAB18-5'); + // expect(bidResponse.meta.secondaryCatIds).to.deep.equal(['IAB8']); + expect(bidResponse.meta.advertiserDomains).to.deep.equal(bid.adomain); + expect(bidResponse.meta.clickUrl).to.equal(bid.adomain[0]); + + expect(bidResponse.ad).to.equal(bid.adm); + expect(bidResponse.width).to.equal(bid.w); + expect(bidResponse.height).to.equal(bid.h); + }); + }); +}); diff --git a/test/spec/modules/braveBidAdapter_spec.js b/test/spec/modules/braveBidAdapter_spec.js index 392f3b9f263..29c15bc0c40 100644 --- a/test/spec/modules/braveBidAdapter_spec.js +++ b/test/spec/modules/braveBidAdapter_spec.js @@ -197,7 +197,7 @@ describe('BraveBidAdapter', function() { }); it('Returns valid URL', function () { - expect(request.url).to.equal('https://point.bravegroup.tv/?t=2&partner=to0QI2aPgkbBZq6vgf0oHitouZduz0qw'); + expect(request.url).to.equal('https://point.braveglobal.tv/?t=2&partner=to0QI2aPgkbBZq6vgf0oHitouZduz0qw'); }); it('Returns empty data if no valid requests are passed', function () { @@ -221,7 +221,7 @@ describe('BraveBidAdapter', function() { }); it('Returns valid URL', function () { - expect(request.url).to.equal('https://point.bravegroup.tv/?t=2&partner=to0QI2aPgkbBZq6vgf0oHitouZduz0qw'); + expect(request.url).to.equal('https://point.braveglobal.tv/?t=2&partner=to0QI2aPgkbBZq6vgf0oHitouZduz0qw'); }); }); @@ -240,7 +240,7 @@ describe('BraveBidAdapter', function() { }); it('Returns valid URL', function () { - expect(request.url).to.equal('https://point.bravegroup.tv/?t=2&partner=to0QI2aPgkbBZq6vgf0oHitouZduz0qw'); + expect(request.url).to.equal('https://point.braveglobal.tv/?t=2&partner=to0QI2aPgkbBZq6vgf0oHitouZduz0qw'); }); }); @@ -304,18 +304,18 @@ describe('BraveBidAdapter', function() { creativeId: response_video.seatbid[0].bid[0].crid, dealId: response_video.seatbid[0].bid[0].dealid, mediaType: 'video', - vastUrl: response_video.seatbid[0].bid[0].adm + vastXml: response_video.seatbid[0].bid[0].adm } let videoResponses = spec.interpretResponse(videoResponse); expect(videoResponses).to.be.an('array').that.is.not.empty; let dataItem = videoResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastUrl', 'ttl', 'creativeId', + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'ttl', 'creativeId', 'netRevenue', 'currency', 'dealId', 'mediaType'); expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.vastUrl).to.equal(expectedBidResponse.vastUrl) + expect(dataItem.vastXml).to.equal(expectedBidResponse.vastXml) expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); expect(dataItem.netRevenue).to.be.true; diff --git a/test/spec/modules/bridBidAdapter_spec.js b/test/spec/modules/bridBidAdapter_spec.js index 7503c748999..20a6542707b 100644 --- a/test/spec/modules/bridBidAdapter_spec.js +++ b/test/spec/modules/bridBidAdapter_spec.js @@ -1,4 +1,6 @@ import { spec } from '../../../modules/bridBidAdapter.js' +import { SYNC_URL } from '../../../libraries/targetVideoUtils/constants.js'; +import { deepClone } from '../../../src/utils.js'; describe('Brid Bid Adapter', function() { const videoRequest = [{ @@ -36,6 +38,29 @@ describe('Brid Bid Adapter', function() { expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal(12345); }); + it('Test the request schain sending', function() { + const globalSchain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'examplewebsite.com', + sid: '00001', + hp: 1 + }] + }; + + let videoRequestCloned = deepClone(videoRequest); + videoRequestCloned[0].schain = globalSchain; + + const request = spec.buildRequests(videoRequestCloned, videoRequestCloned[0]); + expect(request).to.not.be.empty; + + const payload = JSON.parse(request[0].data); + expect(payload).to.not.be.empty; + expect(payload.source.ext.schain).to.exist; + expect(payload.source.ext.schain).to.deep.equal(globalSchain); + }); + it('Test nobid responses', function () { const responseBody = { 'id': 'test-id', @@ -126,4 +151,23 @@ describe('Brid Bid Adapter', function() { expect(payload.regs.ext.gdpr).to.be.undefined; expect(payload.regs.ext.us_privacy).to.equal(uspConsentString); }); + + it('Test userSync have only one object and it should have a property type=iframe', function () { + let userSync = spec.getUserSyncs({ iframeEnabled: true }); + expect(userSync).to.be.an('array'); + expect(userSync.length).to.be.equal(1); + expect(userSync[0]).to.have.property('type'); + expect(userSync[0].type).to.be.equal('iframe'); + }); + + it('Test userSync valid sync url for iframe', function () { + let [userSync] = spec.getUserSyncs({ iframeEnabled: true }, {}, {consentString: 'anyString'}); + expect(userSync.url).to.contain(SYNC_URL + 'load-cookie.html?endpoint=brid&gdpr=0&gdpr_consent=anyString'); + expect(userSync.type).to.be.equal('iframe'); + }); + + it('Test userSyncs iframeEnabled=false', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: false}); + expect(userSyncs).to.have.lengthOf(0); + }); }); diff --git a/test/spec/modules/bridgeuppBidAdapter_spec.js b/test/spec/modules/bridgeuppBidAdapter_spec.js new file mode 100644 index 00000000000..b73b25848e5 --- /dev/null +++ b/test/spec/modules/bridgeuppBidAdapter_spec.js @@ -0,0 +1,1162 @@ +import { expect } from 'chai'; +import { + spec, BIDDER_CODE, SERVER_PATH_US1_SYNC, SERVER_PATH_US1_EVENTS +} from '../../../modules/bridgeuppBidAdapter.js'; +import * as utils from 'src/utils.js'; +import * as ajax from 'src/ajax.js'; +import { hook } from '../../../src/hook'; +import { config } from '../../../src/config.js'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; +import 'modules/consentManagementTcf.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/consentManagementGpp.js'; +import 'modules/priceFloors.js'; +import sinon from 'sinon'; + +// constants +const SITE_DOMAIN_NAME = 'sonargames.com'; +const SITE_PAGE = 'https://sonargames.com'; +describe('bridgeuppBidAdapter_spec', function () { + let utilsMock, sandbox, ajaxStub, fetchStub; + + afterEach(function () { + sandbox.restore(); + utilsMock.restore(); + ajaxStub.restore(); + fetchStub.restore(); + }); + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + utilsMock = sinon.mock(utils); + ajaxStub = sandbox.stub(ajax, 'ajax'); + fetchStub = sinon.stub(global, 'fetch').resolves(new Response('OK')); + }); + + describe('isBidRequestValid', function () { + let bid; + beforeEach(function () { + bid = { + 'bidder': BIDDER_CODE, + 'params': { + 'siteId': 'site-id-12', + } + }; + }); + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false when missing bidder', function () { + delete bid.bidder; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when bidder is not valid', function () { + bid.bidder = 'invalid-bidder'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when missing siteId', function () { + delete bid.params.siteId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + before(() => { + hook.ready(); + }) + afterEach(function () { + config.resetConfig(); + }); + const bidderRequest = { + refererInfo: { + page: 'https://sonarads.com/home', + ref: 'https://referrer' + }, + timeout: 1000, + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '1300': 1 + }, + }, + apiVersion: 1, + }, + }; + + it('request should build with correct siteId', async function () { + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'sonarads', + adUnitCode: 'bid-12', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + 'siteId': 'site-id-12' + } + }, + ]; + + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].ext.bidder.siteId).to.deep.equal('site-id-12'); + }); + + it('request should build with correct imp', async function () { + const expectedMetric = { + url: 'https://sonarads.com' + } + const bidRequests = [{ + bidId: 'bidId', + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + ortb2Imp: { + instl: 1, + metric: expectedMetric, + ext: { + gpid: 'gpid_sonarads' + }, + rwdd: 1 + }, + params: { + siteId: 'site-id-12', + bidfloor: 2.12, + bidfloorcur: 'USD' + } + }]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].id).to.deep.equal('bidId'); + expect(ortbRequest.imp[0].tagid).to.deep.equal('impId'); + expect(ortbRequest.imp[0].instl).to.equal(1); + expect(ortbRequest.imp[0].bidfloor).to.equal(2.12); + expect(ortbRequest.imp[0].bidfloorcur).to.equal('USD'); + expect(ortbRequest.imp[0].metric).to.deep.equal(expectedMetric); + expect(ortbRequest.imp[0].ext.gpid).to.equal('gpid_sonarads'); + expect(ortbRequest.imp[0].secure).to.equal(1); + expect(ortbRequest.imp[0].rwdd).to.equal(1); + }); + + it('request should build with proper site data', async function () { + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12' + }, + }, + ]; + const ortb2 = { + site: { + name: SITE_DOMAIN_NAME, + domain: SITE_DOMAIN_NAME, + keywords: 'keyword1, keyword2', + cat: ['IAB2'], + pagecat: ['IAB3'], + sectioncat: ['IAB4'], + page: SITE_PAGE, + ref: 'google.com', + privacypolicy: 1, + content: { + url: SITE_PAGE + '/games1' + } + } + }; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.site.domain).to.equal(SITE_DOMAIN_NAME); + expect(ortbRequest.site.publisher.domain).to.equal('sonarads.com'); + expect(ortbRequest.site.page).to.equal(SITE_PAGE); + expect(ortbRequest.site.name).to.equal(SITE_DOMAIN_NAME); + expect(ortbRequest.site.keywords).to.equal('keyword1, keyword2'); + expect(ortbRequest.site.cat).to.deep.equal(['IAB2']); + expect(ortbRequest.site.pagecat).to.deep.equal(['IAB3']); + expect(ortbRequest.site.sectioncat).to.deep.equal(['IAB4']); + expect(ortbRequest.site.ref).to.equal('google.com'); + expect(ortbRequest.site.privacypolicy).to.equal(1); + expect(ortbRequest.site.content.url).to.equal(SITE_PAGE + '/games1') + }); + + it('request should build with proper device data', async function () { + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12' + }, + }, + ]; + const ortb2 = { + device: { + dnt: 1, + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + ip: '203.0.113.42', + h: 800, + w: 1280, + language: 'fr', + lmt: 0, + js: 0, + connectiontype: 2, + hwv: 'iPad', + model: 'Pro', + mccmnc: '234-030', + geo: { + lat: 48.8566, + lon: 2.3522 + } + } + }; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.device.dnt).to.equal(1); + expect(ortbRequest.device.lmt).to.equal(0); + expect(ortbRequest.device.js).to.equal(0); + expect(ortbRequest.device.connectiontype).to.equal(2); + expect(ortbRequest.device.ua).to.equal('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36'); + expect(ortbRequest.device.ip).to.equal('203.0.113.42'); + expect(ortbRequest.device.h).to.equal(800); + expect(ortbRequest.device.w).to.equal(1280); + expect(ortbRequest.device.language).to.deep.equal('fr'); + expect(ortbRequest.device.hwv).to.deep.equal('iPad'); + expect(ortbRequest.device.model).to.deep.equal('Pro'); + expect(ortbRequest.device.mccmnc).to.deep.equal('234-030'); + expect(ortbRequest.device.geo.lat).to.deep.equal(48.8566); + expect(ortbRequest.device.geo.lon).to.deep.equal(2.3522); + }); + + it('should properly build a request with source object', async function () { + const expectedSchain = {id: 'prebid'}; + const ortb2 = { + source: { + pchain: 'sonarads', + schain: expectedSchain + } + }; + const bidRequests = [ + { + bidder: 'sonarads', + bidId: 'bidId', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12', + }, + }, + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.source.schain).to.deep.equal(expectedSchain); + expect(ortbRequest.source.pchain).to.equal('sonarads'); + }); + + it('should properly user object', async function () { + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12' + } + }, + ]; + const br = { + ...bidderRequest, + ortb2: { + user: { + yob: 2012, + keyowrds: 'test test', + gender: 'M', + customdata: 'test no', + geo: { + lat: 48.8566, + lon: 2.3522 + }, + ext: { + eids: [ + { + source: 'sonarads.com', + uids: [{ + id: 'iid', + atype: 1 + }] + } + ] + } + } + } + } + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(br)); + const ortbRequest = request.data; + expect(ortbRequest.user.yob).to.deep.equal(2012); + expect(ortbRequest.user.keyowrds).to.deep.equal('test test'); + expect(ortbRequest.user.gender).to.deep.equal('M'); + expect(ortbRequest.user.customdata).to.deep.equal('test no'); + expect(ortbRequest.user.geo.lat).to.deep.equal(48.8566); + expect(ortbRequest.user.geo.lon).to.deep.equal(2.3522); + expect(ortbRequest.user.ext.eids).to.deep.equal([ + { + source: 'sonarads.com', + uids: [{ + id: 'iid', + atype: 1 + }] + } + ]); + }); + + it('should properly build a request regs object', async function () { + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12' + }, + }, + ]; + const ortb2 = { + regs: { + coppa: 1, + gpp: 'consent_string', + gpp_sid: [0, 1, 2], + us_privacy: 'yes us privacy applied' + } + }; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.regs.coppa).to.equal(1); + expect(ortbRequest.regs.gpp).to.equal('consent_string'); + expect(ortbRequest.regs.gpp_sid).to.deep.equal([0, 1, 2]); + expect(ortbRequest.regs.us_privacy).to.deep.equal('yes us privacy applied'); + }); + + it('gdpr test', async function () { + // using privacy params from global bidder Request + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12' + }, + }, + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs.ext.gdpr).to.deep.equal(1); + expect(ortbRequest.user.ext.consent).to.equal('consentDataString'); + }); + + it('should properly set tmax if available', async function () { + // using tmax from global bidder Request + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'bid-1', + transactionId: 'trans-1', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12' + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.tmax).to.equal(bidderRequest.timeout); + }); + + it('should properly set auction data', async function () { + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'bid-1', + transactionId: 'trans-1', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12' + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.auctionStart).to.equal(bidderRequest.auctionStart); + }); + + it('should properly build a request with bcat field', async function () { + const bcat = ['IAB1', 'IAB2']; + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12', + }, + }, + ]; + const bidderRequest = { + ortb2: { + bcat + } + }; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.bcat).to.deep.equal(bcat); + }); + + it('should properly build a request with badv field', async function () { + const badv = ['nike.com']; + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12', + }, + }, + ]; + const bidderRequest = { + ortb2: { + badv + } + }; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.badv).to.deep.equal(badv); + }); + + it('should properly build a request with bapp field', async function () { + const bapp = ['nike.com']; + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12', + }, + }, + ]; + const bidderRequest = { + ortb2: { + bapp + } + }; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.bapp).to.deep.equal(bapp); + }); + + it('banner request test', async function () { + const bidderRequest = {}; + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'sonarads', + adUnitCode: 'bid-12', + mediaTypes: { + banner: { + sizes: [[320, 250]], + pos: 1, + topframe: 0, + } + }, + params: { + siteId: 'site-id-12' + }, + ortb2Imp: { + banner: { + api: [1, 2, 3], + mimes: ['image/jpg', 'image/gif'] + } + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].banner).not.to.be.null; + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(320); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(250); + expect(ortbRequest.imp[0].banner.pos).to.equal(1); + expect(ortbRequest.imp[0].banner.topframe).to.equal(0); + expect(ortbRequest.imp[0].banner.api).to.deep.equal([1, 2, 3]); + expect(ortbRequest.imp[0].banner.mimes).to.deep.equal(['image/jpg', 'image/gif']); + }); + + it('banner request test with sizes > 1', async function () { + const bidderRequest = {}; + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'sonarads', + adUnitCode: 'bid-12', + mediaTypes: { + banner: { + sizes: [[336, 336], [720, 50]] + } + }, + params: { + siteId: 'site-id-12' + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].banner).not.to.be.null; + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(336); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(336); + expect(ortbRequest.imp[0].banner.format[1].w).to.equal(720); + expect(ortbRequest.imp[0].banner.format[1].h).to.equal(50); + }); + + it('should properly build a request when coppa is true', async function () { + const bidRequests = []; + const bidderRequest = {}; + config.setConfig({coppa: true}); + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs.coppa).to.equal(1); + }); + + it('should properly build a request when coppa is false', async function () { + const bidRequests = []; + const bidderRequest = {}; + config.setConfig({coppa: false}); + let buildRequests = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = buildRequests.data; + expect(ortbRequest.regs.coppa).to.equal(0); + }); + + it('should properly build a request when coppa is not defined', async function () { + const bidRequests = []; + const bidderRequest = {}; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs?.coppa).to.be.undefined; + }); + + it('build a banner request with bidFloor', async function () { + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50], [720, 90]] + } + }, + params: { + siteId: 'site-id-12', + bidfloor: 1, + bidfloorcur: 'USD' + } + } + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].bidfloor).to.deep.equal(1); + expect(ortbRequest.imp[0].bidfloorcur).to.deep.equal('USD'); + }); + + it('build a banner request with getFloor', async function () { + const bidRequests = [ + { + bidder: 'sonarads', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12' + }, + getFloor: () => { + return {currency: 'USD', floor: 1.23, size: '*', mediaType: '*'}; + } + } + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].bidfloor).equal(1.23); + expect(ortbRequest.imp[0].bidfloorcur).equal('USD'); + }); + }); + + describe('interpretResponse', function () { + const bidderRequest = { + refererInfo: { + page: 'https://sonarads.com/home', + ref: 'https://referrer' + }, + timeout: 1000, + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '1300': 1 + }, + }, + apiVersion: 1, + }, + } + + function mockResponse(bidId, mediaType) { + return { + id: 'sonarads-response-id-hash-123123', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: 'sonarads-seatbid-bid-id-hash-123qaasd34', + impid: bidId, + price: 1.12, + adomain: ['advertiserDomain.sandbox.sonarads.com'], + crid: 'd3868d0b2f43c11e4bd60333e5e5ee8be471c4061b3b0cd454539c7edb68a1ca', + w: 320, + h: 250, + adm: 'test-ad', + mtype: mediaType, + nurl: 'https://et-l.w.sonarads.com/c.asm/', + api: 3, + cat: [], + ext: { + prebid: { + meta: { + advertiserDomains: ['advertiserDomain.sandbox.sonarads.com'], + networkName: 'sonarads' + } + } + } + } + ] + } + ], + ext: { + prebidjs: { + urls: [ + { url: 'https://sync.sonarads.com/prebidjs' }, + { url: 'https://eus.rubiconproject.com/usync.html?p=test' } + ] + } + } + } + } + + it('should returns an empty array when bid response is empty', async function () { + const bidRequests = []; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const serverResponse = { + headers: { + get: function () { + return undefined + } + }, + body: {} + }; + + const interpretedBids = spec.interpretResponse(serverResponse, request); + expect(interpretedBids).to.have.lengthOf(0); + expect(spec.reportEventsEnabled).to.equal(false); + }); + + it('should return an empty array when there is no bid response', async function () { + const bidRequests = []; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const serverResponse = { + headers: { + get: function () { + return undefined + } + }, + body: {seatbid: []} + }; + + const interpretedBids = spec.interpretResponse(serverResponse, request); + expect(interpretedBids).to.have.lengthOf(0); + expect(spec.reportEventsEnabled).to.equal(false); + }); + + it('return banner response', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12', + } + }]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const serverResponse = { + headers: { + get: function () { + return undefined + } + }, + body: mockResponse('bidId', 1) + }; + const interpretedBids = spec.interpretResponse(serverResponse, request); + expect(spec.reportEventsEnabled).to.equal(false); + expect(interpretedBids).to.have.length(1); + expect(interpretedBids[0].currency).to.deep.equal('USD'); + expect(interpretedBids[0].mediaType).to.deep.equal('banner'); + expect(interpretedBids[0].requestId).to.deep.equal('bidId'); + expect(interpretedBids[0].seatBidId).to.deep.equal('sonarads-seatbid-bid-id-hash-123qaasd34'); + expect(interpretedBids[0].cpm).to.equal(1.12); + expect(interpretedBids[0].width).to.equal(320); + expect(interpretedBids[0].height).to.equal(250); + expect(interpretedBids[0].creativeId).to.deep.equal('d3868d0b2f43c11e4bd60333e5e5ee8be471c4061b3b0cd454539c7edb68a1ca'); + expect(interpretedBids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.sonarads.com'); + }); + + it('should set the reportEventsEnabled to true as part of the response', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 320], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + siteId: 'site-id-12', + }, + }, { + adUnitCode: 'impId', + bidId: 'bidId2', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + siteId: 'site-id-12', + } + }]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const serverResponse = { + headers: { + get: function (header) { + if (header === 'reportEventsEnabled') { + return 1; + } + } + }, + body: mockResponse('bidId2', 1) + }; + + const interpretedBids = spec.interpretResponse(serverResponse, request); + expect(spec.reportEventsEnabled).to.equal(true); + expect(interpretedBids).to.have.length(1); + }); + + it('bid response when banner wins among two ad units', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 320], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + siteId: 'site-id-12', + }, + }, { + adUnitCode: 'impId', + bidId: 'bidId2', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: { + siteId: 'site-id-12', + } + }]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const serverResponse = { + headers: { + get: function (header) { + if (header === 'reportEventsEnabled') { + return 1; + } + } + }, + body: mockResponse('bidId2', 1) + }; + + const interpretedBids = spec.interpretResponse(serverResponse, request); + expect(spec.reportEventsEnabled).to.equal(true); + expect(interpretedBids[0].currency).to.deep.equal('USD'); + expect(interpretedBids[0].mediaType).to.deep.equal('banner'); + expect(interpretedBids[0].requestId).to.deep.equal('bidId2'); + expect(interpretedBids[0].cpm).to.equal(1.12); + expect(interpretedBids[0].seatBidId).to.deep.equal('sonarads-seatbid-bid-id-hash-123qaasd34'); + expect(interpretedBids[0].creativeId).to.deep.equal('d3868d0b2f43c11e4bd60333e5e5ee8be471c4061b3b0cd454539c7edb68a1ca'); + expect(interpretedBids[0].width).to.equal(320); + expect(interpretedBids[0].height).to.equal(250); + expect(interpretedBids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.sonarads.com'); + }); + }); + + describe('getUserSyncs', function () { + it('should return empty if iframe disabled and pixelEnabled is false', function () { + const res = spec.getUserSyncs({}); + expect(res).to.deep.equal([]); + }); + + it('should return user sync if iframe enabled is true', function () { + const res = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }); + expect(res).to.deep.equal([{ type: 'iframe', url: `${SERVER_PATH_US1_SYNC}` }]); + }); + + it('should return user sync if iframe enabled is true and gdpr not present', function () { + const res = spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: false }); + expect(res).to.deep.equal([{ type: 'iframe', url: `${SERVER_PATH_US1_SYNC}` }]); + }); + + it('should return user sync if iframe enabled is true and gdpr = 1 and gdpr consent present', function () { + const res = spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: 'GDPR_CONSENT' }); + expect(res).to.deep.equal([{ type: 'iframe', url: `${SERVER_PATH_US1_SYNC}?gdpr=1&gdpr_consent=GDPR_CONSENT` }]); + }); + + it('should return user sync if iframe enabled is true and gdpr = 1 , gdpr consent present and usp_consent present', function () { + const res = spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: 'GDPR_CONSENT' }, 'USP_CONSENT'); + expect(res).to.deep.equal([{ type: 'iframe', url: `${SERVER_PATH_US1_SYNC}?gdpr=1&gdpr_consent=GDPR_CONSENT&us_privacy=USP_CONSENT` }]); + }); + + it('should return user sync if iframe enabled is true usp_consent present and gppConsent present', function () { + const res = spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: false, consentString: undefined }, 'USP_CONSENT', { gppString: 'GPP_STRING', applicableSections: [32, 51] }); + expect(res).to.deep.equal([{ type: 'iframe', url: `${SERVER_PATH_US1_SYNC}?us_privacy=USP_CONSENT&gpp=GPP_STRING&gpp_sid=32%2C51` }]); + }); + }); + + describe('onTimeout', function () { + it('onTimeout function should be defined', function () { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + + it('should invoke onTimeout, resolving the eventType and domain', function () { + const bid = [ + { + bidder: 'sonarads', + bidId: 'sonar-bid-id-1231232', + adUnitCode: 'sonar-ad-1231232', + timeout: 1000, + auctionId: 'sonar-auction-1231232' + } + ]; + spec.reportEventsEnabled = true; + spec.onTimeout(bid); + + // expected url and payload + const expectedUrl = `${SERVER_PATH_US1_EVENTS}`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + prebidVersion: '$prebid.version$', + eventType: 'onTimeout', + eventPayload: bid + }); + + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + + const fetchArgs = fetchStub.getCall(0).args; + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'application/json', + }); + }); + + it('onTimeout should not be called if the bid data is null', function () { + // Call onTimeout with null data + spec.onTimeout(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); + + describe('onSetTargeting', function () { + it('onSetTargeting function should be defined', function () { + expect(spec.onSetTargeting).to.exist.and.to.be.a('function'); + }); + + it('should invoke onSetTargeting, resolving the eventType and domain', function () { + const bid = [ + { + bidder: 'sonarads', + bidId: 'sonar-bid-id-1231232', + adUnitCode: 'sonar-ad-1231232', + timeout: 1000, + auctionId: 'sonar-auction-1231232' + } + ]; + spec.reportEventsEnabled = true; + spec.onSetTargeting(bid); + + // expected url and payload + const expectedUrl = `${SERVER_PATH_US1_EVENTS}`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + prebidVersion: '$prebid.version$', + eventType: 'onSetTargeting', + eventPayload: bid + }); + + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + + const fetchArgs = fetchStub.getCall(0).args; + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'application/json', + }); + }); + + it('onSetTargeting should not be called if the bid data is null', function () { + // Call onSetTargeting with null data + spec.onSetTargeting(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); + + describe('onAdRenderSucceeded', function () { + it('onAdRenderSucceeded function should be defined', function () { + expect(spec.onAdRenderSucceeded).to.exist.and.to.be.a('function'); + }); + + it('should invoke onAdRenderSucceeded, resolving the eventType and domain', function () { + const bid = [ + { + bidder: 'sonarads', + bidId: 'sonar-bid-id-1231232', + adUnitCode: 'sonar-ad-1231232', + timeout: 1000, + auctionId: 'sonar-auction-1231232' + } + ]; + + spec.reportEventsEnabled = true; + spec.onAdRenderSucceeded(bid); + + // expected url and payload + const expectedUrl = `${SERVER_PATH_US1_EVENTS}`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + prebidVersion: '$prebid.version$', + eventType: 'onAdRenderSucceeded', + eventPayload: bid + }); + + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + + const fetchArgs = fetchStub.getCall(0).args; + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'application/json', + }); + }); + + it('onAdRenderSucceeded should not be called if the bid data is null', function () { + // Call onAdRenderSucceeded with null data + spec.onAdRenderSucceeded(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); + + describe('onBidderError', function () { + it('onBidderError function should be defined', function () { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + + it('should invoke onBidderError, resolving the eventType and domain', function () { + const bid = [ + { + bidder: 'sonarads', + bidId: 'sonar-bid-id-1231232', + adUnitCode: 'sonar-ad-1231232', + timeout: 1000, + auctionId: 'sonar-auction-1231232' + } + ]; + spec.reportEventsEnabled = true; + spec.onBidderError(bid); + + // expected url and payload + const expectedUrl = `${SERVER_PATH_US1_EVENTS}`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + prebidVersion: '$prebid.version$', + eventType: 'onBidderError', + eventPayload: bid + }); + + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + + const fetchArgs = fetchStub.getCall(0).args; + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'application/json', + }); + }); + + it('onBidderError should not be called if the bid data is null', function () { + // Call onBidderError with null data + spec.onBidderError(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); + + describe('onBidWon', function () { + it('onBidWon function should be defined', function () { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + + it('should invoke onBidWon, resolving the eventType and domain', function () { + const bid = [ + { + bidder: 'sonarads', + bidId: 'sonar-bid-id-1231232', + adUnitCode: 'sonar-ad-1231232', + timeout: 1000, + auctionId: 'sonar-auction-1231232' + } + ]; + spec.reportEventsEnabled = true; + spec.onBidWon(bid); + + // expected url and payload + const expectedUrl = `${SERVER_PATH_US1_EVENTS}`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + prebidVersion: '$prebid.version$', + eventType: 'onBidWon', + eventPayload: bid + }); + + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + + const fetchArgs = fetchStub.getCall(0).args; + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'application/json', + }); + }); + + it('onBidWon should not be called if the bid data is null', function () { + // Call onBidWon with null data + spec.onBidWon(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); +}); diff --git a/test/spec/modules/brightcomBidAdapter_spec.js b/test/spec/modules/brightcomBidAdapter_spec.js deleted file mode 100644 index 1ae73708d00..00000000000 --- a/test/spec/modules/brightcomBidAdapter_spec.js +++ /dev/null @@ -1,411 +0,0 @@ -import { expect } from 'chai'; -import * as utils from 'src/utils.js'; -import { spec } from 'modules/brightcomBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; -import {config} from '../../../src/config'; - -const URL = 'https://brightcombid.marphezis.com/hb'; - -describe('brightcomBidAdapter', function() { - const adapter = newBidder(spec); - let element, win; - let bidRequests; - let sandbox; - - beforeEach(function() { - element = { - x: 0, - y: 0, - - width: 0, - height: 0, - - getBoundingClientRect: () => { - return { - width: element.width, - height: element.height, - - left: element.x, - top: element.y, - right: element.x + element.width, - bottom: element.y + element.height - }; - } - }; - win = { - document: { - visibilityState: 'visible' - }, - - innerWidth: 800, - innerHeight: 600 - }; - bidRequests = [{ - 'bidder': 'brightcom', - 'params': { - 'publisherId': 1234567 - }, - 'adUnitCode': 'adunit-code', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [300, 600]] - } - }, - 'bidId': '5fb26ac22bde4', - 'bidderRequestId': '4bf93aeb730cb9', - 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', - 'schain': { - 'ver': '1.0', - 'complete': 1, - 'nodes': [ - { - 'asi': 'exchange1.com', - 'sid': '1234', - 'hp': 1, - 'rid': 'bid-request-1', - 'name': 'publisher', - 'domain': 'publisher.com' - } - ] - }, - }]; - - sandbox = sinon.sandbox.create(); - sandbox.stub(document, 'getElementById').withArgs('adunit-code').returns(element); - sandbox.stub(utils, 'getWindowTop').returns(win); - sandbox.stub(utils, 'getWindowSelf').returns(win); - }); - - afterEach(function() { - sandbox.restore(); - }); - - describe('isBidRequestValid', function () { - let bid = { - 'bidder': 'brightcom', - 'params': { - 'publisherId': 1234567 - }, - 'adUnitCode': 'adunit-code', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [300, 600]] - } - }, - 'bidId': '5fb26ac22bde4', - 'bidderRequestId': '4bf93aeb730cb9', - 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', - }; - - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - - it('should return false when publisherId not passed correctly', function () { - bid.params.publisherId = undefined; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when require params are not passed', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - }); - - describe('buildRequests', function () { - it('sends bid request to our endpoint via POST', function () { - const request = spec.buildRequests(bidRequests); - expect(request.method).to.equal('POST'); - }); - - it('request url should match our endpoint url', function () { - const request = spec.buildRequests(bidRequests); - expect(request.url).to.equal(URL); - }); - - it('sets the proper banner object', function() { - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); - }); - - it('accepts a single array as a size', function() { - bidRequests[0].mediaTypes.banner.sizes = [300, 250]; - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); - }); - - it('sends bidfloor param if present', function () { - bidRequests[0].params.bidFloor = 0.05; - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].bidfloor).to.equal(0.05); - }); - - it('sends tagid', function () { - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].tagid).to.equal('adunit-code'); - }); - - it('sends publisher id', function () { - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.site.publisher.id).to.equal(1234567); - }); - - it('sends gdpr info if exists', function () { - const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; - const bidderRequest = { - 'bidderCode': 'brightcom', - 'auctionId': '1d1a030790a437', - 'bidderRequestId': '22edbae2744bf5', - 'timeout': 3000, - gdprConsent: { - consentString: consentString, - gdprApplies: true - }, - refererInfo: { - page: 'http://example.com/page.html', - domain: 'example.com', - } - }; - bidderRequest.bids = bidRequests; - - const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); - - expect(data.regs.ext.gdpr).to.exist.and.to.be.a('number'); - expect(data.regs.ext.gdpr).to.equal(1); - expect(data.user.ext.consent).to.exist.and.to.be.a('string'); - expect(data.user.ext.consent).to.equal(consentString); - }); - - it('sends us_privacy', function () { - const bidderRequest = { - uspConsent: '1YYY' - }; - const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data) - - expect(data.regs).to.not.equal(null); - expect(data.regs.ext).to.not.equal(null); - expect(data.regs.ext.us_privacy).to.equal('1YYY'); - }); - - it('sends coppa', function () { - sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); - - const data = JSON.parse(spec.buildRequests(bidRequests).data) - expect(data.regs).to.not.be.undefined; - expect(data.regs.coppa).to.equal(1); - }); - - it('sends schain', function () { - const data = JSON.parse(spec.buildRequests(bidRequests).data); - expect(data).to.not.be.undefined; - expect(data.source).to.not.be.undefined; - expect(data.source.ext).to.not.be.undefined; - expect(data.source.ext.schain).to.not.be.undefined; - expect(data.source.ext.schain.complete).to.equal(1); - expect(data.source.ext.schain.ver).to.equal('1.0'); - expect(data.source.ext.schain.nodes).to.not.be.undefined; - expect(data.source.ext.schain.nodes).to.lengthOf(1); - expect(data.source.ext.schain.nodes[0].asi).to.equal('exchange1.com'); - expect(data.source.ext.schain.nodes[0].sid).to.equal('1234'); - expect(data.source.ext.schain.nodes[0].hp).to.equal(1); - expect(data.source.ext.schain.nodes[0].rid).to.equal('bid-request-1'); - expect(data.source.ext.schain.nodes[0].name).to.equal('publisher'); - expect(data.source.ext.schain.nodes[0].domain).to.equal('publisher.com'); - }); - - it('sends user eid parameters', function () { - bidRequests[0].userIdAsEids = [{ - source: 'pubcid.org', - uids: [{ - id: 'userid_pubcid' - }] - }, { - source: 'adserver.org', - uids: [{ - id: 'userid_ttd', - ext: { - rtiPartner: 'TDID' - } - }] - } - ]; - - const data = JSON.parse(spec.buildRequests(bidRequests).data); - - expect(data.user).to.not.be.undefined; - expect(data.user.ext).to.not.be.undefined; - expect(data.user.ext.eids).to.not.be.undefined; - expect(data.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); - }); - - it('sends user id parameters', function () { - const userId = { - sharedid: { - id: '01*******', - third: '01E*******' - } - }; - - bidRequests[0].userId = userId; - - const data = JSON.parse(spec.buildRequests(bidRequests).data); - expect(data.user).to.not.be.undefined; - expect(data.user.ext).to.not.be.undefined; - expect(data.user.ext.ids).is.deep.equal(userId); - }); - - context('when element is fully in view', function() { - it('returns 100', function() { - Object.assign(element, { width: 600, height: 400 }); - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(100); - }); - }); - - context('when element is out of view', function() { - it('returns 0', function() { - Object.assign(element, { x: -300, y: 0, width: 207, height: 320 }); - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(0); - }); - }); - - context('when element is partially in view', function() { - it('returns percentage', function() { - Object.assign(element, { width: 800, height: 800 }); - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(75); - }); - }); - - context('when width or height of the element is zero', function() { - it('try to use alternative values', function() { - Object.assign(element, { width: 0, height: 0 }); - bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(25); - }); - }); - - context('when nested iframes', function() { - it('returns \'na\'', function() { - Object.assign(element, { width: 600, height: 400 }); - - utils.getWindowTop.restore(); - utils.getWindowSelf.restore(); - sandbox.stub(utils, 'getWindowTop').returns(win); - sandbox.stub(utils, 'getWindowSelf').returns({}); - - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal('na'); - }); - }); - - context('when tab is inactive', function() { - it('returns 0', function() { - Object.assign(element, { width: 600, height: 400 }); - - utils.getWindowTop.restore(); - win.document.visibilityState = 'hidden'; - sandbox.stub(utils, 'getWindowTop').returns(win); - - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(0); - }); - }); - }); - - describe('interpretResponse', function () { - let response; - beforeEach(function () { - response = { - body: { - 'id': '37386aade21a71', - 'seatbid': [{ - 'bid': [{ - 'id': '376874781', - 'impid': '283a9f4cd2415d', - 'price': 0.35743275, - 'nurl': '', - 'adm': '', - 'w': 300, - 'h': 250, - 'adomain': ['example.com'] - }] - }] - } - }; - }); - - it('should get the correct bid response', function () { - let expectedResponse = [{ - 'requestId': '283a9f4cd2415d', - 'cpm': 0.35743275, - 'width': 300, - 'height': 250, - 'creativeId': '376874781', - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', - 'ad': `
`, - 'ttl': 60, - 'meta': { - 'advertiserDomains': ['example.com'] - } - }]; - - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); - }); - - it('crid should default to the bid id if not on the response', function () { - let expectedResponse = [{ - 'requestId': '283a9f4cd2415d', - 'cpm': 0.35743275, - 'width': 300, - 'height': 250, - 'creativeId': response.body.seatbid[0].bid[0].id, - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', - 'ad': `
`, - 'ttl': 60, - 'meta': { - 'advertiserDomains': ['example.com'] - } - }]; - - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); - }); - - it('handles empty bid response', function () { - let response = { - body: '' - }; - let result = spec.interpretResponse(response); - expect(result.length).to.equal(0); - }); - }); - - describe('getUserSyncs ', () => { - let syncOptions = {iframeEnabled: true, pixelEnabled: true}; - - it('should not return', () => { - let returnStatement = spec.getUserSyncs(syncOptions, []); - expect(returnStatement).to.be.empty; - }); - }); -}); diff --git a/test/spec/modules/brightcomSSPBidAdapter_spec.js b/test/spec/modules/brightcomSSPBidAdapter_spec.js deleted file mode 100644 index 6f35a7a290b..00000000000 --- a/test/spec/modules/brightcomSSPBidAdapter_spec.js +++ /dev/null @@ -1,411 +0,0 @@ -import { expect } from 'chai'; -import * as utils from 'src/utils.js'; -import { spec } from 'modules/brightcomSSPBidAdapter'; -import { newBidder } from 'src/adapters/bidderFactory.js'; -import {config} from '../../../src/config'; - -const URL = 'https://rt.marphezis.com/hb'; - -describe('brightcomSSPBidAdapter', function() { - const adapter = newBidder(spec); - let element, win; - let bidRequests; - let sandbox; - - beforeEach(function() { - element = { - x: 0, - y: 0, - - width: 0, - height: 0, - - getBoundingClientRect: () => { - return { - width: element.width, - height: element.height, - - left: element.x, - top: element.y, - right: element.x + element.width, - bottom: element.y + element.height - }; - } - }; - win = { - document: { - visibilityState: 'visible' - }, - - innerWidth: 800, - innerHeight: 600 - }; - bidRequests = [{ - 'bidder': 'bcmssp', - 'params': { - 'publisherId': 1234567 - }, - 'adUnitCode': 'adunit-code', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [300, 600]] - } - }, - 'bidId': '5fb26ac22bde4', - 'bidderRequestId': '4bf93aeb730cb9', - 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', - 'schain': { - 'ver': '1.0', - 'complete': 1, - 'nodes': [ - { - 'asi': 'exchange1.com', - 'sid': '1234', - 'hp': 1, - 'rid': 'bid-request-1', - 'name': 'publisher', - 'domain': 'publisher.com' - } - ] - }, - }]; - - sandbox = sinon.sandbox.create(); - sandbox.stub(document, 'getElementById').withArgs('adunit-code').returns(element); - sandbox.stub(utils, 'getWindowTop').returns(win); - sandbox.stub(utils, 'getWindowSelf').returns(win); - }); - - afterEach(function() { - sandbox.restore(); - }); - - describe('isBidRequestValid', function () { - let bid = { - 'bidder': 'bcmssp', - 'params': { - 'publisherId': 1234567 - }, - 'adUnitCode': 'adunit-code', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [300, 600]] - } - }, - 'bidId': '5fb26ac22bde4', - 'bidderRequestId': '4bf93aeb730cb9', - 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', - }; - - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - - it('should return false when publisherId not passed correctly', function () { - bid.params.publisherId = undefined; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when require params are not passed', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - }); - - describe('buildRequests', function () { - it('sends bid request to our endpoint via POST', function () { - const request = spec.buildRequests(bidRequests); - expect(request.method).to.equal('POST'); - }); - - it('request url should match our endpoint url', function () { - const request = spec.buildRequests(bidRequests); - expect(request.url).to.equal(URL); - }); - - it('sets the proper banner object', function() { - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); - }); - - it('accepts a single array as a size', function() { - bidRequests[0].mediaTypes.banner.sizes = [300, 250]; - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); - }); - - it('sends bidfloor param if present', function () { - bidRequests[0].params.bidFloor = 0.05; - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].bidfloor).to.equal(0.05); - }); - - it('sends tagid', function () { - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].tagid).to.equal('adunit-code'); - }); - - it('sends publisher id', function () { - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.site.publisher.id).to.equal(1234567); - }); - - it('sends gdpr info if exists', function () { - const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; - const bidderRequest = { - 'bidderCode': 'bcmssp', - 'auctionId': '1d1a030790a437', - 'bidderRequestId': '22edbae2744bf5', - 'timeout': 3000, - gdprConsent: { - consentString: consentString, - gdprApplies: true - }, - refererInfo: { - page: 'http://example.com/page.html', - domain: 'example.com', - } - }; - bidderRequest.bids = bidRequests; - - const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); - - expect(data.regs.ext.gdpr).to.exist.and.to.be.a('number'); - expect(data.regs.ext.gdpr).to.equal(1); - expect(data.user.ext.consent).to.exist.and.to.be.a('string'); - expect(data.user.ext.consent).to.equal(consentString); - }); - - it('sends us_privacy', function () { - const bidderRequest = { - uspConsent: '1YYY' - }; - const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data) - - expect(data.regs).to.not.equal(null); - expect(data.regs.ext).to.not.equal(null); - expect(data.regs.ext.us_privacy).to.equal('1YYY'); - }); - - it('sends coppa', function () { - sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); - - const data = JSON.parse(spec.buildRequests(bidRequests).data) - expect(data.regs).to.not.be.undefined; - expect(data.regs.coppa).to.equal(1); - }); - - it('sends schain', function () { - const data = JSON.parse(spec.buildRequests(bidRequests).data); - expect(data).to.not.be.undefined; - expect(data.source).to.not.be.undefined; - expect(data.source.ext).to.not.be.undefined; - expect(data.source.ext.schain).to.not.be.undefined; - expect(data.source.ext.schain.complete).to.equal(1); - expect(data.source.ext.schain.ver).to.equal('1.0'); - expect(data.source.ext.schain.nodes).to.not.be.undefined; - expect(data.source.ext.schain.nodes).to.lengthOf(1); - expect(data.source.ext.schain.nodes[0].asi).to.equal('exchange1.com'); - expect(data.source.ext.schain.nodes[0].sid).to.equal('1234'); - expect(data.source.ext.schain.nodes[0].hp).to.equal(1); - expect(data.source.ext.schain.nodes[0].rid).to.equal('bid-request-1'); - expect(data.source.ext.schain.nodes[0].name).to.equal('publisher'); - expect(data.source.ext.schain.nodes[0].domain).to.equal('publisher.com'); - }); - - it('sends user eid parameters', function () { - bidRequests[0].userIdAsEids = [{ - source: 'pubcid.org', - uids: [{ - id: 'userid_pubcid' - }] - }, { - source: 'adserver.org', - uids: [{ - id: 'userid_ttd', - ext: { - rtiPartner: 'TDID' - } - }] - } - ]; - - const data = JSON.parse(spec.buildRequests(bidRequests).data); - - expect(data.user).to.not.be.undefined; - expect(data.user.ext).to.not.be.undefined; - expect(data.user.ext.eids).to.not.be.undefined; - expect(data.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); - }); - - it('sends user id parameters', function () { - const userId = { - sharedid: { - id: '01*******', - third: '01E*******' - } - }; - - bidRequests[0].userId = userId; - - const data = JSON.parse(spec.buildRequests(bidRequests).data); - expect(data.user).to.not.be.undefined; - expect(data.user.ext).to.not.be.undefined; - expect(data.user.ext.ids).is.deep.equal(userId); - }); - - context('when element is fully in view', function() { - it('returns 100', function() { - Object.assign(element, { width: 600, height: 400 }); - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(100); - }); - }); - - context('when element is out of view', function() { - it('returns 0', function() { - Object.assign(element, { x: -300, y: 0, width: 207, height: 320 }); - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(0); - }); - }); - - context('when element is partially in view', function() { - it('returns percentage', function() { - Object.assign(element, { width: 800, height: 800 }); - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(75); - }); - }); - - context('when width or height of the element is zero', function() { - it('try to use alternative values', function() { - Object.assign(element, { width: 0, height: 0 }); - bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(25); - }); - }); - - context('when nested iframes', function() { - it('returns \'na\'', function() { - Object.assign(element, { width: 600, height: 400 }); - - utils.getWindowTop.restore(); - utils.getWindowSelf.restore(); - sandbox.stub(utils, 'getWindowTop').returns(win); - sandbox.stub(utils, 'getWindowSelf').returns({}); - - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal('na'); - }); - }); - - context('when tab is inactive', function() { - it('returns 0', function() { - Object.assign(element, { width: 600, height: 400 }); - - utils.getWindowTop.restore(); - win.document.visibilityState = 'hidden'; - sandbox.stub(utils, 'getWindowTop').returns(win); - - const request = spec.buildRequests(bidRequests); - const payload = JSON.parse(request.data); - expect(payload.imp[0].banner.ext.viewability).to.equal(0); - }); - }); - }); - - describe('interpretResponse', function () { - let response; - beforeEach(function () { - response = { - body: { - 'id': '37386aade21a71', - 'seatbid': [{ - 'bid': [{ - 'id': '376874781', - 'impid': '283a9f4cd2415d', - 'price': 0.35743275, - 'nurl': '', - 'adm': '', - 'w': 300, - 'h': 250, - 'adomain': ['example.com'] - }] - }] - } - }; - }); - - it('should get the correct bid response', function () { - let expectedResponse = [{ - 'requestId': '283a9f4cd2415d', - 'cpm': 0.35743275, - 'width': 300, - 'height': 250, - 'creativeId': '376874781', - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', - 'ad': `
`, - 'ttl': 60, - 'meta': { - 'advertiserDomains': ['example.com'] - } - }]; - - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); - }); - - it('crid should default to the bid id if not on the response', function () { - let expectedResponse = [{ - 'requestId': '283a9f4cd2415d', - 'cpm': 0.35743275, - 'width': 300, - 'height': 250, - 'creativeId': response.body.seatbid[0].bid[0].id, - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', - 'ad': `
`, - 'ttl': 60, - 'meta': { - 'advertiserDomains': ['example.com'] - } - }]; - - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); - }); - - it('handles empty bid response', function () { - let response = { - body: '' - }; - let result = spec.interpretResponse(response); - expect(result.length).to.equal(0); - }); - }); - - describe('getUserSyncs ', () => { - let syncOptions = {iframeEnabled: true, pixelEnabled: true}; - - it('should not return', () => { - let returnStatement = spec.getUserSyncs(syncOptions, []); - expect(returnStatement).to.be.empty; - }); - }); -}); diff --git a/test/spec/modules/britepoolIdSystem_spec.js b/test/spec/modules/britepoolIdSystem_spec.js deleted file mode 100644 index ddb61806006..00000000000 --- a/test/spec/modules/britepoolIdSystem_spec.js +++ /dev/null @@ -1,129 +0,0 @@ -import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; -import * as utils from '../../../src/utils.js'; - -describe('BritePool Submodule', () => { - const api_key = '1111'; - const aaid = '4421ea96-34a9-45df-a4ea-3c41a48a18b1'; - const idfa = '2d1c4fac-5507-4e28-991c-ca544e992dba'; - const bpid = '279c0161-5152-487f-809e-05d7f7e653fd'; - const url_override = 'https://override'; - const getter_override = function(params) { - return JSON.stringify({ 'primaryBPID': bpid }); - }; - const getter_callback_override = function(params) { - return callback => { - callback(JSON.stringify({ 'primaryBPID': bpid })); - }; - }; - - let triggerPixelStub; - - beforeEach(function (done) { - triggerPixelStub = sinon.stub(utils, 'triggerPixel'); - done(); - }); - - afterEach(function () { - triggerPixelStub.restore(); - }); - - it('trigger id resolution pixel when no identifiers set', () => { - britepoolIdSubmodule.getId({ params: {} }); - expect(triggerPixelStub.called).to.be.true; - }); - - it('trigger id resolution pixel when no identifiers set with api_key param', () => { - britepoolIdSubmodule.getId({ params: { api_key } }); - expect(triggerPixelStub.called).to.be.true; - }); - - it('does not trigger id resolution pixel when identifiers set', () => { - britepoolIdSubmodule.getId({ params: { api_key, aaid } }); - expect(triggerPixelStub.called).to.be.false; - }); - - it('sends x-api-key in header and one identifier', () => { - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); - assert(errors.length === 0, errors); - expect(headers['x-api-key']).to.equal(api_key); - expect(params).to.eql({ aaid }); - }); - - it('sends x-api-key in header and two identifiers', () => { - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, idfa }); - assert(errors.length === 0, errors); - expect(headers['x-api-key']).to.equal(api_key); - expect(params).to.eql({ aaid, idfa }); - }); - - it('allows call without api_key', () => { - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ aaid, idfa }); - expect(params).to.eql({ aaid, idfa }); - expect(errors.length).to.equal(0); - }); - - it('test url override', () => { - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, url: url_override }); - expect(url).to.equal(url_override); - // Making sure it did not become part of params - expect(params.url).to.be.undefined; - }); - - it('test gdpr consent string in url', () => { - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: true, consentString: 'expectedConsentString' }); - expect(url).to.equal('https://api.britepool.com/v1/britepool/id?gdprString=expectedConsentString'); - }); - - it('test gdpr consent string not in url if gdprApplies false', () => { - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: false, consentString: 'expectedConsentString' }); - expect(url).to.equal('https://api.britepool.com/v1/britepool/id'); - }); - - it('test gdpr consent string not in url if consent string undefined', () => { - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: true, consentString: undefined }); - expect(url).to.equal('https://api.britepool.com/v1/britepool/id'); - }); - - it('dynamic pub params should be added to params', () => { - window.britepool_pubparams = { ppid: '12345' }; - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); - expect(params).to.eql({ aaid, ppid: '12345' }); - window.britepool_pubparams = undefined; - }); - - it('dynamic pub params should override submodule params', () => { - window.britepool_pubparams = { ppid: '67890' }; - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, ppid: '12345' }); - expect(params).to.eql({ ppid: '67890' }); - window.britepool_pubparams = undefined; - }); - - it('if dynamic pub params undefined do nothing', () => { - window.britepool_pubparams = undefined; - const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); - expect(params).to.eql({ aaid }); - window.britepool_pubparams = undefined; - }); - - it('test getter override with value', () => { - const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, url: url_override, getter: getter_override }); - expect(getter).to.equal(getter_override); - // Making sure it did not become part of params - expect(params.getter).to.be.undefined; - const response = britepoolIdSubmodule.getId({ params: { api_key, aaid, url: url_override, getter: getter_override } }); - assert.deepEqual(response, { id: { 'primaryBPID': bpid } }); - }); - - it('test getter override with callback', done => { - const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, url: url_override, getter: getter_callback_override }); - expect(getter).to.equal(getter_callback_override); - // Making sure it did not become part of params - expect(params.getter).to.be.undefined; - const response = britepoolIdSubmodule.getId({ params: { api_key, aaid, url: url_override, getter: getter_callback_override } }); - expect(response.callback).to.not.be.undefined; - response.callback(result => { - assert.deepEqual(result, { 'primaryBPID': bpid }); - done(); - }); - }); -}); diff --git a/test/spec/modules/browsiAnalyticsAdapter_spec.js b/test/spec/modules/browsiAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..aa9b806ff59 --- /dev/null +++ b/test/spec/modules/browsiAnalyticsAdapter_spec.js @@ -0,0 +1,321 @@ +import browsiAnalytics from '../../../modules/browsiAnalyticsAdapter.js'; +import { setStaticData, getStaticData } from '../../../modules/browsiAnalyticsAdapter.js'; +import adapterManager from '../../../src/adapterManager'; +import { expect } from 'chai'; +import { EVENTS } from '../../../src/constants.js'; +import { server } from '../../../test/mocks/xhr.js'; +import { getGlobal } from '../../../src/prebidGlobal.js'; +import * as utils from '../../../src/utils.js'; + +let events = require('src/events'); + +describe('browsi analytics adapter', function () { + const timestamp = 1740559971388; + const auctionId = 'abe18da6-cee1-438b-9013-dc5a62c9d4a8'; + + const auctionEnd = { + 'auctionId': auctionId, + 'timestamp': 1740559969178, + 'auctionEnd': 1740559971388, + 'auctionStatus': 'completed', + 'adUnits': [ + { + 'code': 'realtid_mobile-mobil-1_:r1:', + 'sizes': [[300, 250], [320, 100], [320, 160], [320, 320]], + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [320, 100], [320, 160], [320, 320]] + } + }, + 'bids': [ + { + 'bidder': 'bidderA', + 'auctionId': 'abe18da6-cee1-438b-9013-dc5a62c9d4a8', + 'ortb2Imp': { + 'ext': { + 'data': { + 'browsi': { + 'scrollDepth': 0.4, + 'density': 0.6, + 'numberOfAds': 3, + 'lcp': 2.5, + 'cls': 0.08, + 'inp': 150, + 'viewability': 0.66, + 'revenue': 0.44, + 'adLocation': 1220 + } + } + } + }, + }, + { + 'bidder': 'adprofitadform', + 'auctionId': 'abe18da6-cee1-438b-9013-dc5a62c9d4a8', + }, + { + 'bidder': 'bidderB', + 'auctionId': 'abe18da6-cee1-438b-9013-dc5a62c9d4a8', + 'ortb2Imp': { + 'ext': { + 'data': { + 'browsi': { + 'scrollDepth': 0.4, + 'density': 0.6, + 'numberOfAds': 3, + 'lcp': 2.5, + 'cls': 0.08, + 'inp': 150, + 'viewability': 0.66, + 'revenue': 0.44, + 'adLocation': 1220 + } + } + } + }, + }, + ], + 'lwPName': 'realtid_mobile-mobil-1', + 'adUnitId': '974f76d1-02af-4bb9-93d8-6aa8d4f81f30', + 'transactionId': '39fe8024-758e-4dbc-82c8-656cfba1d06b', + 'adserverTargeting': { + 'browsiViewability': [ + '0.60' + ], + 'browsiScroll': [ + '0.40' + ], + 'browsiRevenue': [ + 'medium' + ] + } + }, + { + 'code': 'realtid_mobile-mobil-2_:r2:', + 'sizes': [[300, 250], [320, 100], [320, 160], [320, 320], [320, 400], [320, 480]], + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [320, 100], [320, 160], [320, 320], [320, 400], [320, 480]] + } + }, + 'bids': [ + { + 'bidder': 'bidderA', + 'auctionId': 'abe18da6-cee1-438b-9013-dc5a62c9d4a8', + 'ortb2Imp': { + 'ext': { + 'data': { + 'browsi': { + 'scrollDepth': 0.4, + 'density': 0.6, + 'numberOfAds': 3, + 'lcp': 2.5, + 'cls': 0.08, + 'inp': 150 + } + } + } + }, + }, + { + 'bidder': 'tmapubmatic', + 'auctionId': 'abe18da6-cee1-438b-9013-dc5a62c9d4a8', + }, + { + 'bidder': 'adprofitadform', + 'auctionId': 'abe18da6-cee1-438b-9013-dc5a62c9d4a8', + }, + ], + 'lwPName': 'realtid_mobile-mobil-2', + 'adUnitId': '644e15c8-4ee4-4205-9bf0-72dd22f8a678', + 'transactionId': '2299ea95-cd7e-492d-952f-38a637afff81', + 'adserverTargeting': { + 'browsiScroll': [ + '0.40' + ], + 'browsiRevenue': [ + 'no fill' + ] + } + } + ], + 'adUnitCodes': [ + 'realtid_mobile-mobil-1_:r1:', + 'realtid_mobile-mobil-2_:r2:' + ] + } + const browsiInit = { + 'moduleName': 'browsi', + 't': 1740559969178, + 'pvid': '123456', + 'pk': 'pub_key', + 'sk': 'site_key', + } + const dataSet1 = { + moduleName: 'browsi', + pvid: '123456', + d: 'MOBILE', + g: 'IL', + aid: 'article_123', + es: true, + sk: 'site_key', + pk: 'pub_key', + t: 1740559969178 + } + const dataSet2 = { + moduleName: 'browsi', + pvid: '123456', + d: 'DESKTOP', + g: 'IL', + aid: 'article_321', + es: false, + sk: 'site_key', + pk: 'pub_key', + t: 1740559969178 + } + + let sandbox; + + before(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(utils, 'timestamp').returns(timestamp); + + adapterManager.registerAnalyticsAdapter({ + code: 'browsi', + adapter: browsiAnalytics + }); + }); + after(() => { + sandbox.restore(); + }); + + beforeEach(() => { + sandbox.stub(events, 'getEvents').returns([]); + browsiAnalytics.enableAnalytics({ + provider: 'browsi', + options: {} + }); + browsiAnalytics._staticData = undefined; + }); + afterEach(() => { + events.getEvents.restore(); + browsiAnalytics.disableAnalytics(); + }); + + it('should send auction data', function () { + setStaticData(dataSet1); + + events.emit(EVENTS.AUCTION_END, auctionEnd); + expect(server.requests.length).to.equal(1); + + const request = server.requests[0]; + const { protocol, hostname, pathname, search } = utils.parseUrl(request.url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('events.browsiprod.com'); + expect(pathname).to.equal('/events/v2/rtd_demand'); + expect(search).to.deep.equal({ 'p': '123456' }); + + const body = JSON.parse(request.requestBody); + expect(body.length).to.equal(1); + + const event = body[0]; + expect(event.to).to.equal(timestamp - dataSet1.t); + expect(event.pvid).to.equal(dataSet1.pvid); + expect(event.pk).to.equal(dataSet1.pk); + expect(event.sk).to.equal(dataSet1.sk); + expect(event.geo).to.equal(dataSet1.g); + expect(event.dp).to.equal(dataSet1.d); + expect(event.aid).to.equal(dataSet1.aid); + expect(event.pbv).to.equal(getGlobal().version); + expect(event.url).to.equal(encodeURIComponent(window.location.href)); + expect(event.et).to.equal('auction_data_sent'); + expect(event.aucid).to.equal(auctionId); + expect(event.ad_units).to.have.length(2); + + expect(event.ad_units[0].plid).to.equal('realtid_mobile-mobil-1_:r1:'); + expect(event.ad_units[0].au).to.be.null; + expect(event.ad_units[0].pbd).to.deep.equal(['bidderA', 'bidderB']); + expect(event.ad_units[0].dpc).to.equal(9); + expect(event.ad_units[0].rtm).to.deep.equal({ + 'scrollDepth': 0.4, + 'density': 0.6, + 'numberOfAds': 3, + 'lcp': 2.5, + 'cls': 0.08, + 'inp': 150, + 'viewability': 0.66, + 'revenue': 0.44, + 'adLocation': 1220 + }); + expect(event.ad_units[1].plid).to.equal('realtid_mobile-mobil-2_:r2:'); + expect(event.ad_units[1].au).to.be.null; + expect(event.ad_units[1].pbd).to.deep.equal(['bidderA']); + expect(event.ad_units[1].dpc).to.equal(6); + expect(event.ad_units[1].rtm).to.deep.equal({ + 'scrollDepth': 0.4, + 'density': 0.6, + 'numberOfAds': 3, + 'lcp': 2.5, + 'cls': 0.08, + 'inp': 150 + }); + }); + it('should send auction data without rtm data', function () { + setStaticData(dataSet2); + + events.emit(EVENTS.AUCTION_END, auctionEnd); + expect(server.requests.length).to.equal(1); + + const request = server.requests[0]; + const body = JSON.parse(request.requestBody); + expect(body.length).to.equal(1); + + const event = body[0]; + expect(event.ad_units[0].rtm).to.not.exist; + expect(event.ad_units[1].rtm).to.not.exist; + }); + it('should send rtd init event', function () { + events.emit(EVENTS.BROWSI_INIT, browsiInit); + expect(server.requests.length).to.equal(1); + + const request = server.requests[0]; + const { protocol, hostname, pathname, search } = utils.parseUrl(request.url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('events.browsiprod.com'); + expect(pathname).to.equal('/events/v2/rtd_supply'); + expect(search).to.deep.equal({ 'p': '123456' }); + + const body = JSON.parse(request.requestBody); + expect(body.length).to.equal(1); + + const event = body[0]; + expect(event.et).to.equal('rtd_init'); + expect(event.to).to.equal(timestamp - dataSet1.t); + expect(event.pvid).to.equal(dataSet1.pvid); + expect(event.pk).to.equal(dataSet1.pk); + expect(event.sk).to.equal(dataSet1.sk); + expect(event.pbv).to.equal(getGlobal().version); + expect(event.url).to.equal(encodeURIComponent(window.location.href)); + }); + it('should not send rtd init event if module name is not browsi', function () { + events.emit(EVENTS.BROWSI_INIT, { moduleName: 'not_browsi' }); + expect(server.requests.length).to.equal(0); + }); + it('should not set static data if module name is not browsi', function () { + events.emit(EVENTS.BROWSI_DATA, { moduleName: 'not_browsi' }); + expect(browsiAnalytics._staticData).to.equal(undefined); + }); + it('should set static data', function () { + events.emit(EVENTS.BROWSI_DATA, dataSet2); + expect(getStaticData()).to.deep.equal({ + pvid: '123456', + device: 'DESKTOP', + geo: 'IL', + aid: 'article_321', + es: false, + sk: 'site_key', + pk: 'pub_key', + t: 1740559969178 + }); + }); +}); diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index 5fcc78f4322..8bc240c4deb 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -1,11 +1,12 @@ import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; -import {makeSlot} from '../integration/faker/googletag.js'; +import * as browsiUtils from '../../../libraries/browsiUtils/browsiUtils.js'; import * as utils from '../../../src/utils' import * as events from '../../../src/events'; import * as sinon from 'sinon'; -import {sendPageviewEvent} from '../../../modules/browsiRtdProvider.js'; +import * as mockGpt from 'test/spec/integration/faker/googletag.js'; +import * as Global from '../../../src/prebidGlobal.js'; -describe('browsi Real time data sub module', function () { +describe('browsi Real time data sub module', function () { const conf = { 'auctionDelay': 250, dataProviders: [{ @@ -18,23 +19,33 @@ describe('browsi Real time data sub module', function () { } }] }; - const auction = {adUnits: [ - { - code: 'adMock', - transactionId: 1 - }, - { - code: 'hasPrediction', - transactionId: 1 - } - ]}; + const auction = { + adUnits: [ + { + code: 'adMock', + transactionId: 1 + }, + { + code: 'hasPrediction', + transactionId: 1 + } + ] + }; + const msPerDay = 24 * 60 * 60 * 1000; let sandbox; let eventsEmitSpy; + let timestampStub; before(() => { sandbox = sinon.sandbox.create(); eventsEmitSpy = sandbox.spy(events, ['emit']); + timestampStub = sandbox.stub(utils, 'timestamp'); + sandbox.stub(Global, 'getGlobal').callsFake(() => { + return { + enableAnalytics: () => { }, + } + }); }); after(() => { @@ -54,51 +65,70 @@ describe('browsi Real time data sub module', function () { expect(script.prebidData.kn).to.equal(conf.dataProviders[0].params.keyName); }); - it('should match placement with ad unit', function () { - const slot = makeSlot({code: '/123/abc', divId: 'browsiAd_1'}); - - const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/abc']); // true - const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/abc', '/456/def']); // true - const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/def']); // false - const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true - - expect(test1).to.equal(true); - expect(test2).to.equal(true); - expect(test3).to.equal(false); - expect(test4).to.equal(true); - }); - it('should return correct macro values', function () { - const slot = makeSlot({code: '/123/abc', divId: 'browsiAd_1'}); + const slot = mockGpt.makeSlot({ code: '/123/abc', divId: 'browsiAd_1' }); slot.setTargeting('test', ['test', 'value']); // slot getTargeting doesn't act like GPT so we can't expect real value - const macroResult = browsiRTD.getMacroId({p: '/'}, slot); + const macroResult = browsiUtils.getMacroId({ p: '/' }, slot); expect(macroResult).to.equal('/123/abc/NA'); - const macroResultB = browsiRTD.getMacroId({}, slot); + const macroResultB = browsiUtils.getMacroId({}, slot); expect(macroResultB).to.equal('browsiAd_1'); - const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); + const macroResultC = browsiUtils.getMacroId({ p: '', s: { s: 0, e: 1 } }, slot); expect(macroResultC).to.equal('/'); }); describe('should return data to RTD module', function () { it('should return empty if no ad units defined', function () { - browsiRTD.setData({}); + browsiRTD.setBrowsiData({}); expect(browsiRTD.browsiSubmodule.getTargetingData([], null, null, auction)).to.eql({}); }); - - it('should return prediction from server', function () { - makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); + it('should return empty if GAM is not defined', function () { + mockGpt.makeSlot({ code: 'slot1', divId: 'slot1' }); const data = { - p: {'hasPrediction': {ps: {0: 0.234}}}, - kn: 'bv', - pmd: undefined + plc: { 'slot1': { viewability: { 0: 0.234 } } }, + pmd: undefined, + sg: false }; - browsiRTD.setData(data); - expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'], null, null, auction)).to.eql({hasPrediction: {bv: '0.20'}}); - }) + browsiRTD.setBrowsiData(data); + expect(browsiRTD.browsiSubmodule.getTargetingData(['slotId'], null, null, auction)).to.eql({}); + }); + it('should return empty if viewability key is not defined', function () { + mockGpt.makeSlot({ code: 'slot2', divId: 'slot2' }); + const data = { + plc: { 'slot2': { someKey: { 0: 0.234 } } }, + pmd: undefined, + sg: true + }; + browsiRTD.setBrowsiData(data); + expect(browsiRTD.browsiSubmodule.getTargetingData(['slot2'], null, null, auction)).to.eql({ 'slot2': {} }); + }); + it('should return all predictions from server', function () { + mockGpt.makeSlot({ code: 'slot3', divId: 'slot3' }); + const data = { + pg: { scrollDepth: 0.456 }, + plc: { 'slot3': { viewability: { 0: 0.234 }, revenue: { 0: 0.567 } } }, + pmd: undefined, + sg: true + }; + browsiRTD.setBrowsiData(data); + expect(browsiRTD.browsiSubmodule.getTargetingData(['slot3'], null, null, auction)).to.eql({ + 'slot3': { bv: '0.20', browsiRevenue: 'medium', browsiScroll: '0.40' } + }); + }); + it('should return the available prediction', function () { + mockGpt.makeSlot({ code: 'slot4', divId: 'slot4' }); + const data = { + pg: { scrollDepth: 0.456 }, + plc: { 'slot4': { someKey: { 0: 0.234 } } }, + pmd: undefined, + sg: true + }; + browsiRTD.setBrowsiData(data); + expect(browsiRTD.browsiSubmodule.getTargetingData(['slot4'], null, null, auction)).to.eql({ 'slot4': { browsiScroll: '0.40' } }); + }); }) describe('should return matching prediction', function () { @@ -111,6 +141,7 @@ describe('browsi Real time data sub module', function () { const singlePrediction = { 0: 0.123 } + const numbericPrediction = 0.456; it('should return raw value if valid', function () { expect(browsiRTD.getCurrentData(predictions, 0)).to.equal(0.123); expect(browsiRTD.getCurrentData(predictions, 1)).to.equal(0.254); @@ -130,28 +161,84 @@ describe('browsi Real time data sub module', function () { expect(browsiRTD.getCurrentData(predictions, 5)).to.equal(0.8); expect(browsiRTD.getCurrentData(predictions, 8)).to.equal(0.8); }) + it('should return prediction if it is a number', function () { + expect(browsiRTD.getCurrentData(numbericPrediction, 0)).to.equal(0.456); + }) }) - describe('should set bid request data', function () { + + describe('should handle bid request data', function () { const data = { - p: { - 'adUnit1': {ps: {0: 0.234}}, - 'adUnit2': {ps: {0: 0.134}}}, - kn: 'bv', + plc: { + 'adUnit1': { keyA: { 0: 0.234 } }, + 'adUnit2': { keyB: { 0: 0.134 } } + }, + pr: ['bidder1'], pmd: undefined - }; - browsiRTD.setData(data); + } const fakeAdUnits = [ { - code: 'adUnit1' + code: 'adUnit1', + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] }, { - code: 'adUnit2' + code: 'adUnit2', + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] } ] - browsiRTD.browsiSubmodule.getBidRequestData({adUnits: fakeAdUnits}, () => {}, {}, null); - it('should set ad unit params with prediction values', function () { - expect(utils.deepAccess(fakeAdUnits[0], 'ortb2Imp.ext.data.browsi')).to.eql({bv: '0.20'}); - expect(utils.deepAccess(fakeAdUnits[1], 'ortb2Imp.ext.data.browsi')).to.eql({bv: '0.10'}); + before(async () => { + browsiRTD.setBrowsiData(data); + browsiRTD.browsiSubmodule.getBidRequestData({ adUnits: fakeAdUnits }, () => { }, {}, null); + }); + it('should set bidder params with prediction values', function () { + expect(utils.deepAccess(fakeAdUnits[0].bids[0], 'ortb2Imp.ext.data.browsi')).to.eql({ keyA: 0.234 }); + expect(utils.deepAccess(fakeAdUnits[1].bids[0], 'ortb2Imp.ext.data.browsi')).to.eql({ keyB: 0.134 }); + }) + it('should not set bidder params if bidder is not in pr', function () { + expect(utils.deepAccess(fakeAdUnits[0].bids[1], 'ortb2Imp.ext.data.browsi')).to.eql(undefined); + expect(utils.deepAccess(fakeAdUnits[1].bids[1], 'ortb2Imp.ext.data.browsi')).to.eql(undefined); + }) + }) + + describe('should not set bid request data', function () { + const data = { + plc: { + 'adUnit1': { keyA: { 0: 0.234 } }, + 'adUnit2': { keyB: { 0: 0.134 } } + }, + pr: [], + pmd: undefined + } + const fakeAdUnits = [ + { + code: 'adUnit1', + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + }, + { + code: 'adUnit2', + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + } + ] + before(() => { + browsiRTD.setBrowsiData(data); + browsiRTD.browsiSubmodule.getBidRequestData({ adUnits: fakeAdUnits }, () => { }, {}, null); + }); + it('should not set bidder params if pr is empty', function () { + expect(utils.deepAccess(fakeAdUnits[0].bids[0], 'ortb2Imp.ext.data.browsi')).to.eql(undefined); + expect(utils.deepAccess(fakeAdUnits[1].bids[0], 'ortb2Imp.ext.data.browsi')).to.eql(undefined); + expect(utils.deepAccess(fakeAdUnits[0].bids[1], 'ortb2Imp.ext.data.browsi')).to.eql(undefined); + expect(utils.deepAccess(fakeAdUnits[1].bids[1], 'ortb2Imp.ext.data.browsi')).to.eql(undefined); }) }) @@ -159,52 +246,55 @@ describe('browsi Real time data sub module', function () { before(() => { const data = { p: { - 'adUnit1': {ps: {0: 0.234}}, - 'adUnit2': {ps: {0: 0.134}}}, - kn: 'bv', + 'adUnit1': { ps: { 0: 0.234 } }, + 'adUnit2': { ps: { 0: 0.134 } } + }, pmd: undefined, bet: 'AD_REQUEST' }; - browsiRTD.setData(data); + browsiRTD.setBrowsiData(data); }) - beforeEach(() => { eventsEmitSpy.resetHistory(); }) it('should send one event per ad unit code', function () { - const auction = {adUnits: [ - { - code: 'a', - transactionId: 1 - }, - { - code: 'b', - transactionId: 2 - }, - { - code: 'a', - transactionId: 3 - }, - ]}; + const auction = { + adUnits: [ + { + code: 'a', + transactionId: 1 + }, + { + code: 'b', + transactionId: 2 + }, + { + code: 'a', + transactionId: 3 + }, + ] + }; browsiRTD.browsiSubmodule.getTargetingData(['a', 'b'], null, null, auction); expect(eventsEmitSpy.callCount).to.equal(2); }) it('should send events only for received ad unit codes', function () { - const auction = {adUnits: [ - { - code: 'a', - transactionId: 1 - }, - { - code: 'b', - transactionId: 2 - }, - { - code: 'c', - transactionId: 3 - }, - ]}; + const auction = { + adUnits: [ + { + code: 'a', + transactionId: 1 + }, + { + code: 'b', + transactionId: 2 + }, + { + code: 'c', + transactionId: 3 + }, + ] + }; browsiRTD.browsiSubmodule.getTargetingData(['a'], null, null, auction); expect(eventsEmitSpy.callCount).to.equal(1); @@ -223,7 +313,8 @@ describe('browsi Real time data sub module', function () { code: 'a', transactionId: 3 }, - ]}; + ] + }; const expectedCall = { vendor: 'browsi', @@ -245,7 +336,7 @@ describe('browsi Real time data sub module', function () { eventsEmitSpy.resetHistory(); }) it('should send event if type is correct', function () { - sendPageviewEvent('PAGEVIEW') + browsiRTD.sendPageviewEvent('PAGEVIEW') const pageViewEvent = new CustomEvent('browsi_pageview', {}); window.dispatchEvent(pageViewEvent); const expectedCall = { @@ -260,10 +351,172 @@ describe('browsi Real time data sub module', function () { expect(callArguments).to.eql(expectedCall); }) it('should not send event if type is incorrect', function () { - sendPageviewEvent('AD_REQUEST'); - sendPageviewEvent('INACTIVE'); - sendPageviewEvent(undefined); + browsiRTD.sendPageviewEvent('AD_REQUEST'); + browsiRTD.sendPageviewEvent('INACTIVE'); + browsiRTD.sendPageviewEvent(undefined); expect(eventsEmitSpy.callCount).to.equal(0); }) }) + + describe('set targeting - invalid params', function () { + const random = Math.floor(Math.random() * 10) + 1; + it('should return false if key is undefined', function () { + expect(browsiUtils.setKeyValue(undefined, random)).to.equal(false); + }) + it('should return false if key is not string', function () { + expect(browsiUtils.setKeyValue(1, random)).to.equal(false); + }) + }) + + describe('set targeting - valid params', function () { + let slot; + const splitKey = 'splitTest'; + const random = Math.floor(Math.random() * 10) + 1; + before(() => { + mockGpt.reset(); + window.googletag.pubads().clearTargeting(); + slot = mockGpt.makeSlot({ code: '/123/split', divId: 'split' }); + browsiUtils.setKeyValue(splitKey, random); + window.googletag.cmd.forEach(cmd => cmd()); + }) + it('should place numeric key value on all slots', function () { + const targetingValue = window.googletag.pubads().getTargeting(splitKey); + expect(targetingValue).to.be.an('array').that.is.not.empty; + expect(targetingValue[0]).to.be.a('string'); + }) + }) + + describe('should get latest avg highest bid', function () { + it('should return lahb', function () { + const currentTimestemp = new Date().getTime(); + const storageTimestemp = currentTimestemp - (msPerDay); + + const diffInMilliseconds = Math.abs(storageTimestemp - currentTimestemp); + const diffInDays = diffInMilliseconds / msPerDay; + + const lahb = { + avg: 0.02, + smp: 3, + time: storageTimestemp + }; + + timestampStub.returns(currentTimestemp); + + expect(browsiUtils.getLahb(lahb, currentTimestemp)).to.deep.equal({ avg: 0.02, age: diffInDays }); + }); + }) + + describe('should get recent avg highest bid', function () { + it('should return rahb', function () { + const currentTimestemp = new Date().getTime(); + const oneDayAgoTimestemp = currentTimestemp - (msPerDay); + const twoDayAgoTimestemp = currentTimestemp - (2 * msPerDay); + const rahb = { + [currentTimestemp]: { 'sum': 20, 'smp': 8 }, + [oneDayAgoTimestemp]: { 'sum': 25, 'smp': 10 }, + [twoDayAgoTimestemp]: { 'sum': 30, 'smp': 12 } + }; + expect(browsiUtils.getRahb(rahb, currentTimestemp)).to.deep.equal({ avg: 2.5 }); + }); + it('should return rahb without timestamps older than a week', function () { + const currentTimestemp = new Date().getTime(); + const oneDayAgoTimestemp = currentTimestemp - (msPerDay); + const twoDayAgoTimestemp = currentTimestemp - (2 * msPerDay); + const twoWeekAgoTimestemp = currentTimestemp - (14 * msPerDay); + const rahb = { + [currentTimestemp]: { 'sum': 20, 'smp': 8 }, + [oneDayAgoTimestemp]: { 'sum': 25, 'smp': 10 }, + [twoDayAgoTimestemp]: { 'sum': 30, 'smp': 12 }, + [twoWeekAgoTimestemp]: { 'sum': 35, 'smp': 20 } + }; + const expected = { + [currentTimestemp]: { 'sum': 20, 'smp': 8 }, + [oneDayAgoTimestemp]: { 'sum': 25, 'smp': 10 }, + [twoDayAgoTimestemp]: { 'sum': 30, 'smp': 12 }, + }; + expect(browsiUtils.getRahbByTs(rahb, currentTimestemp)).to.deep.equal(expected); + }); + it('should return an empty object if all timestamps are older than a week', function () { + const currentTimestemp = new Date().getTime(); + const eightDaysAgoTimestemp = currentTimestemp - (8 * msPerDay); + const twoWeekAgoTimestemp = currentTimestemp - (14 * msPerDay); + const rahb = { + [eightDaysAgoTimestemp]: { 'sum': 20, 'smp': 8 }, + [twoWeekAgoTimestemp]: { 'sum': 25, 'smp': 10 } + }; + expect(browsiUtils.getRahbByTs(rahb, currentTimestemp)).to.deep.equal({}); + }); + }) + + describe('should get avg highest bid metrics', function () { + const currentTimestemp = new Date().getTime(); + const oneDayAgoTimestemp = currentTimestemp - (msPerDay); + const twoWeekAgoTimestemp = currentTimestemp - (14 * msPerDay); + + const uahb = { avg: 0.2991556234740213, smp: 28 }; + const lahb = { avg: 0.02, smp: 3, time: oneDayAgoTimestemp }; + const rahb = { [currentTimestemp]: { sum: 20, smp: 8 }, [oneDayAgoTimestemp]: { sum: 25, smp: 10 }, }; + + const getExpected = function (bus) { + return { + uahb: +bus.uahb?.avg.toFixed(3), + rahb: +(2.5).toFixed(3), + lahb: +bus.lahb?.avg.toFixed(3), + lbsa: +(1).toFixed(3), + } + } + + before(() => { + timestampStub.returns(currentTimestemp); + browsiRTD.setTimestamp(); + }); + it('should return undefined if bus is not defined', function () { + expect(browsiUtils.getHbm(undefined)).to.equal(undefined); + }); + it('should return metrics if bus is defined', function () { + const bus = { uahb, lahb, rahb }; + expect(browsiUtils.getHbm(bus, currentTimestemp)).to.deep.equal({ + uahb: getExpected(bus).uahb, + rahb: getExpected(bus).rahb, + lahb: getExpected(bus).lahb, + lbsa: getExpected(bus).lbsa + }); + }); + it('should return metrics without lahb if its not defined', function () { + const bus = { uahb, rahb }; + expect(browsiUtils.getHbm(bus, currentTimestemp)).to.deep.equal({ + uahb: getExpected(bus).uahb, + rahb: getExpected(bus).rahb, + lahb: undefined, + lbsa: undefined + }); + }); + it('should return metrics without rahb if its not defined', function () { + const bus = { uahb, lahb }; + expect(browsiUtils.getHbm(bus, currentTimestemp)).to.deep.equal({ + uahb: getExpected(bus).uahb, + rahb: undefined, + lahb: getExpected(bus).lahb, + lbsa: getExpected(bus).lbsa + }); + }); + it('should return metrics without uahb if its not defined', function () { + const bus = { lahb, rahb }; + expect(browsiUtils.getHbm(bus, currentTimestemp)).to.deep.equal({ + uahb: undefined, + rahb: getExpected(bus).rahb, + lahb: getExpected(bus).lahb, + lbsa: getExpected(bus).lbsa + }); + }); + it('should return metrics without rahb if timestamps are older than a week', function () { + const bus = { uahb, lahb, rahb: { [twoWeekAgoTimestemp]: { sum: 25, smp: 10 } } }; + expect(browsiUtils.getHbm(bus, currentTimestemp)).to.deep.equal({ + uahb: getExpected(bus).uahb, + rahb: undefined, + lahb: getExpected(bus).lahb, + lbsa: getExpected(bus).lbsa + }); + }); + }) }); diff --git a/test/spec/modules/c1xBidAdapter_spec.js b/test/spec/modules/c1xBidAdapter_spec.js index 315680cba26..c93b43d571b 100644 --- a/test/spec/modules/c1xBidAdapter_spec.js +++ b/test/spec/modules/c1xBidAdapter_spec.js @@ -31,9 +31,9 @@ describe('C1XAdapter', () => { }); it('should return false when require params are not passed', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(c1xAdapter.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + invalidBid.params = {}; + expect(c1xAdapter.isBidRequestValid(invalidBid)).to.equal(false); }); }); describe('buildRequests', () => { diff --git a/test/spec/modules/cadentApertureMXBidAdapter_spec.js b/test/spec/modules/cadentApertureMXBidAdapter_spec.js index 3ccb5405552..d8686e3c667 100644 --- a/test/spec/modules/cadentApertureMXBidAdapter_spec.js +++ b/test/spec/modules/cadentApertureMXBidAdapter_spec.js @@ -48,49 +48,11 @@ describe('cadent_aperture_mx Adapter', function () { 'auctionId': '1d1a01234a475' }; let noBid = {}; - let otherBid = { - 'bidder': 'emxdigital', - 'params': { - 'tagid': '25251' - }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250]] - } - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c2501de1e', - 'bidderRequestId': '22edbae3120bf6', - 'auctionId': '1d1a01234a475' - }; - let noMediaSizeBid = { - 'bidder': 'emxdigital', - 'params': { - 'tagid': '25251' - }, - 'mediaTypes': { - 'banner': {} - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c2501de1e', - 'bidderRequestId': '22edbae3120bf6', - 'auctionId': '1d1a01234a475' - }; it('should return true when required params found', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); expect(spec.isBidRequestValid(badBid)).to.equal(false); expect(spec.isBidRequestValid(noBid)).to.equal(false); - expect(spec.isBidRequestValid(otherBid)).to.equal(false); - expect(spec.isBidRequestValid(noMediaSizeBid)).to.equal(false); }); }); @@ -451,10 +413,27 @@ describe('cadent_aperture_mx Adapter', function () { }); }); - it('should add gpid to request if present', () => { + it('should add gpid to request if present in ext.gpid', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { gpid, data: { adserver: { adslot: gpid + '1' }, pbadslot: gpid + '2' } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + }); + + it('should add gpid to request if present in ext.data.adserver.adslot', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid }, pbadslot: gpid + '1' } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + }); + + it('should add gpid to request if present in ext.data.pbadslot', () => { const gpid = '/12345/my-gpt-tag-0'; let bid = utils.deepClone(bidderRequest.bids[0]); - bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; let requestWithGPID = spec.buildRequests([bid], bidderRequest); requestWithGPID = JSON.parse(requestWithGPID.data); diff --git a/test/spec/modules/carodaBidAdapter_spec.js b/test/spec/modules/carodaBidAdapter_spec.js index f575e31e85d..780c81ebe9f 100644 --- a/test/spec/modules/carodaBidAdapter_spec.js +++ b/test/spec/modules/carodaBidAdapter_spec.js @@ -3,6 +3,8 @@ import { assert } from 'chai'; import { spec } from 'modules/carodaBidAdapter.js'; import { config } from 'src/config.js'; import { createEidsArray } from 'modules/userId/eids.js'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; describe('Caroda adapter', function () { let bids = []; @@ -185,29 +187,28 @@ describe('Caroda adapter', function () { }); it('should send currency if defined', function () { - config.setConfig({ currency: { adServerCurrency: 'EUR' } }); + setCurrencyConfig({ adServerCurrency: 'EUR' }); let validBidRequests = [{ params: {} }]; - let refererInfo = { page: 'page' }; - let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo })[0].data); - - assert.deepEqual(request.currency, 'EUR'); + const bidderRequest = { refererInfo: { page: 'page' } }; + return addFPDToBidderRequest(bidderRequest).then(res => { + let request = JSON.parse(spec.buildRequests(validBidRequests, res)[0].data); + assert.deepEqual(request.currency, 'EUR'); + setCurrencyConfig({}); + }); }); it('should pass extended ids', function () { let validBidRequests = [{ bid_id: 'bidId', params: {}, - userIdAsEids: createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE' - }) + userIdAsEids: [ + { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, + { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } + ] }]; let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } })[0].data); - assert.deepEqual(request.user.eids, [ - { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, - { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } - ]); + assert.deepEqual(request.user.eids, validBidRequests[0].userIdAsEids); }); describe('user privacy', function () { @@ -304,11 +305,15 @@ describe('Caroda adapter', function () { }); it('should request floor price in adserver currency', function () { - config.setConfig({ currency: { adServerCurrency: 'DKK' } }); const validBidRequests = [ getBidWithFloor() ]; - const imp = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } })[0].data); - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'DKK'); + setCurrencyConfig({ adServerCurrency: 'DKK' }); + const bidderRequest = { refererInfo: { page: 'page' } }; + return addFPDToBidderRequest(bidderRequest).then(res => { + const imp = JSON.parse(spec.buildRequests(validBidRequests, res)[0].data); + assert.equal(imp.bidfloor, undefined); + assert.equal(imp.bidfloorcur, 'DKK'); + setCurrencyConfig({}); + }); }); it('should add correct floor values', function () { diff --git a/test/spec/modules/ccxBidAdapter_spec.js b/test/spec/modules/ccxBidAdapter_spec.js index cbae441e7e7..f207e466d3e 100644 --- a/test/spec/modules/ccxBidAdapter_spec.js +++ b/test/spec/modules/ccxBidAdapter_spec.js @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import {addFPDToBidderRequest} from '../../helpers/fpd'; import { spec } from 'modules/ccxBidAdapter.js'; import * as utils from 'src/utils.js'; @@ -39,6 +40,7 @@ describe('ccxAdapter', function () { transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9' } ]; + describe('isBidRequestValid', function () { it('Valid bid requests', function () { expect(spec.isBidRequestValid(bids[0])).to.be.true; @@ -75,6 +77,7 @@ describe('ccxAdapter', function () { expect(spec.isBidRequestValid(bidsClone[0])).to.be.true; }); }); + describe('buildRequests', function () { it('No valid bids', function () { expect(spec.buildRequests([])).to.be.undefined; @@ -173,6 +176,7 @@ describe('ccxAdapter', function () { expect(data.imp).to.deep.have.same.members(imps); }); + it('Valid bid request - sizes old style', function () { let bidsClone = utils.deepClone(bids); delete (bidsClone[0].mediaTypes); @@ -218,6 +222,7 @@ describe('ccxAdapter', function () { expect(data.imp).to.deep.have.same.members(imps); }); + it('Valid bid request - sizes old style - no media type', function () { let bidsClone = utils.deepClone(bids); delete (bidsClone[0].mediaTypes); @@ -385,6 +390,7 @@ describe('ccxAdapter', function () { expect(spec.interpretResponse({})).to.be.empty; }); }); + describe('getUserSyncs', function () { it('Valid syncs - all', function () { let syncOptions = { @@ -434,6 +440,7 @@ describe('ccxAdapter', function () { expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.be.empty; }); }); + describe('mediaTypesVideoParams', function () { it('Valid video mediaTypes', function () { let bids = [ @@ -488,4 +495,78 @@ describe('ccxAdapter', function () { expect(data.imp).to.deep.have.same.members(imps); }); }); + + describe('FLEDGE', function () { + it('should properly build a request when FLEDGE is enabled', async function () { + let bidderRequest = { + paapi: { + enabled: true + } + }; + let bids = [ + { + adUnitCode: 'banner', + auctionId: '0b9de793-8eda-481e-a548-aaaaaaaaaaa1', + bidId: '2e56e1af51ccc1', + bidder: 'ccx', + bidderRequestId: '17e7b9f58accc1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 609 + }, + sizes: [[300, 250]], + transactionId: 'befddd38-cfa0-48ab-8bdd-bbbbbbbbbbb1', + ortb2Imp: { + ext: { + ae: 1 + } + } + } + ]; + + let ortbRequest = spec.buildRequests(bids, await addFPDToBidderRequest(bidderRequest)); + let data = JSON.parse(ortbRequest.data); + expect(data.imp[0].ext.ae).to.equal(1); + }); + + it('should properly build a request when FLEDGE is disabled', async function () { + let bidderRequest = { + paapi: { + enabled: false + } + }; + let bids = [ + { + adUnitCode: 'banner', + auctionId: '0b9de793-8eda-481e-a548-aaaaaaaaaaa2', + bidId: '2e56e1af51ccc2', + bidder: 'ccx', + bidderRequestId: '17e7b9f58accc2', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 610 + }, + sizes: [[300, 250]], + transactionId: 'befddd38-cfa0-48ab-8bdd-bbbbbbbbbbb2', + ortb2Imp: { + ext: { + ae: 1 + } + } + } + ]; + + let ortbRequest = spec.buildRequests(bids, await addFPDToBidderRequest(bidderRequest)); + let data = JSON.parse(ortbRequest.data); + expect(data.imp[0].ext.ae).to.be.undefined; + }); + }); }); diff --git a/test/spec/modules/ceeIdSystem_spec.js b/test/spec/modules/ceeIdSystem_spec.js new file mode 100644 index 00000000000..23ee258c1b2 --- /dev/null +++ b/test/spec/modules/ceeIdSystem_spec.js @@ -0,0 +1,275 @@ +import { ceeIdSubmodule, storage, readId, fetchCeeIdToken } from 'modules/ceeIdSystem.js'; +import { expect } from 'chai'; +import { server } from 'test/mocks/xhr.js'; +import { logError } from 'src/utils.js'; +import sinon from 'sinon'; + +describe('ceeIdSystem', () => { + let sandbox; + let getCookieStub; + let getDataFromLocalStorageStub; + + const consentData = { + gdprApplies: true, + consentString: 'abc123==' + } + + const MODULE_NAME = 'ceeId'; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + getCookieStub = sandbox.stub(storage, 'getCookie'); + getDataFromLocalStorageStub = sandbox.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('getId', () => { + it('should return an object with id when ceeIdToken is found in LS', () => { + const config = { + name: 'ceeId', + storage: { + type: 'cookie', + name: 'ceeIdToken', + expires: 7, + refreshInSeconds: 360, + }, + params: { + cookieName: 'WPxid', + }, + }; + + getDataFromLocalStorageStub.returns('testToken'); + getCookieStub.returns('testToken'); + + const result = ceeIdSubmodule.getId(config, consentData); + + expect(result).to.deep.equal({ id: 'testToken' }); + }); + + it('should return undefined when ceeIdToken is not found', () => { + const config = { + name: 'ceeId', + storage: { + type: 'cookie', + name: 'ceeIdToken', + expires: 7, + refreshInSeconds: 360, + }, + params: { + cookieName: 'WPxid', + }, + }; + + const result = ceeIdSubmodule.getId(config, consentData); + + expect(result).to.be.undefined; + }); + + it('should call the callback with the response body if status 200 is sent', (done) => { + const config = { + params: { + publisherId: 'testPublisher', + type: 'testType', + value: 'testValue' + } + }; + + const consentData = { + consentString: 'testConsentString' + }; + + const callbackSpy = sinon.spy(); + const callback = ceeIdSubmodule.getId(config, consentData).callback; + callback(callbackSpy); + + const request = server.requests[0]; + expect(request).to.not.be.undefined; + expect(request.url).to.include('https://ceeid.eu/api/token/generate'); + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ value: 'testToken' })); + + setTimeout(() => { + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.args[0]).to.deep.equal('testToken'); + done(); + }, 0); + }); + }); + + describe('fetchCeeIdToken', () => { + it('should resolve with the token if the response is successful', () => { + const consentString = 'testConsentString'; + const requestData = { + publisherId: 'testPublisher', + type: 'testType', + value: 'testValue', + properties: { + consent: consentString + }, + }; + const responseBody = JSON.stringify({ value: 'testToken' }); + + fetchCeeIdToken(requestData).then((token) => { + expect(token).to.equal('testToken'); + }).catch(); + + const request = server.requests[0]; + expect(request).to.not.be.undefined; + expect(request.url).to.equal('https://ceeid.eu/api/token/generate'); + expect(request.method).to.equal('POST'); + expect(request.requestHeaders['Content-Type']).to.equal('application/json'); + expect(request.requestBody).to.equal(JSON.stringify(requestData)); + + request.respond(200, { 'Content-Type': 'application/json' }, responseBody); + }); + + it('should reject if there is no token in the response', () => { + const consentString = 'testConsentString'; + const requestData = { + publisherId: 'testPublisher', + type: 'testType', + value: 'testValue', + properties: { + consent: consentString + }, + }; + const responseBody = JSON.stringify({}); + + fetchCeeIdToken(requestData).then(() => { + throw new Error('Promise should not be resolved'); + }).catch((error) => { + expect(error.message).to.equal('No token in response'); + }); + + setTimeout(() => { + const request = server.requests[0]; + request.respond(200, { 'Content-Type': 'application/json' }, responseBody); + }, 0); + }); + + it('should reject if the response is not valid JSON', () => { + const consentString = 'testConsentString'; + const requestData = { + publisherId: 'testPublisher', + type: 'testType', + value: 'testValue', + properties: { + consent: consentString + }, + }; + const responseBody = 'Invalid JSON'; + + fetchCeeIdToken(requestData).then(() => { + throw new Error('Promise should not be resolved'); + }).catch((error) => { + expect(error.message).to.equal('Unexpected token I in JSON at position 0'); + }); + + setTimeout(() => { + const request = server.requests[0]; + expect(request).to.not.be.undefined; + request.respond(400, { 'Content-Type': 'application/json' }, responseBody); + }, 0); + }); + + it('should reject if the request encounters an error', (done) => { + const consentString = 'testConsentString'; + const requestData = { + publisherId: 'testPublisher', + type: 'testType', + value: 'testValue', + properties: { + consent: consentString + }, + }; + + fetchCeeIdToken(requestData).then(() => { + throw new Error('Promise should not be resolved'); + }).catch((error) => { + expect(error.message).to.equal('Network Error'); + }); + + setTimeout(() => { + const request = server.requests[0]; + expect(request).to.not.be.undefined; + request.error(); + done(); + }, 0); + }); + + it('should handle fetchCeeIdToken network error and log it', (done) => { + const requestData = { + publisherId: 'testPublisher', + type: 'testType', + value: 'testValue' + }; + const logErrorSpy = sinon.spy(logError); + + fetchCeeIdToken(requestData).then(() => { + throw new Error('Promise should not be resolved'); + }).catch((error) => { + expect(error.message).to.equal('Network Error'); + expect(logErrorSpy.calledOnce).to.be.true; + expect(logErrorSpy.calledWith(`${MODULE_NAME}: ID fetch encountered an error`, sinon.match.instanceOf(Error))).to.be.true; + done(); + }); + + setTimeout(() => { + const request = server.requests[0]; + expect(request).to.not.be.undefined; + request.error(); + done(); + }, 0); + }); + }); + + describe('decode', () => { + it('should return an object with ceeId when value is a string', () => { + const result = ceeIdSubmodule.decode('testId'); + + expect(result).to.deep.equal({ ceeId: 'testId' }); + }); + + it('should return undefined when value is not a string', () => { + const result = ceeIdSubmodule.decode({}); + + expect(result).to.be.undefined; + }); + }); + + describe('readId', () => { + it('should return data from local storage when it exists', () => { + const cookieName = 'testToken'; + + getDataFromLocalStorageStub.returns('local_storage_data'); + + const result = readId(cookieName); + + expect(result).to.equal('local_storage_data'); + }); + + it('should return data from cookie when local storage data does not exist', () => { + const cookieName = 'testToken'; + + getDataFromLocalStorageStub.returns(null); + getCookieStub.returns('cookie_data'); + + const result = readId(cookieName); + + expect(result).to.equal('cookie_data'); + }); + + it('should return null when neither local storage data nor cookie data exists', () => { + const cookieName = 'testToken'; + + getDataFromLocalStorageStub.returns(null); + getCookieStub.returns(null); + + const result = readId(cookieName); + + expect(result).to.be.null; + }); + }); +}); diff --git a/test/spec/modules/cleanioRtdProvider_spec.js b/test/spec/modules/cleanioRtdProvider_spec.js index 3145108c373..0211a9ae588 100644 --- a/test/spec/modules/cleanioRtdProvider_spec.js +++ b/test/spec/modules/cleanioRtdProvider_spec.js @@ -5,6 +5,7 @@ import * as events from '../../../src/events.js'; import { EVENTS } from '../../../src/constants.js'; import { __TEST__ } from '../../../modules/cleanioRtdProvider.js'; +import {MODULE_TYPE_RTD} from '../../../src/activities/modules.js'; const { readConfig, @@ -70,7 +71,7 @@ describe('clean.io RTD module', function () { pageInitStepProtectPage(fakeScriptURL); sinon.assert.calledOnce(loadExternalScriptStub); - sinon.assert.calledWith(loadExternalScriptStub, fakeScriptURL, 'clean.io'); + sinon.assert.calledWith(loadExternalScriptStub, fakeScriptURL, MODULE_TYPE_RTD, 'clean.io'); }); }); @@ -139,7 +140,7 @@ describe('clean.io RTD module', function () { const { init, onBidResponseEvent } = getModule(); expect(init({ params: { cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', protectionMode: 'full' } }, {})).to.equal(true); sinon.assert.calledOnce(loadExternalScriptStub); - sinon.assert.calledWith(loadExternalScriptStub, 'https://abc1234567890.cloudfront.net/script.js', 'clean.io'); + sinon.assert.calledWith(loadExternalScriptStub, 'https://abc1234567890.cloudfront.net/script.js', MODULE_TYPE_RTD, 'clean.io'); const fakeBidResponse = makeFakeBidResponse(); onBidResponseEvent(fakeBidResponse, {}, {}); diff --git a/test/spec/modules/cleanmedianetBidAdapter_spec.js b/test/spec/modules/cleanmedianetBidAdapter_spec.js index 3c73dac07de..58270438772 100644 --- a/test/spec/modules/cleanmedianetBidAdapter_spec.js +++ b/test/spec/modules/cleanmedianetBidAdapter_spec.js @@ -376,7 +376,7 @@ describe('CleanmedianetAdapter', () => { const bidRequestWithVideo = utils.deepClone(bidRequest); bidRequestWithVideo.params.video = { - placement: 1, + plcmt: 1, minduration: 1, } @@ -395,7 +395,7 @@ describe('CleanmedianetAdapter', () => { expect(response.data.imp[0].video.mimes).to.equal(bidRequestWithVideo.mediaTypes.video.mimes); expect(response.data.imp[0].video.skip).to.not.exist; - expect(response.data.imp[0].video.placement).to.equal(1); + expect(response.data.imp[0].video.plcmt).to.equal(1); expect(response.data.imp[0].video.minduration).to.equal(1); expect(response.data.imp[0].video.playbackmethod).to.equal(1); expect(response.data.imp[0].video.startdelay).to.equal(1); @@ -405,7 +405,7 @@ describe('CleanmedianetAdapter', () => { playerSize: [302, 252], mimes: ['video/mpeg'], skip: 1, - placement: 1, + plcmt: 1, minduration: 1, playbackmethod: 1, startdelay: 1, @@ -428,7 +428,7 @@ describe('CleanmedianetAdapter', () => { context: 'instream', mimes: ['video/mpeg'], skip: 1, - placement: 1, + plcmt: 1, minduration: 1, playbackmethod: 1, startdelay: 1, @@ -457,7 +457,7 @@ describe('CleanmedianetAdapter', () => { context: 'instream', mimes: ['video/mpeg'], skip: 1, - placement: 1, + plcmt: 1, minduration: 1, playbackmethod: 1, startdelay: 1, diff --git a/test/spec/modules/clickforceBidAdapter_spec.js b/test/spec/modules/clickforceBidAdapter_spec.js index c4c4d77e954..99aef433890 100644 --- a/test/spec/modules/clickforceBidAdapter_spec.js +++ b/test/spec/modules/clickforceBidAdapter_spec.js @@ -31,12 +31,12 @@ describe('ClickforceAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'someIncorrectParam': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/colombiaBidAdapter_spec.js b/test/spec/modules/colombiaBidAdapter_spec.js new file mode 100644 index 00000000000..1b61e1a92b4 --- /dev/null +++ b/test/spec/modules/colombiaBidAdapter_spec.js @@ -0,0 +1,155 @@ +import { expect } from 'chai'; +import { spec } from 'modules/colombiaBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +const HOST_NAME = document.location.protocol + '//' + window.location.host; +const ENDPOINT = 'https://ade.clmbtech.com/cde/prebid.htm'; + +describe('colombiaBidAdapter', function() { + const adapter = newBidder(spec); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '23beaa6af6cdde', + 'bidderRequestId': '19c0c1efdf37e7', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when placementId not passed correctly', function () { + bid.params.placementId = ''; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', function () { + let invalidBid = Object.assign({}, bid); + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code1', + 'sizes': [ + [300, 250] + ], + 'bidId': '23beaa6af6cdde', + 'bidderRequestId': '19c0c1efdf37e7', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + }, + { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code2', + 'sizes': [ + [300, 250] + ], + 'bidId': '382091349b149f"', + 'bidderRequestId': '"1f9c98192de2511"', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + } + ]; + let bidderRequest = { + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'http://example.com', + stack: ['http://example.com'] + } + }; + + const request = spec.buildRequests(bidRequests); + it('sends bid request to our endpoint via POST', function () { + expect(request[0].method).to.equal('POST'); + }); + + it('attaches source and version to endpoint URL as query params', function () { + expect(request[0].url).to.equal(ENDPOINT); + }); + }); + + describe('interpretResponse', function () { + let bidRequest = [ + { + 'method': 'POST', + 'url': 'https://ade.clmbtech.com/cde/prebid.htm', + 'data': { + 'v': 'hb1', + 'p': '307466', + 'w': '300', + 'h': '250', + 'cb': 12892917383, + 'r': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836', + 'uid': '23beaa6af6cdde', + 't': 'i', + } + } + ]; + + let serverResponse = [{ + 'ad': '
This is test case for colombia adapter
', + 'cpm': 3.14, + 'creativeId': '6b958110-612c-4b03-b6a9-7436c9f746dc-1sk24', + 'currency': 'USD', + 'requestId': '23beaa6af6cdde', + 'width': 728, + 'height': 90, + 'netRevenue': true, + 'ttl': 600, + 'dealid': '', + 'referrer': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836' + }]; + + it('should get the correct bid response', function () { + let expectedResponse = [{ + 'requestId': '23beaa6af6cdde', + 'cpm': 3.14, + 'width': 728, + 'height': 90, + 'creativeId': '6b958110-612c-4b03-b6a9-7436c9f746dc-1sk24', + 'dealId': '', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'referrer': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836', + 'ad': '
This is test case for colombia adapter
' + }]; + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + }); + + it('handles empty bid response', function () { + let response = { + body: { + 'uid': '23beaa6af6cdde', + 'height': 0, + 'creativeId': '', + 'statusMessage': 'Bid returned empty or error response', + 'width': 0, + 'cpm': 0 + } + }; + let result = spec.interpretResponse(response, bidRequest[0]); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index ebe1e9be4d4..22a98df633f 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -257,7 +257,6 @@ describe('ColossussspAdapter', function () { describe('buildRequests with user ids', function () { var clonedBid = JSON.parse(JSON.stringify(bid)); clonedBid.userId = {} - clonedBid.userId.britepoolid = 'britepoolid123'; clonedBid.userId.idl_env = 'idl_env123'; clonedBid.userId.tdid = 'tdid123'; clonedBid.userId.id5id = { uid: 'id5id123' }; @@ -303,11 +302,11 @@ describe('ColossussspAdapter', function () { let placement = placements[i]; expect(placement).to.have.property('eids') expect(placement.eids).to.be.an('array') - expect(placement.eids.length).to.be.equal(8) + expect(placement.eids.length).to.be.equal(7) for (let index in placement.eids) { let v = placement.eids[index]; expect(v).to.have.all.keys('source', 'uids') - expect(v.source).to.be.oneOf(['pubcid.org', 'adserver.org', 'neustar.biz', 'britepool.com', 'identityLink', 'id5-sync.com', 'adserver.org', 'uidapi.com']) + expect(v.source).to.be.oneOf(['pubcid.org', 'adserver.org', 'neustar.biz', 'identityLink', 'id5-sync.com', 'adserver.org', 'uidapi.com']) expect(v.uids).to.be.an('array'); expect(v.uids.length).to.be.equal(1) expect(v.uids[0]).to.have.property('id') diff --git a/test/spec/modules/compassBidAdapter_spec.js b/test/spec/modules/compassBidAdapter_spec.js index 6a761e63ea1..48be231d7d4 100644 --- a/test/spec/modules/compassBidAdapter_spec.js +++ b/test/spec/modules/compassBidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/compassBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'compass' +const bidder = 'compass'; describe('CompassBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('CompassBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('CompassBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('CompassBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -73,9 +86,20 @@ describe('CompassBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -112,6 +136,7 @@ describe('CompassBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -120,7 +145,11 @@ describe('CompassBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -129,7 +158,7 @@ describe('CompassBidAdapter', function () { 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('string'); + 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); @@ -145,6 +174,56 @@ describe('CompassBidAdapter', function () { 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 + } + ]; + + let 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'); @@ -170,8 +249,10 @@ describe('CompassBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -186,12 +267,38 @@ describe('CompassBidAdapter', function () { 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -395,5 +502,17 @@ describe('CompassBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://sa-cs.deliverimp.com/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sa-cs.deliverimp.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js index 0a76ed3e62d..2fb43236081 100644 --- a/test/spec/modules/concertBidAdapter_spec.js +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -249,6 +249,22 @@ describe('ConcertAdapter', function () { }); }); + it('should include dealId when present in bidResponse', function() { + const bids = spec.interpretResponse({ + body: { + bids: [ + { ...bidResponse.body.bids[0], dealid: 'CON-123' } + ] + } + }, bidRequest); + expect(bids[0]).to.have.property('dealId'); + }); + + it('should exclude dealId when absent in bidResponse', function() { + const bids = spec.interpretResponse(bidResponse, bidRequest); + expect(bids[0]).to.not.have.property('dealId'); + }); + it('should return empty bids if there is no response from server', function() { const bids = spec.interpretResponse({ body: null }, bidRequest); expect(bids).to.have.lengthOf(0); diff --git a/test/spec/modules/condorxBidAdapter_spec.js b/test/spec/modules/condorxBidAdapter_spec.js new file mode 100644 index 00000000000..cd0873dd1b0 --- /dev/null +++ b/test/spec/modules/condorxBidAdapter_spec.js @@ -0,0 +1,257 @@ +import { expect } from 'chai'; +import { bidderSpec as adapterSpec } from 'modules/condorxBidAdapter.js'; +import * as utils from 'src/utils.js'; + +describe('CondorX Bid Adapter Tests', function () { + let basicBidRequests; + let nativeBidData; + const defaultRequestParams = { + widget: 274572, + website: 195491, + url: 'current url' + }; + + beforeEach(function () { + basicBidRequests = [ + { + bidder: 'condorx', + params: defaultRequestParams + } + ]; + + nativeBidData = [ + { + bidder: 'condorx', + params: defaultRequestParams, + nativeParams: { + title: { + required: true, + len: 100 + }, + image: { + required: true, + sizes: [100, 100] + }, + sponsoredBy: { + required: true + } + } + } + ]; + }); + + describe('Bid Size Validation', function () { + const bid = { + bidder: 'condorx', + params: defaultRequestParams + }; + + it('should accept 300x250 size', function () { + bid.sizes = [[300, 250]]; + const isValid = adapterSpec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('should accept 100x100 size', function () { + bid.sizes = [[100, 100]]; + const isValid = adapterSpec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('should accept 600x600 size', function () { + bid.sizes = [[600, 600]]; + const isValid = adapterSpec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + }); + + describe('Bid Request Validation', function () { + it('should validate a correct bid request', function () { + const validBid = { + bidder: 'condorx', + params: defaultRequestParams, + sizes: [[300, 250]] + }; + const isValid = adapterSpec.isBidRequestValid(validBid); + expect(isValid).to.be.true; + }); + + it('should invalidate an empty params bid request', function () { + const invalidBid = { + bidder: 'condorx', + params: {} + }; + const isValid = adapterSpec.isBidRequestValid(invalidBid); + expect(isValid).to.be.false; + }); + + it('should invalidate a bid request with invalid parameters', function () { + const invalidBid = { + bidder: 'condorx', + params: { + widget: '55765a', // Invalid value for widget + website: 195491, + url: 'current url' + }, + sizes: [[300, 250]] + }; + const isValid = adapterSpec.isBidRequestValid(invalidBid); + expect(isValid).to.be.false; + }); + }); + + describe('Request Building and HTTP Calls', function () { + it('should verify the API HTTP method', function () { + const request = adapterSpec.buildRequests(basicBidRequests)[0]; + expect(request.url).to.include('https://api.condorx.io/cxb/get.json'); + expect(request.method).to.equal('GET'); + }); + + it('should not mutate the original bid object', function () { + const originalBidRequests = utils.deepClone(basicBidRequests); + const request = adapterSpec.buildRequests(basicBidRequests); + expect(basicBidRequests).to.deep.equal(originalBidRequests); + }); + + it('should maintain the integrity of the native bid object', function () { + const originalBidRequests = utils.deepClone(nativeBidData); + const request = adapterSpec.buildRequests(nativeBidData); + expect(nativeBidData).to.deep.equal(originalBidRequests); + }); + + it('should correctly extract and validate request parameters', function () { + const request = adapterSpec.buildRequests(basicBidRequests)[0]; + const urlParams = new URL(request.url).searchParams; + expect(parseInt(urlParams.get('wg'))).to.exist.and.to.equal(basicBidRequests[0].params.widget); + expect(parseInt(urlParams.get('w'))).to.exist.and.to.equal(basicBidRequests[0].params.website); + }); + + it('should validate the custom URL parameter', function () { + const customUrl = 'https://i.am.url'; + basicBidRequests[0].params.url = customUrl; + const request = adapterSpec.buildRequests(basicBidRequests)[0]; + const urlParams = new URL(request.url).searchParams; + expect(urlParams.get('u')).to.exist.and.to.equal(customUrl); + }); + }); + + describe('Response Validation', function () { + let nativeResponseData; + let bannerResponseData; + + beforeEach(() => { + const baseResponse = { + tiles: [ + { + postId: '12345', + imageUrl: 'https://cdn.condorx.io/img/condorx_logo_500.jpg', + domain: 'condorx.test', + title: 'Test title', + clickUrl: '//click.test', + pecpm: 0.5, + url: '//url.test', + displayName: 'Test sponsoredBy', + trackers: { + impressionPixels: ['//impression.test'], + viewPixels: ['//view.test'], + } + } + ], + imageWidth: 300, + imageHeight: 250, + ireqId: 'condorx121212', + widgetViewPixel: '//view.pixel', + }; + + nativeResponseData = { + ...baseResponse, + pbtypeId: 1, + }; + + bannerResponseData = { + ...baseResponse, + pbtypeId: 2, + widget: { + config: '{"css":".__condorx_banner_title{display:block!important;}"}' + }, + }; + }); + + it('should return an empty array for missing response', function () { + const result = adapterSpec.interpretResponse({}, []); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should return an empty array for no bids', function () { + const noBidsResponse = { + tiles: [], + imageWidth: 300, + imageHeight: 250, + ireqId: 'condorx121212', + pbtypeId: 2, + widgetViewPixel: '//view.pixel', + }; + const request = adapterSpec.buildRequests(basicBidRequests)[0]; + const result = adapterSpec.interpretResponse({ body: noBidsResponse }, request); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should correctly interpret a native response', function () { + const expectedNativeResult = [ + { + requestId: 'condorx121212', + cpm: 0.5, + width: 300, + height: 250, + netRevenue: true, + currency: 'USD', + creativeId: '12345', + ttl: 360, + meta: { + advertiserDomains: ['condorx.test'] + }, + native: { + title: 'Test title', + body: '', + image: { + url: 'https://cdn.condorx.io/img/condorx_logo_500.jpg', + width: 300, + height: 250 + }, + privacyLink: '', + clickUrl: '//click.test', + displayUrl: '//url.test', + cta: '', + sponsoredBy: 'Test sponsoredBy', + impressionTrackers: ['//impression.test', '//view.test', '//view.pixel'], + }, + } + ]; + const request = adapterSpec.buildRequests(basicBidRequests)[0]; + const result = adapterSpec.interpretResponse({ body: nativeResponseData }, request); + expect(result).to.deep.equal(expectedNativeResult); + }); + + it('should correctly interpret a banner response', function () { + const expectedBannerResult = [ + { + requestId: 'condorx121212', + cpm: 0.5, + width: 300, + height: 250, + netRevenue: true, + currency: 'USD', + creativeId: '12345', + ttl: 360, + meta: { + advertiserDomains: ['condorx.test'] + }, + ad: `
`, + } + ]; + const request = adapterSpec.buildRequests(basicBidRequests)[0]; + const result = adapterSpec.interpretResponse({ body: bannerResponseData }, request); + expect(result).to.deep.equal(expectedBannerResult); + }); + }); +}); diff --git a/test/spec/modules/connatixBidAdapter_spec.js b/test/spec/modules/connatixBidAdapter_spec.js index 4d816c4e816..62c730c7cc4 100644 --- a/test/spec/modules/connatixBidAdapter_spec.js +++ b/test/spec/modules/connatixBidAdapter_spec.js @@ -1,9 +1,25 @@ import { expect } from 'chai'; +import sinon from 'sinon'; import { + _getBidRequests, + _canSelectViewabilityContainer as connatixCanSelectViewabilityContainer, + detectViewability as connatixDetectViewability, + getBidFloor as connatixGetBidFloor, + _getMinSize as connatixGetMinSize, + _getViewability as connatixGetViewability, + hasQueryParams as connatixHasQueryParams, + _isViewabilityMeasurable as connatixIsViewabilityMeasurable, + readFromAllStorages as connatixReadFromAllStorages, + saveOnAllStorages as connatixSaveOnAllStorages, spec, - getBidFloor as connatixGetBidFloor + storage } from '../../../modules/connatixBidAdapter.js'; -import { BANNER } from '../../../src/mediaTypes.js'; +import adapterManager from '../../../src/adapterManager.js'; +import * as ajax from '../../../src/ajax.js'; +import { ADPOD, BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import * as utils from '../../../src/utils.js'; + +const BIDDER_CODE = 'connatix'; describe('connatixBidAdapter', function () { let bid; @@ -24,6 +40,484 @@ describe('connatixBidAdapter', function () { }; }; + function addVideoToBidMock(bid) { + const mediaTypes = { + video: { + context: 'instream', + w: 1280, + h: 720, + playerSize: [1280, 720], + placement: 1, + plcmt: 1, + api: [1, 2], + mimes: ['video/mp4', 'application/javascript'], + minduration: 30, + maxduration: 60, + startdelay: 0, + } + } + + bid.mediaTypes = mediaTypes; + } + + describe('connatixGetMinSize', () => { + it('should return the smallest size based on area', () => { + const sizes = [ + { w: 300, h: 250 }, + { w: 728, h: 90 }, + { w: 160, h: 600 } + ]; + const result = connatixGetMinSize(sizes); + expect(result).to.deep.equal({ w: 728, h: 90 }); + }); + + it('should handle an array with one size', () => { + const sizes = [{ w: 300, h: 250 }]; + const result = connatixGetMinSize(sizes); + expect(result).to.deep.equal({ w: 300, h: 250 }); + }); + + it('should handle empty array', () => { + const sizes = []; + const result = connatixGetMinSize(sizes); + expect(result).to.be.undefined; + }); + }); + + describe('_isIframe', () => { + let querySelectorStub; + + beforeEach(() => { + querySelectorStub = sinon.stub(window.top.document, 'querySelector'); + }); + + afterEach(() => { + querySelectorStub.restore(); + }); + + it('should return true when window.top.document.querySelector does not throw an error', () => { + querySelectorStub.returns({}); + expect(connatixCanSelectViewabilityContainer()).to.be.true; + }); + + it('should return false when window.top.document.querySelector throws an error', () => { + querySelectorStub.throws(new Error('test error')); + expect(connatixCanSelectViewabilityContainer()).to.be.false; + }); + }); + + describe('_isViewabilityMeasurable', () => { + let querySelectorStub; + + beforeEach(() => { + querySelectorStub = sinon.stub(window.top.document, 'querySelector'); + }); + + afterEach(() => { + querySelectorStub.restore(); + }); + + it('should return false if the element is null or undefined', () => { + expect(connatixIsViewabilityMeasurable(null)).to.be.false; + expect(connatixIsViewabilityMeasurable(undefined)).to.be.false; + }); + + it('should return false if _isIframe returns true', () => { + querySelectorStub.throws(new Error('test error')); + + const element = document.createElement('div'); + expect(connatixIsViewabilityMeasurable(element)).to.be.false; + }); + + it('should return true if _isIframe returns false', () => { + querySelectorStub.returns(document.createElement('div')) + + const element = document.createElement('div'); + expect(connatixIsViewabilityMeasurable(element)).to.be.true; + }); + }); + + describe('_getViewability', () => { + let element; + let getBoundingClientRectStub; + let topWinMock; + + beforeEach(() => { + element = document.createElement('div'); + getBoundingClientRectStub = sinon.stub(element, 'getBoundingClientRect'); + + topWinMock = { + document: { + visibilityState: 'visible' + }, + innerWidth: 800, + innerHeight: 600 + }; + }); + + afterEach(() => { + getBoundingClientRectStub.restore(); + }); + + it('should return 0 if the document is not visible', () => { + topWinMock.document.visibilityState = 'hidden'; + + const viewability = connatixGetViewability(element, topWinMock); + + expect(viewability).to.equal(0); + }); + + it('should return 100% if the element is fully in view', () => { + const boundingBox = { left: 100, top: 100, right: 300, bottom: 300, width: 200, height: 200 }; + getBoundingClientRectStub.returns(boundingBox); + + const viewability = connatixGetViewability(element, topWinMock); + + expect(viewability).to.equal(100); + }); + + it('should return the correct percentage if the element is partially in view', () => { + const boundingBox = { left: 700, top: 500, right: 900, bottom: 700, width: 200, height: 200 }; + getBoundingClientRectStub.returns(boundingBox); + const getWinDimensionsStub = sinon.stub(utils, 'getWinDimensions'); + getWinDimensionsStub.returns({ innerWidth: topWinMock.innerWidth, innerHeight: topWinMock.innerHeight}); + + const viewability = connatixGetViewability(element, topWinMock); + + expect(viewability).to.equal(25); // 100x100 / 200x200 = 0.25 -> 25% + getWinDimensionsStub.restore(); + }); + + it('should return 0% if the element is not in view', () => { + const getWinDimensionsStub = sinon.stub(utils, 'getWinDimensions'); + getWinDimensionsStub.returns({ innerWidth: topWinMock.innerWidth, innerHeight: topWinMock.innerHeight}); + const boundingBox = { left: 900, top: 700, right: 1100, bottom: 900, width: 200, height: 200 }; + getBoundingClientRectStub.returns(boundingBox); + + const viewability = connatixGetViewability(element, topWinMock); + + expect(viewability).to.equal(0); + getWinDimensionsStub.restore(); + }); + + it('should use provided width and height if element dimensions are zero', () => { + const boundingBox = { left: 100, top: 100, right: 100, bottom: 100, width: 0, height: 0 }; + getBoundingClientRectStub.returns(boundingBox); + + const dimensions = { w: 200, h: 200 }; + const viewability = connatixGetViewability(element, topWinMock, dimensions); + + expect(viewability).to.equal(100); // Element fully in view with provided dimensions + }); + }); + + describe('detectViewability', () => { + let element; + let getBoundingClientRectStub; + let topWinMock; + let querySelectorStub; + let getElementByIdStub; + + beforeEach(() => { + element = document.createElement('div'); + getBoundingClientRectStub = sinon.stub(element, 'getBoundingClientRect'); + + topWinMock = { + document: { + visibilityState: 'visible' + }, + innerWidth: 800, + innerHeight: 600 + }; + + querySelectorStub = sinon.stub(window.top.document, 'querySelector'); + getElementByIdStub = sinon.stub(document, 'getElementById'); + }); + + afterEach(() => { + getBoundingClientRectStub.restore(); + querySelectorStub.restore(); + getElementByIdStub.restore(); + }); + + it('should return 100% viewability when the element is fully within view and has a valid viewabilityContainerIdentifier', () => { + const bid = { + params: { viewabilityContainerIdentifier: '#validElement' }, + adUnitCode: 'adUnitCode123', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + sizes: [[300, 250]] + }; + + getBoundingClientRectStub.returns({ + left: 100, + top: 100, + right: 400, + bottom: 350, + width: 300, + height: 250 + }); + + querySelectorStub.withArgs('#validElement').returns(element); + getElementByIdStub.returns(null); + + const result = connatixDetectViewability(bid); + + // Expected calculation: the element is fully in view, so 100% viewability + expect(result).to.equal(100); + }); + + it('should fall back to using bid sizes and adUnitCode when the viewabilityContainerIdentifier is invalid or was not provided', () => { + const bid = { + params: { viewabilityContainerIdentifier: '#invalidElement' }, + adUnitCode: 'adUnitCode123', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + sizes: [[300, 250]] + }; + + getBoundingClientRectStub.returns({ + left: 200, + top: 100, + right: 500, + bottom: 350, + width: 300, + height: 250 + }); + + querySelectorStub.withArgs('#invalidElement').returns(null); + getElementByIdStub.withArgs('adUnitCode123').returns(element); + + const result = connatixDetectViewability(bid); + + expect(result).to.equal(100); // Full viewability + }); + + it('should use the adUnitCode as a fallback when querying an element fails due to a browser error, and return 100% viewability because adUnitCode container is fully in view', () => { + const bid = { + params: { viewabilityContainerIdentifier: '#invalidElement' }, + adUnitCode: 'adUnitCode123', + sizes: [[300, 250]] + }; + + // Simulate an error when querying the element + querySelectorStub.withArgs('#invalidElement').throws(new Error('Query failed')); + + getBoundingClientRectStub.returns({ + left: 100, + top: 100, + right: 400, + bottom: 350, + width: 300, + height: 250 + }); + + // The fallback should use the adUnitCode to find the element + getElementByIdStub.withArgs('adUnitCode123').returns(element); + + const result = connatixDetectViewability(bid); + + expect(result).to.equal(100); // Expect the fallback to work and return 100% viewability + }); + + it('should return null when querying the element by the provided identifier fails and the adUnitCode viewability container is unavailable', () => { + const bid = { + params: { viewabilityContainerIdentifier: '#invalidElement' }, + adUnitCode: 'adUnitCode123', + sizes: [[300, 250]] + }; + + // Simulate an error when querying the element + querySelectorStub.withArgs('#invalidElement').throws(new Error('Query failed')); + + getBoundingClientRectStub.returns({ + left: 100, + top: 100, + right: 400, + bottom: 350, + width: 300, + height: 250 + }); + + const result = connatixDetectViewability(bid); + + expect(result).to.equal(null); + }); + }); + + describe('_getBidRequests', function () { + let bid; + + // Mock a bid request similar to the one already used in connatixBidAdapter tests + function mockBidRequest() { + const mediaTypes = { + banner: { + sizes: [16, 9], + } + }; + return { + bidId: 'testing', + bidder: 'connatix', + params: { + placementId: '30e91414-545c-4f45-a950-0bec9308ff22', + viewabilityContainerIdentifier: 'viewabilityId', + }, + mediaTypes, + sizes: [300, 250] + }; + } + + it('should map valid bid requests and include the expected fields', function () { + bid = mockBidRequest(); + + const result = _getBidRequests([bid]); + + expect(result).to.have.lengthOf(1); + expect(result[0]).to.have.property('bidId', bid.bidId); + expect(result[0]).to.have.property('mediaTypes', bid.mediaTypes); + expect(result[0]).to.have.property('sizes', bid.sizes); + expect(result[0]).to.have.property('placementId', bid.params.placementId); + expect(result[0]).to.have.property('hasViewabilityContainerId', true); + }); + + it('should set hasViewabilityContainerId to false when viewabilityContainerIdentifier is absent', function () { + bid = mockBidRequest(); + delete bid.params.viewabilityContainerIdentifier; + + const result = _getBidRequests([bid]); + + expect(result[0]).to.have.property('hasViewabilityContainerId', false); + }); + + it('should call getBidFloor for each bid and return the correct floor value', function () { + bid = mockBidRequest(); + const floorValue = 5; + + // Mock getFloor method on bid + bid.getFloor = function() { + return { floor: floorValue }; + }; + + const result = _getBidRequests([bid]); + + expect(result[0]).to.have.property('floor', floorValue); + }); + + it('should return floor as 0 if getBidFloor throws an error', function () { + bid = mockBidRequest(); + + // Mock getFloor method to throw an error + bid.getFloor = function() { + throw new Error('error'); + }; + + const result = _getBidRequests([bid]); + + expect(result[0]).to.have.property('floor', 0); + }); + }); + + describe('onTimeout', function () { + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'triggerEvent') + }) + + afterEach(() => { + ajaxStub.restore() + }); + + it('call event if bidder is connatix', () => { + const result = spec.onTimeout([{ + bidder: 'connatix', + timeout: 500, + }]); + expect(ajaxStub.calledOnce).to.equal(true); + + const data = ajaxStub.firstCall.args[0]; + expect(data.type).to.equal('Timeout'); + expect(data.timeout).to.equal(500); + }); + + it('timeout event is not triggered if bidder is not connatix', () => { + const result = spec.onTimeout([{ + bidder: 'otherBidder', + timeout: 500, + }]); + expect(ajaxStub.notCalled).to.equal(true); + }); + }); + + describe('onBidWon', function () { + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'triggerEvent'); + }); + + afterEach(() => { + ajaxStub.restore(); + }); + + it('calls triggerEvent with correct data when bidWinData is provided', () => { + const bidWinData = { + bidder: 'connatix', + cpm: 2.5, + requestId: 'abc123', + bidId: 'dasdas-dsawda-dwaddw-dwdwd', + adUnitCode: 'adunit_1', + timeToRespond: 300, + auctionId: 'auction_456', + }; + + spec.onBidWon(bidWinData); + expect(ajaxStub.calledOnce).to.equal(true); + + const eventData = ajaxStub.firstCall.args[0]; + expect(eventData.type).to.equal('BidWon'); + expect(eventData.bestBidBidder).to.equal('connatix'); + expect(eventData.bestBidPrice).to.equal(2.5); + expect(eventData.requestId).to.equal('abc123'); + expect(eventData.bidId).to.equal('dasdas-dsawda-dwaddw-dwdwd'); + expect(eventData.adUnitCode).to.equal('adunit_1'); + expect(eventData.timeToRespond).to.equal(300); + expect(eventData.auctionId).to.equal('auction_456'); + }); + + it('does not call triggerEvent if bidWinData is null', () => { + spec.onBidWon(null); + expect(ajaxStub.notCalled).to.equal(true); + }); + + it('does not call triggerEvent if bidWinData is undefined', () => { + spec.onBidWon(undefined); + expect(ajaxStub.notCalled).to.equal(true); + }); + }); + + describe('triggerEvent', function () { + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(ajax, 'ajax'); + }); + + afterEach(() => { + ajaxStub.restore(); + }); + + it('should call ajax with the correct parameters', () => { + const data = { type: 'BidWon', bestBidBidder: 'bidder1', bestBidPrice: 1.5, requestId: 'req123', adUnitCode: 'ad123', timeToRespond: 250, auctionId: 'auc123', context: {} }; + spec.triggerEvent(data); + + expect(ajaxStub.calledOnce).to.equal(true); + const [url, _, payload, options] = ajaxStub.firstCall.args; + expect(url).to.equal('https://capi.connatix.com/tr/am'); + expect(payload).to.equal(JSON.stringify(data)); + expect(options.method).to.equal('POST'); + expect(options.withCredentials).to.equal(false); + }); + }); + describe('isBidRequestValid', function () { this.beforeEach(function () { bid = mockBidRequest(); @@ -32,10 +526,6 @@ describe('connatixBidAdapter', function () { it('Should return true if all required fileds are present', function () { expect(spec.isBidRequestValid(bid)).to.be.true; }); - it('Should return false if bidder does not correspond', function () { - bid.bidder = 'abc'; - expect(spec.isBidRequestValid(bid)).to.be.false; - }); it('Should return false if bidId is missing', function () { delete bid.bidId; expect(spec.isBidRequestValid(bid)).to.be.false; @@ -52,7 +542,7 @@ describe('connatixBidAdapter', function () { delete bid.mediaTypes; expect(spec.isBidRequestValid(bid)).to.be.false; }); - it('Should return false if banner is missing from mediaTypes ', function () { + it('Should return false if both banner and video are missing from mediaTypes', function () { delete bid.mediaTypes.banner; expect(spec.isBidRequestValid(bid)).to.be.false; }); @@ -68,6 +558,15 @@ describe('connatixBidAdapter', function () { bid.mediaTypes.banner.sizes = []; expect(spec.isBidRequestValid(bid)).to.be.false; }); + it('Should return true if video is set correctly', function () { + addVideoToBidMock(bid); + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if context is set to adpod on video media type', function() { + addVideoToBidMock(bid); + bid.mediaTypes.video.context = ADPOD; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); it('Should return true if add an extra field was added to the bidRequest', function () { bid.params.test = 1; expect(spec.isBidRequestValid(bid)).to.be.true; @@ -76,6 +575,7 @@ describe('connatixBidAdapter', function () { describe('buildRequests', function () { let serverRequest; + let setCookieStub, setDataInLocalStorageStub; let bidderRequest = { refererInfo: { canonicalUrl: '', @@ -104,10 +604,21 @@ describe('connatixBidAdapter', function () { }; this.beforeEach(function () { + const mockIdentityProviderData = { mockKey: 'mockValue' }; + const CNX_IDS_EXPIRY = 24 * 30 * 60 * 60 * 1000; + setCookieStub = sinon.stub(storage, 'setCookie'); + setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); + connatixSaveOnAllStorages('test_ids_cnx', mockIdentityProviderData, CNX_IDS_EXPIRY); + bid = mockBidRequest(); serverRequest = spec.buildRequests([bid], bidderRequest); }) + this.afterEach(function() { + setCookieStub.restore(); + setDataInLocalStorageStub.restore(); + }); + it('Creates a ServerRequest object with method, URL and data', function () { expect(serverRequest).to.exist; expect(serverRequest.method).to.exist; @@ -134,6 +645,7 @@ describe('connatixBidAdapter', function () { expect(serverRequest.data.uspConsent).to.equal(bidderRequest.uspConsent); expect(serverRequest.data.gppConsent).to.equal(bidderRequest.gppConsent); expect(serverRequest.data.ortb2).to.equal(bidderRequest.ortb2); + expect(serverRequest.data.identityProviderData).to.deep.equal({ mockKey: 'mockValue' }); }); }); @@ -197,12 +709,37 @@ describe('connatixBidAdapter', function () { expect(bidResponses[0].cpm).to.equal(firstBidCpm); expect(bidResponses[1].cpm).to.equal(secondBidCpm); }); + + it('Should contain specific values for banner bids', function () { + const adHtml = 'ad html' + serverResponse.body.Bids = [ { ...Bid, Ad: adHtml } ]; + + const bidResponses = spec.interpretResponse(serverResponse); + const [ bidResponse ] = bidResponses; + + expect(bidResponse.vastXml).to.be.undefined; + expect(bidResponse.ad).to.equal(adHtml); + expect(bidResponse.mediaType).to.equal(BANNER); + }); + + it('Should contain specific values for video bids', function () { + const adVastXml = 'ad vast xml' + serverResponse.body.Bids = [ { ...Bid, VastXml: adVastXml } ]; + + const bidResponses = spec.interpretResponse(serverResponse); + const [ bidResponse ] = bidResponses; + + expect(bidResponse.ad).to.be.undefined; + expect(bidResponse.vastXml).to.equal(adVastXml); + expect(bidResponse.mediaType).to.equal(VIDEO); + }); }); describe('getUserSyncs', function() { const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; const UserSyncEndpoint = 'https://connatix.com/sync' + const UserSyncEndpointWithParams = 'https://connatix.com/sync?param1=value1' const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; const serverResponse = { @@ -212,6 +749,13 @@ describe('connatixBidAdapter', function () { }, headers: function() { } }; + const serverResponse2 = { + body: { + UserSyncEndpoint: UserSyncEndpointWithParams, + Bids: [ Bid ] + }, + headers: function() { } + }; it('Should return an empty array when iframeEnabled: false', function () { expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; @@ -301,6 +845,88 @@ describe('connatixBidAdapter', function () { const { url } = userSyncList[0]; expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); }); + it('Should correctly append all consents to the sync url if the url contains query params', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse2], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + {consent: '1'} + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpointWithParams}&gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + }); + + describe('userIdAsEids', function() { + let validBidRequests; + + this.beforeEach(function () { + bid = mockBidRequest(); + validBidRequests = [bid]; + }) + + it('Connatix adapter reads EIDs from Prebid user models and adds it to Request', function() { + validBidRequests[0].userIdAsEids = [{ + 'source': 'adserver.org', + 'uids': [{ + 'id': 'TTD_ID_FROM_USER_ID_MODULE', + 'atype': 1, + 'ext': { + 'stype': 'ppuid', + 'rtiPartner': 'TDID' + } + }] + }, + { + 'source': 'pubserver.org', + 'uids': [{ + 'id': 'TDID_FROM_USER_ID_MODULE', + 'atype': 1 + }] + }]; + let serverRequest = spec.buildRequests(validBidRequests, {}); + expect(serverRequest.data.userIdList).to.deep.equal(validBidRequests[0].userIdAsEids); + }); + }); + + describe('isConnatix', function () { + let aliasRegistryStub; + + beforeEach(() => { + aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry').value({}); + }); + + afterEach(() => { + aliasRegistryStub.restore(); + }); + + it('should return false if aliasName is undefined or null', () => { + expect(spec.isConnatix(undefined)).to.be.false; + expect(spec.isConnatix(null)).to.be.false; + }); + + it('should return true if aliasName matches BIDDER_CODE', () => { + const aliasName = BIDDER_CODE; + expect(spec.isConnatix(aliasName)).to.be.true; + }); + + it('should return true if aliasName is mapped to BIDDER_CODE in aliasRegistry', () => { + const aliasName = 'connatixAlias'; + aliasRegistryStub.value({ 'connatixAlias': BIDDER_CODE }); + expect(spec.isConnatix(aliasName)).to.be.true; + }); + + it('should return false if aliasName does not match BIDDER_CODE', () => { + const aliasName = 'otherBidder'; + expect(spec.isConnatix(aliasName)).to.be.false; + }); + + it('should return false if aliasName is mapped to a different bidder in aliasRegistry', () => { + const aliasName = 'someOtherAlias'; + aliasRegistryStub.value({ 'someOtherAlias': 'otherBidder' }); + expect(spec.isConnatix(aliasName)).to.be.false; + }); }); describe('getBidFloor', function () { @@ -352,4 +978,127 @@ describe('connatixBidAdapter', function () { expect(floor).to.equal(0); }); }); + describe('getUserSyncs with message event listener', function() { + const CNX_IDS_EXPIRY = 24 * 30 * 60 * 60 * 1000; + const CNX_IDS_LOCAL_STORAGE_COOKIES_KEY = 'cnx_user_ids'; + const ALL_PROVIDERS_RESOLVED_EVENT = 'cnx_all_identity_providers_resolved'; + + const mockData = { + data: { + supplementalEids: [{ provider: 2, group: 1, eidsList: ['123', '456'] }] + } + }; + + function messageHandler(event) { + if (!event.data || event.origin !== 'https://cds.connatix.com' || !event.data.cnx) { + return; + } + + const { message, data } = event.data.cnx; + + if (message === ALL_PROVIDERS_RESOLVED_EVENT) { + window.removeEventListener('message', messageHandler); + event.stopImmediatePropagation(); + } + + if (message === ALL_PROVIDERS_RESOLVED_EVENT || message === IDENTITY_PROVIDER_COLLECTION_UPDATED_EVENT) { + if (data) { + connatixSaveOnAllStorages(CNX_IDS_LOCAL_STORAGE_COOKIES_KEY, data, CNX_IDS_EXPIRY); + } + } + } + + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(window, 'removeEventListener'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getCookie'); + sandbox.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('Should set a cookie and save to local storage when a valid message is received', () => { + const fakeEvent = { + data: { cnx: { message: 'cnx_all_identity_providers_resolved', data: mockData } }, + origin: 'https://cds.connatix.com', + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.calledOnce).to.be.true; + expect(window.removeEventListener.calledWith('message', messageHandler)).to.be.true; + expect(storage.setCookie.calledWith(CNX_IDS_LOCAL_STORAGE_COOKIES_KEY, JSON.stringify(mockData), sinon.match.string)).to.be.true; + expect(storage.setDataInLocalStorage.calledWith(CNX_IDS_LOCAL_STORAGE_COOKIES_KEY, JSON.stringify(mockData))).to.be.true; + + storage.getCookie.returns(JSON.stringify(mockData)); + storage.getDataFromLocalStorage.returns(JSON.stringify(mockData)); + + const retrievedData = connatixReadFromAllStorages(CNX_IDS_LOCAL_STORAGE_COOKIES_KEY); + expect(retrievedData).to.deep.equal(mockData); + }); + + it('Should not do anything when there is no data in the payload', () => { + const fakeEvent = { + data: null, + origin: 'https://cds.connatix.com', + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.notCalled).to.be.true; + expect(window.removeEventListener.notCalled).to.be.true; + expect(storage.setCookie.notCalled).to.be.true; + expect(storage.setDataInLocalStorage.notCalled).to.be.true; + }); + + it('Should not do anything when the origin is invalid', () => { + const fakeEvent = { + data: { cnx: { message: 'cnx_all_identity_providers_resolved', data: mockData } }, + origin: 'https://notConnatix.com', + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.notCalled).to.be.true; + expect(window.removeEventListener.notCalled).to.be.true; + expect(storage.setCookie.notCalled).to.be.true; + expect(storage.setDataInLocalStorage.notCalled).to.be.true; + }); + }); + describe('connatixHasQueryParams', () => { + it('Should return false if there is no query param in the url', () => { + const url = 'http://example.com' + const result = connatixHasQueryParams(url); + expect(result).to.equal(false); + }); + + it('Should return true if there is one query param in the url', () => { + const url = 'http://example.com?query1=value1' + const result = connatixHasQueryParams(url); + expect(result).to.equal(true); + }); + + it('Should return true if there is multiple query params in the url', () => { + const url = 'http://example.com?query1=value1&query2=value2' + const result = connatixHasQueryParams(url); + expect(result).to.equal(true); + }); + + it('Should return false if the url is invalid', () => { + const url = 'example' + const result = connatixHasQueryParams(url); + expect(result).to.equal(false); + }); + }); }); diff --git a/test/spec/modules/connectIdSystem_spec.js b/test/spec/modules/connectIdSystem_spec.js index 686c3d63a63..8b846a5fc72 100644 --- a/test/spec/modules/connectIdSystem_spec.js +++ b/test/spec/modules/connectIdSystem_spec.js @@ -2,7 +2,6 @@ import {expect} from 'chai'; import {connectIdSubmodule, storage} from 'modules/connectIdSystem.js'; import {server} from '../../mocks/xhr'; import {parseQS, parseUrl} from 'src/utils.js'; -import {uspDataHandler, gppDataHandler} from 'src/adapterManager.js'; import * as refererDetection from '../../../src/refererDetection'; const TEST_SERVER_URL = 'http://localhost:9876/'; @@ -38,8 +37,6 @@ describe('Yahoo ConnectID Submodule', () => { let cookiesEnabledStub; let localStorageEnabledStub; let removeLocalStorageDataStub; - let uspConsentDataStub; - let gppConsentDataStub; let consentData; beforeEach(() => { @@ -53,17 +50,19 @@ describe('Yahoo ConnectID Submodule', () => { setLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); localStorageEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); removeLocalStorageDataStub = sinon.stub(storage, 'removeDataFromLocalStorage'); - uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); - gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); cookiesEnabledStub.returns(true); localStorageEnabledStub.returns(true); - uspConsentDataStub.returns(USP_DATA); - gppConsentDataStub.returns(GPP_DATA); consentData = { - gdprApplies: 1, - consentString: 'GDPR_CONSENT_STRING' + gdpr: { + gdprApplies: 1, + consentString: 'GDPR_CONSENT_STRING' + }, + gpp: { + ...GPP_DATA + }, + usp: USP_DATA }; }); @@ -76,8 +75,6 @@ describe('Yahoo ConnectID Submodule', () => { cookiesEnabledStub.restore(); localStorageEnabledStub.restore(); removeLocalStorageDataStub.restore(); - uspConsentDataStub.restore(); - gppConsentDataStub.restore(); }); function invokeGetIdAPI(configParams, consentData) { @@ -383,7 +380,7 @@ describe('Yahoo ConnectID Submodule', () => { pixelId: PIXEL_ID, '1p': '0', gdpr: '1', - gdpr_consent: consentData.consentString, + gdpr_consent: consentData.gdpr.consentString, v: '1', url: TEST_SERVER_URL, us_privacy: USP_DATA, @@ -418,7 +415,7 @@ describe('Yahoo ConnectID Submodule', () => { pixelId: PIXEL_ID, '1p': '0', gdpr: '1', - gdpr_consent: consentData.consentString, + gdpr_consent: consentData.gdpr.consentString, v: '1', url: TEST_SERVER_URL, us_privacy: USP_DATA, @@ -453,7 +450,7 @@ describe('Yahoo ConnectID Submodule', () => { he: HASHED_EMAIL, '1p': '0', gdpr: '1', - gdpr_consent: consentData.consentString, + gdpr_consent: consentData.gdpr.consentString, v: '1', url: TEST_SERVER_URL, us_privacy: USP_DATA, @@ -549,24 +546,28 @@ describe('Yahoo ConnectID Submodule', () => { expect(result.callback).to.be.a('function'); }); + function mockOptout(value) { + getLocalStorageStub.callsFake((key) => { + if (key === 'connectIdOptOut') return value; + }) + } + it('returns an undefined if the Yahoo specific opt-out key is present in local storage', () => { - localStorage.setItem('connectIdOptOut', '1'); + mockOptout('1'); expect(invokeGetIdAPI({ he: HASHED_EMAIL, pixelId: PIXEL_ID }, consentData)).to.be.undefined; - localStorage.removeItem('connectIdOptOut'); }); it('returns an object with the callback function if the correct params are passed and Yahoo opt-out value is not "1"', () => { - localStorage.setItem('connectIdOptOut', 'true'); + mockOptout('true'); let result = invokeGetIdAPI({ he: HASHED_EMAIL, pixelId: PIXEL_ID }, consentData); expect(result).to.be.an('object').that.has.all.keys('callback'); expect(result.callback).to.be.a('function'); - localStorage.removeItem('connectIdOptOut'); }); it('Makes an ajax GET request to the production API endpoint with pixelId and he query params', () => { @@ -580,7 +581,7 @@ describe('Yahoo ConnectID Submodule', () => { pixelId: PIXEL_ID, '1p': '0', gdpr: '1', - gdpr_consent: consentData.consentString, + gdpr_consent: consentData.gdpr.consentString, v: '1', url: TEST_SERVER_URL, us_privacy: USP_DATA, @@ -606,7 +607,7 @@ describe('Yahoo ConnectID Submodule', () => { gdpr: '1', puid: PUBLISHER_USER_ID, pixelId: PIXEL_ID, - gdpr_consent: consentData.consentString, + gdpr_consent: consentData.gdpr.consentString, url: TEST_SERVER_URL, us_privacy: USP_DATA, gpp: GPP_DATA.gppString, @@ -632,7 +633,7 @@ describe('Yahoo ConnectID Submodule', () => { pixelId: PIXEL_ID, '1p': '0', gdpr: '1', - gdpr_consent: consentData.consentString, + gdpr_consent: consentData.gdpr.consentString, v: '1', url: TEST_SERVER_URL, us_privacy: USP_DATA, @@ -656,7 +657,7 @@ describe('Yahoo ConnectID Submodule', () => { he: HASHED_EMAIL, '1p': '0', gdpr: '1', - gdpr_consent: consentData.consentString, + gdpr_consent: consentData.gdpr.consentString, v: '1', url: TEST_SERVER_URL, us_privacy: USP_DATA, @@ -671,7 +672,7 @@ describe('Yahoo ConnectID Submodule', () => { }); it('Makes an ajax GET request to the specified override API endpoint without GPP', () => { - gppConsentDataStub.returns(undefined); + consentData.gpp = undefined; invokeGetIdAPI({ he: HASHED_EMAIL, endpoint: OVERRIDE_ENDPOINT @@ -681,7 +682,7 @@ describe('Yahoo ConnectID Submodule', () => { he: HASHED_EMAIL, '1p': '0', gdpr: '1', - gdpr_consent: consentData.consentString, + gdpr_consent: consentData.gdpr.consentString, v: '1', url: TEST_SERVER_URL, us_privacy: USP_DATA @@ -710,11 +711,11 @@ describe('Yahoo ConnectID Submodule', () => { const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); expect(requestQueryParams.gdpr).to.equal('1'); - expect(requestQueryParams.gdpr_consent).to.equal(consentData.consentString); + expect(requestQueryParams.gdpr_consent).to.equal(consentData.gdpr.consentString); }); it('sets GDPR consent data flag correctly when call is NOT under GDPR jurisdiction.', () => { - consentData.gdprApplies = false; + consentData.gdpr.gdprApplies = false; invokeGetIdAPI({ he: HASHED_EMAIL, @@ -804,6 +805,25 @@ describe('Yahoo ConnectID Submodule', () => { }); }); }); + describe('userHasOptedOut()', () => { + it('should return a function', () => { + expect(connectIdSubmodule.userHasOptedOut).to.be.a('function'); + }); + + it('should return false when local storage key has not been set function', () => { + expect(connectIdSubmodule.userHasOptedOut()).to.be.false; + }); + + it('should return true when local storage key has been set to "1"', () => { + getLocalStorageStub.returns('1'); + expect(connectIdSubmodule.userHasOptedOut()).to.be.true; + }); + + it('should return false when local storage key has not been set to "1"', () => { + getLocalStorageStub.returns('hello'); + expect(connectIdSubmodule.userHasOptedOut()).to.be.false; + }); + }); }); describe('decode()', () => { @@ -884,28 +904,4 @@ describe('Yahoo ConnectID Submodule', () => { })).to.be.true; }); }); - - describe('userHasOptedOut()', () => { - afterEach(() => { - localStorage.removeItem('connectIdOptOut'); - }); - - it('should return a function', () => { - expect(connectIdSubmodule.userHasOptedOut).to.be.a('function'); - }); - - it('should return false when local storage key has not been set function', () => { - expect(connectIdSubmodule.userHasOptedOut()).to.be.false; - }); - - it('should return true when local storage key has been set to "1"', () => { - localStorage.setItem('connectIdOptOut', '1'); - expect(connectIdSubmodule.userHasOptedOut()).to.be.true; - }); - - it('should return false when local storage key has not been set to "1"', () => { - localStorage.setItem('connectIdOptOut', 'hello'); - expect(connectIdSubmodule.userHasOptedOut()).to.be.false; - }); - }); }); diff --git a/test/spec/modules/connectadBidAdapter_spec.js b/test/spec/modules/connectadBidAdapter_spec.js index d8dfcb0ce98..f067d801c5e 100644 --- a/test/spec/modules/connectadBidAdapter_spec.js +++ b/test/spec/modules/connectadBidAdapter_spec.js @@ -2,6 +2,7 @@ import {expect} from 'chai'; import {spec} from 'modules/connectadBidAdapter.js'; import { config } from 'src/config.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import assert from 'assert'; describe('ConnectAd Adapter', function () { let bidRequests; @@ -26,7 +27,13 @@ describe('ConnectAd Adapter', function () { bidId: '2f95c00074b931', auctionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', bidderRequestId: '1c56ad30b9b8ca8', - transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df' + transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', + ortb2Imp: { + ext: { + tid: '601bda1a-01a9-4de9-b8f3-649d3bdd0d8f', + gpid: '/12345/homepage-leftnav' + } + }, } ]; @@ -69,6 +76,10 @@ describe('ConnectAd Adapter', function () { } }); + afterEach(function () { + config.resetConfig(); + }); + describe('inherited functions', function () { it('should exists and is a function', function () { const adapter = newBidder(spec); @@ -193,30 +204,28 @@ describe('ConnectAd Adapter', function () { }); it('should build a request if Consent but no gdprApplies', function () { - let bidderRequest = { + let localbidderRequest = { timeout: 3000, gdprConsent: { gdprApplies: false, consentString: 'consentDataString', }, } - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, localbidderRequest); const requestparse = JSON.parse(request.data); - expect(requestparse.placements[0].adTypes).to.be.an('array'); expect(requestparse.placements[0].siteId).to.equal(123456); expect(requestparse.user.ext.consent).to.equal('consentDataString'); }); it('should build a request if gdprConsent empty', function () { - let bidderRequest = { + let localbidderRequest = { timeout: 3000, gdprConsent: {} } - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, localbidderRequest); const requestparse = JSON.parse(request.data); - expect(requestparse.placements[0].adTypes).to.be.an('array'); expect(requestparse.placements[0].siteId).to.equal(123456); }); @@ -239,7 +248,7 @@ describe('ConnectAd Adapter', function () { it('should not include schain when not provided', function () { const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); - expect(requestparse.source).to.not.exist; + expect(requestparse).to.not.have.property('source.ext.schain'); }); it('should submit coppa if set in config', function () { @@ -248,7 +257,17 @@ describe('ConnectAd Adapter', function () { .returns(true); const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); - expect(requestparse.user.coppa).to.equal(1); + expect(requestparse.regs.coppa).to.equal(1); + config.getConfig.restore(); + }); + + it('should not set coppa when coppa is not provided or is set to false', function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(false); + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + assert.equal(requestparse.regs.coppa, undefined); config.getConfig.restore(); }); @@ -259,27 +278,96 @@ describe('ConnectAd Adapter', function () { expect(requestparse.user.ext.eids[0].uids[0].id).to.equal('123456'); }); - it('should add referer info', function () { - const bidRequest = Object.assign({}, bidRequests[0]) - const bidderRequ = { - refererInfo: { - page: 'https://connectad.io/page.html', - legacy: { - referer: 'https://connectad.io/page.html', - reachedTop: true, - numIframes: 2, - stack: [ - 'https://connectad.io/page.html', - 'https://connectad.io/iframe1.html', - 'https://connectad.io/iframe2.html' - ] + it('should include DSA signals', function () { + const dsa = { + dsarequired: 3, + pubrender: 0, + datatopub: 2, + transparency: [ + { + domain: 'domain1.com', + dsaparams: [1] + }, + { + domain: 'domain2.com', + dsaparams: [1, 2] + } + ] + }; + + let bidRequest = { + ortb2: { + regs: { + ext: { + dsa + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + assert.deepEqual(data.regs.ext.dsa, dsa); + }); + + it('should pass auction level tid', function() { + const bidRequest = Object.assign([], bidRequests); + + const localBidderRequest = { + ...bidderRequest, + ortb2: { + source: { + tid: '9XSL9B79XM' } } } - const request = spec.buildRequests([bidRequest], bidderRequ); + + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + expect(data.source?.tid).to.equal('9XSL9B79XM') + }); + + it('should pass gpid', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].gpid).to.equal('/12345/homepage-leftnav'); + }); + + it('should pass impression level tid', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].tid).to.equal('601bda1a-01a9-4de9-b8f3-649d3bdd0d8f'); + }); + + it('should pass first party data', function() { + const bidRequest = Object.assign([], bidRequests); + + const localBidderRequest = { + ...bidderRequest, + ortb2: { + bcat: ['IAB1', 'IAB2-1'], + badv: ['xyz.com', 'zyx.com'], + site: { ext: { data: 'some site data' } }, + device: { ext: { data: 'some device data' } }, + user: { ext: { data: 'some user data' } }, + regs: { ext: { data: 'some regs data' } } + } + }; + + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + expect(data.bcat).to.deep.equal(localBidderRequest.ortb2.bcat); + expect(data.badv).to.deep.equal(localBidderRequest.ortb2.badv); + expect(data.site).to.nested.include({'ext.data': 'some site data'}); + expect(data.device).to.nested.include({'ext.data': 'some device data'}); + expect(data.user).to.nested.include({'ext.data': 'some user data'}); + expect(data.regs).to.nested.include({'ext.data': 'some regs data'}); + }); + + it('should accept tmax from global config if not set by requestBids method', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); - expect(requestparse.referrer_info).to.exist; + expect(requestparse.tmax).to.deep.equal(3000); }); it('should populate schain', function () { @@ -313,6 +401,76 @@ describe('ConnectAd Adapter', function () { }); }); + describe('GPP Implementation', function() { + it('should check with GPP Consent', function () { + let bidRequest = { + gppConsent: { + 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'fullGppData': { + 'sectionId': 3, + 'gppVersion': 1, + 'sectionList': [ + 5, + 7 + ], + 'applicableSections': [ + 5 + ], + 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'pingData': { + 'cmpStatus': 'loaded', + 'gppVersion': '1.0', + 'cmpDisplayStatus': 'visible', + 'supportedAPIs': [ + 'tcfca', + 'usnat', + 'usca', + 'usva', + 'usco', + 'usut', + 'usct' + ], + 'cmpId': 31 + }, + 'eventName': 'sectionChange' + }, + 'applicableSections': [ + 5 + ], + 'apiVersion': 1 + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + expect(data.regs.gpp_sid[0]).to.equal(5); + }); + + it('should check without GPP Consent', function () { + let bidRequest = {}; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal(undefined); + }); + + it('should check with GPP Consent read from OpenRTB2', function () { + let bidRequest = { + ortb2: { + regs: { + 'gpp': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'gpp_sid': [ + 5 + ] + } + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + expect(data.regs.gpp_sid[0]).to.equal(5); + }); + }); + describe('bid responses', function () { it('should return complete bid response with adomain', function () { const ADOMAINS = ['connectad.io']; @@ -348,6 +506,53 @@ describe('ConnectAd Adapter', function () { expect(bids[0].meta.advertiserDomains).to.deep.equal(ADOMAINS); }); + it('should process meta response object', function () { + const ADOMAINS = ['connectad.io']; + const dsa = { + behalf: 'Advertiser', + paid: 'Advertiser', + transparency: [{ + domain: 'dsp1domain.com', + dsaparams: [1, 2] + }], + adrender: 1 + }; + + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + adomain: ['connectad.io'], + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '250', + width: '300', + dsa: dsa, + category: 'IAB123', + pricing: { + clearPrice: 11.899999999999999 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].cpm).to.equal(11.899999999999999); + expect(bids[0].width).to.equal('300'); + expect(bids[0].height).to.equal('250'); + expect(bids[0].ad).to.have.length.above(1); + expect(bids[0].meta.advertiserDomains).to.deep.equal(ADOMAINS); + expect(bids[0].meta.dsa).to.equal(dsa); + expect(bids[0].meta.primaryCatId).to.equal('IAB123'); + }); + it('should return complete bid response with empty adomain', function () { const ADOMAINS = []; @@ -471,22 +676,59 @@ describe('ConnectAd Adapter', function () { }); }); + describe('GPP Sync', function() { + it('should concatenate gppString and applicableSections values in the returned image url', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [5] }; + const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'image', + url: `https://sync.connectad.io/ImageSyncer?gpp=DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN&gpp_sid=5&` + }]); + }); + + it('should concatenate gppString and applicableSections values in the returned iFrame url', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [5, 6] }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', + url: `https://sync.connectad.io/iFrameSyncer?gpp=DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN&gpp_sid=5%2C6&` + }]); + }); + + it('should return url without Gpp consent if gppConsent is undefined', () => { + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, undefined); + expect(result).to.deep.equal([{ + type: 'iframe', + url: `https://sync.connectad.io/iFrameSyncer?` + }]); + }); + + it('should return iFrame url without Gpp consent if gppConsent.gppString is undefined', () => { + const gppConsent = { applicableSections: ['5'] }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', + url: `https://sync.connectad.io/iFrameSyncer?` + }]); + }); + }); + describe('getUserSyncs', () => { let testParams = [ { name: 'iframe/no gdpr or ccpa', - arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, null], + arguments: [{ iframeEnabled: true, pixelEnabled: false }, {}, null], expect: { type: 'iframe', - pixels: ['https://cdn.connectad.io/connectmyusers.php?'] + pixels: ['https://sync.connectad.io/iFrameSyncer?'] } }, { name: 'iframe/gdpr', - arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, {gdprApplies: true, consentString: '234234'}], + arguments: [{ iframeEnabled: true, pixelEnabled: false }, {}, {gdprApplies: true, consentString: '234234'}], expect: { type: 'iframe', - pixels: ['https://cdn.connectad.io/connectmyusers.php?gdpr=1&gdpr_consent=234234&'] + pixels: ['https://sync.connectad.io/iFrameSyncer?gdpr=1&gdpr_consent=234234&'] } }, { @@ -494,15 +736,39 @@ describe('ConnectAd Adapter', function () { arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, null, 'YN12'], expect: { type: 'iframe', - pixels: ['https://cdn.connectad.io/connectmyusers.php?us_privacy=YN12&'] + pixels: ['https://sync.connectad.io/iFrameSyncer?us_privacy=YN12&'] } }, { name: 'iframe/ccpa & gdpr', - arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, {gdprApplies: true, consentString: '234234'}, 'YN12'], + arguments: [{ iframeEnabled: true, pixelEnabled: false }, {}, {gdprApplies: true, consentString: '234234'}, 'YN12'], + expect: { + type: 'iframe', + pixels: ['https://sync.connectad.io/iFrameSyncer?gdpr=1&gdpr_consent=234234&us_privacy=YN12&'] + } + }, + { + name: 'image/ccpa & gdpr', + arguments: [{ iframeEnabled: false, pixelEnabled: true }, {}, {gdprApplies: true, consentString: '234234'}, 'YN12'], + expect: { + type: 'image', + pixels: ['https://sync.connectad.io/ImageSyncer?gdpr=1&gdpr_consent=234234&us_privacy=YN12&'] + } + }, + { + name: 'image/gdpr', + arguments: [{ iframeEnabled: false, pixelEnabled: true }, {}, {gdprApplies: true, consentString: '234234'}], + expect: { + type: 'image', + pixels: ['https://sync.connectad.io/ImageSyncer?gdpr=1&gdpr_consent=234234&'] + } + }, + { + name: 'should prioritize iframe over image for user sync', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, null], expect: { type: 'iframe', - pixels: ['https://cdn.connectad.io/connectmyusers.php?gdpr=1&gdpr_consent=234234&us_privacy=YN12&'] + pixels: ['https://sync.connectad.io/iFrameSyncer?'] } } ]; diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 93a876d0233..a4727a5820f 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -1,22 +1,31 @@ import { - consentTimeout, + consentConfig, GPPClient, - requestBidsHook, resetConsentData, setConsentConfig, - userCMP } from 'modules/consentManagementGpp.js'; import {gppDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {config} from 'src/config.js'; import 'src/prebid.js'; -import {MODE_CALLBACK, MODE_MIXED} from '../../../libraries/cmp/cmpClient.js'; -import {GreedyPromise} from '../../../src/utils/promise.js'; let expect = require('chai').expect; describe('consentManagementGpp', function () { beforeEach(resetConsentData); + after(resetConsentData); + + async function runHook(request = {}) { + let hookRan = false; + consentConfig.requestBidsHook(() => { + hookRan = true; + }, request); + try { + await consentConfig.loadConsentData() + } catch (e) { + } + return hookRan; + } describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { @@ -32,36 +41,36 @@ describe('consentManagementGpp', function () { resetConsentData(); }); - it('should use system default values', function () { - setConsentConfig({ + it('should use system default values', async function () { + await setConsentConfig({ gpp: {} }); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(10000); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(10000); sinon.assert.callCount(utils.logInfo, 3); }); - it('should exit consent manager if config is not an object', function () { - setConsentConfig(''); - expect(userCMP).to.be.undefined; + it('should exit consent manager if config is not an object', async function () { + await setConsentConfig(''); + expect(consentConfig.cmpHandler).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); - it('should exit consentManagement module if config is "undefined"', function () { - setConsentConfig(undefined); - expect(userCMP).to.be.undefined; + it('should exit consentManagement module if config is "undefined"', async function () { + await setConsentConfig(undefined); + expect(consentConfig.cmpHandler).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); - it('should not produce any consent metadata', function () { - setConsentConfig(undefined) + it('should not produce any consent metadata', async function () { + await setConsentConfig(undefined) let consentMetadata = gppDataHandler.getConsentMeta(); expect(consentMetadata).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }) - it('should immediately look up consent data', () => { - setConsentConfig({ + it('should immediately look up consent data', async () => { + await setConsentConfig({ gpp: { cmpApi: 'invalid' } @@ -75,7 +84,7 @@ describe('consentManagementGpp', function () { config.resetConfig(); }); - it('results in all user settings overriding system defaults', function () { + it('results in all user settings overriding system defaults', async function () { let allConfig = { gpp: { cmpApi: 'iab', @@ -83,22 +92,22 @@ describe('consentManagementGpp', function () { } }; - setConsentConfig(allConfig); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(7500); + await setConsentConfig(allConfig); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(7500); }); - it('should recognize config.gpp, with default cmpApi and timeout', function () { - setConsentConfig({ + it('should recognize config.gpp, with default cmpApi and timeout', async function () { + await setConsentConfig({ gpp: {} }); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(10000); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(10000); }); - it('should enable gppDataHandler', () => { - setConsentConfig({ + it('should enable gppDataHandler', async () => { + await setConsentConfig({ gpp: {} }); expect(gppDataHandler.enabled).to.be.true; @@ -110,7 +119,7 @@ describe('consentManagementGpp', function () { config.resetConfig(); }); - it('results in user settings overriding system defaults', () => { + it('results in user settings overriding system defaults', async () => { let staticConfig = { gpp: { cmpApi: 'static', @@ -132,8 +141,8 @@ describe('consentManagementGpp', function () { } }; - setConsentConfig(staticConfig); - expect(userCMP).to.be.equal('static'); + await setConsentConfig(staticConfig); + expect(consentConfig.cmpHandler).to.be.equal('static'); const consent = gppDataHandler.getConsentData(); expect(consent.gppString).to.eql(staticConfig.gpp.consentData.gppString); expect(consent.gppData).to.eql(staticConfig.gpp.consentData); @@ -141,170 +150,33 @@ describe('consentManagementGpp', function () { }); }); }); - describe('GPPClient.ping', () => { - function mkPingData(gppVersion) { - return { - gppVersion - } - } - Object.entries({ - 'unknown': { - expectedMode: MODE_CALLBACK, - pingData: mkPingData(), - apiVersion: '1.1', - client({callback}) { - callback(this.pingData); - } - }, - '1.0': { - expectedMode: MODE_MIXED, - pingData: mkPingData('1.0'), - apiVersion: '1.0', - client() { - return this.pingData; - } - }, - '1.1 that runs callback immediately': { - expectedMode: MODE_CALLBACK, - pingData: mkPingData('1.1'), - apiVersion: '1.1', - client({callback}) { - callback(this.pingData); - } - }, - '1.1 that defers callback': { - expectedMode: MODE_CALLBACK, - pingData: mkPingData('1.1'), - apiVersion: '1.1', - client({callback}) { - setTimeout(() => callback(this.pingData), 10); - } - }, - '> 1.1': { - expectedMode: MODE_CALLBACK, - pingData: mkPingData('1.2'), - apiVersion: '1.1', - client({callback}) { - setTimeout(() => callback(this.pingData), 10); - } - } - }).forEach(([t, scenario]) => { - describe(`using CMP version ${t}`, () => { - let clients, mkClient; - beforeEach(() => { - clients = []; - mkClient = ({mode}) => { - const mockClient = function (args) { - if (args.command === 'ping') { - return Promise.resolve(scenario.client(args)); - } - } - mockClient.mode = mode; - mockClient.close = sinon.stub(); - clients.push(mockClient); - return mockClient; - } - }); - - it('should resolve to client with the correct mode', () => { - return GPPClient.ping(mkClient).then(([client]) => { - expect(client.cmp.mode).to.eql(scenario.expectedMode); - }); - }); - - it('should resolve to pingData', () => { - return GPPClient.ping(mkClient).then(([_, pingData]) => { - expect(pingData).to.eql(scenario.pingData); - }); - }); - - it('should .close the probing client', () => { - return GPPClient.ping(mkClient).then(([client]) => { - sinon.assert.called(clients[0].close); - sinon.assert.notCalled(client.cmp.close); - }) - }); - - it('should .tag the client with version', () => { - return GPPClient.ping(mkClient).then(([client]) => { - expect(client.apiVersion).to.eql(scenario.apiVersion); - }) - }) - }) - }); - - it('should reject when mkClient returns null (CMP not found)', () => { - return GPPClient.ping(() => null).catch((err) => { - expect(err.message).to.match(/not found/); - }); - }); - - it('should reject when client rejects', () => { - const err = {some: 'prop'}; - const mockClient = () => Promise.reject(err); - mockClient.close = sinon.stub(); - return GPPClient.ping(() => mockClient).catch((result) => { - expect(result).to.eql(err); - sinon.assert.called(mockClient.close); - }); - }); - - it('should reject when callback is invoked with success = false', () => { - const err = 'error'; - const mockClient = ({callback}) => callback(err, false); - mockClient.close = sinon.stub(); - return GPPClient.ping(() => mockClient).catch((result) => { - expect(result).to.eql(err); - sinon.assert.called(mockClient.close); - }) - }) - }); - describe('GPPClient.init', () => { - let makeCmp, cmpCalls, cmpResult; + describe('GPPClient.get', () => { + let makeCmp; beforeEach(() => { - cmpResult = {signalStatus: 'ready', gppString: 'mock-str'}; - cmpCalls = []; makeCmp = sinon.stub().callsFake(() => { - function mockCmp(args) { - cmpCalls.push(args); - return GreedyPromise.resolve(cmpResult); - } - mockCmp.close = sinon.stub(); - return mockCmp; + return sinon.stub() }); }); - it('should re-use same client', (done) => { - GPPClient.init(makeCmp).then(([client]) => { - GPPClient.init(makeCmp).then(([client2, consentPm]) => { - expect(client2).to.equal(client); - expect(cmpCalls.filter((el) => el.command === 'ping').length).to.equal(2) // recycled client should be refreshed - consentPm.then((consent) => { - expect(consent.gppString).to.eql('mock-str'); - done() - }) - }); - }); + it('should re-use same client', () => { + expect(GPPClient.get(makeCmp)).to.equal(GPPClient.get(makeCmp)); + sinon.assert.calledOnce(makeCmp); }); - it('should not re-use errors', (done) => { - cmpResult = GreedyPromise.reject(new Error()); - GPPClient.init(makeCmp).catch(() => { - cmpResult = {signalStatus: 'ready'}; - return GPPClient.init(makeCmp).then(([client]) => { - expect(client).to.exist; - done() - }) - }) + it('should not re-use errors', () => { + try { + GPPClient.get(sinon.stub().throws(new Error())); + } catch (e) {} + expect(GPPClient.get(makeCmp)).to.exist; }) }) describe('GPP client', () => { const CHANGE_EVENTS = ['sectionChange', 'signalStatus']; - let gppClient, gppData, cmpReady, eventListener; + let gppClient, gppData, eventListener; function mockClient(apiVersion = '1.1', cmpVersion = '1.1') { const mockCmp = sinon.stub().callsFake(function ({command, callback}) { @@ -314,10 +186,8 @@ describe('consentManagementGpp', function () { throw new Error('unexpected command: ' + command); } }) - const client = new GPPClient(cmpVersion, mockCmp); + const client = new GPPClient(mockCmp); client.apiVersion = apiVersion; - client.getGPPData = sinon.stub().callsFake(() => Promise.resolve(gppData)); - client.isCMPReady = sinon.stub().callsFake(() => cmpReady); client.events = CHANGE_EVENTS; return client; } @@ -325,7 +195,6 @@ describe('consentManagementGpp', function () { beforeEach(() => { gppDataHandler.reset(); eventListener = null; - cmpReady = true; gppData = { applicableSections: [7], gppString: 'mock-string', @@ -346,7 +215,7 @@ describe('consentManagementGpp', function () { describe('updateConsent', () => { it('should update data handler with consent data', () => { - return gppClient.updateConsent().then(data => { + return gppClient.updateConsent(gppData).then(data => { sinon.assert.match(data, gppData); sinon.assert.match(gppDataHandler.getConsentData(), gppData); expect(gppDataHandler.ready).to.be.true; @@ -358,8 +227,7 @@ describe('consentManagementGpp', function () { 'missing': null }).forEach(([t, data]) => { it(`should not update, and reject promise, when gpp data is ${t}`, (done) => { - gppData = data; - gppClient.updateConsent().catch(err => { + gppClient.updateConsent(data).catch(err => { expect(err.message).to.match(/empty/); expect(err.args).to.eql(data == null ? [] : [data]); expect(gppDataHandler.ready).to.be.false; @@ -368,15 +236,6 @@ describe('consentManagementGpp', function () { }); }) - it('should not update when gpp data rejects', (done) => { - gppData = Promise.reject(new Error('err')); - gppClient.updateConsent().catch(err => { - expect(gppDataHandler.ready).to.be.false; - expect(err.message).to.eql('err'); - done(); - }) - }); - describe('consent data validation', () => { Object.entries({ applicableSections: { @@ -394,7 +253,7 @@ describe('consentManagementGpp', function () { describe(t, () => { it('should not update', (done) => { Object.assign(gppData, {[prop]: value}); - gppClient.updateConsent().catch(err => { + gppClient.updateConsent(gppData).catch(err => { expect(err.message).to.match(/unexpected/); expect(err.args).to.eql([gppData]); expect(gppDataHandler.ready).to.be.false; @@ -409,23 +268,14 @@ describe('consentManagementGpp', function () { }); describe('init', () => { - beforeEach(() => { - gppClient.isCMPReady = function (pingData) { - return pingData.ready; - } - gppClient.getGPPData = function (pingData) { - return Promise.resolve(pingData); - } - }) - it('does not use initial pingData if CMP is not ready', () => { - gppClient.init({...gppData, ready: false}); + gppClient.init({...gppData, signalStatus: 'not ready'}); expect(eventListener).to.exist; expect(gppDataHandler.ready).to.be.false; }); it('uses initial pingData (and resolves promise) if CMP is ready', () => { - return gppClient.init({...gppData, ready: true}).then(data => { + return gppClient.init({...gppData, signalStatus: 'ready'}).then(data => { expect(eventListener).to.exist; sinon.assert.match(data, gppData); sinon.assert.match(gppDataHandler.getConsentData(), gppData); @@ -433,7 +283,7 @@ describe('consentManagementGpp', function () { }); it('rejects promise when CMP errors out', (done) => { - gppClient.init({ready: false}).catch((err) => { + gppClient.init({signalStatus: 'not ready'}).catch((err) => { expect(err.message).to.match(/error/); expect(err.args).to.eql(['error']) done(); @@ -447,7 +297,7 @@ describe('consentManagementGpp', function () { 'irrelevant': {eventName: 'irrelevant'} }).forEach(([t, evt]) => { it(`ignores ${t} events`, () => { - let pm = gppClient.init({ready: false}).catch((err) => err.args[0] !== 'done' && Promise.reject(err)); + let pm = gppClient.init({signalStatus: 'not ready'}).catch((err) => err.args[0] !== 'done' && Promise.reject(err)); eventListener(evt); eventListener('done', false); return pm; @@ -456,7 +306,7 @@ describe('consentManagementGpp', function () { it('rejects the promise when cmpStatus is "error"', (done) => { const evt = {eventName: 'other', pingData: {cmpStatus: 'error'}}; - gppClient.init({ready: false}).catch(err => { + gppClient.init({signalStatus: 'not ready'}).catch(err => { expect(err.message).to.match(/error/); expect(err.args).to.eql([evt]); done(); @@ -479,171 +329,44 @@ describe('consentManagementGpp', function () { }); it('does not fire consent data updates if the CMP is not ready', (done) => { - gppClient.init({ready: false}).catch(() => { + gppClient.init({signalStatus: 'not ready'}).catch(() => { expect(gppDataHandler.ready).to.be.false; done(); }); - eventListener({...gppData2, ready: false}); + eventListener({...gppData2, signalStatus: 'not ready'}); eventListener('done', false); }) it('fires consent data updates (and resolves promise) if CMP is ready', (done) => { - gppClient.init({ready: false}).then(data => { + gppClient.init({signalStatus: 'not ready'}).then(data => { sinon.assert.match(data, gppData2); done() }); - cmpReady = true; - eventListener(makeEvent({...gppData2, ready: true})); + eventListener(makeEvent({...gppData2, signalStatus: 'ready'})); }); it('keeps updating consent data on new events', () => { - let pm = gppClient.init({ready: false}).then(data => { + let pm = gppClient.init({signalStatus: 'not ready'}).then(data => { sinon.assert.match(data, gppData); sinon.assert.match(gppDataHandler.getConsentData(), gppData); }); - eventListener(makeEvent({...gppData, ready: true})); + eventListener(makeEvent({...gppData, signalStatus: 'ready'})); return pm.then(() => { - eventListener(makeEvent({...gppData2, ready: true})) + eventListener(makeEvent({...gppData2, signalStatus: 'ready'})) }).then(() => { sinon.assert.match(gppDataHandler.getConsentData(), gppData2); }); }); - }) - }) - }); - }); - - describe('GPP 1.0 protocol', () => { - let mockCmp, gppClient; - beforeEach(() => { - mockCmp = sinon.stub(); - gppClient = new (GPPClient.getClient('1.0'))('1.0', mockCmp); - }); - - describe('isCMPReady', () => { - Object.entries({ - 'loaded': [true, 'loaded'], - 'other': [false, 'other'], - 'undefined': [false, undefined] - }).forEach(([t, [expected, cmpStatus]]) => { - it(`should be ${expected} when cmpStatus is ${t}`, () => { - expect(gppClient.isCMPReady(Object.assign({}, {cmpStatus}))).to.equal(expected); }); }); }); - - describe('getGPPData', () => { - let gppData, pingData; - beforeEach(() => { - gppData = { - gppString: 'mock-string', - supportedAPIs: ['usnat'], - applicableSections: [7, 8] - } - pingData = { - supportedAPIs: gppData.supportedAPIs - }; - }); - - function mockCmpCommands(commands) { - mockCmp.callsFake(({command, parameter}) => { - if (commands.hasOwnProperty((command))) { - return Promise.resolve(commands[command](parameter)); - } else { - return Promise.reject(new Error(`unrecognized command ${command}`)) - } - }) - } - - it('should retrieve consent string and applicableSections', () => { - mockCmpCommands({ - getGPPData: () => gppData - }) - return gppClient.getGPPData(pingData).then(data => { - sinon.assert.match(data, gppData); - }) - }); - - it('should reject when getGPPData rejects', (done) => { - mockCmpCommands({ - getGPPData: () => Promise.reject(new Error('err')) - }); - gppClient.getGPPData(pingData).catch(err => { - expect(err.message).to.eql('err'); - done(); - }); - }); - - it('should not choke if supportedAPIs is missing', () => { - [gppData, pingData].forEach(ob => { delete ob.supportedAPIs; }) - mockCmpCommands({ - getGPPData: () => gppData - }); - return gppClient.getGPPData(pingData).then(res => { - expect(res.gppString).to.eql(gppData.gppString); - expect(res.parsedSections).to.eql({}); - }) - }) - - describe('section data', () => { - let usnat, parsedUsnat; - - function mockSections(sections) { - mockCmpCommands({ - getGPPData: () => gppData, - getSection: (api) => (sections[api]) - }); - }; - - beforeEach(() => { - usnat = { - MockField: 'val', - OtherField: 'o', - Gpc: true - }; - parsedUsnat = [ - { - MockField: 'val', - OtherField: 'o' - }, - { - SubsectionType: 1, - Gpc: true - } - ] - }); - - it('retrieves section data', () => { - mockSections({usnat}); - return gppClient.getGPPData(pingData).then(data => { - expect(data.parsedSections).to.eql({usnat: parsedUsnat}) - }); - }); - - it('does not choke if a section is missing', () => { - mockSections({usnat}); - gppData.supportedAPIs = ['usnat', 'missing']; - return gppClient.getGPPData(pingData).then(data => { - expect(data.parsedSections).to.eql({usnat: parsedUsnat}); - }) - }); - - it('does not choke if a section fails', () => { - mockSections({usnat, err: Promise.reject(new Error('err'))}); - gppData.supportedAPIs = ['usnat', 'err']; - return gppClient.getGPPData(pingData).then(data => { - expect(data.parsedSections).to.eql({usnat: parsedUsnat}); - }) - }); - }) - }); }); describe('GPP 1.1 protocol', () => { let mockCmp, gppClient; beforeEach(() => { mockCmp = sinon.stub(); - gppClient = new (GPPClient.getClient('1.1'))('1.1', mockCmp); + gppClient = new GPPClient(mockCmp); }); describe('isCMPReady', () => { @@ -657,85 +380,9 @@ describe('consentManagementGpp', function () { }); }); }); - - it('gets GPPData from pingData', () => { - mockCmp.throws(new Error()); - const pingData = { - 'gppVersion': '1.1', - 'cmpStatus': 'loaded', - 'cmpDisplayStatus': 'disabled', - 'supportedAPIs': [ - '5:tcfcav1', - '7:usnat', - '8:usca', - '9:usva', - '10:usco', - '11:usut', - '12:usct' - ], - 'signalStatus': 'ready', - 'cmpId': 31, - 'sectionList': [ - 7 - ], - 'applicableSections': [ - 7 - ], - 'gppString': 'DBABL~BAAAAAAAAgA.QA', - 'parsedSections': { - 'usnat': [ - { - 'Version': 1, - 'SharingNotice': 0, - 'SaleOptOutNotice': 0, - 'SharingOptOutNotice': 0, - 'TargetedAdvertisingOptOutNotice': 0, - 'SensitiveDataProcessingOptOutNotice': 0, - 'SensitiveDataLimitUseNotice': 0, - 'SaleOptOut': 0, - 'SharingOptOut': 0, - 'TargetedAdvertisingOptOut': 0, - 'SensitiveDataProcessing': [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - 'KnownChildSensitiveDataConsents': [ - 0, - 0 - ], - 'PersonalDataConsents': 0, - 'MspaCoveredTransaction': 2, - 'MspaOptOutOptionMode': 0, - 'MspaServiceProviderMode': 0 - }, - { - 'SubsectionType': 1, - 'Gpc': false - } - ] - } - }; - return gppClient.getGPPData(pingData).then((gppData) => { - sinon.assert.match(gppData, { - gppString: pingData.gppString, - applicableSections: pingData.applicableSections, - parsedSections: pingData.parsedSections - }) - }) - }) }) - describe('requestBidsHook tests:', function () { + describe('moduleConfig.requestBidsHook tests:', function () { let goodConfig = { gpp: { cmpApi: 'iab', @@ -772,142 +419,147 @@ describe('consentManagementGpp', function () { config.resetConfig(); }); - it('should throw a warning and return to hooked function when an unknown CMP framework ID is used', function () { + it('should throw a warning and return to hooked function when an unknown CMP framework ID is used', async function () { let badCMPConfig = { gpp: { cmpApi: 'bad' } }; - setConsentConfig(badCMPConfig); - expect(userCMP).to.be.equal(badCMPConfig.gpp.cmpApi); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); + await setConsentConfig(badCMPConfig); + expect(consentConfig.cmpHandler).to.be.equal(badCMPConfig.gpp.cmpApi); + expect(await runHook()).to.be.true; let consent = gppDataHandler.getConsentData(); - - sinon.assert.calledOnce(utils.logWarn); - expect(didHookReturn).to.be.true; expect(consent).to.be.null; + sinon.assert.calledOnce(utils.logWarn); }); - it('should call gppDataHandler.setConsentData() when unknown CMP api is used', () => { - setConsentConfig({ + it('should call gppDataHandler.setConsentData() when unknown CMP api is used', async () => { + await setConsentConfig({ gpp: { cmpApi: 'invalid' } }); - let hookRan = false; - requestBidsHook(() => { - hookRan = true; - }, {}); - expect(hookRan).to.be.true; + expect(await runHook()).to.be.true; expect(gppDataHandler.ready).to.be.true; }) - it('should throw proper errors when CMP is not found', function () { - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); + it('should throw proper errors when CMP is not found', async function () { + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.false; let consent = gppDataHandler.getConsentData(); // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) sinon.assert.calledTwice(utils.logError); - expect(didHookReturn).to.be.false; expect(consent).to.be.null; expect(gppDataHandler.ready).to.be.true; }); - it('should not trip when adUnits have no size', () => { - setConsentConfig(staticConfig); - let ran = false; - requestBidsHook(() => { - ran = true; - }, { + it('should not trip when adUnits have no size', async () => { + await setConsentConfig(staticConfig); + const request = { adUnits: [{ code: 'test', mediaTypes: { video: {} } }] - }); - return gppDataHandler.promise.then(() => { - expect(ran).to.be.true; - }); + }; + expect(await runHook(request)).to.be.true; }); - it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { - window.__gpp = function () {}; - setConsentConfig({ + it('should continue the auction after timeout, if cmp does not reply', async () => { + window.__gpp = function () { + }; + await setConsentConfig({ gpp: { cmpApi: 'iab', - timeout: 0 + timeout: 10 } }); try { - requestBidsHook(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([]); - expect(consent.gppString).to.be.undefined; - done(); - }, {}) + expect(await runHook()).to.be.true; + const consent = gppDataHandler.getConsentData(); + expect(consent.applicableSections).to.deep.equal([]); + expect(consent.gppString).to.be.undefined; } finally { delete window.__gpp; } }); }); - describe('already known consentData:', function () { - let cmpStub = sinon.stub(); - - function mockCMP(pingData) { - return function (command, callback) { + describe('on CMP sectionChange events', () => { + let pingData, triggerCMPEvent; + beforeEach(() => { + pingData = { + applicableSections: [7], + gppString: 'xyz', + }; + triggerCMPEvent = null; + window.__gpp = sinon.stub().callsFake(function (command, callback) { switch (command) { case 'addEventListener': - // eslint-disable-next-line standard/no-callback-literal - callback({eventName: 'sectionChange', pingData}) + triggerCMPEvent = (event, payload = {}) => callback({eventName: event, pingData: {...pingData, ...payload}}) break; case 'ping': callback(pingData) break; + default: + throw new Error('unexpected __gpp invocation') } - } - } - - beforeEach(function () { - didHookReturn = false; - window.__gpp = function () {}; + }); + setConsentConfig(goodConfig); }); - afterEach(function () { - config.resetConfig(); - cmpStub.restore(); + afterEach(() => { delete window.__gpp; resetConsentData(); }); - it('should bypass CMP and simply use previously stored consentData', function () { - let testConsentData = { - applicableSections: [7], - gppString: 'xyz', - }; - - cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP({...testConsentData, signalStatus: 'ready'})); - setConsentConfig(goodConfig); - requestBidsHook(() => {}, {}); - cmpStub.reset(); - - requestBidsHook(() => { - didHookReturn = true; + function startHook() { + let hookRan = false; + consentConfig.requestBidsHook(() => { + hookRan = true; }, {}); - let consent = gppDataHandler.getConsentData(); + return () => new Promise((resolve) => setTimeout(resolve(hookRan), 5)); + } - expect(didHookReturn).to.be.true; - expect(consent.gppString).to.equal(testConsentData.gppString); - expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); - sinon.assert.notCalled(cmpStub); + it('should wait for signalStatus: ready', async () => { + const didHookRun = startHook(); + expect(await didHookRun()).to.be.false; + triggerCMPEvent('sectionChange', {signalStatus: 'not ready'}); + expect(await didHookRun()).to.be.false; + triggerCMPEvent('sectionChange', {signalStatus: 'ready'}); + await consentConfig.loadConsentData(); + expect(await didHookRun()).to.be.true; + expect(gppDataHandler.getConsentData().gppString).to.eql('xyz'); + }); + + it('should re-use GPP data once ready', async () => { + let didHookRun = startHook(); + await didHookRun(); + triggerCMPEvent('sectionChange', {signalStatus: 'ready'}); + await consentConfig.loadConsentData(); + window.__gpp.reset(); + didHookRun = startHook(); + await consentConfig.loadConsentData(); + expect(await didHookRun()).to.be.true; + sinon.assert.notCalled(window.__gpp); + }); + + it('after signalStatus: ready, should wait again for signalStatus: ready', async () => { + let didHookRun = startHook(); + await didHookRun(); + triggerCMPEvent('sectionChange', {signalStatus: 'ready'}); + await consentConfig.loadConsentData(); + for (let run of ['first', 'second']) { + triggerCMPEvent('cmpDisplayStatus', {signalStatus: 'not ready'}); + didHookRun = startHook(); + expect(await didHookRun()).to.be.false; + triggerCMPEvent('sectionChange', {signalStatus: 'ready', gppString: run}); + await consentConfig.loadConsentData(); + expect(await didHookRun()).to.be.true; + expect(gppDataHandler.getConsentData().gppString).to.eql(run); + } }); - }); + }) }); }); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index c372c66f7f0..0e8eedb2a39 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -493,7 +493,6 @@ describe('consentManagement', function () { sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); - expect(consentMeta.usp).to.equal(testConsentData.uspString); expect(consentMeta.generatedAt).to.be.above(1644367751709); }); @@ -515,7 +514,6 @@ describe('consentManagement', function () { if (cmd === 'registerDeletion') { throw new Error('CMP not compliant'); } else if (cmd === 'getUSPData') { - // eslint-disable-next-line standard/no-callback-literal cb({uspString: 'string'}, true); } }); @@ -528,7 +526,6 @@ describe('consentManagement', function () { if (cmd === 'registerDeletion') { cb(null, false); } else { - // eslint-disable-next-line standard/no-callback-literal cb({uspString: 'string'}, true); } }); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index c1ed042a2c8..e511a117ba9 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,14 +1,4 @@ -import { - actionTimeout, - consentTimeout, - gdprScope, - loadConsentData, - requestBidsHook, - resetConsentData, - setConsentConfig, - staticConsentData, - userCMP -} from 'modules/consentManagement.js'; +import {consentConfig, gdprScope, resetConsentData, setConsentConfig, } from 'modules/consentManagementTcf.js'; import {gdprDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {config} from 'src/config.js'; @@ -17,6 +7,12 @@ import 'src/prebid.js'; let expect = require('chai').expect; describe('consentManagement', function () { + function mockCMP(cmpResponse) { + return function(...args) { + args[2](Object.assign({eventStatus: 'tcloaded'}, cmpResponse), true); + } + } + describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { @@ -31,41 +27,40 @@ describe('consentManagement', function () { resetConsentData(); }); - it('should use system default values', function () { - setConsentConfig({}); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(10000); + it('should use system default values', async function () { + await setConsentConfig({}); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(10000); expect(gdprScope).to.be.equal(false); - sinon.assert.callCount(utils.logInfo, 3); }); - it('should exit consent manager if config is not an object', function () { - setConsentConfig(''); - expect(userCMP).to.be.undefined; + it('should exit consent manager if config is not an object', async function () { + await setConsentConfig(''); + expect(consentConfig.cmpHandler).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); - it('should exit consent manager if gdpr not set with new config structure', function () { - setConsentConfig({ usp: { cmpApi: 'iab', timeout: 50 } }); - expect(userCMP).to.be.undefined; + it('should exit consent manager if gdpr not set with new config structure', async function () { + await setConsentConfig({usp: {cmpApi: 'iab', timeout: 50}}); + expect(consentConfig.cmpHandler).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); - it('should exit consentManagement module if config is "undefined"', function() { - setConsentConfig(undefined); - expect(userCMP).to.be.undefined; + it('should exit consentManagement module if config is "undefined"', async function () { + await setConsentConfig(undefined); + expect(consentConfig.cmpHandler).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); - it('should not produce any consent metadata', function() { - setConsentConfig(undefined) + it('should not produce any consent metadata', async function () { + await setConsentConfig(undefined) let consentMetadata = gdprDataHandler.getConsentMeta(); expect(consentMetadata).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }) - it('should immediately look up consent data', () => { - setConsentConfig({gdpr: {cmpApi: 'invalid'}}); + it('should immediately look up consent data', async () => { + await setConsentConfig({gdpr: {cmpApi: 'invalid'}}); expect(gdprDataHandler.ready).to.be.true; }) }); @@ -75,71 +70,71 @@ describe('consentManagement', function () { config.resetConfig(); }); - it('results in all user settings overriding system defaults', function () { + it('results in all user settings overriding system defaults', async function () { let allConfig = { cmpApi: 'iab', timeout: 7500, defaultGdprScope: true }; - setConsentConfig(allConfig); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(7500); + await setConsentConfig(allConfig); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(7500); expect(gdprScope).to.be.true; }); - it('should use new consent manager config structure for gdpr', function () { - setConsentConfig({ - gdpr: { cmpApi: 'daa', timeout: 8700 } + it('should use new consent manager config structure for gdpr', async function () { + await setConsentConfig({ + gdpr: {cmpApi: 'daa', timeout: 8700} }); - expect(userCMP).to.be.equal('daa'); - expect(consentTimeout).to.be.equal(8700); + expect(consentConfig.cmpHandler).to.be.equal('daa'); + expect(consentConfig.cmpTimeout).to.be.equal(8700); }); - it('should ignore config.usp and use config.gdpr, with default cmpApi', function () { - setConsentConfig({ - gdpr: { timeout: 5000 }, - usp: { cmpApi: 'daa', timeout: 50 } + it('should ignore config.usp and use config.gdpr, with default cmpApi', async function () { + await setConsentConfig({ + gdpr: {timeout: 5000}, + usp: {cmpApi: 'daa', timeout: 50} }); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(5000); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(5000); }); - it('should ignore config.usp and use config.gdpr, with default cmpAip and timeout', function () { - setConsentConfig({ + it('should ignore config.usp and use config.gdpr, with default cmpAip and timeout', async function () { + await setConsentConfig({ gdpr: {}, - usp: { cmpApi: 'daa', timeout: 50 } + usp: {cmpApi: 'daa', timeout: 50} }); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(10000); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(10000); }); - it('should recognize config.gdpr, with default cmpAip and timeout', function () { - setConsentConfig({ + it('should recognize config.gdpr, with default cmpAip and timeout', async function () { + await setConsentConfig({ gdpr: {} }); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(10000); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(10000); }); - it('should fallback to old consent manager config object if no config.gdpr', function () { - setConsentConfig({ + it('should fallback to old consent manager config object if no config.gdpr', async function () { + await setConsentConfig({ cmpApi: 'iab', timeout: 3333, gdpr: false }); - expect(userCMP).to.be.equal('iab'); - expect(consentTimeout).to.be.equal(3333); + expect(consentConfig.cmpHandler).to.be.equal('iab'); + expect(consentConfig.cmpTimeout).to.be.equal(3333); expect(gdprScope).to.be.equal(false); }); - it('should enable gdprDataHandler', () => { - setConsentConfig({gdpr: {}}); + it('should enable gdprDataHandler', async () => { + await setConsentConfig({gdpr: {}}); expect(gdprDataHandler.enabled).to.be.true; }); }); @@ -154,7 +149,7 @@ describe('consentManagement', function () { config.resetConfig(); }); - it('results in user settings overriding system defaults for v2 spec', () => { + it('results in user settings overriding system defaults for v2 spec', async () => { const consentData = { 'tcString': 'COuqj-POu90rDBcBkBENAZCgAPzAAAPAACiQFwwBAABAA1ADEAbQC4YAYAAgAxAG0A', 'cmpId': 92, @@ -220,18 +215,17 @@ describe('consentManagement', function () { } }; - setConsentConfig({ + await setConsentConfig({ cmpApi: 'static', timeout: 7500, consentData: packageCfg(consentData) }); - expect(userCMP).to.be.equal('static'); - expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used + expect(consentConfig.cmpHandler).to.be.equal('static'); expect(gdprScope).to.be.equal(false); const consent = gdprDataHandler.getConsentData(); expect(consent.consentString).to.eql(consentData.tcString); expect(consent.vendorData).to.eql(consentData); - expect(staticConsentData).to.be.equal(consentData); + expect(consentConfig.staticConsentData).to.be.equal(consentData); }); }); }); @@ -250,14 +244,23 @@ describe('consentManagement', function () { consentData: {} } - let didHookReturn; - beforeEach(resetConsentData); after(resetConsentData) + async function runHook(request = {}) { + let hookRan = false; + consentConfig.requestBidsHook(() => { + hookRan = true + }, request); + try { + await consentConfig.loadConsentData(); + } catch (e) { + } + return hookRan; + } + describe('error checks:', function () { beforeEach(function () { - didHookReturn = false; sinon.stub(utils, 'logWarn'); sinon.stub(utils, 'logError'); }); @@ -268,69 +271,70 @@ describe('consentManagement', function () { config.resetConfig(); }); - it('should throw a warning and return to hooked function when an unknown CMP framework ID is used', function () { + it('should throw a warning and return to hooked function when an unknown CMP framework ID is used', async function () { let badCMPConfig = { cmpApi: 'bad' }; - setConsentConfig(badCMPConfig); - expect(userCMP).to.be.equal(badCMPConfig.cmpApi); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); + await setConsentConfig(badCMPConfig); + expect(consentConfig.cmpHandler).to.be.equal(badCMPConfig.cmpApi); + expect(await runHook()).to.be.true; let consent = gdprDataHandler.getConsentData(); sinon.assert.calledOnce(utils.logWarn); - expect(didHookReturn).to.be.true; expect(consent).to.be.null; }); - it('should call gpdrDataHandler.setConsentData() when unknown CMP api is used', () => { - setConsentConfig({gdpr: {cmpApi: 'invalid'}}); - let hookRan = false; - requestBidsHook(() => { hookRan = true; }, {}); - expect(hookRan).to.be.true; + it('should call gdprDataHandler.setConsentData() when unknown CMP api is used', async () => { + await setConsentConfig({gdpr: {cmpApi: 'invalid'}}); + expect(await runHook()).to.be.true; expect(gdprDataHandler.ready).to.be.true; }) - it('should throw proper errors when CMP is not found', function () { - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); + it('should throw proper errors when CMP is not found', async function () { + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.false; let consent = gdprDataHandler.getConsentData(); // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) sinon.assert.calledTwice(utils.logError); - expect(didHookReturn).to.be.false; expect(consent).to.be.null; expect(gdprDataHandler.ready).to.be.true; }); - it('should not trip when adUnits have no size', () => { - setConsentConfig(staticConfig); - let ran = false; - requestBidsHook(() => { - ran = true; - }, {adUnits: [{code: 'test', mediaTypes: {video: {}}}]}); - return gdprDataHandler.promise.then(() => { - expect(ran).to.be.true; + it('should poll again to check if it appears later', async () => { + await setConsentConfig({ + cmpApi: 'iab', + timeout: 10, }); + expect(await runHook()).to.be.false; + try { + window.__tcfapi = mockCMP({ + gdprApplies: true, + tcString: 'xyz', + }); + expect(await runHook()).to.be.true; + expect(gdprDataHandler.getConsentData().consentString).to.eql('xyz') + } finally { + delete window.__tcfapi + } + }) + + it('should not trip when adUnits have no size', async () => { + await setConsentConfig(staticConfig); + expect(await runHook({adUnits: [{code: 'test', mediaTypes: {video: {}}}]})).to.be.true; }); - it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { - setConsentConfig({ + it('should continue the auction immediately, without consent data, if timeout is 0', async () => { + window.__tcfapi = function () { + }; + await setConsentConfig({ cmpApi: 'iab', timeout: 0, defaultGdprScope: true }); - window.__tcfapi = function () {}; try { - requestBidsHook(() => { - const consent = gdprDataHandler.getConsentData(); - expect(consent.gdprApplies).to.be.true; - expect(consent.consentString).to.be.undefined; - done(); - }, {}) + expect(await runHook()).to.be.true; + const consent = gdprDataHandler.getConsentData(); + expect(consent.gdprApplies).to.be.true; + expect(consent.consentString).to.be.undefined; } finally { delete window.__tcfapi; } @@ -340,14 +344,7 @@ describe('consentManagement', function () { describe('already known consentData:', function () { let cmpStub = sinon.stub(); - function mockCMP(cmpResponse) { - return function(...args) { - args[2](Object.assign({eventStatus: 'tcloaded'}, cmpResponse), true); - } - } - beforeEach(function () { - didHookReturn = false; window.__tcfapi = function () { }; }); @@ -358,29 +355,25 @@ describe('consentManagement', function () { resetConsentData(); }); - it('should bypass CMP and simply use previously stored consentData', function () { + it('should bypass CMP and simply use previously stored consentData', async function () { let testConsentData = { gdprApplies: true, tcString: 'xyz', }; cmpStub = sinon.stub(window, '__tcfapi').callsFake(mockCMP(testConsentData)); - setConsentConfig(goodConfig); - requestBidsHook(() => { }, {}); + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; cmpStub.reset(); - requestBidsHook(() => { - didHookReturn = true; - }, {}); + expect(await runHook()).to.be.true; let consent = gdprDataHandler.getConsentData(); - - expect(didHookReturn).to.be.true; expect(consent.consentString).to.equal(testConsentData.tcString); expect(consent.gdprApplies).to.be.true; sinon.assert.notCalled(cmpStub); }); - it('should not set consent.gdprApplies to true if defaultGdprScope is true', function () { + it('should not set consent.gdprApplies to true if defaultGdprScope is true', async function () { let testConsentData = { gdprApplies: false, tcString: 'xyz', @@ -388,18 +381,14 @@ describe('consentManagement', function () { cmpStub = sinon.stub(window, '__tcfapi').callsFake(mockCMP(testConsentData)); - setConsentConfig({ + await setConsentConfig({ cmpApi: 'iab', timeout: 7500, defaultGdprScope: true }); - requestBidsHook(() => { - didHookReturn = true; - }, {}); - + expect(await runHook()).to.be.true; let consent = gdprDataHandler.getConsentData(); - expect(consent.gdprApplies).to.be.false; }); }); @@ -437,17 +426,15 @@ describe('consentManagement', function () { } function testIFramedPage(testName, messageFormatString, tarConsentString, ver) { - it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { + it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, async () => { stringifyResponse = messageFormatString; - setConsentConfig(goodConfig); - requestBidsHook(() => { - let consent = gdprDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(consent.consentString).to.equal(tarConsentString); - expect(consent.gdprApplies).to.be.true; - expect(consent.apiVersion).to.equal(ver); - done(); - }, {}); + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + let consent = gdprDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logError); + expect(consent.consentString).to.equal(tarConsentString); + expect(consent.gdprApplies).to.be.true; + expect(consent.apiVersion).to.equal(ver); }); } @@ -493,7 +480,6 @@ describe('consentManagement', function () { let cmpStub = sinon.stub(); beforeEach(function () { - didHookReturn = false; sinon.stub(utils, 'logError'); sinon.stub(utils, 'logWarn'); }); @@ -515,7 +501,7 @@ describe('consentManagement', function () { delete window.__tcfapi; }); - it('performs lookup check and stores consentData for a valid existing user', function () { + it('performs lookup check and stores consentData for a valid existing user', async function () { let testConsentData = { tcString: 'abc12345234', gdprApplies: true, @@ -526,20 +512,16 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; let consent = gdprDataHandler.getConsentData(); sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; expect(consent.consentString).to.equal(testConsentData.tcString); expect(consent.gdprApplies).to.be.true; expect(consent.apiVersion).to.equal(2); }); - it('produces gdpr metadata', function () { + it('produces gdpr metadata', async function () { let testConsentData = { tcString: 'abc12345234', gdprApplies: true, @@ -553,11 +535,9 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfig); + await setConsentConfig(goodConfig); - requestBidsHook(() => { - didHookReturn = true; - }, {}); + expect(await runHook()).to.be.true; let consentMeta = gdprDataHandler.getConsentMeta(); sinon.assert.notCalled(utils.logError); expect(consentMeta.consentStringSize).to.be.above(0) @@ -566,7 +546,7 @@ describe('consentManagement', function () { expect(consentMeta.generatedAt).to.be.above(1644367751709); }); - it('performs lookup check and stores consentData for a valid existing user with additional consent', function () { + it('performs lookup check and stores consentData for a valid existing user with additional consent', async function () { let testConsentData = { tcString: 'abc12345234', addtlConsent: 'superduperstring', @@ -578,21 +558,17 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; let consent = gdprDataHandler.getConsentData(); sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; expect(consent.consentString).to.equal(testConsentData.tcString); expect(consent.addtlConsent).to.equal(testConsentData.addtlConsent); expect(consent.gdprApplies).to.be.true; expect(consent.apiVersion).to.equal(2); }); - it('throws an error when processCmpData check fails + does not call requestBids callback', function () { + it('throws an error when processCmpData check fails + does not call requestBids callback', async function () { let testConsentData = {}; let bidsBackHandlerReturn = false; @@ -600,21 +576,18 @@ describe('consentManagement', function () { args[2](testConsentData); }); - setConsentConfig(goodConfig); + await setConsentConfig(goodConfig); sinon.assert.notCalled(utils.logWarn); sinon.assert.notCalled(utils.logError); [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); - requestBidsHook(() => { - didHookReturn = true; - }, { bidsBackHandler: () => bidsBackHandlerReturn = true }); + expect(await runHook({bidsBackHandler: () => bidsBackHandlerReturn = true})).to.be.false; let consent = gdprDataHandler.getConsentData(); sinon.assert.calledOnce(utils.logError); sinon.assert.notCalled(utils.logWarn); - expect(didHookReturn).to.be.false; expect(bidsBackHandlerReturn).to.be.true; expect(consent).to.be.null; expect(gdprDataHandler.ready).to.be.true; @@ -623,24 +596,24 @@ describe('consentManagement', function () { describe('when proper consent is not available', () => { let tcfStub; - function runAuction() { - setConsentConfig({ + async function runAuction() { + await setConsentConfig({ cmpApi: 'iab', timeout: 10, defaultGdprScope: true }); return new Promise((resolve, reject) => { - requestBidsHook(() => { - didHookReturn = true; + let hookRan = false; + consentConfig.requestBidsHook(() => { + hookRan = true; }, {}); - setTimeout(() => didHookReturn ? resolve() : reject(new Error('Auction did not run')), 20); + setTimeout(() => hookRan ? resolve() : reject(new Error('Auction did not run')), 20); }) } function mockTcfEvent(tcdata) { tcfStub.callsFake((api, version, cb) => { if (api === 'addEventListener' && version === 2) { - // eslint-disable-next-line standard/no-callback-literal cb(tcdata, true) } }); @@ -678,44 +651,42 @@ describe('consentManagement', function () { }); }); - it('should timeout after actionTimeout from the first CMP event', (done) => { + it('should timeout after actionTimeout from the first CMP event', async () => { mockTcfEvent({ eventStatus: 'cmpuishown', tcString: 'mock-consent-string', vendorData: {} }); - setConsentConfig({ + await setConsentConfig({ timeout: 1000, actionTimeout: 100, cmpApi: 'iab', defaultGdprScope: true }); let hookRan = false; - requestBidsHook(() => { + consentConfig.requestBidsHook(() => { hookRan = true; }, {}); - setTimeout(() => { - expect(hookRan).to.be.true; - done(); - }, 200) + return new Promise((resolve) => setTimeout(resolve, 200)) + .then(() => { + expect(hookRan).to.be.true; + }) }); - it('should still pick up consent data when actionTimeout is 0', (done) => { + it('should still pick up consent data when actionTimeout is 0', async () => { mockTcfEvent({ eventStatus: 'tcloaded', tcString: 'mock-consent-string', vendorData: {} }); - setConsentConfig({ + await setConsentConfig({ timeout: 1000, actionTimeout: 0, cmpApi: 'iab', defaultGdprScope: true }); - requestBidsHook(() => { - expect(gdprDataHandler.getConsentData().consentString).to.eql('mock-consent-string'); - done(); - }, {}) + expect(await runHook()).to.be.true; + expect(gdprDataHandler.getConsentData().consentString).to.eql('mock-consent-string'); }) Object.entries({ @@ -740,7 +711,7 @@ describe('consentManagement', function () { }); }); - it('It still considers it a valid cmp response if gdprApplies is not a boolean', function () { + it('It still considers it a valid cmp response if gdprApplies is not a boolean', async function () { // gdprApplies is undefined, should just still store consent response but use whatever defaultGdprScope was let testConsentData = { tcString: 'abc12345234', @@ -751,18 +722,14 @@ describe('consentManagement', function () { args[2](testConsentData, true); }); - setConsentConfig({ + await setConsentConfig({ cmpApi: 'iab', timeout: 7500, defaultGdprScope: true }); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); + expect(await runHook()).to.be.true; let consent = gdprDataHandler.getConsentData(); sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; expect(consent.consentString).to.equal(testConsentData.tcString); expect(consent.gdprApplies).to.be.true; expect(consent.apiVersion).to.equal(2); diff --git a/test/spec/modules/consumableBidAdapter_spec.js b/test/spec/modules/consumableBidAdapter_spec.js index d8e75454245..4b0eefd7e8d 100644 --- a/test/spec/modules/consumableBidAdapter_spec.js +++ b/test/spec/modules/consumableBidAdapter_spec.js @@ -66,6 +66,11 @@ const BIDDER_REQUEST_1 = { 'http://example.com/iframe1.html', 'http://example.com/iframe2.html' ] + }, + ortb2: { + device: { + language: 'en' + } } }; @@ -130,6 +135,11 @@ const BIDDER_REQUEST_2 = { 'http://example.com/iframe1.html', 'http://example.com/iframe2.html' ] + }, + ortb2: { + device: { + language: 'en' + } } }; @@ -177,6 +187,11 @@ const BIDDER_REQUEST_VIDEO = { 'http://example.com/iframe1.html', 'http://example.com/iframe2.html' ] + }, + ortb2: { + device: { + language: 'en' + } } }; @@ -188,6 +203,11 @@ const BIDDER_REQUEST_EMPTY = { gdprConsent: { consentString: 'consent-test', gdprApplies: false + }, + ortb2: { + device: { + language: 'en' + } } }; @@ -519,6 +539,12 @@ describe('Consumable BidAdapter', function () { expect(data1.placements[0].bidfloor).to.equal(0.05); expect(data2.placements[0].bidfloor).to.equal(0.15); }); + it('should contain the language param', function () { + let request = spec.buildRequests(BIDDER_REQUEST_1.bidRequest, BIDDER_REQUEST_1); + let data = JSON.parse(request.data); + + expect(data.lang).to.equal('en'); + }); }); describe('interpretResponse validation', function () { it('response should have valid bidderCode', function () { @@ -729,6 +755,48 @@ describe('Consumable BidAdapter', function () { expect(data.user.eids).to.deep.equal(bidderRequest.bidRequest[0].userIdAsEids); }); + it('Request should remove non-objects for userIdAsEids', function () { + bidderRequest.bidRequest[0].userId = {}; + bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; + bidderRequest.bidRequest[0].userIdAsEids = [ + { + source: 'adserver.org', + uids: [ + { + id: 'TTD_ID_FROM_USER_ID_MODULE', + atype: 1, + ext: { + rtiPartner: 'TDID', + }, + }, + ], + }, + 'RANDOM_IDENTIFIER_STRING' + ]; + let scrubbedEids = [ + { + source: 'adserver.org', + uids: [ + { + id: 'TTD_ID_FROM_USER_ID_MODULE', + atype: 1, + ext: { + rtiPartner: 'TDID', + }, + }, + ], + }, + ]; + let request = spec.buildRequests( + bidderRequest.bidRequest, + BIDDER_REQUEST_1 + ); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal( + scrubbedEids + ); + }); + it('Request should NOT have adsrvrOrgId params if userId is NOT object', function() { let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); let data = JSON.parse(request.data); diff --git a/test/spec/modules/contentexchangeBidAdapter_spec.js b/test/spec/modules/contentexchangeBidAdapter_spec.js index 1b3dc4f19c9..c006aa520e1 100644 --- a/test/spec/modules/contentexchangeBidAdapter_spec.js +++ b/test/spec/modules/contentexchangeBidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/contentexchangeBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'contentexchange' +const bidder = 'contentexchange'; describe('ContentexchangeBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,9 +26,9 @@ describe('ContentexchangeBidAdapter', function () { } }, params: { - placementId: 'test', - adFormat: BANNER - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -31,9 +41,9 @@ describe('ContentexchangeBidAdapter', function () { } }, params: { - placementId: 'test', - adFormat: VIDEO - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -55,9 +65,9 @@ describe('ContentexchangeBidAdapter', function () { } }, params: { - placementId: 'test', - adFormat: NATIVE - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -70,15 +80,26 @@ describe('ContentexchangeBidAdapter', function () { } }, params: { - adFormat: BANNER + } } const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -115,6 +136,7 @@ describe('ContentexchangeBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -123,7 +145,11 @@ describe('ContentexchangeBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -132,7 +158,7 @@ describe('ContentexchangeBidAdapter', function () { 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('string'); + 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); @@ -142,11 +168,62 @@ describe('ContentexchangeBidAdapter', function () { const { placements } = serverRequest.data; for (let i = 0, len = placements.length; i < len; i++) { const placement = placements[i]; - expect(placement.placementId).to.be.equal('test'); + 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 + } + ]; + + let 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'); @@ -172,8 +249,10 @@ describe('ContentexchangeBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -188,12 +267,38 @@ describe('ContentexchangeBidAdapter', function () { 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -372,6 +477,7 @@ describe('ContentexchangeBidAdapter', function () { expect(serverResponses).to.be.an('array').that.is.empty; }); }); + describe('getUserSyncs', function() { it('Should return array of objects with proper sync config , include GDPR', function() { const syncData = spec.getUserSyncs({}, {}, { @@ -396,5 +502,17 @@ describe('ContentexchangeBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://sync2.adnetwork.agency/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync2.adnetwork.agency/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/contxtfulBidAdapter_spec.js b/test/spec/modules/contxtfulBidAdapter_spec.js new file mode 100644 index 00000000000..14b4b94f062 --- /dev/null +++ b/test/spec/modules/contxtfulBidAdapter_spec.js @@ -0,0 +1,484 @@ +import { spec } from 'modules/contxtfulBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import * as ajax from 'src/ajax.js'; +const VERSION = 'v1'; +const CUSTOMER = 'CUSTOMER'; +const BIDDER_ENDPOINT = 'prebid.receptivity.io'; +const RX_FROM_API = { ReceptivityState: 'Receptive', test_info: 'rx_from_engine' }; + +describe('contxtful bid adapter', function () { + const adapter = newBidder(spec); + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('is a functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('valid code', function () { + it('should return the bidder code of contxtful', function () { + expect(spec.code).to.eql('contxtful'); + }); + }); + + let bidRequests = + [ + { + bidder: 'contxtful', + bidId: 'bId1', + custom_param_1: 'value_1', + transactionId: 'tId1', + params: { + bcat: ['cat1', 'cat2'], + badv: ['adv1', 'adv2'], + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + }, + }, + ortb2Imp: { + ext: { + tid: 't-id-test-1', + gpid: 'gpid-id-unitest-1' + }, + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'schain-seller-1.com', + sid: '00001', + hp: 1, + }, + ], + }, + getFloor: () => ({ currency: 'CAD', floor: 10 }), + } + ]; + + let expectedReceptivityData = { + rx: RX_FROM_API, + params: { + ev: VERSION, + ci: CUSTOMER, + }, + }; + + let bidderRequest = { + refererInfo: { + ref: 'https://my-referer-custom.com', + }, + ortb2: { + source: { + tid: 'auction-id', + }, + property_1: 'string_val_1', + regs: { + coppa: 1, + ext: { + us_privacy: '12345' + } + }, + user: { + data: [ + { + name: 'contxtful', + ext: expectedReceptivityData + } + ], + ext: { + eids: [ + { + source: 'id5-sync.com', + uids: [ + { + atype: 1, + id: 'fake-id5id', + }, + ] + } + ] + } + } + + }, + timeout: 1234, + uspConsent: '12345' + }; + + describe('valid configuration', function() { + const theories = [ + [ + null, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'null object for config', + ], + [ + {}, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'empty object for config', + ], + [ + { customer: CUSTOMER }, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'customer only in config', + ], + [ + { version: VERSION }, + 'contxfulBidAdapter: contxtful.customer should be a non-empty string', + 'version only in config', + ], + [ + { customer: CUSTOMER, version: '' }, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'empty string for version', + ], + [ + { customer: '', version: VERSION }, + 'contxfulBidAdapter: contxtful.customer should be a non-empty string', + 'empty string for customer', + ], + [ + { customer: '', version: '' }, + 'contxfulBidAdapter: contxtful.version should be a non-empty string', + 'empty string for version & customer', + ], + ]; + + theories.forEach(([params, expectedErrorMessage, description]) => { + it('detects invalid configuration and throws the expected error (' + description + ')', () => { + config.setConfig({ + contxtful: params + }); + expect(() => spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + })).to.throw( + expectedErrorMessage + ); + }); + }); + + it('uses a valid configuration and returns the right url', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION} + }); + const bidRequest = spec.buildRequests(bidRequests); + expect(bidRequest.url).to.eq('https://' + BIDDER_ENDPOINT + `/${VERSION}/prebid/${CUSTOMER}/bid`) + }); + + it('will take specific ortb2 configuration parameters and returns it in ortb2 object', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + expect(bidRequest.data.ortb2.property_1).to.equal('string_val_1'); + }); + }); + + describe('valid bid request', function () { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + + it('will return a data property containing properties ortb2, bidRequests, bidderRequest and config', () => { + expect(bidRequest.data).not.to.be.undefined; + expect(bidRequest.data.ortb2).not.to.be.undefined; + expect(bidRequest.data.bidRequests).not.to.be.undefined; + expect(bidRequest.data.bidderRequest).not.to.be.undefined; + expect(bidRequest.data.config).not.to.be.undefined; + }); + + it('will take custom parameters in the bid request and within the bidRequests array', () => { + expect(bidRequest.data.bidRequests[0].custom_param_1).to.equal('value_1') + }); + + it('will return any supply chain parameters within the bidRequests array', () => { + expect(bidRequest.data.bidRequests[0].schain.ver).to.equal('1.0'); + }); + + it('will return floor request within the bidFloor parameter in the bidRequests array', () => { + expect(bidRequest.data.bidRequests[0].bidFloor.currency).to.equal('CAD'); + expect(bidRequest.data.bidRequests[0].bidFloor.floor).to.equal(10); + }); + + it('will return the usp string in the uspConsent parameter within the bidderRequest property', () => { + expect(bidRequest.data.bidderRequest.uspConsent).to.equal('12345'); + }); + + it('will contains impressions array on ortb2.imp object for all ad units', () => { + expect(bidRequest.data.ortb2.imp.length).to.equal(1); + expect(bidRequest.data.ortb2.imp[0].id).to.equal('bId1'); + }); + + it('will contains the registration on ortb2.regs object', () => { + expect(bidRequest.data.ortb2.regs).not.to.be.undefined; + expect(bidRequest.data.ortb2.regs.coppa).to.equal(1); + expect(bidRequest.data.ortb2.regs.ext.us_privacy).to.equal('12345') + }) + + it('will contains the eids modules within the ortb2.user.ext.eids', () => { + expect(bidRequest.data.ortb2.user.ext.eids).not.to.be.undefined; + expect(bidRequest.data.ortb2.user.ext.eids[0].source).to.equal('id5-sync.com'); + expect(bidRequest.data.ortb2.user.ext.eids[0].uids[0].id).to.equal('fake-id5id'); + }); + + it('will contains the receptivity value within the ortb2.user.data with contxtful name', () => { + let obtained_receptivity_data = bidRequest.data.ortb2.user.data.filter(function(userData) { + return userData.name == 'contxtful'; + }); + expect(obtained_receptivity_data.length).to.equal(1); + expect(obtained_receptivity_data[0].ext).to.deep.equal(expectedReceptivityData); + }); + + it('will contains ortb2Imp of the bid request within the ortb2.imp.ext', () => { + let first_imp = bidRequest.data.ortb2.imp[0]; + expect(first_imp.ext).not.to.be.undefined; + expect(first_imp.ext.tid).to.equal('t-id-test-1'); + expect(first_imp.ext.gpid).to.equal('gpid-id-unitest-1'); + }); + }); + + describe('valid bid request with no floor module', () => { + let noFloorsBidRequests = + [ + { + bidder: 'contxtful', + bidId: 'bId1', + transactionId: 'tId1', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + }, + }, + }, + { + bidder: 'contxtful', + bidId: 'bId2', + transactionId: 'tId2', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + }, + }, + params: { + bidfloor: 54 + } + }, + ]; + + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + + const bidRequest = spec.buildRequests(noFloorsBidRequests, bidderRequest); + it('will contains default value of floor if the bid request do not contains floor function', () => { + expect(bidRequest.data.bidRequests[0].bidFloor.currency).to.equal('USD'); + expect(bidRequest.data.bidRequests[0].bidFloor.floor).to.equal(0); + }); + + it('will take the param.bidfloor as floor value if possible', () => { + expect(bidRequest.data.bidRequests[1].bidFloor.currency).to.equal('USD'); + expect(bidRequest.data.bidRequests[1].bidFloor.floor).to.equal(54); + }); + }); + + describe('valid bid response', () => { + const bidResponse = [ + { + 'requestId': 'arequestId', + 'originalCpm': 1.5, + 'cpm': 1.35, + 'currency': 'CAD', + 'width': 300, + 'height': 600, + 'creativeId': 'creativeid', + 'netRevenue': true, + 'ttl': 300, + 'ad': '', + 'mediaType': 'banner', + 'syncs': [ + { + 'url': 'mysyncurl.com?qparam1=qparamv1&qparam2=qparamv2' + } + ] + } + ]; + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + + it('will interpret response correcly', () => { + const bids = spec.interpretResponse({ body: bidResponse }, bidRequest); + expect(bids).not.to.be.undefined; + expect(bids).to.have.lengthOf(1); + expect(bids).to.deep.equal(bidResponse); + }); + + it('will return empty response if bid response is empty', () => { + const bids = spec.interpretResponse({ body: [] }, bidRequest); + expect(bids).to.have.lengthOf(0); + }) + + it('will trigger user sync if enable pixel mode', () => { + const syncOptions = { + pixelEnabled: true + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [{ body: bidResponse }]); + expect(userSyncs).to.deep.equal([ + { + 'url': 'mysyncurl.com/image?pbjs=1&coppa=0&qparam1=qparamv1&qparam2=qparamv2', + 'type': 'image' + } + ]); + }); + + it('will trigger user sync if enable iframe mode', () => { + const syncOptions = { + iframeEnabled: true + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [{ body: bidResponse }]); + expect(userSyncs).to.deep.equal([ + { + 'url': 'mysyncurl.com/iframe?pbjs=1&coppa=0&qparam1=qparamv1&qparam2=qparamv2', + 'type': 'iframe' + } + ]); + }); + + describe('no sync option', () => { + it('will return image sync if no sync options', () => { + const userSyncs = spec.getUserSyncs({}, [{ body: bidResponse }]); + expect(userSyncs).to.deep.equal([ + { + 'url': 'mysyncurl.com/image?pbjs=1&coppa=0&qparam1=qparamv1&qparam2=qparamv2', + 'type': 'image' + } + ]); + }); + it('will return empty value if no server response', () => { + const userSyncs = spec.getUserSyncs({}, []); + expect(userSyncs).to.have.lengthOf(0); + const userSyncs2 = spec.getUserSyncs({}, null); + expect(userSyncs2).to.have.lengthOf(0); + }); + }); + + it('will return empty value if no server response', () => { + const syncOptions = { + iframeEnabled: true + }; + + const userSyncs = spec.getUserSyncs(syncOptions, []); + expect(userSyncs).to.have.lengthOf(0); + const userSyncs2 = spec.getUserSyncs(syncOptions, null); + expect(userSyncs2).to.have.lengthOf(0); + }); + + describe('onTimeout callback', () => { + it('will always call server with sendBeacon available', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + + const beaconStub = sandbox.stub(ajax, 'sendBeacon').returns(true); + const ajaxStub = sandbox.stub(ajax, 'ajax'); + expect(spec.onTimeout({'customData': 'customvalue'})).to.not.throw; + expect(beaconStub.called).to.be.true; + expect(ajaxStub.called).to.be.false; + }); + + it('will always call server with sendBeacon not available', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + + const ajaxStub = sandbox.stub(ajax, 'ajax'); + const beaconStub = sandbox.stub(ajax, 'sendBeacon').returns(false); + expect(spec.onTimeout({'customData': 'customvalue'})).to.not.throw; + expect(beaconStub.called).to.be.true; + expect(beaconStub.returned(false)).to.be.true; + expect(ajaxStub.calledOnce).to.be.true; + }); + }); + + describe('on onBidderError callback', () => { + it('will always call server', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + + const ajaxStub = sandbox.stub(ajax, 'ajax'); + const beaconStub = sandbox.stub(ajax, 'sendBeacon').returns(false); + spec.onBidderError({'customData': 'customvalue'}); + expect(ajaxStub.calledOnce).to.be.true; + expect(beaconStub.returned(false)).to.be.true; + }); + }); + + describe('on onBidWon callback', () => { + it('will always call server', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION}, + }); + + const ajaxStub = sandbox.stub(ajax, 'ajax'); + const beaconStub = sandbox.stub(ajax, 'sendBeacon').returns(false); + spec.onBidWon({'customData': 'customvalue'}); + expect(ajaxStub.calledOnce).to.be.true; + expect(beaconStub.returned(false)).to.be.true; + }); + }); + + describe('on onBidBillable callback', () => { + it('will always call server when sampling rate is configured to be 1.0', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION, sampling: {onBidBillable: 1.0}}, + }); + const ajaxStub = sandbox.stub(ajax, 'ajax'); + const beaconStub = sandbox.stub(ajax, 'sendBeacon').returns(false); + spec.onBidBillable({'customData': 'customvalue'}); + expect(ajaxStub.calledOnce).to.be.true; + expect(beaconStub.returned(false)).to.be.true; + }); + }); + + describe('on onAdRenderSucceeded callback', () => { + it('will always call server when sampling rate is configured to be 1.0', () => { + config.setConfig({ + contxtful: {customer: CUSTOMER, version: VERSION, sampling: {onAdRenderSucceeded: 1.0}}, + }); + const ajaxStub = sandbox.stub(ajax, 'ajax'); + const beaconStub = sandbox.stub(ajax, 'sendBeacon').returns(false); + spec.onAdRenderSucceeded({'customData': 'customvalue'}); + expect(ajaxStub.calledOnce).to.be.true; + expect(beaconStub.returned(false)).to.be.true; + }); + }); + }); +}); diff --git a/test/spec/modules/contxtfulRtdProvider_spec.js b/test/spec/modules/contxtfulRtdProvider_spec.js index 541c0e6e6dd..ed313cf4501 100644 --- a/test/spec/modules/contxtfulRtdProvider_spec.js +++ b/test/spec/modules/contxtfulRtdProvider_spec.js @@ -1,41 +1,108 @@ -import { contxtfulSubmodule } from '../../../modules/contxtfulRtdProvider.js'; +import { contxtfulSubmodule, extractParameters } from '../../../modules/contxtfulRtdProvider.js'; import { expect } from 'chai'; import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; - +import { getStorageManager } from '../../../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../../../src/activities/modules.js'; import * as events from '../../../src/events'; +import * as utils from 'src/utils.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js' +import Sinon from 'sinon'; +import { deepClone, getWinDimensions } from '../../../src/utils.js'; + +const MODULE_NAME = 'contxtful'; -const _ = null; const VERSION = 'v1'; const CUSTOMER = 'CUSTOMER'; -const CONTXTFUL_CONNECTOR_ENDPOINT = `https://api.receptivity.io/${VERSION}/prebid/${CUSTOMER}/connector/p.js`; -const INITIAL_RECEPTIVITY = { ReceptivityState: 'INITIAL_RECEPTIVITY' }; -const INITIAL_RECEPTIVITY_EVENT = new CustomEvent('initialReceptivity', { detail: INITIAL_RECEPTIVITY }); +const SM = 'SM'; +const CONTXTFUL_CONNECTOR_ENDPOINT = `https://api.receptivity.io/${VERSION}/prebid/${CUSTOMER}/connector/rxConnector.js`; + +const RX_FROM_SESSION_STORAGE = { ReceptivityState: 'Receptive', test_info: 'rx_from_session_storage' }; +const RX_FROM_API = { ReceptivityState: 'Receptive', test_info: 'rx_from_engine' }; + +const RX_API_MOCK = { receptivity: sinon.stub(), receptivityBatched: sinon.stub() }; +const RX_API_MOCK_WITH_BUNDLE = { receptivity: sinon.stub(), receptivityBatched: sinon.stub(), getOrtb2Fragment: sinon.stub() } + +const RX_CONNECTOR_MOCK = { + fetchConfig: sinon.stub(), + rxApiBuilder: sinon.stub(), +}; -const CONTXTFUL_API = { GetReceptivity: sinon.stub() } -const RX_ENGINE_IS_READY_EVENT = new CustomEvent('rxEngineIsReady', {detail: CONTXTFUL_API}); +const TIMEOUT = 10; +const RX_CONNECTOR_IS_READY_EVENT = new CustomEvent('rxConnectorIsReady', { detail: { [CUSTOMER]: RX_CONNECTOR_MOCK }, bubbles: true }); function buildInitConfig(version, customer) { return { name: 'contxtful', params: { - version, - customer, + version: version, + customer: customer, + hostname: 'api.receptivity.io', + bidders: ['mock-bidder-code'], + adServerTargeting: true, }, }; } +function fakeGetElementById(width, height, x, y) { + const obj = { x, y, width, height }; + + return { + ...obj, + getBoundingClientRect: () => { + return { + width: obj.width, + height: obj.height, + left: obj.x, + top: obj.y, + right: obj.x + obj.width, + bottom: obj.y + obj.height + }; + } + }; +} + describe('contxtfulRtdProvider', function () { let sandbox = sinon.sandbox.create(); let loadExternalScriptTag; let eventsEmitSpy; + const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); + beforeEach(() => { loadExternalScriptTag = document.createElement('script'); loadExternalScriptStub.callsFake((_url, _moduleName) => loadExternalScriptTag); - CONTXTFUL_API.GetReceptivity.reset(); + RX_API_MOCK.receptivity.reset(); + RX_API_MOCK.receptivity.callsFake(() => RX_FROM_API); + + RX_API_MOCK.receptivityBatched.reset(); + RX_API_MOCK.receptivityBatched.callsFake((bidders) => bidders.reduce((accumulator, bidder) => { accumulator[bidder] = RX_FROM_API; return accumulator; }, {})); + + RX_API_MOCK_WITH_BUNDLE.receptivity.reset(); + RX_API_MOCK_WITH_BUNDLE.receptivity.callsFake(() => RX_FROM_API); + + RX_API_MOCK_WITH_BUNDLE.receptivityBatched.reset(); + RX_API_MOCK_WITH_BUNDLE.receptivityBatched.callsFake((bidders) => bidders.reduce((accumulator, bidder) => { accumulator[bidder] = RX_FROM_API; return accumulator; }, {})); + + RX_API_MOCK_WITH_BUNDLE.getOrtb2Fragment.reset(); + RX_API_MOCK_WITH_BUNDLE.getOrtb2Fragment.callsFake((bidders, reqBidsConfigObj) => { + let bidderObj = bidders.reduce((accumulator, bidder) => { accumulator[bidder] = { user: { data: [{ name: MODULE_NAME, value: RX_FROM_API }] } }; return accumulator; }, {}); + return { global: { user: { site: { id: 'globalsiteId' } } }, bidder: bidderObj } + } + ); + + RX_CONNECTOR_MOCK.fetchConfig.reset(); + RX_CONNECTOR_MOCK.fetchConfig.callsFake((tagId) => new Promise((resolve, reject) => resolve({ tag_id: tagId }))); + + RX_CONNECTOR_MOCK.rxApiBuilder.reset(); + RX_CONNECTOR_MOCK.rxApiBuilder.callsFake((_config) => new Promise((resolve, reject) => resolve(RX_API_MOCK))); eventsEmitSpy = sandbox.spy(events, ['emit']); + + sandbox.stub(utils, 'generateUUID').returns(SM); + + let tagId = CUSTOMER; + sessionStorage.clear(); }); afterEach(function () { @@ -43,7 +110,7 @@ describe('contxtfulRtdProvider', function () { sandbox.restore(); }); - describe('extractParameters with invalid configuration', () => { + describe('extractParameters', () => { const { params: { customer, version }, } = buildInitConfig(VERSION, CUSTOMER); @@ -87,27 +154,27 @@ describe('contxtfulRtdProvider', function () { theories.forEach(([params, expectedErrorMessage, _description]) => { const config = { name: 'contxtful', params }; - it('throws the expected error', () => { - expect(() => contxtfulSubmodule.extractParameters(config)).to.throw( + it('detects invalid configuration and throws the expected error', () => { + expect(() => extractParameters(config)).to.throw( expectedErrorMessage ); }); }); }); - describe('initialization with invalid config', function () { - it('returns false', () => { + describe('extractParameters', function () { + it('detects invalid configuration and returns false', () => { expect(contxtfulSubmodule.init({})).to.be.false; }); }); - describe('initialization with valid config', function () { - it('returns true when initializing', () => { + describe('init', function () { + it('uses a valid configuration and returns true when initializing', () => { const config = buildInitConfig(VERSION, CUSTOMER); expect(contxtfulSubmodule.init(config)).to.be.true; }); - it('loads contxtful module script asynchronously', (done) => { + it('loads a RX connector script asynchronously', (done) => { contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); setTimeout(() => { @@ -115,54 +182,84 @@ describe('contxtfulRtdProvider', function () { expect(loadExternalScriptStub.args[0][0]).to.equal( CONTXTFUL_CONNECTOR_ENDPOINT ); + done(); - }, 10); + }, TIMEOUT); }); }); - describe('load external script return falsy', function () { + describe('init', function () { it('returns true when initializing', () => { - loadExternalScriptStub.callsFake(() => {}); + loadExternalScriptStub.callsFake((url, moduleCode, callback, doc, attributes) => { + return { addEventListener: (type, listener) => { } }; + }); const config = buildInitConfig(VERSION, CUSTOMER); expect(contxtfulSubmodule.init(config)).to.be.true; }); }); - describe('rxEngine from external script', function () { - it('use rxEngine api to get receptivity', () => { - contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); - loadExternalScriptTag.dispatchEvent(RX_ENGINE_IS_READY_EVENT); + describe('init', function () { + it('uses the RX API to get receptivity', (done) => { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); - contxtfulSubmodule.getTargetingData(['ad-slot']); + setTimeout(() => { + contxtfulSubmodule.getTargetingData(['ad-slot'], config); + expect(RX_API_MOCK.receptivity.callCount, 'receptivity 42').to.be.equal(1); + expect(RX_API_MOCK.receptivity.firstCall.returnValue, 'receptivity').to.be.equal(RX_FROM_API); + done(); + }, TIMEOUT); + }); + }); - expect(CONTXTFUL_API.GetReceptivity.calledOnce).to.be.true; + describe('init', function () { + it('gets the RX API returned by an external script', (done) => { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + setTimeout(() => { + contxtfulSubmodule.getTargetingData(['ad-slot'], config); + expect(RX_CONNECTOR_MOCK.fetchConfig.callCount, 'fetchConfig').at.least(1); + expect(RX_CONNECTOR_MOCK.rxApiBuilder.callCount, 'rxApiBuilder').at.least(1); + done(); + }, TIMEOUT); }); }); - describe('initial receptivity is not dispatched', function () { - it('does not initialize receptivity value', () => { - contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + describe('init', function () { + it('detect that initial receptivity is not dispatched and it does not initialize receptivity value', (done) => { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); - let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); - expect(targetingData).to.deep.equal({}); + setTimeout(() => { + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot'], config); + expect(targetingData).to.deep.equal({}); + done(); + }, TIMEOUT); }); }); - describe('initial receptivity is invalid', function () { + describe('init', function () { const theories = [ [new Event('initialReceptivity'), 'event without details'], - [new CustomEvent('initialReceptivity', { }), 'custom event without details'], + [new CustomEvent('initialReceptivity', {}), 'custom event without details'], [new CustomEvent('initialReceptivity', { detail: {} }), 'custom event with invalid details'], [new CustomEvent('initialReceptivity', { detail: { ReceptivityState: '' } }), 'custom event with details without ReceptivityState'], ]; theories.forEach(([initialReceptivityEvent, _description]) => { - it('does not initialize receptivity value', () => { - contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + it('figures out that initial receptivity is invalid and it does not initialize receptivity value', (done) => { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); loadExternalScriptTag.dispatchEvent(initialReceptivityEvent); - let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); - expect(targetingData).to.deep.equal({}); + setTimeout(() => { + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot'], config); + expect(targetingData).to.deep.equal({}); + done(); + }, TIMEOUT); }); }) }); @@ -173,28 +270,782 @@ describe('contxtfulRtdProvider', function () { [[], {}, 'empty ad-slots'], [ ['ad-slot'], - { 'ad-slot': { ReceptivityState: 'INITIAL_RECEPTIVITY' } }, + { 'ad-slot': RX_FROM_API }, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + 'ad-slot-1': RX_FROM_API, + 'ad-slot-2': RX_FROM_API, + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, description]) => { + it('adds receptivity to the ad units using the RX API', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + setTimeout(() => { + let targetingData = contxtfulSubmodule.getTargetingData(adUnits, config); + expect(targetingData, description).to.deep.equal(expected, description); + done(); + }, TIMEOUT); + }); + }); + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + {}, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, description]) => { + it('honours "adServerTargeting" and the RX API is not called', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + config.params.adServerTargeting = false; + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + setTimeout(() => { + let _ = contxtfulSubmodule.getTargetingData(adUnits, config); + expect(RX_API_MOCK.receptivity.callCount).to.be.equal(0); + done(); + }, TIMEOUT); + }); + + it('honours adServerTargeting and it does not add receptivity to the ad units', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + config.params.adServerTargeting = false; + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + setTimeout(() => { + let targetingData = contxtfulSubmodule.getTargetingData(adUnits, config); + expect(targetingData, description).to.deep.equal(expected); + done(); + }, TIMEOUT); + }); + }); + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + { 'ad-slot': RX_FROM_SESSION_STORAGE }, 'single ad-slot', ], [ ['ad-slot-1', 'ad-slot-2'], { - 'ad-slot-1': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, - 'ad-slot-2': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, + 'ad-slot-1': RX_FROM_SESSION_STORAGE, + 'ad-slot-2': RX_FROM_SESSION_STORAGE, }, 'many ad-slots', ], ]; theories.forEach(([adUnits, expected, _description]) => { - it('adds "ReceptivityState" to the adUnits', function () { - contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); - loadExternalScriptTag.dispatchEvent(INITIAL_RECEPTIVITY_EVENT); + it('uses non-expired info from session storage and adds receptivity to the ad units using session storage', function (done) { + // Simulate that there was a write to sessionStorage in the past. + storage.setDataInSessionStorage(CUSTOMER, JSON.stringify({ exp: new Date().getTime() + 1000, rx: RX_FROM_SESSION_STORAGE })) - expect(contxtfulSubmodule.getTargetingData(adUnits)).to.deep.equal( - expected - ); + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + + let targetingData = contxtfulSubmodule.getTargetingData(adUnits, config); + expect(targetingData).to.deep.equal(expected); + + done(); + }); + }); + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + {}, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, _description]) => { + it('ignores expired info from session storage and does not forward the info to ad units', function (done) { + // Simulate that there was a write to sessionStorage in the past. + storage.setDataInSessionStorage(CUSTOMER, JSON.stringify({ exp: new Date().getTime() - 100, rx: RX_FROM_SESSION_STORAGE })); + + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + + let targetingData = contxtfulSubmodule.getTargetingData(adUnits, config); + expect(targetingData).to.deep.equal(expected); + + done(); }); }); }); + + describe('getBidRequestData', function () { + it('calls once the onDone callback', function (done) { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, buildInitConfig(VERSION, CUSTOMER)); + expect(onDoneSpy.calledOnce).to.be.true; + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('does not write receptivity to the global OpenRTB 2 fragment', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + const onDone = () => 42; + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDone, config); + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.equal({}); + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('writes receptivity to the configured bidder OpenRTB 2 fragments', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + let expectedData = { + name: 'contxtful', + ext: { + rx: RX_FROM_API, + params: { + ev: config.params?.version, + ci: config.params?.customer, + }, + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let data = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0]; + + expect(data.name).to.deep.equal(expectedData.name); + expect(data.ext.rx).to.deep.equal(expectedData.ext.rx); + expect(data.ext.params).to.deep.equal(expectedData.ext.params); + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('uses non-expired info from session storage and adds receptivity to the reqBidsConfigObj', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + + // Simulate that there was a write to sessionStorage in the past. + let bidder = config.params.bidders[0]; + + storage.setDataInSessionStorage(`${config.params.customer}_${bidder}`, JSON.stringify({ exp: new Date().getTime() + 1000, rx: RX_FROM_SESSION_STORAGE })); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + contxtfulSubmodule.init(config); + + // Since the RX_CONNECTOR_IS_READY_EVENT event was not dispatched, the RX engine is not loaded. + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, () => { }, config); + + setTimeout(() => { + let ortb2BidderFragment = reqBidsConfigObj.ortb2Fragments.bidder[bidder]; + let userData = ortb2BidderFragment.user.data; + let contxtfulData = userData[0]; + + expect(contxtfulData.name).to.be.equal('contxtful'); + expect(contxtfulData.ext.rx).to.deep.equal(RX_FROM_SESSION_STORAGE); + expect(contxtfulData.ext.params).to.deep.equal({ + ev: config.params.version, + ci: config.params.customer, + }); + + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('uses the RX API', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + expect(RX_CONNECTOR_MOCK.fetchConfig.callCount).at.least(1); + expect(RX_CONNECTOR_MOCK.rxApiBuilder.callCount).at.least(1); + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + expect(onDoneSpy.callCount).to.equal(1); + expect(RX_API_MOCK.receptivityBatched.callCount).to.equal(1); + done(); + }, TIMEOUT); + }); + }); + + describe('getBidRequestData', function () { + it('adds receptivity to the reqBidsConfigObj', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + let expectedData = { + name: 'contxtful', + ext: { + rx: RX_FROM_API, + sm: SM, + params: { + ev: config.params?.version, + ci: config.params?.customer, + }, + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let data = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0]; + + expect(data.name).to.deep.equal(expectedData.name); + expect(data.ext.rx).to.deep.equal(expectedData.ext.rx); + expect(data.ext.sm).to.deep.equal(expectedData.ext.sm); + expect(data.ext.params).to.deep.equal(expectedData.ext.params); + done(); + }, TIMEOUT); + }); + + it('does not change the sm', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let firstReqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + let secondReqBidsConfigObj = deepClone(firstReqBidsConfigObj); + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(firstReqBidsConfigObj, onDoneSpy, config); + contxtfulSubmodule.getBidRequestData(secondReqBidsConfigObj, onDoneSpy, config); + + let firstData = firstReqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0]; + let secondData = secondReqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0]; + + expect(firstData.ext.sm).to.equal(secondData.ext.sm); + + done(); + }, TIMEOUT); + }); + + describe('before rxApi is loaded', function () { + const moveEventTheories = [ + [ + new PointerEvent('pointermove', { clientX: 1, clientY: 2 }), + { x: 1, y: 2 }, + 'pointer move', + ] + ]; + + moveEventTheories.forEach(([event, expected, _description]) => { + it('adds move event', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + + window.dispatchEvent(event); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + + let events = JSON.parse(atob(ext.events)); + + expect(events.ui.position.x).to.be.deep.equal(expected.x); + expect(events.ui.position.y).to.be.deep.equal(expected.y); + expect(Sinon.match.number.test(events.ui.position.timestampMs)).to.be.true; + done(); + }, TIMEOUT); + }); + }); + + it('adds screen event', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + + // Cannot change the window size from JS + // So we take the current size as expectation + const { innerHeight: height, innerWidth: width } = getWinDimensions() + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + + let events = JSON.parse(atob(ext.events)); + + expect(events.ui.screen.topLeft).to.be.deep.equal({ x: 0, y: 0 }, 'screen top left'); + expect(events.ui.screen.width).to.be.deep.equal(width, 'screen width'); + expect(events.ui.screen.height).to.be.deep.equal(height, 'screen height'); + expect(Sinon.match.number.test(events.ui.screen.timestampMs), 'screen timestamp').to.be.true; + done(); + }, TIMEOUT); + }); + }); + }); + + describe('when there is no ad units', function () { + it('adds empty ad unit positions', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(0); + done(); + }, TIMEOUT); + }); + }); + + describe('when there are ad units', function () { + it('return empty objects for ad units that we can\'t get position of', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + let reqBidsConfigObj = { + adUnits: [ + { code: 'code1' }, + { code: 'code2' } + ], + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(0); + done(); + }, TIMEOUT); + }); + + it('returns the IAB position if the ad unit div id cannot be bound but property pos can be found in the ad unit', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + let reqBidsConfigObj = { + adUnits: [ + { code: 'code1', mediaTypes: { banner: { pos: 4 } } }, + { code: 'code2', mediaTypes: { banner: { pos: 5 } } }, + { code: 'code3', mediaTypes: { banner: { pos: 0 } } }, + ], + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(3); + expect(pos['code1'].p).to.be.equal(4); + expect(pos['code2'].p).to.be.equal(5); + expect(pos['code3'].p).to.be.equal(0); + done(); + }, TIMEOUT); + }) + + function getFakeRequestBidConfigObj() { + return { + adUnits: [ + { code: 'code1', ortb2Imp: { ext: { data: { divId: 'divId1' } } } }, + { code: 'code2', ortb2Imp: { ext: { data: { divId: 'divId2' } } } } + ], + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + } + + function InitDivStubPositions(config, withIframe, isVisible, forceGetElementById = true) { + let fakeElem = fakeGetElementById(100, 100, 30, 30); + if (isVisible) { + fakeElem.checkVisibility = function () { return true }; + sandbox.stub(window.top, 'getComputedStyle').returns({ display: 'block' }); + } else { + fakeElem.checkVisibility = function () { return false }; + sandbox.stub(window.top, 'getComputedStyle').returns({ display: 'none' }); + } + + if (withIframe) { + let ws = { + frameElement: { + getBoundingClientRect: () => fakeElem.getBoundingClientRect() + }, + document: { + getElementById: (id) => fakeElem, + + } + } + sandbox.stub(utils, 'getWindowSelf').returns(window.top); + sandbox.stub(utils, 'inIframe').returns(true); + sandbox.stub(fakeElem, 'checkVisibility').returns(isVisible); + } else { + sandbox.stub(utils, 'inIframe').returns(false); + sandbox.stub(fakeElem, 'checkVisibility').returns(isVisible); + } + if (forceGetElementById) { + sandbox.stub(window.top.document, 'getElementById').returns(fakeElem); + } + contxtfulSubmodule.init(config); + } + + describe('when the div id cannot be found, we should try with GPT method', function () { + it('returns an empty list if gpt not find the div', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + let reqBidsConfigObj = { + adUnits: [ + { code: 'code1' }, + { code: 'code2' } + ], + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + InitDivStubPositions(config, false, true, false); + let fakeElem = fakeGetElementById(100, 100, 30, 30); + sandbox.stub(window.top.document, 'getElementById').returns(function (id) { + if (id == 'code1' || id == 'code2') { + return undefined; + } else { + return fakeElem; + } + }); + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(0); + done(); + }, TIMEOUT); + }) + + it('returns object visibility and position if gpt not found but the div id is the ad unit code', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + let reqBidsConfigObj = { + adUnits: [ + { code: 'code1' }, + { code: 'code2' } + ], + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + InitDivStubPositions(config, false, true); + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(2); + expect(pos['code1'].p.x).to.be.equal(30); + expect(pos['code1'].p.y).to.be.equal(30); + expect(pos['code1'].v).to.be.equal(true); + done(); + }, TIMEOUT); + }); + + it('returns object visibility and position if gpt finds the div', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + let reqBidsConfigObj = { + adUnits: [ + { code: 'code1' }, + { code: 'code2' } + ], + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + InitDivStubPositions(config, false, true); + sandbox.stub(gptUtils, 'getGptSlotInfoForAdUnitCode').returns({ divId: 'div1' }); + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(2); + expect(pos['code1'].p.x).to.be.equal(30); + expect(pos['code1'].p.y).to.be.equal(30); + expect(pos['code1'].v).to.be.equal(true); + done(); + }, TIMEOUT); + }); + }); + + describe('when we get object visibility and position for ad units that we can get div id', function () { + let config = buildInitConfig(VERSION, CUSTOMER); + + describe('when we are not in an iframe', function () { + it('return object visibility true if element is visible', function (done) { + let reqBidsConfigObj = getFakeRequestBidConfigObj(); + InitDivStubPositions(config, false, true); + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(2); + expect(pos['code1'].p.x).to.be.equal(30); + expect(pos['code1'].p.y).to.be.equal(30); + expect(pos['code1'].v).to.be.equal(true); + done(); + }, TIMEOUT); + }); + + it('return object visibility false if element is not visible', function (done) { + let reqBidsConfigObj = getFakeRequestBidConfigObj(); + InitDivStubPositions(config, false, false); + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(2); + expect(pos['code1'].v).to.be.equal(false); + expect(pos['code2'].v).to.be.equal(false); + done(); + }, TIMEOUT); + }); + }); + + describe('when we are in an iframe', function () { + it('return object visibility true if element is visible', function (done) { + let reqBidsConfigObj = getFakeRequestBidConfigObj(); + InitDivStubPositions(config, true, true) + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(2); + expect(pos['code1'].p.x).to.be.equal(30); + expect(pos['code1'].p.y).to.be.equal(30); + expect(pos['code1'].v).to.be.equal(true); + done(); + }, TIMEOUT); + }); + + it('return object visibility false if element is not visible', function (done) { + let reqBidsConfigObj = getFakeRequestBidConfigObj(); + InitDivStubPositions(config, true, false); + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + let pos = JSON.parse(atob(ext.pos)); + + expect(Object.keys(pos).length).to.be.equal(2); + expect(pos['code1'].v).to.be.equal(false); + done(); + }, TIMEOUT); + }); + }); + }); + }); + + describe('after rxApi is loaded', function () { + it('should add event', function (done) { + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + + let ext = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]].user.data[0].ext; + + let events = ext.events; + + expect(events).to.be.not.undefined; + done(); + }, TIMEOUT); + }); + }) + + describe('when rxConnector contains getOrtb2Fragment function', () => { + it('should just take whatever it contains and merge to the fragment', function (done) { + RX_CONNECTOR_MOCK.rxApiBuilder.reset(); + RX_CONNECTOR_MOCK.rxApiBuilder.callsFake((_config) => new Promise((resolve, reject) => resolve(RX_API_MOCK_WITH_BUNDLE))); + + let config = buildInitConfig(VERSION, CUSTOMER); + contxtfulSubmodule.init(config); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + + let reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {}, + }, + }; + + setTimeout(() => { + const onDoneSpy = sinon.spy(); + contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); + let global = reqBidsConfigObj.ortb2Fragments.global; + let bidder = reqBidsConfigObj.ortb2Fragments.bidder[config.params.bidders[0]]; + + let globalExpected = { user: { site: { id: 'globalsiteId' } } }; + let bidderExpected = { user: { data: [{ name: MODULE_NAME, value: RX_FROM_API }] } }; + expect(RX_API_MOCK_WITH_BUNDLE.getOrtb2Fragment.callCount).to.equal(1); + expect(global).to.deep.equal(globalExpected); + expect(bidder).to.deep.equal(bidderExpected); + done(); + }, TIMEOUT); + }) + }) }); diff --git a/test/spec/modules/conversantAnalyticsAdapter_spec.js b/test/spec/modules/conversantAnalyticsAdapter_spec.js index 75cb63b02f6..f46de31b19c 100644 --- a/test/spec/modules/conversantAnalyticsAdapter_spec.js +++ b/test/spec/modules/conversantAnalyticsAdapter_spec.js @@ -14,7 +14,7 @@ describe('Conversant analytics adapter tests', function() { let clock; // clock stub from sinon to mock our cache cleanup interval let logInfoStub; - const PREBID_VERSION = '1.2'; + const PREBID_VERSION = '$prebid.version$'; const SITE_ID = 108060; let requests; diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 9503a050092..c190fa81704 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -8,10 +8,11 @@ import 'src/prebid.js' import 'modules/currency.js'; import 'modules/userId/index.js'; // handles eids import 'modules/priceFloors.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; // handles schain import {hook} from '../../../src/hook.js' +import {BANNER} from '../../../src/mediaTypes'; describe('Conversant adapter tests', function() { const siteId = '108060'; @@ -228,7 +229,7 @@ describe('Conversant adapter tests', function() { expect(spec.aliases).to.be.an('array').with.lengthOf(2); expect(spec.aliases[0]).to.equal('cnvr'); expect(spec.aliases[1]).to.equal('epsilon'); - expect(spec.supportedMediaTypes).to.be.an('array').with.lengthOf(2); + expect(spec.supportedMediaTypes).to.be.an('array').with.lengthOf(3); expect(spec.supportedMediaTypes[1]).to.equal('video'); }); @@ -440,6 +441,13 @@ describe('Conversant adapter tests', function() { expect(payload.site.content).to.have.property('title'); }); + it('Verify currency', () => { + const bidderRequest = { timeout: 9999, ortb2: {cur: ['EUR']} }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = request.data; + expect(payload.cur).deep.equal(['USD']); + }) + it('Verify supply chain data', () => { const bidderRequest = {refererInfo: {page: 'http://test.com?a=b&c=123'}}; const schain = {complete: 1, ver: '1.0', nodes: [{asi: 'bidderA.com', sid: '00001', hp: 1}]}; @@ -465,7 +473,7 @@ describe('Conversant adapter tests', function() { before(() => { request = spec.buildRequests(bidRequests, {}); - response = spec.interpretResponse(bidResponses, request); + response = spec.interpretResponse(bidResponses, request).bids; }); it('Banner', function() { @@ -518,6 +526,72 @@ describe('Conversant adapter tests', function() { } }); + describe('Verify Native Ads', function () { + let request, response; + + const nativeOrtbRequest = { + ver: '1.2', + assets: [ + { id: 1, required: 1, title: { len: 80 } }] + }; + const nativeBidRequests = [{ + bidder: 'conversant', + params: { + site_id: 10806 + }, + adUnitCode: 'adunit', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + native: { + ...nativeOrtbRequest + }, + }, + nativeOrtbRequest, + bidId: '0', + bidderRequestId: 'requestId', + }]; + + const nativeMarkup = JSON.stringify({ + native: { + assets: [ + {id: 1, title: {text: 'TextValue!'}}, + {id: 5, data: {value: 'Epsilon'}}, + ], + link: { url: 'https://www.epsilon.com/us', }, } + }); + + const nativeBidResponse = { + body: { + bidId: 'requestId', + seatbid: [{ + bid: [{ + impid: '0', + price: 0.25, + mtype: 4, + adm: nativeMarkup + }] + }], + cur: 'USD' + } + }; + + if (FEATURES.NATIVE) { + it('Request', function () { + request = spec.buildRequests(nativeBidRequests, {}); + const payload = request.data; + const native = JSON.parse(payload.imp[0].native.request); + expect(native).to.deep.equal(nativeBidRequests[0].nativeOrtbRequest); + }); + it('Response', function () { + response = spec.interpretResponse(nativeBidResponse, request); + const result = response.bids[0]; + expect(result.ad).to.equal(nativeMarkup); + expect(result.mediaType).to.equal(BANNER); + expect(result.cpm).to.equal(nativeBidResponse.body.seatbid[0].bid[0].price); + }); + } + }) + it('Verify publisher commond id support', function() { // clone bidRequests let requests = utils.deepClone(bidRequests); @@ -552,16 +626,10 @@ describe('Conversant adapter tests', function() { // clone bidRequests let requests = utils.deepClone(bidRequests); - const uid = {pubcid: '112233', idl_env: '334455'}; const eidArray = [{'source': 'pubcid.org', 'uids': [{'id': '112233', 'atype': 1}]}, {'source': 'liveramp.com', 'uids': [{'id': '334455', 'atype': 3}]}]; - // add pubcid to every entry - requests.forEach((unit) => { - Object.assign(unit, {userId: uid}); - Object.assign(unit, {userIdAsEids: eidArray}); - }); // construct http post payload - const payload = spec.buildRequests(requests, {}).data; + const payload = spec.buildRequests(requests, {ortb2: {user: {ext: {eids: eidArray}}}}).data; expect(payload).to.have.deep.nested.property('user.ext.eids', [ {source: 'pubcid.org', uids: [{id: '112233', atype: 1}]}, {source: 'liveramp.com', uids: [{id: '334455', atype: 3}]} diff --git a/test/spec/modules/copper6sspBidAdapter_spec.js b/test/spec/modules/copper6sspBidAdapter_spec.js new file mode 100644 index 00000000000..4e62a416fb8 --- /dev/null +++ b/test/spec/modules/copper6sspBidAdapter_spec.js @@ -0,0 +1,519 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/copper6sspBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'copper6ssp'; + +describe('Copper6SSPBidAdapter', 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 valid URL', function () { + expect(serverRequest.url).to.equal('https://endpoint.copper6.com/pbjs'); + }); + + it('Returns general data valid', function () { + let 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 + } + ]; + + let 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); + let 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); + let 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] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + + 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 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let 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 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let 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' + }] + }; + + let 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' + }] + }; + let 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', + }] + }; + let 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' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://сsync.copper6.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://сsync.copper6.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://сsync.copper6.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/cpmstarBidAdapter_spec.js b/test/spec/modules/cpmstarBidAdapter_spec.js index 285fca9690a..f9b1ba59cde 100755 --- a/test/spec/modules/cpmstarBidAdapter_spec.js +++ b/test/spec/modules/cpmstarBidAdapter_spec.js @@ -149,7 +149,7 @@ describe('Cpmstar Bid Adapter', function () { }; var requests = spec.buildRequests(reqs, bidderRequest); expect(requests[0]).to.have.property('url'); - expect(requests[0].url).to.include('&schain=1.0,1!exchange1.com,1234,1,,,!exchange2.com,abcd,1,,,'); + expect(requests[0].url).to.include('&schain=1.0%2C1%21exchange1.com%2C1234%2C1%2C%2C%2C%21exchange2.com%2Cabcd%2C1%2C%2C%2C'); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/craftBidAdapter_spec.js b/test/spec/modules/craftBidAdapter_spec.js index dfdbebde738..d74477c5b8b 100644 --- a/test/spec/modules/craftBidAdapter_spec.js +++ b/test/spec/modules/craftBidAdapter_spec.js @@ -40,21 +40,21 @@ describe('craftAdapter', function () { }); it('should return false when params.sitekey not found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { placementId: '1234abcd' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when params.placementId not found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { sitekey: 'craft-prebid-example' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when AMP cotext found', function () { @@ -158,7 +158,7 @@ describe('craftAdapter', function () { height: 250, mediaType: 'banner', meta: null, - netRevenue: false, + netRevenue: true, requestId: '0396fae4eb5f47', ttl: 360, width: 300, diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index def35b13955..551db4fc72d 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -1,17 +1,17 @@ -import { expect } from 'chai'; -import { - tryGetCriteoFastBid, - spec, - storage, - PROFILE_ID_PUBLISHERTAG, - ADAPTER_VERSION, - canFastBid, getFastBidUrl, FAST_BID_VERSION_CURRENT -} from 'modules/criteoBidAdapter.js'; +import {expect} from 'chai'; +import {spec, storage} from 'modules/criteoBidAdapter.js'; import * as utils from 'src/utils.js'; import * as refererDetection from 'src/refererDetection.js'; import * as ajax from 'src/ajax.js'; -import { config } from '../../../src/config.js'; -import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; +import {config} from '../../../src/config.js'; +import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; +import {addFPDToBidderRequest} from '../../helpers/fpd'; +import 'modules/userId/index.js'; +import 'modules/consentManagementTcf.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/consentManagementGpp.js'; +import 'modules/schain.js'; +import {hook} from '../../../src/hook'; describe('The Criteo bidding adapter', function () { let utilsMock, sandbox, ajaxStub; @@ -120,7 +120,9 @@ describe('The Criteo bidding adapter', function () { getCookieStub, setCookieStub, getDataFromLocalStorageStub, - removeDataFromLocalStorageStub; + setDataInLocalStorageStub, + removeDataFromLocalStorageStub, + triggerPixelStub; beforeEach(function () { getConfigStub = sinon.stub(config, 'getConfig'); @@ -142,7 +144,10 @@ describe('The Criteo bidding adapter', function () { getCookieStub = sinon.stub(storage, 'getCookie'); setCookieStub = sinon.stub(storage, 'setCookie'); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); removeDataFromLocalStorageStub = sinon.stub(storage, 'removeDataFromLocalStorage'); + + triggerPixelStub = sinon.stub(utils, 'triggerPixel'); }); afterEach(function () { @@ -154,15 +159,9 @@ describe('The Criteo bidding adapter', function () { getCookieStub.restore(); setCookieStub.restore(); getDataFromLocalStorageStub.restore(); + setDataInLocalStorageStub.restore(); removeDataFromLocalStorageStub.restore(); - }); - - it('should not trigger sync if publisher is using fast bid', function () { - getConfigStub.withArgs('criteo.fastBidVersion').returns('latest'); - - const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, undefined, undefined); - - expect(userSyncs).to.eql([]); + triggerPixelStub.restore(); }); it('should not trigger sync if publisher did not enable iframe based syncs', function () { @@ -306,6 +305,62 @@ describe('The Criteo bidding adapter', function () { expect(removeDataFromLocalStorageStub.called).to.be.false; expect(ajaxStub.called).to.be.false; }); + + it('should trigger sync pixel from iframe response', function (done) { + const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, undefined, undefined); + + const event = new MessageEvent('message', { + data: { + requestId: '123456', + callbacks: [ + 'https://example.com/pixel1', + 'https://example.com/pixel2' + ] + }, + origin: 'https://gum.criteo.com' + }); + + window.dispatchEvent(event); + setTimeout(() => { + expect(triggerPixelStub.calledTwice).to.be.true; + expect(triggerPixelStub.firstCall.calledWith('https://example.com/pixel1')).to.be.true; + expect(triggerPixelStub.secondCall.calledWith('https://example.com/pixel2')).to.be.true; + + done(); + }, 0); + }); + + it('should write cookie only on TLD+1 level', function(done) { + const cookies = {}; + + const userSyncs = spec.getUserSyncs(syncOptionsIframeEnabled, undefined, undefined, undefined); + + setCookieStub.callsFake((name, value, expires, _, domain) => { + if (domain != '.com') { + cookies[name] = value; + } + }); + + getCookieStub.callsFake((name) => cookies[name]); + + const event = new MessageEvent('message', { + data: { + requestId: '123456', + bundle: 'bundle' + }, + origin: 'https://gum.criteo.com' + }); + + window.dispatchEvent(event); + setTimeout(() => { + expect(setCookieStub.calledWith('cto_bundle', 'bundle', sinon.match.string, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bundle', 'bundle', sinon.match.string, null, '.abc.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bundle', 'bundle', sinon.match.string, null, '.www.abc.com')).to.be.false; + expect(cookies).to.deep.equal({ 'cto_bundle': 'bundle' }); + + done(); + }, 0); + }); }); describe('isBidRequestValid', function () { @@ -602,8 +657,8 @@ describe('The Criteo bidding adapter', function () { }, timeout: 3000, gdprConsent: { - gdprApplies: 1, - consentString: 'concentDataString', + gdprApplies: true, + consentString: 'consentDataString', vendorData: { vendorConsents: { '91': 1 @@ -615,6 +670,10 @@ describe('The Criteo bidding adapter', function () { let localStorageIsEnabledStub; + before(() => { + hook.ready(); + }); + this.beforeEach(function () { localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); localStorageIsEnabledStub.returns(true); @@ -625,16 +684,14 @@ describe('The Criteo bidding adapter', function () { config.resetConfig(); }); - it('should properly build a request using random uuid as auction id', function () { + it('should properly build a request using random uuid as auction id', async function () { const generateUUIDStub = sinon.stub(utils, 'generateUUID'); generateUUIDStub.returns('def'); - const bidderRequest = { - }; + const bidderRequest = {}; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -643,13 +700,13 @@ describe('The Criteo bidding adapter', function () { params: {} }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; expect(ortbRequest.id).to.equal('def'); generateUUIDStub.restore(); }); - it('should properly transmit source.tid if available', function () { + it('should properly transmit source.tid if available', async function () { const bidderRequest = { ortb2: { source: { @@ -661,7 +718,6 @@ describe('The Criteo bidding adapter', function () { { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -670,12 +726,12 @@ describe('The Criteo bidding adapter', function () { params: {} }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; expect(ortbRequest.source.tid).to.equal('abc'); }); - it('should properly transmit tmax if available', function () { + it('should properly transmit tmax if available', async function () { const bidRequests = [ { bidder: 'criteo', @@ -689,49 +745,18 @@ describe('The Criteo bidding adapter', function () { params: {} }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; expect(ortbRequest.tmax).to.equal(bidderRequest.timeout); }); - it('should properly transmit bidId if available', function () { - const bidderRequest = { - ortb2: { - source: { - tid: 'abc' - } - } - }; - const bidRequests = [ - { - bidId: 'bidId', - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - params: {} - }, - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].slotid).to.equal('bidId'); - }); - - it('should properly build a request if refererInfo is not provided', function () { + it('should properly transmit bidId if available', async function () { const bidderRequest = {}; const bidRequests = [ { + bidId: 'bidId', bidder: 'criteo', adUnitCode: 'bid-123', - ortb2Imp: { - ext: { - tid: 'transaction-123', - }, - }, mediaTypes: { banner: { sizes: [[728, 90]] @@ -740,12 +765,12 @@ describe('The Criteo bidding adapter', function () { params: {} }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; - expect(ortbRequest.publisher.url).to.equal(''); + expect(ortbRequest.imp[0].id).to.equal('bidId'); }); - it('should properly build a zoneId request', function () { + it('should properly build a zoneId request', async function () { const bidRequests = [ { bidder: 'criteo', @@ -763,103 +788,58 @@ describe('The Criteo bidding adapter', function () { params: { zoneId: 123, publisherSubId: '123', - nativeCallback: function () { }, integrationMode: 'amp' }, }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d+&lsavail=1&im=1&debug=1&nolog=1/); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + expect(request.url).to.match(/^https:\/\/grid-bidder\.criteo\.com\/openrtb_2_5\/pbjs\/auction\/request\?profileId=207&av=\d+&wv=[^&]+&cb=\d+&lsavail=[01]&im=1&debug=[01]&nolog=[01]$/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; - expect(ortbRequest.publisher.url).to.equal(refererUrl); - expect(ortbRequest.slots).to.have.lengthOf(1); - expect(ortbRequest.slots[0].impid).to.equal('bid-123'); - expect(ortbRequest.slots[0].transactionid).to.equal('transaction-123'); - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(1); - expect(ortbRequest.slots[0].sizes[0]).to.equal('728x90'); - expect(ortbRequest.slots[0].zoneid).to.equal(123); - expect(ortbRequest.gdprConsent.consentData).to.equal('concentDataString'); - expect(ortbRequest.gdprConsent.gdprApplies).to.equal(true); - expect(ortbRequest.gdprConsent.version).to.equal(1); - }); - - it('should keep undefined sizes for non native banner', function () { - const bidRequests = [ - { - mediaTypes: { - banner: { - sizes: [[undefined, undefined]] - } - }, - params: {}, - }, - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(1); - expect(ortbRequest.slots[0].sizes[0]).to.equal('undefinedxundefined'); - }); - - it('should keep undefined size for non native banner', function () { - const bidRequests = [ - { - mediaTypes: { - banner: { - sizes: [undefined, undefined] - } - }, - params: {}, - }, - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(1); - expect(ortbRequest.slots[0].sizes[0]).to.equal('undefinedxundefined'); - }); - - it('should properly detect and forward native flag', function () { - const bidRequests = [ - { - mediaTypes: { - banner: { - sizes: [[undefined, undefined]] - } - }, - params: { - nativeCallback: function () { } - }, - }, - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].native).to.equal(true); - }); - - it('should properly forward eids', function () { + expect(ortbRequest.site.page).to.equal(refererUrl); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].tagid).to.equal('bid-123'); + expect(ortbRequest.imp[0].banner.format).to.have.lengthOf(1); + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(728); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(90); + expect(ortbRequest.imp[0].ext.bidder.zoneid).to.equal(123); + expect(ortbRequest.user.ext.consent).to.equal('consentDataString'); + expect(ortbRequest.regs.ext.gdpr).to.equal(1); + expect(ortbRequest.regs.ext.gdprversion).to.equal(1); + }); + + it('should properly forward eids', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] } }, - userIdAsEids: [ - { - source: 'criteo.com', - uids: [{ - id: 'abc', - atype: 1 - }] - } - ], params: {} }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const br = { + ...bidderRequest, + ortb2: { + user: { + ext: { + eids: [ + { + source: 'criteo.com', + uids: [{ + id: 'abc', + atype: 1 + }] + } + ] + } + } + } + } + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(br)); const ortbRequest = request.data; expect(ortbRequest.user.ext.eids).to.deep.equal([ { @@ -872,81 +852,83 @@ describe('The Criteo bidding adapter', function () { ]); }); - it('should properly detect and forward native flag', function () { - const bidRequests = [ - { - mediaTypes: { - banner: { - sizes: [undefined, undefined] - } - }, - params: { - nativeCallback: function () { } + if (FEATURES.NATIVE) { + it('should properly build a native request without assets', async function () { + const bidRequests = [ + { + mediaTypes: { + native: {} + }, + params: {} }, - }, - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].native).to.equal(true); - }); + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].native.request_native).to.not.be.null; + expect(ortbRequest.imp[0].native.request_native.assets).to.be.undefined; + }); + } - it('should map ortb native assets to slot ext assets', function () { - const assets = [{ - required: 1, - id: 1, - img: { - type: 3, - wmin: 100, - hmin: 100, - } - }, - { - required: 1, - id: 2, - title: { - len: 140, - } - }, - { - required: 1, - id: 3, - data: { - type: 1, - } - }, - { - required: 0, - id: 4, - data: { - type: 2, - } - }, - { - required: 0, - id: 5, - img: { - type: 1, - wmin: 20, - hmin: 20, - } - }]; - const bidRequests = [ + if (FEATURES.NATIVE) { + it('should properly build a native request with assets', async function () { + const assets = [{ + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }, { - nativeOrtbRequest: { - assets: assets - }, - params: { - nativeCallback: function () { } - }, + required: 1, + id: 2, + title: { + len: 140, + } }, - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].native).to.equal(true); - expect(ortbRequest.slots[0].ext.assets).to.deep.equal(assets); - }); + { + required: 1, + id: 3, + data: { + type: 1, + } + }, + { + required: 0, + id: 4, + data: { + type: 2, + } + }, + { + required: 0, + id: 5, + img: { + type: 1, + wmin: 20, + hmin: 20, + } + }]; + const bidRequests = [ + { + mediaTypes: { + native: {} + }, + nativeOrtbRequest: { + assets: assets + }, + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].native.request_native).to.not.be.null; + expect(ortbRequest.imp[0].native.request_native.assets).to.deep.equal(assets); + }); + } - it('should properly build a networkId request', function () { + it('should properly build a networkId request', async function () { const bidderRequest = { refererInfo: { page: refererUrl, @@ -954,7 +936,7 @@ describe('The Criteo bidding adapter', function () { }, timeout: 3000, gdprConsent: { - gdprApplies: 0, + gdprApplies: false, consentString: undefined, vendorData: { vendorConsents: { @@ -982,23 +964,23 @@ describe('The Criteo bidding adapter', function () { }, }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + expect(request.url).to.match(/^https:\/\/grid-bidder\.criteo\.com\/openrtb_2_5\/pbjs\/auction\/request\?profileId=207&av=\d+&wv=[^&]+&cb=\d+&lsavail=[01]&debug=[01]&nolog=[01]&networkId=456$/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; - expect(ortbRequest.publisher.url).to.equal(refererUrl); - expect(ortbRequest.publisher.networkid).to.equal(456); - expect(ortbRequest.slots).to.have.lengthOf(1); - expect(ortbRequest.slots[0].impid).to.equal('bid-123'); - expect(ortbRequest.slots[0].transactionid).to.equal('transaction-123'); - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(2); - expect(ortbRequest.slots[0].sizes[0]).to.equal('300x250'); - expect(ortbRequest.slots[0].sizes[1]).to.equal('728x90'); - expect(ortbRequest.gdprConsent.consentData).to.equal(undefined); - expect(ortbRequest.gdprConsent.gdprApplies).to.equal(false); - }); - - it('should properly build a mixed request', function () { + expect(ortbRequest.site.page).to.equal(refererUrl); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].tagid).to.equal('bid-123'); + expect(ortbRequest.imp[0].banner.format).to.have.lengthOf(2); + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(300); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(250); + expect(ortbRequest.imp[0].banner.format[1].w).to.equal(728); + expect(ortbRequest.imp[0].banner.format[1].h).to.equal(90); + expect(ortbRequest.user?.ext?.consent).to.equal(undefined); + expect(ortbRequest.regs.ext.gdpr).to.equal(0); + }); + + it('should properly build a mixed request with both a zoneId and a networkId', async function () { const bidderRequest = { refererInfo: { page: refererUrl, @@ -1042,31 +1024,33 @@ describe('The Criteo bidding adapter', function () { }, }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + expect(request.url).to.match(/^https:\/\/grid-bidder\.criteo\.com\/openrtb_2_5\/pbjs\/auction\/request\?profileId=207&av=\d+&wv=[^&]+&cb=\d+&lsavail=[01]&debug=[01]&nolog=[01]&networkId=456$/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; - expect(ortbRequest.publisher.url).to.equal(refererUrl); - expect(ortbRequest.publisher.networkid).to.equal(456); - expect(ortbRequest.slots).to.have.lengthOf(2); - expect(ortbRequest.slots[0].impid).to.equal('bid-123'); - expect(ortbRequest.slots[0].transactionid).to.equal('transaction-123'); - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(1); - expect(ortbRequest.slots[0].sizes[0]).to.equal('728x90'); - expect(ortbRequest.slots[1].impid).to.equal('bid-234'); - expect(ortbRequest.slots[1].transactionid).to.equal('transaction-234'); - expect(ortbRequest.slots[1].sizes).to.have.lengthOf(2); - expect(ortbRequest.slots[1].sizes[0]).to.equal('300x250'); - expect(ortbRequest.slots[1].sizes[1]).to.equal('728x90'); - expect(ortbRequest.gdprConsent).to.equal(undefined); - }); - - it('should properly build a request with undefined gdpr consent fields when they are not provided', function () { + expect(ortbRequest.site.page).to.equal(refererUrl); + expect(ortbRequest.imp).to.have.lengthOf(2); + expect(ortbRequest.imp[0].tagid).to.equal('bid-123'); + expect(ortbRequest.imp[0].banner.format).to.have.lengthOf(1); + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(728); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(90); + expect(ortbRequest.imp[0].ext.tid).to.equal('transaction-123'); + expect(ortbRequest.imp[0].ext.bidder.zoneid).to.equal(123); + expect(ortbRequest.imp[1].tagid).to.equal('bid-234'); + expect(ortbRequest.imp[1].banner.format).to.have.lengthOf(2); + expect(ortbRequest.imp[1].banner.format[0].w).to.equal(300); + expect(ortbRequest.imp[1].banner.format[0].h).to.equal(250); + expect(ortbRequest.imp[1].banner.format[1].w).to.equal(728); + expect(ortbRequest.imp[1].banner.format[1].h).to.equal(90); + expect(ortbRequest.imp[1].ext.tid).to.equal('transaction-234'); + expect(ortbRequest.user?.ext?.consent).to.equal(undefined); + }); + + it('should properly build a request with undefined gdpr consent fields when they are not provided', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1082,17 +1066,16 @@ describe('The Criteo bidding adapter', function () { gdprConsent: {}, }; - const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; - expect(ortbRequest.gdprConsent.consentData).to.equal(undefined); - expect(ortbRequest.gdprConsent.gdprApplies).to.equal(undefined); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.user?.ext?.consent).to.equal(undefined); + expect(ortbRequest.regs?.ext?.gdpr).to.equal(undefined); }); - it('should properly build a request with ccpa consent field', function () { + it('should properly build a request with ccpa consent field', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1108,42 +1091,43 @@ describe('The Criteo bidding adapter', function () { uspConsent: '1YNY', }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.user).to.not.be.null; - expect(request.data.user.uspIab).to.equal('1YNY'); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs.ext.us_privacy).to.equal('1YNY'); }); - it('should properly build a request with site and app ortb fields', function () { - const bidRequests = []; - let app = { - publisher: { - id: 'appPublisherId' - } - }; - let site = { - publisher: { - id: 'sitePublisherId' - } - }; + it('should properly build a request with overridden tmax', async function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + }, + }, + ]; const bidderRequest = { - ortb2: { - app: app, - site: site - } + timeout: 1234 }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.app).to.equal(app); - expect(request.data.site).to.equal(site); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.tmax).to.equal(1234); }); - it('should properly build a request with device sua field', function () { - const sua = {} + it('should properly build a request with device sua field', async function () { + const sua = { + platform: { + brand: 'abc' + } + } const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1164,17 +1148,16 @@ describe('The Criteo bidding adapter', function () { } }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.user.ext.sua).to.not.be.null; - expect(request.data.user.ext.sua).to.equal(sua); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.device.ext.sua).not.to.be.null; + expect(ortbRequest.device.ext.sua.platform.brand).to.equal('abc'); }); - it('should properly build a request with gpp consent field', function () { + it('should properly build a request with gpp consent field', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1192,18 +1175,16 @@ describe('The Criteo bidding adapter', function () { } }; - const request = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2 }); - expect(request.data.regs).to.not.be.null; - expect(request.data.regs.gpp).to.equal('gpp_consent_string'); - expect(request.data.regs.gpp_sid).to.deep.equal([0, 1, 2]); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.regs.ext.gpp).to.equal('gpp_consent_string'); + expect(ortbRequest.regs.ext.gpp_sid).to.deep.equal([0, 1, 2]); }); - it('should properly build a request with dsa object', function () { + it('should properly build a request with dsa object', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1234,13 +1215,11 @@ describe('The Criteo bidding adapter', function () { } }; - const request = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2 }); - expect(request.data.regs).to.not.be.null; - expect(request.data.regs.ext).to.not.be.null; - expect(request.data.regs.ext.dsa).to.deep.equal(dsa); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.regs.ext.dsa).to.deep.equal(dsa); }); - it('should properly build a request with schain object', function () { + it('should properly build a request with schain object', async function () { const expectedSchain = { someProperty: 'someValue' }; @@ -1249,7 +1228,6 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', schain: expectedSchain, adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1261,17 +1239,16 @@ describe('The Criteo bidding adapter', function () { }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.source.ext.schain).to.equal(expectedSchain); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.source.ext.schain).to.equal(expectedSchain); }); - it('should properly build a request with bcat field', function () { + it('should properly build a request with bcat field', async function () { const bcat = ['IAB1', 'IAB2']; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1288,18 +1265,16 @@ describe('The Criteo bidding adapter', function () { } }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bcat).to.not.be.null; - expect(request.data.bcat).to.equal(bcat); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.bcat).to.deep.equal(bcat); }); - it('should properly build a request with badv field', function () { + it('should properly build a request with badv field', async function () { const badv = ['ford.com']; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1316,18 +1291,16 @@ describe('The Criteo bidding adapter', function () { } }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.badv).to.not.be.null; - expect(request.data.badv).to.equal(badv); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.badv).to.deep.equal(badv); }); - it('should properly build a request with bapp field', function () { + it('should properly build a request with bapp field', async function () { const bapp = ['com.foo.mygame']; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1344,270 +1317,206 @@ describe('The Criteo bidding adapter', function () { } }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bapp).to.not.be.null; - expect(request.data.bapp).to.equal(bapp); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.bapp).to.deep.equal(bapp); }); - it('should properly build a request with if ccpa consent field is not provided', function () { - const bidRequests = [ - { - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } + if (FEATURES.VIDEO) { + it('should properly build a video request', async function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + sizes: [[640, 480]], + mediaTypes: { + video: { + context: 'inbanner', + playerSize: [640, 480], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30, + api: [1, 2], + protocols: [2, 3], + plcmt: 3, + w: 640, + h: 480, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], + } + }, + params: { + zoneId: 123, + video: { + skip: 1, + minduration: 5, + startdelay: 5, + playbackmethod: [1, 3], + placement: 2 + } + }, }, - params: { - zoneId: 123, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + expect(request.url).to.match(/^https:\/\/grid-bidder\.criteo\.com\/openrtb_2_5\/pbjs\/auction\/request\?profileId=207&av=\d+&wv=[^&]+&cb=\d+&lsavail=[01]&debug=[01]&nolog=[01]$/); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(ortbRequest.imp[0].video.maxduration).to.equal(30); + expect(ortbRequest.imp[0].video.api).to.deep.equal([1, 2]); + expect(ortbRequest.imp[0].video.protocols).to.deep.equal([2, 3]); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.startdelay).to.equal(5); + expect(ortbRequest.imp[0].video.playbackmethod).to.deep.equal([1, 3]); + expect(ortbRequest.imp[0].video.placement).to.equal(2); + expect(ortbRequest.imp[0].video.w).to.equal(640); + expect(ortbRequest.imp[0].video.h).to.equal(480); + expect(ortbRequest.imp[0].video.linearity).to.equal(1); + expect(ortbRequest.imp[0].video.skipmin).to.equal(30); + expect(ortbRequest.imp[0].video.skipafter).to.equal(30); + expect(ortbRequest.imp[0].video.minbitrate).to.equal(10000); + expect(ortbRequest.imp[0].video.maxbitrate).to.equal(48000); + expect(ortbRequest.imp[0].video.delivery).to.deep.equal([1, 2, 3]); + expect(ortbRequest.imp[0].video.pos).to.equal(1); + expect(ortbRequest.imp[0].video.playbackend).to.equal(1); + expect(ortbRequest.imp[0].video.ext.context).to.equal('inbanner'); + expect(ortbRequest.imp[0].video.ext.playersizes).to.deep.equal(['640x480']); + expect(ortbRequest.imp[0].video.ext.plcmt).to.equal(3); + expect(ortbRequest.imp[0].video.ext.poddur).to.equal(30); + expect(ortbRequest.imp[0].video.ext.rqddurs).to.deep.equal([1, 30]); + }); + } + + if (FEATURES.VIDEO) { + it('should properly build a video request with more than one player size', async function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + sizes: [[640, 480], [800, 600]], + mediaTypes: { + video: { + playerSize: [[640, 480], [800, 600]], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30, + api: [1, 2], + protocols: [2, 3] + } + }, + params: { + zoneId: 123, + video: { + skip: 1, + minduration: 5, + startdelay: 5, + playbackmethod: [1, 3], + placement: 2 + } + }, }, - }, - ]; - const bidderRequest = { - timeout: 3000 - }; + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + expect(request.url).to.match(/^https:\/\/grid-bidder\.criteo\.com\/openrtb_2_5\/pbjs\/auction\/request\?profileId=207&av=\d+&wv=[^&]+&cb=\d+&lsavail=[01]&debug=[01]&nolog=[01]$/); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(ortbRequest.imp[0].video.maxduration).to.equal(30); + expect(ortbRequest.imp[0].video.api).to.deep.equal([1, 2]); + expect(ortbRequest.imp[0].video.protocols).to.deep.equal([2, 3]); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.startdelay).to.equal(5); + expect(ortbRequest.imp[0].video.playbackmethod).to.deep.equal([1, 3]); + expect(ortbRequest.imp[0].video.placement).to.equal(2); + expect(ortbRequest.imp[0].video.w).to.equal(640); + expect(ortbRequest.imp[0].video.h).to.equal(480); + expect(ortbRequest.imp[0].video.ext.playersizes).to.deep.equal(['640x480', '800x600']); + expect(ortbRequest.imp[0].ext.bidder.zoneid).to.equal(123); + }); + } - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.user).to.not.be.null; - expect(request.data.user.uspIab).to.equal(undefined); - }); + if (FEATURES.VIDEO) { + it('should properly build a video request when mediaTypes.video.skip=0', async function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + sizes: [[300, 250]], + mediaTypes: { + video: { + playerSize: [[300, 250]], + mimes: ['video/mp4', 'video/MPV', 'video/H264', 'video/webm', 'video/ogg'], + minduration: 1, + maxduration: 30, + playbackmethod: [2, 3, 4, 5, 6], + api: [1, 2, 3, 4, 5, 6], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 0 + } + }, + params: { + networkId: 456 + } + } + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + expect(request.url).to.match(/^https:\/\/grid-bidder\.criteo\.com\/openrtb_2_5\/pbjs\/auction\/request\?profileId=207&av=\d+&wv=[^&]+&cb=\d+&lsavail=[01]&debug=[01]&nolog=[01]&networkId=456$/); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].video.mimes).to.deep.equal(['video/mp4', 'video/MPV', 'video/H264', 'video/webm', 'video/ogg']); + expect(ortbRequest.imp[0].video.minduration).to.equal(1); + expect(ortbRequest.imp[0].video.maxduration).to.equal(30); + expect(ortbRequest.imp[0].video.playbackmethod).to.deep.equal([2, 3, 4, 5, 6]); + expect(ortbRequest.imp[0].video.api).to.deep.equal([1, 2, 3, 4, 5, 6]); + expect(ortbRequest.imp[0].video.protocols).to.deep.equal([1, 2, 3, 4, 5, 6, 7, 8]); + expect(ortbRequest.imp[0].video.skip).to.equal(0); + expect(ortbRequest.imp[0].video.w).to.equal(300); + expect(ortbRequest.imp[0].video.h).to.equal(250); + expect(ortbRequest.imp[0].video.ext.playersizes).to.deep.equal(['300x250']); + }); + } - it('should properly build a video request', function () { + it('should properly build a request without first party data', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', - sizes: [[640, 480]], mediaTypes: { - video: { - context: 'instream', - playerSize: [640, 480], - mimes: ['video/mp4', 'video/x-flv'], - maxduration: 30, - api: [1, 2], - protocols: [2, 3], - plcmt: 3, - w: 640, - h: 480, - linearity: 1, - skipmin: 30, - skipafter: 30, - minbitrate: 10000, - maxbitrate: 48000, - delivery: [1, 2, 3], - pos: 1, - playbackend: 1, - adPodDurationSec: 30, - durationRangeSec: [1, 30], + banner: { + sizes: [[728, 90]] } }, params: { - zoneId: 123, - video: { - skip: 1, - minduration: 5, - startdelay: 5, - playbackmethod: [1, 3], - placement: 2 - } - }, - }, - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].video.context).to.equal('instream'); - expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(ortbRequest.slots[0].sizes).to.deep.equal([]); - expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480']); - expect(ortbRequest.slots[0].video.maxduration).to.equal(30); - expect(ortbRequest.slots[0].video.api).to.deep.equal([1, 2]); - expect(ortbRequest.slots[0].video.protocols).to.deep.equal([2, 3]); - expect(ortbRequest.slots[0].video.skip).to.equal(1); - expect(ortbRequest.slots[0].video.minduration).to.equal(5); - expect(ortbRequest.slots[0].video.startdelay).to.equal(5); - expect(ortbRequest.slots[0].video.playbackmethod).to.deep.equal([1, 3]); - expect(ortbRequest.slots[0].video.placement).to.equal(2); - expect(ortbRequest.slots[0].video.plcmt).to.equal(3); - expect(ortbRequest.slots[0].video.w).to.equal(640); - expect(ortbRequest.slots[0].video.h).to.equal(480); - expect(ortbRequest.slots[0].video.linearity).to.equal(1); - expect(ortbRequest.slots[0].video.skipmin).to.equal(30); - expect(ortbRequest.slots[0].video.skipafter).to.equal(30); - expect(ortbRequest.slots[0].video.minbitrate).to.equal(10000); - expect(ortbRequest.slots[0].video.maxbitrate).to.equal(48000); - expect(ortbRequest.slots[0].video.delivery).to.deep.equal([1, 2, 3]); - expect(ortbRequest.slots[0].video.pos).to.equal(1); - expect(ortbRequest.slots[0].video.playbackend).to.equal(1); - expect(ortbRequest.slots[0].video.adPodDurationSec).to.equal(30); - expect(ortbRequest.slots[0].video.durationRangeSec).to.deep.equal([1, 30]); - }); - - it('should properly build a video request with more than one player size', function () { - const bidRequests = [ - { - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - sizes: [[640, 480], [800, 600]], - mediaTypes: { - video: { - playerSize: [[640, 480], [800, 600]], - mimes: ['video/mp4', 'video/x-flv'], - maxduration: 30, - api: [1, 2], - protocols: [2, 3] - } - }, - params: { - zoneId: 123, - video: { - skip: 1, - minduration: 5, - startdelay: 5, - playbackmethod: [1, 3], - placement: 2 - } - }, - }, - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].sizes).to.deep.equal([]); - expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480', '800x600']); - expect(ortbRequest.slots[0].video.maxduration).to.equal(30); - expect(ortbRequest.slots[0].video.api).to.deep.equal([1, 2]); - expect(ortbRequest.slots[0].video.protocols).to.deep.equal([2, 3]); - expect(ortbRequest.slots[0].video.skip).to.equal(1); - expect(ortbRequest.slots[0].video.minduration).to.equal(5); - expect(ortbRequest.slots[0].video.startdelay).to.equal(5); - expect(ortbRequest.slots[0].video.playbackmethod).to.deep.equal([1, 3]); - expect(ortbRequest.slots[0].video.placement).to.equal(2); - }); - - it('should properly build a video request when mediaTypes.video.skip=0', function () { - const bidRequests = [ - { - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - sizes: [[300, 250]], - mediaTypes: { - video: { - playerSize: [[300, 250]], - mimes: ['video/mp4', 'video/MPV', 'video/H264', 'video/webm', 'video/ogg'], - minduration: 1, - maxduration: 30, - playbackmethod: [2, 3, 4, 5, 6], - api: [1, 2, 3, 4, 5, 6], - protocols: [1, 2, 3, 4, 5, 6, 7, 8], - skip: 0 - } - }, - params: { - networkId: 123 - } - } - ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - expect(ortbRequest.slots[0].sizes).to.deep.equal([]); - expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['300x250']); - expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/MPV', 'video/H264', 'video/webm', 'video/ogg']); - expect(ortbRequest.slots[0].video.minduration).to.equal(1); - expect(ortbRequest.slots[0].video.maxduration).to.equal(30); - expect(ortbRequest.slots[0].video.playbackmethod).to.deep.equal([2, 3, 4, 5, 6]); - expect(ortbRequest.slots[0].video.api).to.deep.equal([1, 2, 3, 4, 5, 6]); - expect(ortbRequest.slots[0].video.protocols).to.deep.equal([1, 2, 3, 4, 5, 6, 7, 8]); - expect(ortbRequest.slots[0].video.skip).to.equal(0); - }); - - it('should properly build a request with ceh', function () { - const bidRequests = [ - { - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - params: { - zoneId: 123, - }, - }, - ]; - config.setConfig({ - criteo: { - ceh: 'hashedemail' - } - }); - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.user).to.not.be.null; - expect(request.data.user.ceh).to.equal('hashedemail'); - }); - - it('should properly build a request without first party data', function () { - const bidRequests = [ - { - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - params: { - zoneId: 123 - } - }, - ]; - - const request = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2: {} }); - expect(request.data.publisher.ext).to.equal(undefined); - expect(request.data.user.ext).to.equal(undefined); - expect(request.data.slots[0].ext).to.equal(undefined); - }); - - it('should properly build a request with criteo specific ad unit first party data', function () { - // TODO: this test does not do what it says - const bidRequests = [ - { - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - params: { - zoneId: 123, - ext: { - bidfloor: 0.75 - } - } + zoneId: 123 + } }, ]; - const request = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2: {} }); - expect(request.data.slots[0].ext).to.deep.equal({ - bidfloor: 0.75, - }); - }); - - it('should properly build a request with first party data', function () { + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({ + ...bidderRequest, + ortb2: {} + })).data; + expect(ortbRequest.site.page).to.equal(refererUrl); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].tagid).to.equal('bid-123'); + expect(ortbRequest.imp[0].banner.format).to.have.lengthOf(1); + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(728); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(90); + expect(ortbRequest.imp[0].ext.bidder.zoneid).to.equal(123); + expect(ortbRequest.user.ext.consent).to.equal('consentDataString'); + expect(ortbRequest.regs.ext.gdpr).to.equal(1); + expect(ortbRequest.regs.ext.gdprversion).to.equal(1); + }); + + it('should properly build a request with first party data', async function () { const siteData = { keywords: ['power tools'], content: { @@ -1617,8 +1526,8 @@ describe('The Criteo bidding adapter', function () { segtax: 3 }, segment: [ - { 'id': '1001' }, - { 'id': '1002' } + {'id': '1001'}, + {'id': '1002'} ] }] }, @@ -1636,8 +1545,8 @@ describe('The Criteo bidding adapter', function () { segtax: 3 }, segment: [ - { 'id': '1001' }, - { 'id': '1002' } + {'id': '1001'}, + {'id': '1002'} ] }], ext: { @@ -1650,7 +1559,6 @@ describe('The Criteo bidding adapter', function () { { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] @@ -1677,49 +1585,46 @@ describe('The Criteo bidding adapter', function () { user: userData }; - const request = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2 }); - expect(request.data.publisher.ext).to.deep.equal({ data: { pageType: 'article' } }); - expect(request.data.user).to.deep.equal(userData); - expect(request.data.site).to.deep.equal(siteData); - expect(request.data.slots[0].ext).to.deep.equal({ - bidfloor: 0.75, - data: { - someContextAttribute: 'abc' - } + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.user).to.deep.equal({...userData, ext: {...userData.ext, consent: 'consentDataString'}}); + expect(ortbRequest.site).to.deep.equal({ + ...siteData, + page: refererUrl, + domain: 'criteo.com', + publisher: {...ortbRequest.site.publisher, domain: 'criteo.com'} }); + expect(ortbRequest.imp[0].ext.bidfloor).to.equal(0.75); + expect(ortbRequest.imp[0].ext.data.someContextAttribute).to.equal('abc') }); - it('should properly build a request when coppa flag is true', function () { + it('should properly build a request when coppa flag is true', async function () { const bidRequests = []; const bidderRequest = {}; - config.setConfig({ coppa: true }); - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.regs.coppa).to.not.be.undefined; - expect(request.data.regs.coppa).to.equal(1); + config.setConfig({coppa: true}); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs.coppa).to.equal(1); }); - it('should properly build a request when coppa flag is false', function () { + it('should properly build a request when coppa flag is false', async function () { const bidRequests = []; const bidderRequest = {}; - config.setConfig({ coppa: false }); - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.regs.coppa).to.not.be.undefined; - expect(request.data.regs.coppa).to.equal(0); + config.setConfig({coppa: false}); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs.coppa).to.equal(0); }); - it('should properly build a request when coppa flag is not defined', function () { + it('should properly build a request when coppa flag is not defined', async function () { const bidRequests = []; const bidderRequest = {}; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.regs.coppa).to.be.undefined; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs?.coppa).to.be.undefined; }); - it('should properly build a banner request with floors', function () { + it('should properly build a banner request with floors', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[300, 250], [728, 90]] @@ -1747,21 +1652,20 @@ describe('The Criteo bidding adapter', function () { }, ]; const bidderRequest = {}; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.slots[0].ext.floors).to.deep.equal({ + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].ext.floors).to.deep.equal({ 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } + '300x250': {'currency': 'USD', 'floor': 1}, + '728x90': {'currency': 'USD', 'floor': 2} } }); }); - it('should properly build a request with static floors', function () { + it('should properly build a request with static floors', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[300, 250], [728, 90]] @@ -1775,21 +1679,20 @@ describe('The Criteo bidding adapter', function () { }, ]; const bidderRequest = {}; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.slots[0].ext.floors).to.deep.equal({ + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].ext.floors).to.deep.equal({ 'banner': { - '300x250': { 'currency': 'EUR', 'floor': 1 }, - '728x90': { 'currency': 'EUR', 'floor': 1 } + '300x250': {'currency': 'EUR', 'floor': 1}, + '728x90': {'currency': 'EUR', 'floor': 1} } }); }); - it('should properly build a video request with several player sizes with floors', function () { + it('should properly build a video request with several player sizes with floors', async function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { video: { playerSize: [[300, 250], [728, 90]] @@ -1817,217 +1720,202 @@ describe('The Criteo bidding adapter', function () { }, ]; const bidderRequest = {}; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.slots[0].ext.floors).to.deep.equal({ + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].ext.floors).to.deep.equal({ 'video': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } + '300x250': {'currency': 'USD', 'floor': 1}, + '728x90': {'currency': 'USD', 'floor': 2} } }); }); - it('should properly build a multi format request with floors', function () { - const bidRequests = [ - { - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - mediaTypes: { - banner: { - sizes: [[300, 250], [728, 90]] + if (FEATURES.VIDEO && FEATURES.NATIVE) { + it('should properly build a multi format request with floors', async function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + }, + video: { + playerSize: [640, 480], + }, + native: {} }, - video: { - playerSize: [640, 480], + params: { + networkId: 456, }, - native: {} - }, - params: { - networkId: 456, - }, - ortb2Imp: { - ext: { - data: { - someContextAttribute: 'abc' + ortb2Imp: { + ext: { + data: { + someContextAttribute: 'abc' + } } - } - }, + }, - getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { - return { - currency: 'USD', - floor: 1.0 - }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { - return { - currency: 'USD', - floor: 2.0 - }; - } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 640 && inputParams.size[1] === 480) { - return { - currency: 'EUR', - floor: 3.2 - }; - } else if (inputParams.mediaType === NATIVE && inputParams.size === '*') { - return { - currency: 'YEN', - floor: 4.99 - }; - } else { - return {} + getFloor: inputParams => { + if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + return { + currency: 'USD', + floor: 2.0 + }; + } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 640 && inputParams.size[1] === 480) { + return { + currency: 'EUR', + floor: 3.2 + }; + } else if (inputParams.mediaType === NATIVE && inputParams.size === '*') { + return { + currency: 'YEN', + floor: 4.99 + }; + } else { + return {} + } } + }, + ]; + const bidderRequest = {}; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].banner).not.to.be.null; + expect(ortbRequest.imp[0].video).not.to.be.null; + expect(ortbRequest.imp[0].native.request_native).not.to.be.null; + expect(ortbRequest.imp[0].ext.data.someContextAttribute).to.deep.equal('abc'); + expect(ortbRequest.imp[0].ext.floors).to.deep.equal({ + 'banner': { + '300x250': {'currency': 'USD', 'floor': 1}, + '728x90': {'currency': 'USD', 'floor': 2} + }, + 'video': { + '640x480': {'currency': 'EUR', 'floor': 3.2} + }, + 'native': { + '*': {'currency': 'YEN', 'floor': 4.99} } - }, - ]; - const bidderRequest = {}; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.slots[0].ext.data.someContextAttribute).to.deep.equal('abc'); - expect(request.data.slots[0].ext.floors).to.deep.equal({ - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } - }, - 'video': { - '640x480': { 'currency': 'EUR', 'floor': 3.2 } - }, - 'native': { - '*': { 'currency': 'YEN', 'floor': 4.99 } - } + }); }); - }); + } - it('should properly build a request when imp.rwdd is present', function () { + it('should properly build a request when imp.rwdd is present', async function () { const bidderRequest = {}; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] } }, params: { - zoneId: 123, - ext: { - bidfloor: 0.75 - } + zoneId: 123 }, ortb2Imp: { - rwdd: 1, - ext: { - data: { - someContextAttribute: 'abc' - } - } + rwdd: 1 } }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.slots[0].rwdd).to.be.not.null; - expect(request.data.slots[0].rwdd).to.equal(1); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].ext.rwdd).to.equal(1); }); - it('should properly build a request when imp.rwdd is false', function () { + it('should properly build a request when imp.rwdd is false', async function () { const bidderRequest = {}; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] } }, params: { - zoneId: 123, - ext: { - bidfloor: 0.75 - } + zoneId: 123 }, ortb2Imp: { - rwdd: 0, - ext: { - data: { - someContextAttribute: 'abc' - } - } + rwdd: 0 } }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.slots[0].rwdd).to.be.undefined; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].ext?.rwdd).to.equal(0); }); - it('should properly build a request when FLEDGE is enabled', function () { + it('should properly build a request when FLEDGE is enabled', async function () { const bidderRequest = { - fledgeEnabled: true, + paapi: { + enabled: true + } }; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] } }, params: { - zoneId: 123, - ext: { - bidfloor: 0.75 - } + zoneId: 123 }, ortb2Imp: { ext: { - ae: 1 + igs: { + ae: 1 + } } } }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.slots[0].ext.ae).to.equal(1); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].ext.igs.ae).to.equal(1); }); - it('should properly build a request when FLEDGE is disabled', function () { + it('should properly build a request when FLEDGE is disabled', async function () { const bidderRequest = { - fledgeEnabled: false, + paapi: { + enabled: false + }, }; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', mediaTypes: { banner: { sizes: [[728, 90]] } }, params: { - zoneId: 123, - ext: { - bidfloor: 0.75 - } + zoneId: 123 }, ortb2Imp: { ext: { - ae: 1 + igs: { + ae: 1 + } } } }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.slots[0].ext).to.not.have.property('ae'); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].ext.igs?.ae).to.be.undefined; }); - it('should properly transmit the pubid and slot uid if available', function () { + it('should properly transmit the pubid and slot uid if available', async function () { const bidderRequest = { ortb2: { site: { @@ -2075,15 +1963,14 @@ describe('The Criteo bidding adapter', function () { }, }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; - expect(ortbRequest.publisher.id).to.be.undefined; expect(ortbRequest.site.publisher.id).to.equal('pub-888'); - expect(request.data.slots[0].ext.bidder).to.be.undefined; - expect(request.data.slots[1].ext.bidder.uid).to.equal(888); + expect(ortbRequest.imp[0].ext.bidder.uid).to.be.undefined; + expect(ortbRequest.imp[1].ext.bidder.uid).to.equal(888); }); - it('should properly transmit device.ext.cdep if available', function () { + it('should properly transmit device.ext.cdep if available', async function () { const bidderRequest = { ortb2: { device: { @@ -2094,131 +1981,202 @@ describe('The Criteo bidding adapter', function () { } }; const bidRequests = []; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; expect(ortbRequest.device.ext.cdep).to.equal('cookieDeprecationLabel'); }); }); describe('interpretResponse', function () { - it('should return an empty array when parsing a no bid response', function () { + const refererUrl = 'https://criteo.com?pbt_debug=1&pbt_nolog=1'; + const bidderRequest = { + refererInfo: { + page: refererUrl, + topmostLocation: refererUrl + }, + timeout: 3000, + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '91': 1 + }, + }, + apiVersion: 1, + }, + }; + + function mockResponse(winningBidId, mediaType) { + return { + id: 'test-requestId', + seatbid: [ + { + seat: 'criteo', + bid: [ + { + id: 'test-bidderId', + impid: winningBidId, + price: 1.23, + adomain: ['criteo.com'], + bundle: '', + iurl: 'http://some_image/', + cid: '123456', + crid: 'test-crId', + dealid: 'deal-code', + w: 728, + h: 90, + adm: 'test-ad', + adm_native: mediaType === NATIVE ? { + ver: '1.2', + assets: [ + { + id: 10, + title: { + text: 'Some product' + } + }, + { + id: 11, + img: { + type: 3, + url: 'https://main_image_url.com', + w: 400, + h: 400 + } + }, + { + id: 12, + data: { + value: 'Some product' + } + }, + { + id: 13, + data: { + value: '1,499 TL' + } + }, + { + id: 15, + data: { + value: 'CTA' + }, + link: { + url: 'https://cta_url.com' + } + }, + { + id: 17, + img: { + type: 1, + url: 'https://main_image_url.com', + w: 200, + h: 200 + }, + link: { + url: 'https://icon_image_url.com' + } + }, + { + id: 16, + data: { + value: 'Some brand' + } + } + ], + eventtrackers: [ + { + event: 1, + method: 1, + url: 'https://eventtrackers.com' + }, + { + event: 1, + method: 1, + url: 'https://test_in_isolation.criteo.com/tpd?dd=HTlW9l9xTEZqRHVlSHFiSWx5Q2VQMlEwSTJhNCUyQkxNazQ1Y29LR3ZmS2VTSDFsUGdkRHNoWjQ2UWp0SGtVZ1RTbHI0TFRpTlVqNWxiUkZOeGVFNjVraW53R0loRVJQNDJOY2R1eWxVdjBBQ1BEdVFvTyUyRlg3aWJaeUFha3UyemNNVGpmJTJCS1prc0FwRjZRJTJCQ2dpaFBJeVhZRmQlMkZURVZocUFRdm03OTdFZHZSbURNZWt4Uzh2M1NSUUxmTmhaTnNnRXd4VkZlOTdJOXdnNGZjaVolMkZWYmdYVjJJMkQ0eGxQaFIwQmVtWk1sQ09tNXlGY0Nwc09GTDladzExJTJGVExGNXJsdGpneERDeTMlMkJuNUlUcEU4NDFLMTZPc2ZoWFUwMmpGbDFpVjBPZUVtTlEwaWNOeHRyRFYyenRKd0lpJTJGTTElMkY1WGZ3Smo3aTh0bUJzdzZRdlZUSXppanNkamo3ekZNZjhKdjl2VDJ5eHV1YnVzdmdRdk5iWnprNXVFMVdmbGs0QU1QY0ozZQ' + } + ], + privacy: 'https://cta_url.com', + ext: { + privacy: { + imageurl: 'https://icon_image_url.com', + clickurl: 'https://cta_url.com', + longlegaltext: '' + } + } + } : undefined, + ext: { + mediatype: mediaType, + displayurl: mediaType === VIDEO ? 'http://test-ad' : undefined, + dsa: { + adrender: 1 + }, + meta: { + networkName: 'Criteo' + }, + videoPlayerType: mediaType === VIDEO ? 'RadiantMediaPlayer' : undefined, + videoPlayerConfig: mediaType === VIDEO ? {} : undefined, + cur: 'CUR' + } + } + ] + } + ] + }; + } + + it('should return an empty array when parsing an empty bid response', async function () { + const bidRequests = []; const response = {}; - const request = { bidRequests: [] }; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); const bids = spec.interpretResponse(response, request); expect(bids).to.have.lengthOf(0); }); - it('should properly parse a bid response with a networkId', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - cpm: 1.23, - creative: 'test-ad', - creativecode: 'test-crId', - width: 728, - height: 90, - deal: 'myDealCode', - adomain: ['criteo.com'], - ext: { - meta: { - networkName: 'Criteo' - } - } - }], - }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - params: { - networkId: 456, + it('should return an empty array when parsing a well-formed no bid response', async function () { + const bidRequests = []; + const response = {seatbid: []}; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(0); + }); + + it('should properly parse a banner bid response', async function () { + const bidRequests = [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + banner: { + sizes: [[728, 90]] } - }] - }; - const bids = spec.interpretResponse(response, request); + }, + params: { + networkId: 456, + } + }]; + const response = mockResponse('test-bidId', BANNER); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(BANNER); expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].seatBidId).to.equal('test-bidderId') expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].ad).to.equal('test-ad'); - expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].currency).to.equal('CUR'); expect(bids[0].width).to.equal(728); expect(bids[0].height).to.equal(90); - expect(bids[0].dealId).to.equal('myDealCode'); + expect(bids[0].ad).to.equal('test-ad'); + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].dealId).to.equal('deal-code'); expect(bids[0].meta.advertiserDomains[0]).to.equal('criteo.com'); expect(bids[0].meta.networkName).to.equal('Criteo'); + expect(bids[0].meta.dsa.adrender).to.equal(1); }); - it('should properly parse a bid response with dsa', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - cpm: 1.23, - creative: 'test-ad', - creativecode: 'test-crId', - width: 728, - height: 90, - deal: 'myDealCode', - adomain: ['criteo.com'], - ext: { - dsa: { - adrender: 1 - }, - meta: { - networkName: 'Criteo' - } - } - }], - }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - params: { - networkId: 456, - } - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].meta.dsa.adrender).to.equal(1); - }); - - it('should properly parse a bid response with a networkId with twin ad unit banner win', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - cpm: 1.23, - creative: 'test-ad', - creativecode: 'test-crId', - width: 728, - height: 90, - deal: 'myDealCode', - adomain: ['criteo.com'], - ext: { - meta: { - networkName: 'Criteo' - } - } - }], - }, - }; - const request = { - bidRequests: [{ + if (FEATURES.VIDEO) { + it('should properly parse a bid response with a video', async function () { + const bidRequests = [{ adUnitCode: 'test-requestId', bidId: 'test-bidId', mediaTypes: { @@ -2232,70 +2190,161 @@ describe('The Criteo bidding adapter', function () { } }, params: { - networkId: 456, + zoneId: 123, }, - }, { + }]; + const response = mockResponse('test-bidId', VIDEO); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].seatBidId).to.equal('test-bidderId') + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].currency).to.equal('CUR'); + expect(bids[0].vastUrl).to.equal('http://test-ad'); + expect(bids[0].vastXml).to.equal('test-ad'); + expect(bids[0].playerWidth).to.equal(640); + expect(bids[0].playerHeight).to.equal(480); + expect(bids[0].renderer).to.equal(undefined); + }); + } + + if (FEATURES.VIDEO) { + it('should properly parse a bid response with an outstream video', async function () { + const bidRequests = [{ adUnitCode: 'test-requestId', - bidId: 'test-bidId2', + bidId: 'test-bidId', mediaTypes: { - banner: { - sizes: [[728, 90]] + video: { + context: 'outstream', + mimes: ['video/mpeg'], + playerSize: [640, 480], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] } }, params: { networkId: 456, + }, + }]; + const response = mockResponse('test-bidId', VIDEO); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].seatBidId).to.equal('test-bidderId') + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].currency).to.equal('CUR'); + expect(bids[0].vastUrl).to.equal('http://test-ad'); + expect(bids[0].vastXml).to.equal('test-ad'); + expect(bids[0].playerWidth).to.equal(640); + expect(bids[0].playerHeight).to.equal(480); + expect(bids[0].renderer.url).to.equal('https://static.criteo.net/js/ld/publishertag.renderer.js'); + expect(typeof bids[0].renderer.config.documentResolver).to.equal('function'); + expect(typeof bids[0].renderer._render).to.equal('function'); + }); + } + + if (FEATURES.NATIVE) { + it('should properly parse a native bid response', async function () { + const bidRequests = [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + params: { + zoneId: '123', + }, + native: true, + }]; + const response = mockResponse('test-bidId', NATIVE); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(NATIVE); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].seatBidId).to.equal('test-bidderId') + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].currency).to.equal('CUR'); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].ad).to.equal(undefined); + expect(bids[0].native.ortb).not.to.be.null; + expect(bids[0].native.ortb).to.equal(response.seatbid[0].bid[0].adm); // adm_native field was moved to adm + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].dealId).to.equal('deal-code'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('criteo.com'); + expect(bids[0].meta.networkName).to.equal('Criteo'); + expect(bids[0].meta.dsa.adrender).to.equal(1); + }); + } + + it('should properly parse a bid response when banner win with twin ad units', async function () { + const bidRequests = [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 480], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] } - }] - }; - const bids = spec.interpretResponse(response, request); + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }]; + const response = mockResponse('test-bidId2', BANNER); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(BANNER); expect(bids[0].requestId).to.equal('test-bidId2'); + expect(bids[0].seatBidId).to.equal('test-bidderId') expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].ad).to.equal('test-ad'); - expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].currency).to.equal('CUR'); expect(bids[0].width).to.equal(728); expect(bids[0].height).to.equal(90); - expect(bids[0].dealId).to.equal('myDealCode'); + expect(bids[0].ad).to.equal('test-ad'); + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].dealId).to.equal('deal-code'); expect(bids[0].meta.advertiserDomains[0]).to.equal('criteo.com'); expect(bids[0].meta.networkName).to.equal('Criteo'); + expect(bids[0].meta.dsa.adrender).to.equal(1); }); - it('should properly parse a bid response with a networkId with twin ad unit video win', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - bidId: 'abc123', - cpm: 1.23, - displayurl: 'http://test-ad', - width: 728, - height: 90, - zoneid: 123, - video: true, - ext: { - meta: { - networkName: 'Criteo' - } - } - }], - }, - }; - const request = { - bidRequests: [{ + if (FEATURES.VIDEO) { + it('should properly parse a bid response when video win with twin ad units', async function () { + const bidRequests = [{ adUnitCode: 'test-requestId', bidId: 'test-bidId', mediaTypes: { video: { context: 'instream', mimes: ['video/mpeg'], - playerSize: [728, 90], + playerSize: [640, 480], protocols: [5, 6], maxduration: 30, api: [1, 2] } }, params: { - networkId: 456, + zoneId: '123' }, }, { adUnitCode: 'test-requestId', @@ -2308,63 +2357,27 @@ describe('The Criteo bidding adapter', function () { params: { networkId: 456, } - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].requestId).to.equal('test-bidId'); - expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].vastUrl).to.equal('http://test-ad'); - expect(bids[0].mediaType).to.equal(VIDEO); - }); + }]; + const response = mockResponse('test-bidId', VIDEO); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].seatBidId).to.equal('test-bidderId') + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].currency).to.equal('CUR'); + expect(bids[0].vastUrl).to.equal('http://test-ad'); + expect(bids[0].vastXml).to.equal('test-ad'); + expect(bids[0].playerWidth).to.equal(640); + expect(bids[0].playerHeight).to.equal(480); + expect(bids[0].renderer).to.equal(undefined); + }); + } - it('should properly parse a bid response with a networkId with twin ad unit native win', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - cpm: 1.23, - creative: 'test-ad', - creativecode: 'test-crId', - width: 728, - height: 90, - deal: 'myDealCode', - adomain: ['criteo.com'], - native: { - 'products': [{ - 'sendTargetingKeys': false, - 'title': 'Product title', - 'description': 'Product desc', - 'price': '100', - 'click_url': 'https://product.click', - 'image': { - 'url': 'https://publisherdirect.criteo.com/publishertag/preprodtest/creative.png', - 'height': 300, - 'width': 300 - }, - 'call_to_action': 'Try it now!' - }], - 'advertiser': { - 'description': 'sponsor', - 'domain': 'criteo.com', - 'logo': { 'url': 'https://www.criteo.com/images/criteo-logo.svg', 'height': 300, 'width': 300 } - }, - 'privacy': { - 'optout_click_url': 'https://info.criteo.com/privacy/informations', - 'optout_image_url': 'https://static.criteo.net/flash/icon/nai_small.png', - }, - 'impression_pixels': [{ 'url': 'https://my-impression-pixel/test/impression' }, { 'url': 'https://cas.com/lg.com' }] - }, - ext: { - meta: { - networkName: 'Criteo' - } - } - }], - }, - }; - const request = { - bidRequests: [{ + if (FEATURES.NATIVE) { + it('should properly parse a bid response when native win with twin ad units', async function () { + const bidRequests = [{ adUnitCode: 'test-requestId', bidId: 'test-bidId', mediaTypes: { @@ -2384,294 +2397,140 @@ describe('The Criteo bidding adapter', function () { params: { networkId: 456, } - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].requestId).to.equal('test-bidId'); - expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].mediaType).to.equal(NATIVE); - }); + }]; + const response = mockResponse('test-bidId', NATIVE); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(NATIVE); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].seatBidId).to.equal('test-bidderId') + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].currency).to.equal('CUR'); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].ad).to.equal(undefined); + expect(bids[0].native.ortb).not.to.be.null; + expect(bids[0].native.ortb).to.equal(response.seatbid[0].bid[0].adm); // adm_native field was moved to adm + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].dealId).to.equal('deal-code'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('criteo.com'); + expect(bids[0].meta.networkName).to.equal('Criteo'); + expect(bids[0].meta.dsa.adrender).to.equal(1); + }); + } - it('should properly parse a bid response with a zoneId', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - bidId: 'abc123', - cpm: 1.23, - creative: 'test-ad', - width: 728, - height: 90, - zoneid: 123, - }], - }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', - params: { - zoneId: 123, + if (FEATURES.NATIVE) { + it('should warn only once if sendTargetingKeys set to true on required fields for native bidRequest', async () => { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + mediaTypes: { + native: {} + }, + nativeOrtbRequest: { + assets: [{ + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }] + }, + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123, + publisherSubId: '123' + }, }, - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].requestId).to.equal('test-bidId'); - expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].ad).to.equal('test-ad'); - expect(bids[0].width).to.equal(728); - expect(bids[0].height).to.equal(90); - }); - - it('should properly parse a bid response with a video', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - bidId: 'abc123', - cpm: 1.23, - displayurl: 'http://test-ad', - width: 728, - height: 90, - zoneid: 123, - video: true - }], - }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', - params: { - zoneId: 123, + { + bidder: 'criteo', + adUnitCode: 'bid-456', + mediaTypes: { + native: {} + }, + nativeOrtbRequest: { + assets: [{ + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }] + }, + transactionId: 'transaction-456', + sizes: [[728, 90]], + params: { + zoneId: 456, + publisherSubId: '456' + }, }, - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].requestId).to.equal('test-bidId'); - expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].vastUrl).to.equal('http://test-ad'); - expect(bids[0].mediaType).to.equal(VIDEO); - }); + ]; - it('should properly parse a bid response with a outstream video', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - bidId: 'abc123', - cpm: 1.23, - displayurl: 'http://test-ad', - width: 728, - height: 90, - zoneid: 123, - video: true, - ext: { - videoPlayerType: 'RadiantMediaPlayer', - videoPlayerConfig: { - - } + const nativeParamsWithSendTargetingKeys = [ + { + nativeParams: { + image: { + sendTargetingKeys: true + }, } - }], - }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', - params: { - zoneId: 123, }, - mediaTypes: { - video: { - context: 'outstream' - } - } - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].requestId).to.equal('test-bidId'); - expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].vastUrl).to.equal('http://test-ad'); - expect(bids[0].renderer.url).to.equal('https://static.criteo.net/js/ld/publishertag.renderer.js'); - expect(typeof bids[0].renderer.config.documentResolver).to.equal('function'); - expect(typeof bids[0].renderer._render).to.equal('function'); - }); - - it('should properly parse a bid response with native', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - bidId: 'abc123', - cpm: 1.23, - width: 728, - height: 90, - zoneid: 123, - native: { - 'products': [{ - 'sendTargetingKeys': false, - 'title': 'Product title', - 'description': 'Product desc', - 'price': '100', - 'click_url': 'https://product.click', - 'image': { - 'url': 'https://publisherdirect.criteo.com/publishertag/preprodtest/creative.png', - 'height': 300, - 'width': 300 - }, - 'call_to_action': 'Try it now!' - }], - 'advertiser': { - 'description': 'sponsor', - 'domain': 'criteo.com', - 'logo': { 'url': 'https://www.criteo.com/images/criteo-logo.svg', 'height': 300, 'width': 300 } + { + nativeParams: { + icon: { + sendTargetingKeys: true }, - 'privacy': { - 'optout_click_url': 'https://info.criteo.com/privacy/informations', - 'optout_image_url': 'https://static.criteo.net/flash/icon/nai_small.png', + } + }, + { + nativeParams: { + clickUrl: { + sendTargetingKeys: true }, - 'impression_pixels': [{ 'url': 'https://my-impression-pixel/test/impression' }, { 'url': 'https://cas.com/lg.com' }] } - }], - }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', - params: { - zoneId: 123, }, - native: true, - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].requestId).to.equal('test-bidId'); - expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].mediaType).to.equal(NATIVE); - }); - - it('should warn only once if sendTargetingKeys set to true on required fields for native bidRequest', () => { - const bidderRequest = {}; - const bidRequests = [ - { - bidder: 'criteo', - adUnitCode: 'bid-123', - transactionId: 'transaction-123', - sizes: [[728, 90]], - params: { - zoneId: 123, - publisherSubId: '123', - nativeCallback: function () { } + { + nativeParams: { + displayUrl: { + sendTargetingKeys: true + }, + } }, - }, - { - bidder: 'criteo', - adUnitCode: 'bid-456', - transactionId: 'transaction-456', - sizes: [[728, 90]], - params: { - zoneId: 456, - publisherSubId: '456', - nativeCallback: function () { } + { + nativeParams: { + privacyLink: { + sendTargetingKeys: true + }, + } }, - }, - ]; - - const nativeParamsWithSendTargetingKeys = [ - { - nativeParams: { - image: { - sendTargetingKeys: true - }, - } - }, - { - nativeParams: { - icon: { - sendTargetingKeys: true - }, - } - }, - { - nativeParams: { - clickUrl: { - sendTargetingKeys: true - }, - } - }, - { - nativeParams: { - displayUrl: { - sendTargetingKeys: true - }, - } - }, - { - nativeParams: { - privacyLink: { - sendTargetingKeys: true - }, - } - }, - { - nativeParams: { - privacyIcon: { - sendTargetingKeys: true - }, + { + nativeParams: { + privacyIcon: { + sendTargetingKeys: true + }, + } } - } - ]; + ]; - utilsMock.expects('logWarn') - .withArgs('Criteo: all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)') - .exactly(nativeParamsWithSendTargetingKeys.length * bidRequests.length); - nativeParamsWithSendTargetingKeys.forEach(nativeParams => { - let transformedBidRequests = { ...bidRequests }; - transformedBidRequests = [Object.assign(transformedBidRequests[0], nativeParams), Object.assign(transformedBidRequests[1], nativeParams)]; - spec.buildRequests(transformedBidRequests, bidderRequest); + utilsMock.expects('logWarn') + .withArgs('Criteo: all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)') + .exactly(nativeParamsWithSendTargetingKeys.length * bidRequests.length); + for (const nativeParams of nativeParamsWithSendTargetingKeys) { + let transformedBidRequests = {...bidRequests}; + transformedBidRequests = [Object.assign(transformedBidRequests[0], nativeParams), Object.assign(transformedBidRequests[1], nativeParams)]; + spec.buildRequests(transformedBidRequests, await addFPDToBidderRequest(bidderRequest)); + } + utilsMock.verify(); }); - utilsMock.verify(); - }); + } - it('should properly parse a bid response with a zoneId passed as a string', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - cpm: 1.23, - creative: 'test-ad', - width: 728, - height: 90, - zoneid: 123, - }], - }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', - params: { - zoneId: '123', - }, - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(1); - expect(bids[0].requestId).to.equal('test-bidId'); - expect(bids[0].cpm).to.equal(1.23); - expect(bids[0].ad).to.equal('test-ad'); - expect(bids[0].width).to.equal(728); - expect(bids[0].height).to.equal(90); - }); - - it('should properly parse a bid response with FLEDGE auction configs', function () { + it('should properly parse a bid response with FLEDGE auction configs', async function () { let auctionConfig1 = { auctionSignals: {}, decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', @@ -2750,34 +2609,6 @@ describe('The Criteo bidding adapter', function () { }, sellerCurrency: '???' }; - const response = { - body: { - ext: { - igi: [{ - impid: 'test-bidId', - igs: [{ - impid: 'test-bidId', - bidId: 'test-bidId', - config: auctionConfig1 - }] - }, { - impid: 'test-bidId-2', - igs: [{ - impid: 'test-bidId-2', - bidId: 'test-bidId-2', - config: auctionConfig2 - }] - }] - }, - }, - }; - const bidderRequest = { - ortb2: { - source: { - tid: 'abc' - } - } - }; const bidRequests = [ { bidId: 'test-bidId', @@ -2810,18 +2641,37 @@ describe('The Criteo bidding adapter', function () { } }, ]; - const request = spec.buildRequests(bidRequests, bidderRequest); - const interpretedResponse = spec.interpretResponse(response, request); + const response = { + ext: { + igi: [{ + impid: 'test-bidId', + igs: [{ + impid: 'test-bidId', + bidId: 'test-bidId', + config: auctionConfig1 + }] + }, { + impid: 'test-bidId-2', + igs: [{ + impid: 'test-bidId-2', + bidId: 'test-bidId-2', + config: auctionConfig2 + }] + }] + }, + }; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const interpretedResponse = spec.interpretResponse({body: response}, request); expect(interpretedResponse).to.have.property('bids'); - expect(interpretedResponse).to.have.property('fledgeAuctionConfigs'); + expect(interpretedResponse).to.have.property('paapi'); expect(interpretedResponse.bids).to.have.lengthOf(0); - expect(interpretedResponse.fledgeAuctionConfigs).to.have.lengthOf(2); - expect(interpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal({ + expect(interpretedResponse.paapi).to.have.lengthOf(2); + expect(interpretedResponse.paapi[0]).to.deep.equal({ bidId: 'test-bidId', impid: 'test-bidId', config: auctionConfig1, }); - expect(interpretedResponse.fledgeAuctionConfigs[1]).to.deep.equal({ + expect(interpretedResponse.paapi[1]).to.deep.equal({ bidId: 'test-bidId-2', impid: 'test-bidId-2', config: auctionConfig2, @@ -2847,163 +2697,92 @@ describe('The Criteo bidding adapter', function () { hasBidResponseLevelPafData: false, hasBidResponseBidLevelPafData: false, shouldContainsBidMetaPafData: false - }].forEach(testCase => { - const bidPafContentId = 'abcdef'; - const pafTransmission = { - version: '12' - }; - const response = { - slots: [ - { - width: 300, - height: 250, - cpm: 10, - impid: 'adUnitId', - ext: (testCase.hasBidResponseBidLevelPafData ? { - paf: { - content_id: bidPafContentId - } - } : undefined) - } - ], - ext: (testCase.hasBidResponseLevelPafData ? { - paf: { - transmission: pafTransmission - } - } : undefined) - }; - - const request = { - bidRequests: [{ + }].forEach(testCase => + it('should properly forward or not meta paf data', async () => { + const bidPafContentId = 'abcdef'; + const pafTransmission = { + version: '12' + }; + const bidRequests = [{ + bidId: 'test-bidId', adUnitCode: 'adUnitId', sizes: [[300, 250]], params: { networkId: 456, } - }] - }; - - const bids = spec.interpretResponse(response, request); - - expect(bids).to.have.lengthOf(1); + }]; + const response = { + id: 'test-requestId', + seatbid: [{ + seat: 'criteo', + bid: [ + { + id: 'test-bidderId', + impid: 'test-bidId', + w: 728, + h: 90, + ext: { + mediatype: BANNER, + paf: testCase.hasBidResponseBidLevelPafData ? { + content_id: bidPafContentId + } : undefined + } + } + ] + }], + ext: (testCase.hasBidResponseLevelPafData ? { + paf: { + transmission: pafTransmission + } + } : undefined) + }; - const theoreticalBidMetaPafData = { - paf: { - content_id: bidPafContentId, - transmission: pafTransmission - } - }; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); - if (testCase.shouldContainsBidMetaPafData) { - expect(bids[0].meta).to.deep.equal(theoreticalBidMetaPafData); - } else { - expect(bids[0].meta).not.to.deep.equal(theoreticalBidMetaPafData); - } - }); - }); + expect(bids).to.have.lengthOf(1); - describe('canFastBid', function () { - it('should properly detect if can do fastbid', function () { - const testCasesAndExpectedResult = [['none', false], ['', true], [undefined, true], [123, true]]; - testCasesAndExpectedResult.forEach(testCase => { - const result = canFastBid(testCase[0]); - expect(result).to.equal(testCase[1]); - }) - }); - }); + const expectedBidMetaPafData = { + paf: { + content_id: bidPafContentId, + transmission: pafTransmission + } + }; - describe('getFastBidUrl', function () { - it('should properly detect the version of fastbid', function () { - const testCasesAndExpectedResult = [ - ['', 'https://static.criteo.net/js/ld/publishertag.prebid.' + FAST_BID_VERSION_CURRENT + '.js'], - [undefined, 'https://static.criteo.net/js/ld/publishertag.prebid.' + FAST_BID_VERSION_CURRENT + '.js'], - [null, 'https://static.criteo.net/js/ld/publishertag.prebid.' + FAST_BID_VERSION_CURRENT + '.js'], - [NaN, 'https://static.criteo.net/js/ld/publishertag.prebid.' + FAST_BID_VERSION_CURRENT + '.js'], - [123, 'https://static.criteo.net/js/ld/publishertag.prebid.123.js'], - ['123', 'https://static.criteo.net/js/ld/publishertag.prebid.123.js'], - ['latest', 'https://static.criteo.net/js/ld/publishertag.prebid.js'] - ]; - testCasesAndExpectedResult.forEach(testCase => { - const result = getFastBidUrl(testCase[0]); - expect(result).to.equal(testCase[1]); + if (testCase.shouldContainsBidMetaPafData) { + expect(bids[0].meta).to.deep.equal(expectedBidMetaPafData); + } else { + expect(bids[0].meta).not.to.deep.equal(expectedBidMetaPafData); + } }) - }); - }); - - describe('tryGetCriteoFastBid', function () { - const VALID_HASH = 'vBeD8Q7GU6lypFbzB07W8hLGj7NL+p7dI9ro2tCxkrmyv0F6stNuoNd75Us33iNKfEoW+cFWypelr6OJPXxki2MXWatRhJuUJZMcK4VBFnxi3Ro+3a0xEfxE4jJm4eGe98iC898M+/YFHfp+fEPEnS6pEyw124ONIFZFrcejpHU='; - const INVALID_HASH = 'invalid'; - const VALID_PUBLISHER_TAG = 'test'; - const INVALID_PUBLISHER_TAG = 'test invalid'; - - const FASTBID_LOCAL_STORAGE_KEY = 'criteo_fast_bid'; - - it('should verify valid hash with valid publisher tag', function () { - localStorage.setItem(FASTBID_LOCAL_STORAGE_KEY, '// Hash: ' + VALID_HASH + '\n' + VALID_PUBLISHER_TAG); - - utilsMock.expects('logInfo').withExactArgs('Using Criteo FastBid').once(); - utilsMock.expects('logWarn').withExactArgs('No hash found in FastBid').never(); - utilsMock.expects('logWarn').withExactArgs('Invalid Criteo FastBid found').never(); - - tryGetCriteoFastBid(); - - expect(localStorage.getItem(FASTBID_LOCAL_STORAGE_KEY)).to.equals('// Hash: ' + VALID_HASH + '\n' + VALID_PUBLISHER_TAG); - utilsMock.verify(); - }); - - it('should verify valid hash with invalid publisher tag', function () { - localStorage.setItem(FASTBID_LOCAL_STORAGE_KEY, '// Hash: ' + VALID_HASH + '\n' + INVALID_PUBLISHER_TAG); - - utilsMock.expects('logInfo').withExactArgs('Using Criteo FastBid').never(); - utilsMock.expects('logWarn').withExactArgs('No hash found in FastBid').never(); - utilsMock.expects('logWarn').withExactArgs('Invalid Criteo FastBid found').once(); - - tryGetCriteoFastBid(); - - expect(localStorage.getItem(FASTBID_LOCAL_STORAGE_KEY)).to.be.null; - utilsMock.verify(); - }); - - it('should verify invalid hash with valid publisher tag', function () { - localStorage.setItem(FASTBID_LOCAL_STORAGE_KEY, '// Hash: ' + INVALID_HASH + '\n' + VALID_PUBLISHER_TAG); - - utilsMock.expects('logInfo').withExactArgs('Using Criteo FastBid').never(); - utilsMock.expects('logWarn').withExactArgs('No hash found in FastBid').never(); - utilsMock.expects('logWarn').withExactArgs('Invalid Criteo FastBid found').once(); - - tryGetCriteoFastBid(); - - expect(localStorage.getItem(FASTBID_LOCAL_STORAGE_KEY)).to.be.null; - utilsMock.verify(); - }); - - it('should verify missing hash', function () { - localStorage.setItem(FASTBID_LOCAL_STORAGE_KEY, VALID_PUBLISHER_TAG); - - utilsMock.expects('logInfo').withExactArgs('Using Criteo FastBid').never(); - utilsMock.expects('logWarn').withExactArgs('No hash found in FastBid').once(); - utilsMock.expects('logWarn').withExactArgs('Invalid Criteo FastBid found').never(); - - tryGetCriteoFastBid(); - - expect(localStorage.getItem(FASTBID_LOCAL_STORAGE_KEY)).to.be.null; - utilsMock.verify(); - }); + ) }); describe('when pubtag prebid adapter is not available', function () { - it('should not warn if sendId is provided on required fields for native bidRequest', () => { + it('should not warn if sendId is provided on required fields for native bidRequest', async () => { const bidderRequest = {}; const bidRequestsWithSendId = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', sizes: [[728, 90]], + mediaTypes: { + native: {} + }, + nativeOrtbRequest: { + assets: [{ + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }] + }, params: { zoneId: 123, - publisherSubId: '123', - nativeCallback: function () { } + publisherSubId: '123' }, nativeParams: { image: { @@ -3029,33 +2808,56 @@ describe('The Criteo bidding adapter', function () { ]; utilsMock.expects('logWarn').withArgs('Criteo: all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)').never(); - const request = spec.buildRequests(bidRequestsWithSendId, bidderRequest); + const request = spec.buildRequests(bidRequestsWithSendId, await addFPDToBidderRequest(bidderRequest)); utilsMock.verify(); }); - it('should warn only once if sendId is not provided on required fields for native bidRequest', () => { + it('should warn only once if sendId is not provided on required fields for native bidRequest', async () => { const bidderRequest = {}; const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', - transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + native: {} + }, + nativeOrtbRequest: { + assets: [{ + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }] + }, params: { zoneId: 123, - publisherSubId: '123', - nativeCallback: function () { } + publisherSubId: '123' }, }, { bidder: 'criteo', adUnitCode: 'bid-456', transactionId: 'transaction-456', - sizes: [[728, 90]], + mediaTypes: { + native: {} + }, + nativeOrtbRequest: { + assets: [{ + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }] + }, params: { zoneId: 456, - publisherSubId: '456', - nativeCallback: function () { } + publisherSubId: '456' }, }, ]; @@ -3108,135 +2910,12 @@ describe('The Criteo bidding adapter', function () { utilsMock.expects('logWarn') .withArgs('Criteo: all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)') .exactly(nativeParamsWithoutSendId.length * bidRequests.length); - nativeParamsWithoutSendId.forEach(nativeParams => { - let transformedBidRequests = { ...bidRequests }; + for (const nativeParams of nativeParamsWithoutSendId) { + let transformedBidRequests = {...bidRequests}; transformedBidRequests = [Object.assign(transformedBidRequests[0], nativeParams), Object.assign(transformedBidRequests[1], nativeParams)]; - spec.buildRequests(transformedBidRequests, bidderRequest); - }); + spec.buildRequests(transformedBidRequests, await addFPDToBidderRequest(bidderRequest)); + } utilsMock.verify(); }); }); - - describe('when pubtag prebid adapter is available', function () { - it('should forward response to pubtag when calling interpretResponse', () => { - const response = {}; - const request = {}; - - const adapter = { interpretResponse: function () { } }; - const adapterMock = sinon.mock(adapter); - adapterMock.expects('interpretResponse').withExactArgs(response, request).once().returns('ok'); - const prebidAdapter = { GetAdapter: function () { } }; - const prebidAdapterMock = sinon.mock(prebidAdapter); - prebidAdapterMock.expects('GetAdapter').withExactArgs(request).once().returns(adapter); - - global.Criteo = { - PubTag: { - Adapters: { - Prebid: prebidAdapter - } - } - }; - - expect(spec.interpretResponse(response, request)).equal('ok'); - adapterMock.verify(); - prebidAdapterMock.verify(); - }); - - it('should forward bid to pubtag when calling onBidWon', () => { - const bid = { auctionId: 123 }; - - const adapter = { handleBidWon: function () { } }; - const adapterMock = sinon.mock(adapter); - adapterMock.expects('handleBidWon').withExactArgs(bid).once(); - const prebidAdapter = { GetAdapter: function () { } }; - const prebidAdapterMock = sinon.mock(prebidAdapter); - prebidAdapterMock.expects('GetAdapter').withExactArgs(bid.auctionId).once().returns(adapter); - - global.Criteo = { - PubTag: { - Adapters: { - Prebid: prebidAdapter - } - } - }; - - spec.onBidWon(bid); - adapterMock.verify(); - prebidAdapterMock.verify(); - }); - - it('should forward bid to pubtag when calling onSetTargeting', () => { - const bid = { auctionId: 123 }; - - const adapter = { handleSetTargeting: function () { } }; - const adapterMock = sinon.mock(adapter); - adapterMock.expects('handleSetTargeting').withExactArgs(bid).once(); - const prebidAdapter = { GetAdapter: function () { } }; - const prebidAdapterMock = sinon.mock(prebidAdapter); - prebidAdapterMock.expects('GetAdapter').withExactArgs(bid.auctionId).once().returns(adapter); - - global.Criteo = { - PubTag: { - Adapters: { - Prebid: prebidAdapter - } - } - }; - - spec.onSetTargeting(bid); - adapterMock.verify(); - prebidAdapterMock.verify(); - }); - - it('should forward bid to pubtag when calling onTimeout', () => { - const timeoutData = [{ auctionId: 123 }]; - - const adapter = { handleBidTimeout: function () { } }; - const adapterMock = sinon.mock(adapter); - adapterMock.expects('handleBidTimeout').once(); - const prebidAdapter = { GetAdapter: function () { } }; - const prebidAdapterMock = sinon.mock(prebidAdapter); - prebidAdapterMock.expects('GetAdapter').withExactArgs(timeoutData[0].auctionId).once().returns(adapter); - - global.Criteo = { - PubTag: { - Adapters: { - Prebid: prebidAdapter - } - } - }; - - spec.onTimeout(timeoutData); - adapterMock.verify(); - prebidAdapterMock.verify(); - }); - - it('should return a POST method with url & data from pubtag', () => { - const bidRequests = {}; - const bidderRequest = {}; - - const prebidAdapter = { buildCdbUrl: function () { }, buildCdbRequest: function () { } }; - const prebidAdapterMock = sinon.mock(prebidAdapter); - prebidAdapterMock.expects('buildCdbUrl').once().returns('cdbUrl'); - prebidAdapterMock.expects('buildCdbRequest').once().returns('cdbRequest'); - - const adapters = { Prebid: function () { } }; - const adaptersMock = sinon.mock(adapters); - adaptersMock.expects('Prebid').withExactArgs(PROFILE_ID_PUBLISHERTAG, ADAPTER_VERSION, bidRequests, bidderRequest, '$prebid.version$', sinon.match.any).once().returns(prebidAdapter); - - global.Criteo = { - PubTag: { - Adapters: adapters - } - }; - - const buildRequestsResult = spec.buildRequests(bidRequests, bidderRequest); - expect(buildRequestsResult.method).equal('POST'); - expect(buildRequestsResult.url).equal('cdbUrl'); - expect(buildRequestsResult.data).equal('cdbRequest'); - - adaptersMock.verify(); - prebidAdapterMock.verify(); - }); - }); }); diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index 975271738e5..eb1f54d7cd2 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -2,6 +2,9 @@ import { criteoIdSubmodule, storage } from 'modules/criteoIdSystem.js'; import * as utils from 'src/utils.js'; import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; import { server } from '../../mocks/xhr'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; const pastDateString = new Date(0).toString() @@ -358,4 +361,20 @@ describe('CriteoId module', function () { expect(callBackSpy.calledOnce).to.be.true; })); + describe('eid', () => { + before(() => { + attachIdSystem(criteoIdSubmodule); + }); + it('criteo', function() { + const userId = { + criteoId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'criteo.com', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + }) }); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index e96867f4e84..8685bc1266f 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -3,7 +3,7 @@ import { getCurrencyRates } from 'test/fixtures/fixtures.js'; -import { getGlobal } from 'src/prebidGlobal.js'; +import {getGlobal} from 'src/prebidGlobal.js'; import { setConfig, @@ -13,9 +13,12 @@ import { responseReady } from 'modules/currency.js'; import {createBid} from '../../../src/bidfactory.js'; -import { EVENTS, STATUS, REJECTION_REASON } from '../../../src/constants.js'; +import * as utils from 'src/utils.js'; +import {EVENTS, STATUS, REJECTION_REASON} from '../../../src/constants.js'; import {server} from '../../mocks/xhr.js'; import * as events from 'src/events.js'; +import { enrichFPD } from '../../../src/fpd/enrichment.js'; +import {requestBidsHook} from '../../../modules/currency.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -522,4 +525,67 @@ describe('currency', function () { expect(innerBid.currency).to.equal('CNY'); }); }); + + describe('enrichFpd', function() { + function fpd(ortb2 = {}) { + return enrichFPD(Promise.resolve(ortb2)); + } + it('should set adServerCurrency on ortb', function () { + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({ adServerCurrency: 'EUR' }); + return fpd({}).then((ortb) => { + expect(ortb.ext.prebid.adServerCurrency).to.eql('EUR') + }) + }) + }); + + describe('auctionDelay param', () => { + const continueAuction = sinon.stub(); + let logWarnSpy; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z + logWarnSpy = sinon.spy(utils, 'logWarn'); + }); + + afterEach(function () { + clock.runAll(); + sandbox.restore(); + clock.restore(); + utils.logWarn.restore(); + continueAuction.resetHistory(); + }); + + it('should delay auction start when auctionDelay set in module config', () => { + setConfig({auctionDelay: 2000, adServerCurrency: 'USD'}); + const reqBidsConfigObj = { + auctionId: '128937' + }; + requestBidsHook(continueAuction, reqBidsConfigObj); + clock.tick(1000); + expect(continueAuction.notCalled).to.be.true; + }); + + it('should start auction when auctionDelay time passed', () => { + setConfig({auctionDelay: 2000, adServerCurrency: 'USD'}); + const reqBidsConfigObj = { + auctionId: '128937' + }; + requestBidsHook(continueAuction, reqBidsConfigObj); + clock.tick(3000); + expect(logWarnSpy.calledOnce).to.equal(true); + expect(continueAuction.calledOnce).to.be.true; + }); + + it('should run auction if rates were fetched before auctionDelay time', () => { + setConfig({auctionDelay: 3000, adServerCurrency: 'USD'}); + const reqBidsConfigObj = { + auctionId: '128937' + }; + fakeCurrencyFileServer.respond(); + requestBidsHook(continueAuction, reqBidsConfigObj); + expect(continueAuction.calledOnce).to.be.true; + }); + }); }); diff --git a/test/spec/modules/cpexIdSystem_spec.js b/test/spec/modules/czechAdIdSystem_spec.js similarity index 54% rename from test/spec/modules/cpexIdSystem_spec.js rename to test/spec/modules/czechAdIdSystem_spec.js index 6e004c9f8ca..19b606b9237 100644 --- a/test/spec/modules/cpexIdSystem_spec.js +++ b/test/spec/modules/czechAdIdSystem_spec.js @@ -1,4 +1,7 @@ -import { czechAdIdSubmodule, storage } from 'modules/czechAdIdSystem.js'; +import {czechAdIdSubmodule, storage} from 'modules/czechAdIdSystem.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; describe('czechAdId module', function () { let getCookieStub; @@ -12,13 +15,13 @@ describe('czechAdId module', function () { getCookieStub.restore(); }); - const cookieTestCasesForEmpty = [undefined, null, ''] + const cookieTestCasesForEmpty = [undefined, null, '']; describe('getId()', function () { it('should return the uid when it exists in cookie', function () { getCookieStub.withArgs('czaid').returns('czechAdIdTest'); const id = czechAdIdSubmodule.getId(); - expect(id).to.be.deep.equal({ id: 'czechAdIdTest' }); + expect(id).to.be.deep.equal({id: 'czechAdIdTest'}); }); cookieTestCasesForEmpty.forEach(testCase => it('should not return the uid when it doesnt exist in cookie', function () { @@ -32,7 +35,22 @@ describe('czechAdId module', function () { it('should return the uid when it exists in cookie', function () { getCookieStub.withArgs('czaid').returns('czechAdIdTest'); const decoded = czechAdIdSubmodule.decode(); - expect(decoded).to.be.deep.equal({ czechAdId: 'czechAdIdTest' }); + expect(decoded).to.be.deep.equal({czechAdId: 'czechAdIdTest'}); + }); + }); + describe('eid', () => { + before(() => { + attachIdSystem(czechAdIdSubmodule); + }); + + it('czechAdId', () => { + const id = 'some-random-id-value'; + const userId = {czechAdId: id}; + const [eid] = createEidsArray(userId); + expect(eid).to.deep.equal({ + source: 'czechadid.cz', + uids: [{id: 'some-random-id-value', atype: 1}] + }); }); }); }); diff --git a/test/spec/modules/dailyhuntBidAdapter_spec.js b/test/spec/modules/dailyhuntBidAdapter_spec.js index f347d6cec5b..ab75264d951 100644 --- a/test/spec/modules/dailyhuntBidAdapter_spec.js +++ b/test/spec/modules/dailyhuntBidAdapter_spec.js @@ -27,10 +27,10 @@ describe('DailyhuntAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); describe('buildRequests', function() { diff --git a/test/spec/modules/dailymotionBidAdapter_spec.js b/test/spec/modules/dailymotionBidAdapter_spec.js index a102c26dca2..3f420c1a48a 100644 --- a/test/spec/modules/dailymotionBidAdapter_spec.js +++ b/test/spec/modules/dailymotionBidAdapter_spec.js @@ -1,7 +1,7 @@ import { config } from 'src/config.js'; import { expect } from 'chai'; import { spec } from 'modules/dailymotionBidAdapter.js'; -import { VIDEO } from '../../../src/mediaTypes'; +import { BANNER, VIDEO } from '../../../src/mediaTypes'; describe('dailymotionBidAdapterTests', () => { // Validate that isBidRequestValid only validates requests with apiKey @@ -12,53 +12,1443 @@ describe('dailymotionBidAdapterTests', () => { params: { apiKey: '', }, + mediaTypes: { + [VIDEO]: { + context: 'instream', + }, + }, + }; + + expect(config.runWithBidder('dailymotion', () => spec.isBidRequestValid(bidWithEmptyApi))).to.be.false; + + const bidWithApi = { + params: { + apiKey: 'test_api_key', + }, + mediaTypes: { + [VIDEO]: { + context: 'instream', + }, + }, + }; + + expect(config.runWithBidder('dailymotion', () => spec.isBidRequestValid(bidWithApi))).to.be.true; + + const bidWithEmptyMediaTypes = { + params: { + apiKey: '', + }, + }; + + expect(config.runWithBidder('dailymotion', () => spec.isBidRequestValid(bidWithEmptyMediaTypes))).to.be.false; + + const bidWithEmptyVideoAdUnit = { + params: { + apiKey: '', + }, + mediaTypes: { + [VIDEO]: {}, + }, + }; + + expect(config.runWithBidder('dailymotion', () => spec.isBidRequestValid(bidWithEmptyVideoAdUnit))).to.be.false; + + const bidWithBannerMediaType = { + params: { + apiKey: 'test_api_key', + }, + mediaTypes: { + [BANNER]: {}, + }, + }; + + expect(config.runWithBidder('dailymotion', () => spec.isBidRequestValid(bidWithBannerMediaType))).to.be.false; + + const bidWithOutstreamContext = { + params: { + apiKey: 'test_api_key', + }, + mediaTypes: { + video: { + context: 'outstream', + }, + }, + }; + + expect(config.runWithBidder('dailymotion', () => spec.isBidRequestValid(bidWithOutstreamContext))).to.be.false; + }); + + // Validate request generation + it('validates buildRequests', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + dmTs: '123456', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerName: 'dailymotion', + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + const { data: reqData } = request; + + expect(request.options.withCredentials).to.eql(false); + expect(request.url).to.equal('https://pb.dmxleo.com'); + + expect(reqData.pbv).to.eql('$prebid.version$'); + expect(reqData.userSyncEnabled).to.be.true; + expect(reqData.bidder_request).to.eql({ + refererInfo: bidderRequestData.refererInfo, + uspConsent: bidderRequestData.uspConsent, + gdprConsent: bidderRequestData.gdprConsent, + gppConsent: bidderRequestData.gppConsent, + }); + expect(reqData.config.api_key).to.eql(bidRequestData[0].params.apiKey); + expect(reqData.config.ts).to.eql(bidRequestData[0].params.dmTs); + expect(reqData.request.auctionId).to.eql(bidRequestData[0].auctionId); + expect(reqData.request.bidId).to.eql(bidRequestData[0].bidId); + expect(reqData.request.mediaTypes.video).to.eql(bidRequestData[0].mediaTypes.video); + expect(reqData.video_metadata).to.eql({ + description: bidRequestData[0].params.video.description, + iabcat1: ['IAB-1'], + iabcat2: bidRequestData[0].params.video.iabcat2, + id: bidRequestData[0].params.video.id, + lang: bidRequestData[0].params.video.lang, + private: bidRequestData[0].params.video.private, + tags: bidRequestData[0].params.video.tags, + title: bidRequestData[0].params.video.title, + url: bidRequestData[0].params.video.url, + topics: bidRequestData[0].params.video.topics, + duration: bidRequestData[0].params.video.duration, + livestream: !!bidRequestData[0].params.video.livestream, + isCreatedForKids: bidRequestData[0].params.video.isCreatedForKids, + context: { + siteOrAppCat: [], + siteOrAppContentCat: [], + videoViewsInSession: bidRequestData[0].params.video.videoViewsInSession, + autoplay: bidRequestData[0].params.video.autoplay, + playerName: bidRequestData[0].params.video.playerName, + playerVolume: bidRequestData[0].params.video.playerVolume, + }, + }); + }); + + it('validates buildRequests with global consent', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerName: 'dailymotion', + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: true + } + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(true); + }); + + it('validates buildRequests without gdpr applying', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerName: 'dailymotion', + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: false, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(true); + }); + + it('validates buildRequests with detailed consent without legitimate interest', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerName: 'dailymotion', + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 7: true, + 9: true, + 10: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(false); + }); + + it('validates buildRequests with detailed consent, with legitimate interest', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerName: 'dailymotion', + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + purpose: { + consents: { + 1: true, + 3: true, + 4: true, + }, + legitimateInterests: { + 2: true, + 7: true, + 9: true, + 10: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(true); + }); + + it('validates buildRequests with detailed consent and legitimate interest but publisher forces consent', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerName: 'dailymotion', + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + publisher: { + restrictions: { + 2: { 573: 1 }, + 7: { 573: 1 }, + 9: { 573: 1 }, + 10: { 573: 1 }, + }, + }, + purpose: { + consents: { + 1: true, + 3: true, + 4: true, + }, + legitimateInterests: { + 2: true, + 7: true, + 9: true, + 10: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(false); + }); + + it('validates buildRequests with detailed consent, no legitimate interest and publisher forces consent', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerName: 'dailymotion', + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + publisher: { + restrictions: { + 2: { 573: 1 }, + 7: { 573: 1 }, + 9: { 573: 1 }, + 10: { 573: 1 }, + }, + }, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 7: true, + 9: true, + 10: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(true); + }); + + it('validates buildRequests with detailed consent but publisher full restriction on purpose 1', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + xid: 'x123456', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + publisher: { + restrictions: { + 1: { + 573: 0, + }, + }, + }, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 7: true, + 9: true, + 10: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(false); + }); + + it('validates buildRequests with detailed consent but publisher restriction 2 on consent purpose 1', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + xid: 'x123456', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + publisher: { + restrictions: { + 1: { + 573: 2, + }, + }, + }, + purpose: { + consents: { + 1: true, + 3: true, + 4: true, + }, + legitimateInterests: { + 2: true, + 7: true, + 9: true, + 10: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(false); + }); + + it('validates buildRequests with detailed consent, legitimate interest and publisher restriction on purpose 1', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + xid: 'x123456', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + publisher: { + restrictions: { + 1: { + 573: 1, + }, + }, + }, + purpose: { + consents: { + 1: true, + 3: true, + 4: true, + }, + legitimateInterests: { + 2: true, + 7: true, + 9: true, + 10: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, + }; + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(true); + }); + + it('validates buildRequests with detailed consent and legitimate interest but publisher restriction on legitimate interest 2', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + xid: 'x123456', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + publisher: { + restrictions: { + 2: { + 573: 2, + }, + }, + }, + purpose: { + consents: { + 1: true, + 3: true, + 4: true, + }, + legitimateInterests: { + 2: true, + 7: true, + 9: true, + 10: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, + }, }; - expect(config.runWithBidder('dailymotion', () => spec.isBidRequestValid(bidWithEmptyApi))).to.be.false; + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); - const bidWithApi = { + expect(request.options.withCredentials).to.eql(true); + }); + + it('validates buildRequests with insufficient consent', () => { + const bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, + startdelay: 0, + w: 1280, + h: 720, + }, + }, + sizes: [[1920, 1080]], params: { apiKey: 'test_api_key', + video: { + description: 'this is a test video', + duration: 556, + iabcat1: ['IAB-1'], + iabcat2: ['6', '17'], + id: '54321', + lang: 'FR', + private: false, + tags: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + topics: 'topic_1, topic_2', + livestream: 1, + isCreatedForKids: true, + videoViewsInSession: 2, + autoplay: true, + playerName: 'dailymotion', + playerVolume: 8, + }, + }, + }]; + + const bidderRequestData = { + refererInfo: { + page: 'https://publisher.com', + }, + uspConsent: '1YN-', + gdprConsent: { + apiVersion: 2, + consentString: 'xxx', + gdprApplies: true, + vendorData: { + hasGlobalConsent: false, + purpose: { + consents: { + 1: true, + 3: true, + 4: true, + }, + }, + vendor: { + consents: { + 573: true + } + }, + }, + }, + gppConsent: { + gppString: 'xxx', + applicableSections: [5], + }, + ortb2: { + regs: { + coppa: 1, + }, + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '200' }], + }, + ], + }, + }, }, }; - expect(config.runWithBidder('dailymotion', () => spec.isBidRequestValid(bidWithApi))).to.be.true; + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.options.withCredentials).to.eql(false); }); - // Validate request generation - it('validates buildRequests', () => { + it('validates buildRequests with content values from App', () => { const bidRequestData = [{ + getFloor: () => ({ currency: 'USD', floor: 3 }), auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidId: 123456, adUnitCode: 'preroll', mediaTypes: { video: { - playerSize: [[1280, 720]], api: [2, 7], - description: 'this is a test video', - duration: 300, - iabcat2: ['6', '17'], - lang: 'ENG', + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + playbackmethod: [3], + plcmt: 1, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1, + skipafter: 5, + skipmin: 10, startdelay: 0, + w: 1280, + h: 720, }, }, sizes: [[1920, 1080]], params: { apiKey: 'test_api_key', video: { - duration: 556, + description: 'this is a test video', + iabcat2: ['6', '17'], id: '54321', lang: 'FR', private: false, tags: 'tag_1,tag_2,tag_3', title: 'test video', + url: 'https://test.com/test', topics: 'topic_1, topic_2', - xid: 'x123456', + livestream: 1, + // Test invalid values + isCreatedForKids: 'false', + videoViewsInSession: -1, + autoplay: 'true', + playerName: 'dailymotion', + playerVolume: 12, }, }, }]; const bidderRequestData = { + timeout: 4242, refererInfo: { page: 'https://publisher.com', }, @@ -73,11 +1463,38 @@ describe('dailymotionBidAdapterTests', () => { applicableSections: [5], }, ortb2: { + bcat: ['IAB-1'], + badv: ['bcav-1'], regs: { coppa: 1, }, - site: { + device: { + lmt: 1, + ifa: 'xxx', + devicetype: 2, + make: 'make', + model: 'model', + os: 'os', + osv: 'osv', + language: 'language', + geo: { + country: 'country', + region: 'region', + city: 'city', + zip: 'zip', + metro: 'metro' + }, + ext: { + atts: 2, + ifa_type: 'ifa_type' + }, + }, + app: { + bundle: 'app-bundle', + storeurl: 'https://play.google.com/store/apps/details?id=app-bundle', content: { + len: 556, + cattax: 3, data: [ { name: 'dataprovider.com', @@ -95,6 +1512,18 @@ describe('dailymotionBidAdapterTests', () => { }, }; + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: ['dailymotion'], + filter: 'include' + } + } + } + }); + const [request] = config.runWithBidder( 'dailymotion', () => spec.buildRequests(bidRequestData, bidderRequestData), @@ -104,6 +1533,119 @@ describe('dailymotionBidAdapterTests', () => { expect(request.url).to.equal('https://pb.dmxleo.com'); + expect(reqData.pbv).to.eql('$prebid.version$'); + + const expectedOrtb = { + 'app': { + 'bundle': 'app-bundle', + 'content': { + 'cattax': 3, + 'data': [ + { + 'ext': { + 'segtax': 4 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': 'IAB-1' + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '200' + } + ], + } + ], + 'len': 556, + }, + 'storeurl': 'https://play.google.com/store/apps/details?id=app-bundle', + }, + 'badv': [ + 'bcav-1' + ], + 'bcat': [ + 'IAB-1' + ], + 'device': { + 'devicetype': 2, + 'ext': { + 'atts': 2, + 'ifa_type': 'ifa_type' + }, + 'geo': { + 'city': 'city', + 'country': 'country', + 'metro': 'metro', + 'region': 'region', + 'zip': 'zip', + }, + 'ifa': 'xxx', + 'language': 'language', + 'lmt': 1, + 'make': 'make', + 'model': 'model', + 'os': 'os', + 'osv': 'osv', + }, + 'imp': [{ + 'bidfloor': 3, + 'bidfloorcur': 'USD', + 'id': 123456, + 'secure': 1, + ...(FEATURES.VIDEO ? { + 'video': { + 'api': [ + 2, + 7 + ], + 'h': 720, + 'maxduration': 30, + 'mimes': [ + 'video/mp4' + ], + 'minduration': 5, + 'playbackmethod': [ + 3 + ], + 'plcmt': 1, + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 1, + 'skipafter': 5, + 'skipmin': 10, + 'startdelay': 0, + 'w': 1280, + } + } : {}), + } + ], + 'regs': { + 'coppa': 1, + }, + 'test': 0, + 'tmax': 4242, + } + + expect(reqData.ortb.id).to.be.not.empty; + delete reqData.ortb.id; // ortb id is generated randomly + expect(reqData.ortb).to.eql(expectedOrtb); + expect(reqData.userSyncEnabled).to.be.true; expect(reqData.bidder_request).to.eql({ refererInfo: bidderRequestData.refererInfo, uspConsent: bidderRequestData.uspConsent, @@ -111,40 +1653,45 @@ describe('dailymotionBidAdapterTests', () => { gppConsent: bidderRequestData.gppConsent, }); expect(reqData.config.api_key).to.eql(bidRequestData[0].params.apiKey); - expect(reqData.coppa).to.be.true; expect(reqData.request.auctionId).to.eql(bidRequestData[0].auctionId); expect(reqData.request.bidId).to.eql(bidRequestData[0].bidId); - expect(reqData.request.mediaTypes.video.api).to.eql(bidRequestData[0].mediaTypes.video.api); - expect(reqData.request.mediaTypes.video.playerSize).to.eql(bidRequestData[0].mediaTypes.video.playerSize); - expect(reqData.request.mediaTypes.video.startDelay).to.eql(bidRequestData[0].mediaTypes.video.startdelay); + + expect(reqData.request.mediaTypes.video).to.eql(bidRequestData[0].mediaTypes.video); + expect(reqData.video_metadata).to.eql({ - description: bidRequestData[0].mediaTypes.video.description, - iabcat1: ['IAB-1'], // Taxonomy v2 or higher is excluded - iabcat2: bidRequestData[0].mediaTypes.video.iabcat2, + description: bidRequestData[0].params.video.description, + iabcat1: ['IAB-1'], + iabcat2: bidRequestData[0].params.video.iabcat2, id: bidRequestData[0].params.video.id, lang: bidRequestData[0].params.video.lang, private: bidRequestData[0].params.video.private, tags: bidRequestData[0].params.video.tags, title: bidRequestData[0].params.video.title, + url: bidRequestData[0].params.video.url, topics: bidRequestData[0].params.video.topics, - xid: bidRequestData[0].params.video.xid, // Overriden through bidder params - duration: bidRequestData[0].params.video.duration, + duration: bidderRequestData.ortb2.app.content.len, + livestream: !!bidRequestData[0].params.video.livestream, + isCreatedForKids: null, + context: { + siteOrAppCat: [], + siteOrAppContentCat: [], + videoViewsInSession: null, + autoplay: null, + playerName: 'dailymotion', + playerVolume: null, + }, }); }); - it('validates buildRequests with fallback values on ortb2 for gpp & iabcat', () => { + it('validates buildRequests with fallback values on ortb2 (gpp, iabcat2, id...)', () => { const bidRequestData = [{ auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidId: 123456, adUnitCode: 'preroll', mediaTypes: { video: { - playerSize: [[1280, 720]], api: [2, 7], - description: 'this is a test video', - duration: 300, - lang: 'ENG', startdelay: 0, }, }, @@ -152,14 +1699,16 @@ describe('dailymotionBidAdapterTests', () => { params: { apiKey: 'test_api_key', video: { + description: 'this is a test video', duration: 556, - id: '54321', - lang: 'FR', private: false, - tags: 'tag_1,tag_2,tag_3', title: 'test video', topics: 'topic_1, topic_2', - xid: 'x123456', + isCreatedForKids: false, + videoViewsInSession: 10, + autoplay: false, + playerName: 'dailymotion', + playerVolume: 0, }, }, }]; @@ -175,13 +1724,23 @@ describe('dailymotionBidAdapterTests', () => { gdprApplies: true, }, ortb2: { + tmax: 31416, regs: { gpp: 'xxx', gpp_sid: [5], coppa: 0, }, site: { + cat: ['IAB-1'], content: { + id: '54321', + language: 'FR', + keywords: 'tag_1,tag_2,tag_3', + title: 'test video', + url: 'https://test.com/test', + livestream: 1, + cat: ['IAB-2'], + cattax: 1, data: [ undefined, // Undefined to check proper handling of edge cases {}, // Empty object to check proper handling of edge cases @@ -203,7 +1762,7 @@ describe('dailymotionBidAdapterTests', () => { }, { name: 'dataprovider.com', - ext: { segtax: 4 }, + ext: { segtax: 5 }, segment: [{ id: 2222 }], // Invalid segment id to check proper handling of edge cases }, { @@ -227,6 +1786,22 @@ describe('dailymotionBidAdapterTests', () => { }, }; + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: ['dailymotion'], + filter: 'include' + }, + iframe: { + bidders: ['dailymotion'], + filter: 'exclude', + }, + } + } + }); + const [request] = config.runWithBidder( 'dailymotion', () => spec.buildRequests(bidRequestData, bidderRequestData), @@ -236,6 +1811,134 @@ describe('dailymotionBidAdapterTests', () => { expect(request.url).to.equal('https://pb.dmxleo.com'); + const expectedOrtb = { + 'imp': [{ + 'id': 123456, + 'secure': 1, + ...(FEATURES.VIDEO ? { + 'video': { + 'api': [ + 2, + 7 + ], + 'startdelay': 0, + } + } : {}) + } + ], + 'regs': { + 'coppa': 0, + 'gpp': 'xxx', + 'gpp_sid': [ + 5 + ], + }, + 'site': { + 'cat': [ + 'IAB-1', + ], + 'content': { + 'cat': [ + 'IAB-2', + ], + 'cattax': 1, + 'data': [ + undefined, + {}, + { + 'ext': {} + }, + { + 'ext': { + 'segtax': 22 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '400' + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': undefined + }, + { + 'ext': { + 'segtax': 4 + }, + 'name': 'dataprovider.com', + 'segment': undefined + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': 2222 + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '6' + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '6' + } + ] + }, + { + 'ext': { + 'segtax': 5 + }, + 'name': 'dataprovider.com', + 'segment': [ + { + 'id': '17' + }, + { + 'id': '20' + } + ] + } + ], + 'id': '54321', + 'keywords': 'tag_1,tag_2,tag_3', + 'language': 'FR', + 'livestream': 1, + 'title': 'test video', + 'url': 'https://test.com/test', + } + }, + 'test': 0, + 'tmax': 31416 + } + + expect(reqData.pbv).to.eql('$prebid.version$'); + expect(reqData.ortb.id).to.be.not.empty; + delete reqData.ortb.id; // ortb id is generated randomly + expect(reqData.ortb).to.eql(expectedOrtb); + + expect(reqData.userSyncEnabled).to.be.true; expect(reqData.bidder_request).to.eql({ refererInfo: bidderRequestData.refererInfo, uspConsent: bidderRequestData.uspConsent, @@ -246,26 +1949,46 @@ describe('dailymotionBidAdapterTests', () => { }, }); expect(reqData.config.api_key).to.eql(bidRequestData[0].params.apiKey); - expect(reqData.coppa).to.be.false; expect(reqData.request.auctionId).to.eql(bidRequestData[0].auctionId); expect(reqData.request.bidId).to.eql(bidRequestData[0].bidId); - expect(reqData.request.mediaTypes.video.api).to.eql(bidRequestData[0].mediaTypes.video.api); - expect(reqData.request.mediaTypes.video.playerSize).to.eql(bidRequestData[0].mediaTypes.video.playerSize); - expect(reqData.request.mediaTypes.video.startDelay).to.eql(bidRequestData[0].mediaTypes.video.startdelay); + + expect(reqData.request.mediaTypes.video).to.eql({ + ...bidRequestData[0].mediaTypes.video, + mimes: [], + minduration: 0, + maxduration: 0, + playbackmethod: [], + plcmt: undefined, + protocols: [], + skip: 0, + skipafter: 0, + skipmin: 0, + w: 0, + h: 0, + }); + expect(reqData.video_metadata).to.eql({ - description: bidRequestData[0].mediaTypes.video.description, - // No iabcat1 here because nothing matches taxonomy - iabcat1: [], + description: bidRequestData[0].params.video.description, + iabcat1: ['IAB-2'], iabcat2: ['6', '17', '20'], - id: bidRequestData[0].params.video.id, - lang: bidRequestData[0].params.video.lang, + id: bidderRequestData.ortb2.site.content.id, + lang: bidderRequestData.ortb2.site.content.language, private: bidRequestData[0].params.video.private, - tags: bidRequestData[0].params.video.tags, - title: bidRequestData[0].params.video.title, + tags: bidderRequestData.ortb2.site.content.keywords, + title: bidderRequestData.ortb2.site.content.title, + url: bidderRequestData.ortb2.site.content.url, topics: bidRequestData[0].params.video.topics, - xid: bidRequestData[0].params.video.xid, - // Overriden through bidder params duration: bidRequestData[0].params.video.duration, + livestream: !!bidderRequestData.ortb2.site.content.livestream, + isCreatedForKids: bidRequestData[0].params.video.isCreatedForKids, + context: { + siteOrAppCat: bidderRequestData.ortb2.site.cat, + siteOrAppContentCat: bidderRequestData.ortb2.site.content.cat, + videoViewsInSession: bidRequestData[0].params.video.videoViewsInSession, + autoplay: bidRequestData[0].params.video.autoplay, + playerName: bidRequestData[0].params.video.playerName, + playerVolume: bidRequestData[0].params.video.playerVolume, + }, }); }); @@ -276,6 +1999,12 @@ describe('dailymotionBidAdapterTests', () => { }, }]; + config.setConfig({ + userSync: { + syncEnabled: false, + } + }); + const [request] = config.runWithBidder( 'dailymotion', () => spec.buildRequests(bidRequestDataWithApi, {}), @@ -284,10 +2013,21 @@ describe('dailymotionBidAdapterTests', () => { const { data: reqData } = request; expect(request.url).to.equal('https://pb.dmxleo.com'); - expect(reqData.config.api_key).to.eql(bidRequestDataWithApi[0].params.apiKey); - expect(reqData.coppa).to.be.false; + expect(reqData.pbv).to.eql('$prebid.version$'); + expect(reqData.ortb.id).to.be.not.empty; + delete reqData.ortb.id; // ortb id is generated randomly + expect(reqData.ortb).to.eql({ + 'imp': [ + { + 'id': undefined, + 'secure': 1 + }, + ], + 'test': 0 + }); + expect(reqData.userSyncEnabled).to.be.false; expect(reqData.bidder_request).to.eql({ gdprConsent: { apiVersion: 1, @@ -310,9 +2050,19 @@ describe('dailymotionBidAdapterTests', () => { adUnitCode: '', mediaTypes: { video: { - playerSize: [], - startDelay: 0, api: [], + mimes: [], + minduration: 0, + maxduration: 0, + playbackmethod: [], + plcmt: undefined, + protocols: [], + skip: 0, + skipafter: 0, + skipmin: 0, + startdelay: undefined, + w: 0, + h: 0, }, }, sizes: [], @@ -328,11 +2078,202 @@ describe('dailymotionBidAdapterTests', () => { private: false, tags: '', title: '', + url: '', topics: '', - xid: '', + livestream: false, + isCreatedForKids: null, + context: { + siteOrAppCat: [], + siteOrAppContentCat: [], + videoViewsInSession: null, + autoplay: null, + playerName: '', + playerVolume: null, + }, + }); + }); + + describe('validates buildRequests for video metadata iabcat1 and iabcat2', () => { + let bidRequestData; + let bidderRequestData; + let request; + + beforeEach(() => { + bidRequestData = [{ + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: 123456, + adUnitCode: 'preroll', + mediaTypes: { + video: { + api: [2, 7], + startdelay: 0, + }, + }, + sizes: [[1920, 1080]], + params: { + apiKey: 'test_api_key', + video: { + iabcat1: ['video-params-iabcat1'], + iabcat2: ['video-params-iabcat2'], + }, + }, + }]; + + bidderRequestData = { + timeout: 4242, + refererInfo: { + page: 'https://publisher.com', + }, + ortb2: { + site: { + content: { + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 4 }, + segment: [{ id: '1' }], + }, + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '6' }], + }, + { + name: 'dataprovider.com', + ext: { segtax: 5 }, + segment: [{ id: '17' }, { id: '20' }], + }, + ] + }, + } + } + }; + + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: ['dailymotion'], + filter: 'include' + }, + iframe: { + bidders: ['dailymotion'], + filter: 'exclude', + }, + }, + }, + }); + + [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); }); + + it('get iabcat1 and iabcat 2 from params video', () => { + expect(request.data.video_metadata.iabcat1).to.eql(bidRequestData[0].params.video.iabcat1); + expect(request.data.video_metadata.iabcat2).to.eql(bidRequestData[0].params.video.iabcat2); + }) + + it('get iabcat1 from content.cat and iabcat2 from data.segment', () => { + const iabCatTestsCases = [[], null, {}]; + + iabCatTestsCases.forEach((iabCat) => { + bidRequestData[0].params.video.iabcat1 = iabCat; + bidRequestData[0].params.video.iabcat2 = iabCat; + bidderRequestData.ortb2.site.content.cat = ['video-content-cat']; + bidderRequestData.ortb2.site.content.cattax = 1; + + [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.data.video_metadata.iabcat1).to.eql(bidderRequestData.ortb2.site.content.cat); + expect(request.data.video_metadata.iabcat2).to.eql(['6', '17', '20']); + }) + }) + + it('get iabcat2 from content.cat and iabcat1 from data.segment', () => { + const iabCatTestsCases = [[], null, {}]; + const cattaxV2 = [2, 5, 6]; + + cattaxV2.forEach((cattax) => { + iabCatTestsCases.forEach((iabCat) => { + bidRequestData[0].params.video.iabcat1 = iabCat; + bidRequestData[0].params.video.iabcat2 = iabCat; + bidderRequestData.ortb2.site.content.cat = ['video-content-cat']; + bidderRequestData.ortb2.site.content.cattax = cattax; + + [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.data.video_metadata.iabcat1).to.eql(['1']); + expect(request.data.video_metadata.iabcat2).to.eql(bidderRequestData.ortb2.site.content.cat); + }) + }) + }) + + it('get iabcat1 and iabcat2 from data.segmnet', () => { + const contentCatTestCases = [[], null, {}]; + const cattaxTestCases = [1, 2, 5, 6]; + + cattaxTestCases.forEach((cattax) => { + contentCatTestCases.forEach((contentCat) => { + bidRequestData[0].params.video.iabcat1 = []; + bidRequestData[0].params.video.iabcat2 = []; + bidderRequestData.ortb2.site.content.cat = contentCat; + bidderRequestData.ortb2.site.content.cattax = cattax; + + [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequestData, bidderRequestData), + ); + + expect(request.data.video_metadata.iabcat1).to.eql(['1']); + expect(request.data.video_metadata.iabcat2).to.eql(['6', '17', '20']); + }) + }) + }) }); + it('validates buildRequests - with null floor as object for getFloor function', () => { + const bidRequest = [{ + params: { + apiKey: 'test_api_key', + }, + getFloor: () => null + }]; + + config.setConfig({ + userSync: { + syncEnabled: false, + } + }); + + const [request] = config.runWithBidder( + 'dailymotion', + () => spec.buildRequests(bidRequest, {}), + ); + + const { data: reqData } = request; + + expect(reqData.ortb.id).to.be.not.empty; + delete reqData.ortb.id; // ortb id is generated randomly + expect(reqData.ortb).to.eql({ + 'imp': [ + { + 'id': undefined, + 'secure': 1 + }, + ], + 'test': 0 + }); + }) + it('validates buildRequests - with empty/undefined validBidRequests', () => { expect(spec.buildRequests([], {})).to.have.lengthOf(0); @@ -361,9 +2302,103 @@ describe('dailymotionBidAdapterTests', () => { expect(bid).to.eql(serverResponse.body); }); + it('validates interpretResponse - without bid (no cpm)', () => { + const serverResponse = { + body: { + requestId: 'test_requestid', + }, + }; + + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(0); + }); + it('validates interpretResponse - with empty/undefined serverResponse', () => { expect(spec.interpretResponse({})).to.have.lengthOf(0); expect(spec.interpretResponse(undefined)).to.have.lengthOf(0); }); + + it('validates getUserSyncs', () => { + // Nothing sent in getUserSyncs + expect(config.runWithBidder('dailymotion', () => spec.getUserSyncs())).to.eql([]); + + // No server response + { + const responses = []; + const syncOptions = { iframeEnabled: true, pixelEnabled: true }; + + expect(config.runWithBidder( + 'dailymotion', + () => spec.getUserSyncs(syncOptions, responses), + )).to.eql([]); + } + + // No permissions + { + const responses = [{ body: { userSyncs: [{ url: 'https://usersyncurl.com', type: 'image' }] } }]; + const syncOptions = { iframeEnabled: false, pixelEnabled: false }; + + expect(config.runWithBidder( + 'dailymotion', + () => spec.getUserSyncs(syncOptions, responses), + )).to.eql([]); + } + + // Has permissions but no userSyncs urls + { + const responses = [{}]; + const syncOptions = { iframeEnabled: false, pixelEnabled: true }; + + expect(config.runWithBidder( + 'dailymotion', + () => spec.getUserSyncs(syncOptions, responses), + )).to.eql([]); + } + + // Return userSyncs urls for pixels + { + const responses = [{ + body: { + userSyncs: [ + { url: 'https://usersyncurl.com', type: 'image' }, + { url: 'https://usersyncurl2.com', type: 'image' }, + { url: 'https://usersyncurl3.com', type: 'iframe' } + ], + } + }]; + + const syncOptions = { iframeEnabled: false, pixelEnabled: true }; + + expect(config.runWithBidder( + 'dailymotion', + () => spec.getUserSyncs(syncOptions, responses), + )).to.eql([ + { type: 'image', url: 'https://usersyncurl.com' }, + { type: 'image', url: 'https://usersyncurl2.com' }, + ]); + } + + // Return userSyncs urls for iframes + { + const responses = [{ + body: { + userSyncs: [ + { url: 'https://usersyncurl.com', type: 'image' }, + { url: 'https://usersyncurl2.com', type: 'image' }, + { url: 'https://usersyncurl3.com', type: 'iframe' } + ], + } + }]; + + const syncOptions = { iframeEnabled: true, pixelEnabled: true }; + + expect(config.runWithBidder( + 'dailymotion', + () => spec.getUserSyncs(syncOptions, responses), + )).to.eql([ + { type: 'iframe', url: 'https://usersyncurl3.com' }, + ]); + } + }); }); diff --git a/test/spec/modules/datawrkzBidAdapter_spec.js b/test/spec/modules/datawrkzBidAdapter_spec.js index 5524e318600..e78d2f68d91 100644 --- a/test/spec/modules/datawrkzBidAdapter_spec.js +++ b/test/spec/modules/datawrkzBidAdapter_spec.js @@ -36,26 +36,26 @@ describe('datawrkzAdapterTests', function () { }); it('should return false when params not found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when required site_id param not found', function () { - let bid = Object.assign({}, bid); - bid.params = {'bidfloor': '1.0'} - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + invalidBid.params = {'bidfloor': '1.0'} + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when adunit is adpod video', function () { - let bid = Object.assign({}, bid); - bid.params = {'bidfloor': '1.0', 'site_id': SITE_ID}; - bid.mediaTypes = { + let invalidBid = Object.assign({}, bid); + invalidBid.params = {'bidfloor': '1.0', 'site_id': SITE_ID}; + invalidBid.mediaTypes = { 'video': { 'context': 'adpod' } } - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/debugging_mod_spec.js b/test/spec/modules/debugging_mod_spec.js index ab99ba2aa0c..438c70fae64 100644 --- a/test/spec/modules/debugging_mod_spec.js +++ b/test/spec/modules/debugging_mod_spec.js @@ -37,7 +37,12 @@ describe('bid interceptor', () => { describe('serializeConfig', () => { Object.entries({ regexes: /pat/, - functions: () => ({}) + functions: () => ({}), + 'undefined': undefined, + date: new Date(), + symbol: Symbol('test'), + map: new Map(), + set: new Set(), }).forEach(([test, arg]) => { it(`should filter out ${test}`, () => { const valid = [{key1: 'value'}, {key2: 'value'}]; @@ -167,8 +172,8 @@ describe('bid interceptor', () => { describe('paapi', () => { it('should accept literals', () => { const mockConfig = [ - {paapi: 1}, - {paapi: 2} + {config: {paapi: 1}}, + {config: {paapi: 2}} ] const paapi = matchingRule({paapi: mockConfig}).paapi({}); expect(paapi).to.eql(mockConfig); @@ -179,6 +184,30 @@ describe('bid interceptor', () => { const args = [{}, {}, {}]; matchingRule({paapi: paapiDef}).paapi(...args); expect(paapiDef.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }); + + Object.entries({ + 'literal': (cfg) => [cfg], + 'function': (cfg) => () => [cfg] + }).forEach(([t, makeConfigs]) => { + describe(`when paapi is defined as a ${t}`, () => { + it('should wrap top-level configs in "config"', () => { + const cfg = {decisionLogicURL: 'example'}; + expect(matchingRule({paapi: makeConfigs(cfg)}).paapi({})).to.eql([{ + config: cfg + }]) + }); + + Object.entries({ + 'config': {config: 1}, + 'igb': {igb: 1}, + 'config and igb': {config: 1, igb: 2} + }).forEach(([t, cfg]) => { + it(`should not wrap configs that define top-level ${t}`, () => { + expect(matchingRule({paapi: makeConfigs(cfg)}).paapi({})).to.eql([cfg]); + }) + }) + }) }) }) @@ -274,8 +303,8 @@ describe('bid interceptor', () => { it('should call addPaapiConfigs when provided', () => { const mockPaapiConfigs = [ - {paapi: 1}, - {paapi: 2} + {config: {paapi: 1}}, + {config: {paapi: 2}} ] setRules({ when: {id: 2}, @@ -325,17 +354,30 @@ describe('Debugging config', () => { }); describe('bidderBidInterceptor', () => { - let next, interceptBids, onCompletion, interceptResult, done, addBid; + let next, interceptBids, onCompletion, interceptResult, done, addBid, wrapCallback, addPaapiConfig, wrapped; - function interceptorArgs({spec = {}, bids = [], bidRequest = {}, ajax = {}, wrapCallback = {}, cbs = {}} = {}) { + function interceptorArgs({spec = {}, bids = [], bidRequest = {}, ajax = {}, cbs = {}} = {}) { return [next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, Object.assign({onCompletion}, cbs)]; } beforeEach(() => { next = sinon.spy(); + wrapped = false; + wrapCallback = sinon.stub().callsFake(cb => { + if (cb == null) return cb; + return function () { + wrapped = true; + try { + return cb.apply(this, arguments) + } finally { + wrapped = false; + } + } + }); interceptBids = sinon.stub().callsFake((opts) => { done = opts.done; addBid = opts.addBid; + addPaapiConfig = opts.addPaapiConfig; return interceptResult; }); onCompletion = sinon.spy(); @@ -343,13 +385,26 @@ describe('bidderBidInterceptor', () => { }); it('should pass to interceptBid an addBid that triggers onBid', () => { - const onBid = sinon.spy(); + const onBid = sinon.stub().callsFake(() => { + expect(wrapped).to.be.true; + }); bidderBidInterceptor(...interceptorArgs({cbs: {onBid}})); - const bid = {}; + const bid = { + bidder: 'bidder' + }; addBid(bid); expect(onBid.calledWith(sinon.match.same(bid))).to.be.true; }); + it('should pass addPaapiConfig that triggers onPaapi', () => { + const onPaapi = sinon.stub().callsFake(() => { + expect(wrapped).to.be.true; + }); + bidderBidInterceptor(...interceptorArgs({cbs: {onPaapi}})); + addPaapiConfig({paapi: 'config'}, {bidId: 'bidId'}); + sinon.assert.calledWith(onPaapi, {paapi: 'config', bidId: 'bidId'}) + }) + describe('with no remaining bids', () => { it('should pass a done callback that triggers onCompletion', () => { bidderBidInterceptor(...interceptorArgs()); @@ -358,6 +413,12 @@ describe('bidderBidInterceptor', () => { expect(onCompletion.calledOnce).to.be.true; }); + it('should call onResponse', () => { + const onResponse = sinon.stub(); + bidderBidInterceptor(...interceptorArgs({cbs: {onResponse}})); + sinon.assert.called(onResponse); + }) + it('should not call next()', () => { bidderBidInterceptor(...interceptorArgs()); expect(next.called).to.be.false; diff --git a/test/spec/modules/deepintentBidAdapter_spec.js b/test/spec/modules/deepintentBidAdapter_spec.js index 644e9255789..b64c29d6800 100644 --- a/test/spec/modules/deepintentBidAdapter_spec.js +++ b/test/spec/modules/deepintentBidAdapter_spec.js @@ -252,6 +252,22 @@ describe('Deepintent adapter', function () { expect(data.imp[0].displaymanager).to.equal('di_prebid'); expect(data.imp[0].displaymanagerver).to.equal('1.0.0'); }); + it('bid request check: bidfloor check', function() { + const requestClone = utils.deepClone(request); + let bRequest = spec.buildRequests(requestClone); + let data = JSON.parse(bRequest.data); + expect(data.imp[0].bidfloor).to.not.exist; + + requestClone[0].params.bidfloor = 0; + bRequest = spec.buildRequests(requestClone); + data = JSON.parse(bRequest.data); + expect(data.imp[0].bidfloor).to.equal(0); + + requestClone[0].params.bidfloor = 1.2; + bRequest = spec.buildRequests(requestClone); + data = JSON.parse(bRequest.data); + expect(data.imp[0].bidfloor).to.equal(1.2); + }); it('bid request check: user object check', function () { let bRequest = spec.buildRequests(request); let data = JSON.parse(bRequest.data); diff --git a/test/spec/modules/deepintentDpesIdsystem_spec.js b/test/spec/modules/deepintentDpesIdsystem_spec.js index 4c26b118a98..252cb2f414c 100644 --- a/test/spec/modules/deepintentDpesIdsystem_spec.js +++ b/test/spec/modules/deepintentDpesIdsystem_spec.js @@ -1,12 +1,10 @@ import { expect } from 'chai'; -import {find} from 'src/polyfill.js'; -import { storage, deepintentDpesSubmodule } from 'modules/deepintentDpesIdSystem.js'; -import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; -import { config } from 'src/config.js'; +import { deepintentDpesSubmodule } from 'modules/deepintentDpesIdSystem.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; -const DI_COOKIE_NAME = '_dpes_id'; -const DI_COOKIE_STORED = '{"id":"2cf40748c4f7f60d343336e08f80dc99"}'; const DI_COOKIE_OBJECT = {id: '2cf40748c4f7f60d343336e08f80dc99'}; +const DI_UPDATED_STORAGE = '2cf40748c4f7f60d343336e08f80dc99'; const cookieConfig = { name: 'deepintentId', @@ -27,50 +25,56 @@ const html5Config = { } describe('Deepintent DPES System', () => { - let getDataFromLocalStorageStub, localStorageIsEnabledStub; - let getCookieStub, cookiesAreEnabledStub; - - beforeEach(() => { - getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); - localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); - getCookieStub = sinon.stub(storage, 'getCookie'); - cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); - }); - - afterEach(() => { - getDataFromLocalStorageStub.restore(); - localStorageIsEnabledStub.restore(); - getCookieStub.restore(); - cookiesAreEnabledStub.restore(); - }); - describe('Deepintent Dpes Sytsem: test "getId" method', () => { - it('Wrong config should fail the tests', () => { - // no config - expect(deepintentDpesSubmodule.getId()).to.be.eq(undefined); - expect(deepintentDpesSubmodule.getId({ })).to.be.eq(undefined); - - expect(deepintentDpesSubmodule.getId({params: {}, storage: {}})).to.be.eq(undefined); - expect(deepintentDpesSubmodule.getId({params: {}, storage: {type: 'cookie'}})).to.be.eq(undefined); - expect(deepintentDpesSubmodule.getId({params: {}, storage: {name: '_dpes_id'}})).to.be.eq(undefined); + it('If nothing is found in cache, return undefined', () => { + let diId = deepintentDpesSubmodule.getId({}, undefined, undefined); + expect(diId).to.be.eq(undefined); }); it('Get value stored in cookie for getId', () => { - getCookieStub.withArgs(DI_COOKIE_NAME).returns(DI_COOKIE_STORED); let diId = deepintentDpesSubmodule.getId(cookieConfig, undefined, DI_COOKIE_OBJECT); expect(diId).to.deep.equal(DI_COOKIE_OBJECT); }); it('provides the stored deepintentId if cookie is absent but present in local storage', () => { - getDataFromLocalStorageStub.withArgs(DI_COOKIE_NAME).returns(DI_COOKIE_STORED); - let idx = deepintentDpesSubmodule.getId(html5Config, undefined, DI_COOKIE_OBJECT); - expect(idx).to.deep.equal(DI_COOKIE_OBJECT); + let idx = deepintentDpesSubmodule.getId(html5Config, undefined, DI_UPDATED_STORAGE); + expect(idx).to.be.eq(DI_UPDATED_STORAGE); }); }); describe('Deepintent Dpes System : test "decode" method', () => { - it('Get the correct decoded value for dpes id', () => { - expect(deepintentDpesSubmodule.decode(DI_COOKIE_OBJECT, cookieConfig)).to.deep.equal({'deepintentId': {'id': '2cf40748c4f7f60d343336e08f80dc99'}}); + it('Get the correct decoded value for dpes id, if an object is set return object', () => { + expect(deepintentDpesSubmodule.decode(DI_COOKIE_OBJECT, cookieConfig)).to.deep.equal({'deepintentId': DI_COOKIE_OBJECT}); + }); + + it('Get the correct decoded value for dpes id, if a string is set return string', () => { + expect(deepintentDpesSubmodule.decode(DI_UPDATED_STORAGE, {})).to.deep.equal({'deepintentId': DI_UPDATED_STORAGE}); }); }); + + describe('Deepintent Dpes System : test "getValue" method in eids', () => { + it('Get the correct string value for dpes id, if an object is set in local storage', () => { + expect(deepintentDpesSubmodule.eids.deepintentId.getValue(DI_COOKIE_OBJECT)).to.be.equal(DI_UPDATED_STORAGE); + }); + + it('Get the correct string value for dpes id, if a string is set in local storage', () => { + expect(deepintentDpesSubmodule.eids.deepintentId.getValue(DI_UPDATED_STORAGE)).to.be.eq(DI_UPDATED_STORAGE); + }); + }); + describe('eid', () => { + before(() => { + attachIdSystem(deepintentDpesSubmodule); + }) + it('deepintentId', function() { + const userId = { + deepintentId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'deepintent.com', + uids: [{id: 'some-random-id-value', atype: 3}] + }); + }); + }) }); diff --git a/test/spec/modules/dexertoBidAdapter_spec.js b/test/spec/modules/dexertoBidAdapter_spec.js new file mode 100644 index 00000000000..419fcbc9dbe --- /dev/null +++ b/test/spec/modules/dexertoBidAdapter_spec.js @@ -0,0 +1,199 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/dexertoBidAdapter'; +import * as utils from '../../../src/utils.js'; + +describe('dexerto adapter', function () { + let request; + let bannerResponse, invalidResponse; + + beforeEach(function () { + request = [ + { + bidder: 'dexerto', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placement_id: 110003, + width: 300, + height: 250, + domain: '', + bid_floor: 0.5 + } + } + ]; + bannerResponse = { + 'body': { + 'id': 'a48b79e7-7104-4213-99f3-55f3234f2e54', + 'seatbid': [{ + 'bid': [{ + 'id': '3d7dd6dc-7665-4cdc-96a4-5ea192df32b8', + 'impid': '285b9c53b2c662', + 'price': 0.5, + 'adid': '3424', + 'adm': "
", + 'adomain': ['google.com'], + 'iurl': 'https://cdn.audiencelogy.com/1_3424_1.jpg', + 'cid': '410011', + 'crid': '410011', + 'w': 300, + 'h': 250, + 'cat': ['IAB1-15'] + }], + 'seat': 'audiencelogy', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_1256' + } + }; + invalidResponse = { + 'body': { + 'id': 'a48b79e7-7104-4213-99f3-55f3234f2e54', + 'seatbid': [{ + 'bid': [{ + 'id': '3d7dd6dc-7665-4cdc-96a4-5ea192df32b8', + 'impid': '285b9c53b2c662', + 'price': 0.5, + 'adid': '3424', + 'adm': 'invalid response', + 'adomain': ['google.com'], + 'iurl': 'https://cdn.audiencelogy.com/1_3424_1.jpg', + 'cid': '410011', + 'crid': '410011', + 'w': 300, + 'h': 250, + 'cat': ['IAB1-15'] + }], + 'seat': 'audiencelogy', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_1256' + } + }; + }); + + describe('validations', function () { + it('isBidValid : placement_id is passed', function () { + let bid = { + bidder: 'dexerto', + params: { + placement_id: 110003 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(true); + }); + it('isBidValid : placement_id is not passed', function () { + let bid = { + bidder: 'dexerto', + params: { + width: 300, + height: 250, + domain: '', + bid_floor: 0.5 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + }); + describe('Validate Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(request), + bidRequest = spec.buildRequests(request); + expect(request).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(request); + expect(_Request.url).to.equal('https://rtb.dexerto.media/hb/dexerto'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(request); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(request[0].bidId); + expect(data[0].placementId).to.equal(110003); + }); + it('Validate bid request : ad size', function () { + let _Request = spec.buildRequests(request); + let data = JSON.parse(_Request.data); + expect(data[0].imp[0].banner).to.be.a('object'); + expect(data[0].imp[0].banner.w).to.equal(300); + expect(data[0].imp[0].banner.h).to.equal(250); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(request); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(request, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate response ', function () { + it('Validate bid response : valid bid response', function () { + let bResponse = spec.interpretResponse(bannerResponse, request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(bannerResponse.body.seatbid[0].bid[0].impid); + expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('banner'); + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['google.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId); + }); + it('Invalid bid response check ', function () { + let bRequest = spec.buildRequests(request); + let response = spec.interpretResponse(invalidResponse, bRequest); + expect(response[0].ad).to.equal('invalid response'); + }); + }); + describe('GPP and coppa', function () { + it('Request params check with GPP Consent', function () { + let bidderReq = { gppConsent: { gppString: 'gpp-string-test', applicableSections: [5] } }; + let _Request = spec.buildRequests(request, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-string-test'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it('Request params check with GPP Consent read from ortb2', function () { + let bidderReq = { + ortb2: { + regs: { + gpp: 'gpp-test-string', + gpp_sid: [5] + } + } + }; + let _Request = spec.buildRequests(request, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-test-string'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it(' Bid request should have coppa flag if its true', () => { + let bidderReq = { ortb2: { regs: { coppa: 1 } } }; + let _Request = spec.buildRequests(request, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.coppa).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 39713c2b51a..c0d5e9d5a33 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -1,20 +1,22 @@ import {expect} from 'chai'; import parse from 'url-parse'; -import {buildAdpodVideoUrl, buildDfpVideoUrl, dep} from 'modules/dfpAdServerVideo.js'; +import {buildDfpVideoUrl, dep} from 'modules/dfpAdServerVideo.js'; import AD_UNIT from 'test/fixtures/video/adUnit.json'; import * as utils from 'src/utils.js'; import {deepClone} from 'src/utils.js'; import {config} from 'src/config.js'; import {targeting} from 'src/targeting.js'; import {auctionManager} from 'src/auctionManager.js'; -import {gdprDataHandler, uspDataHandler} from 'src/adapterManager.js'; -import * as adpod from 'modules/adpod.js'; -import {server} from 'test/mocks/xhr.js'; +import {gdprDataHandler} from 'src/adapterManager.js'; + import * as adServer from 'src/adserver.js'; import {hook} from '../../../src/hook.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {AuctionIndex} from '../../../src/auctionIndex.js'; +import { getVastXml } from '../../../modules/dfpAdServerVideo.js'; +import { server } from '../../mocks/xhr.js'; +import { generateUUID } from '../../../src/utils.js'; describe('The DFP video support module', function () { before(() => { @@ -272,7 +274,7 @@ describe('The DFP video support module', function () { video: { playbackmethod: [7, 1] }, - expected: undefined + expected: '2' } ], vpa: [ @@ -708,251 +710,164 @@ describe('The DFP video support module', function () { expect(customParams).to.have.property('hb_rand', 'random'); }); - describe('adpod unit tests', function () { - let amStub; - let amGetAdUnitsStub; - - before(function () { - let adUnits = [{ - code: 'adUnitCode-1', - mediaTypes: { - video: { - context: 'adpod', - playerSize: [640, 480], - adPodDurationSec: 60, - durationRangeSec: [15, 30], - requireExactDuration: true - } - }, - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 14542875, - } - } - ] - }]; - - amGetAdUnitsStub = sinon.stub(auctionManager, 'getAdUnits'); - amGetAdUnitsStub.returns(adUnits); - amStub = sinon.stub(auctionManager, 'getBidsReceived'); - }); - - beforeEach(function () { - config.setConfig({ - adpod: { - brandCategoryExclusion: true, - deferCaching: false - } + it('should return unmodified fetched gam vast wrapper if local cache is not used', (done) => { + const gamWrapper = ( + `` + + `` + + `` + + `prebid.org wrapper` + + `` + + `` + + `` + + `` + ); + server.respondWith(gamWrapper); + + getVastXml({}) + .then((finalGamWrapper) => { + expect(finalGamWrapper).to.deep.eql(gamWrapper); + done(); }); - }) - afterEach(function() { - config.resetConfig(); - }); + server.respond(); + }); - after(function () { - amGetAdUnitsStub.restore(); - amStub.restore(); - }); + it('should substitue vast ad tag uri in gam wrapper with blob content in data uri format', (done) => { + config.setConfig({cache: { useLocal: true }}); + const url = 'https://pubads.g.doubleclick.net/gampad/ads' + const blobContent = '` + + `` + + `` + + `prebid.org wrapper` + + `` + + `` + + `` + + `` + ); + + const dataUrl = `data://text/xml;base64,${btoa(blobContent)}`; + const expectedOutput = ( + `` + + `` + + `` + + `prebid.org wrapper` + + `` + + `` + + `` + + `` + ); + + server.respondWith(/^https:\/\/pubads.*/, gamWrapper); + server.respondWith(/^blob:http:*/, blobContent); + + getVastXml({url, adUnit: {}, bid: {}}, localMap) + .then((vastXml) => { + expect(vastXml).to.deep.eql(expectedOutput); + done(); + }) + .finally(config.resetConfig); - it('should return masterTag url', function() { - amStub.returns(getBidsReceived()); - let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); - uspDataHandlerStub.returns('1YYY'); - let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); - gdprDataHandlerStub.returns({ - gdprApplies: true, - consentString: 'consent', - addtlConsent: 'moreConsent' - }); - let url; - parse(buildAdpodVideoUrl({ - code: 'adUnitCode-1', - callback: handleResponse, - params: { - 'iu': 'my/adUnit', - 'description_url': 'someUrl.com', - } - })); + server.respond(); - function handleResponse(err, masterTag) { - if (err) { - return; - } - url = parse(masterTag); - - expect(url.protocol).to.equal('https:'); - expect(url.host).to.equal('securepubads.g.doubleclick.net'); - - const queryParams = utils.parseQS(url.query); - expect(queryParams).to.have.property('correlator'); - expect(queryParams).to.have.property('description_url', 'someUrl.com'); - expect(queryParams).to.have.property('env', 'vp'); - expect(queryParams).to.have.property('gdfp_req', '1'); - expect(queryParams).to.have.property('iu', 'my/adUnit'); - expect(queryParams).to.have.property('output', 'vast'); - expect(queryParams).to.have.property('sz', '640x480'); - expect(queryParams).to.have.property('unviewed_position_start', '1'); - expect(queryParams).to.have.property('url'); - expect(queryParams).to.have.property('cust_params'); - expect(queryParams).to.have.property('gdpr', '1'); - expect(queryParams).to.have.property('gdpr_consent', 'consent'); - expect(queryParams).to.have.property('addtl_consent', 'moreConsent'); - - const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); - expect(custParams).to.have.property('hb_cache_id', '123'); - expect(custParams).to.have.property('hb_pb_cat_dur', '15.00_395_15s,15.00_406_30s,10.00_395_15s'); - uspDataHandlerStub.restore(); - gdprDataHandlerStub.restore(); - } - }); + let timeout; - it('should return masterTag url with correct custom params when brandCategoryExclusion is false', function() { - config.setConfig({ - adpod: { - brandCategoryExclusion: false, - } - }); - function getBids() { - let bids = [ - createBid(10, 'adUnitCode-1', 15, '10.00_15s', '123', '395', '10.00'), - createBid(15, 'adUnitCode-1', 15, '15.00_15s', '123', '395', '15.00'), - createBid(25, 'adUnitCode-1', 30, '15.00_30s', '123', '406', '25.00'), - ]; - bids.forEach((bid) => { - delete bid.meta; - }); - return bids; + const waitForSecondRequest = () => { + if (server.requests.length >= 2) { + server.respond(); + clearTimeout(timeout); + } else { + timeout = setTimeout(waitForSecondRequest, 50); } - amStub.returns(getBids()); - let url; - parse(buildAdpodVideoUrl({ - code: 'adUnitCode-1', - callback: handleResponse, - params: { - 'iu': 'my/adUnit', - 'description_url': 'someUrl.com', - } - })); + }; - function handleResponse(err, masterTag) { - if (err) { - return; - } - url = parse(masterTag); - expect(url.protocol).to.equal('https:'); - expect(url.host).to.equal('securepubads.g.doubleclick.net'); - - const queryParams = utils.parseQS(url.query); - expect(queryParams).to.have.property('correlator'); - expect(queryParams).to.have.property('description_url', 'someUrl.com'); - expect(queryParams).to.have.property('env', 'vp'); - expect(queryParams).to.have.property('gdfp_req', '1'); - expect(queryParams).to.have.property('iu', 'my/adUnit'); - expect(queryParams).to.have.property('output', 'xml_vast3'); - expect(queryParams).to.have.property('sz', '640x480'); - expect(queryParams).to.have.property('unviewed_position_start', '1'); - expect(queryParams).to.have.property('url'); - expect(queryParams).to.have.property('cust_params'); - - const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); - expect(custParams).to.have.property('hb_cache_id', '123'); - expect(custParams).to.have.property('hb_pb_cat_dur', '10.00_15s,15.00_15s,15.00_30s'); - } - }); + waitForSecondRequest(); + }); - it('should handle error when cache fails', function() { - config.setConfig({ - adpod: { - brandCategoryExclusion: true, - deferCaching: true - } - }); - amStub.returns(getBidsReceived()); + it('should return unmodified gam vast wrapper if it doesn\'nt contain locally cached uuid', (done) => { + config.setConfig({cache: { useLocal: true }}); + const uuidNotPresentInCache = '4536229c-eddb-45b3-a919-89d889e925aa'; + const uuidPresentInCache = '64fcdc86-5325-4750-bc60-02f63b23175a'; + const bidCacheUrl = 'https://prebid-test-cache-server.org/cache?uuid=' + uuidNotPresentInCache; + const gamWrapper = ( + `` + + `` + + `` + + `prebid.org wrapper` + + `` + + `` + + `` + + `` + ); + const localCacheMap = new Map([[uuidPresentInCache, 'blob:http://localhost:9999/uri']]); + server.respondWith(gamWrapper); + + getVastXml({}, localCacheMap) + .then((finalGamWrapper) => { + expect(finalGamWrapper).to.deep.eql(gamWrapper); + done(); + }) + .finally(config.resetConfig) - parse(buildAdpodVideoUrl({ - code: 'adUnitCode-1', - callback: handleResponse, - params: { - 'iu': 'my/adUnit', - 'description_url': 'someUrl.com', - } - })); + server.respond(); + }); - server.requests[0].respond(503, { - 'Content-Type': 'plain/text', - }, 'The server could not save anything at the moment.'); + it('should return unmodified gam vast wrapper if it contains more than 1 saved uuids', (done) => { + config.setConfig({cache: { useLocal: true }}); + const uuid1 = '4536229c-eddb-45b3-a919-89d889e925aa'; + const uuid2 = '64fcdc86-5325-4750-bc60-02f63b23175a'; + const bidCacheUrl = `https://prebid-test-cache-server.org/cache?uuid=${uuid1}&uuid_alt=${uuid2}` + const gamWrapper = ( + `` + + `` + + `` + + `prebid.org wrapper` + + `` + + `` + + `` + + `` + ); + const localCacheMap = new Map([ + [uuid1, 'blob:http://localhost:9999/uri'], + [uuid2, 'blob:http://localhost:9999/uri'], + ]); + server.respondWith(gamWrapper); + + getVastXml({}, localCacheMap) + .then((finalGamWrapper) => { + expect(finalGamWrapper).to.deep.eql(gamWrapper); + done(); + }) + .finally(config.resetConfig) - function handleResponse(err, masterTag) { - expect(masterTag).to.be.null; - expect(err).to.be.an('error'); - } - }); - }) -}); + server.respond(); + }); -function getBidsReceived() { - return [ - createBid(10, 'adUnitCode-1', 15, '10.00_395_15s', '123', '395', '10.00'), - createBid(15, 'adUnitCode-1', 15, '15.00_395_15s', '123', '395', '15.00'), - createBid(25, 'adUnitCode-1', 30, '15.00_406_30s', '123', '406', '25.00'), - ] -} - -function createBid(cpm, adUnitCode, durationBucket, priceIndustryDuration, uuid, label, hbpb) { - return { - 'bidderCode': 'appnexus', - 'width': 640, - 'height': 360, - 'statusMessage': 'Bid available', - 'adId': '28f24ced14586c', - 'mediaType': 'video', - 'source': 'client', - 'requestId': '28f24ced14586c', - 'cpm': cpm, - 'creativeId': 97517771, - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 3600, - 'adUnitCode': adUnitCode, - 'video': { - 'context': 'adpod', - 'durationBucket': durationBucket - }, - 'appnexus': { - 'buyerMemberId': 9325 - }, - 'vastUrl': 'http://some-vast-url.com', - 'vastImpUrl': 'http://some-vast-imp-url.com', - 'auctionId': 'ec266b31-d652-49c5-8295-e83fafe5532b', - 'responseTimestamp': 1548442460888, - 'requestTimestamp': 1548442460827, - 'bidder': 'appnexus', - 'timeToRespond': 61, - 'pbLg': '5.00', - 'pbMg': '5.00', - 'pbHg': '5.00', - 'pbAg': '5.00', - 'pbDg': '5.00', - 'pbCg': '', - 'size': '640x360', - 'adserverTargeting': { - 'hb_bidder': 'appnexus', - 'hb_adid': '28f24ced14586c', - 'hb_pb': hbpb, - 'hb_size': '640x360', - 'hb_source': 'client', - 'hb_format': 'video', - 'hb_pb_cat_dur': priceIndustryDuration, - 'hb_cache_id': uuid - }, - 'customCacheKey': `${priceIndustryDuration}_${uuid}`, - 'meta': { - 'primaryCatId': 'iab-1', - 'adServerCatId': label - }, - 'videoCacheKey': '4cf395af-8fee-4960-af0e-88d44e399f14' - } -} + it('should return returned unmodified gam vast wrapper if exception has been thrown', (done) => { + config.setConfig({cache: { useLocal: true }}); + const gamWrapper = ( + `` + + `` + + `` + + `prebid.org wrapper` + + `` + + `` + + `` + + `` + ); + server.respondWith(gamWrapper); + getVastXml({}, null) // exception thrown when passing null as localCacheMap + .then((finalGamWrapper) => { + expect(finalGamWrapper).to.deep.eql(gamWrapper); + done(); + }) + .finally(config.resetConfig); + server.respond(); + }); +}); diff --git a/test/spec/modules/dfpAdpod_spec.js b/test/spec/modules/dfpAdpod_spec.js new file mode 100644 index 00000000000..33d724dac26 --- /dev/null +++ b/test/spec/modules/dfpAdpod_spec.js @@ -0,0 +1,257 @@ +import {auctionManager} from '../../../src/auctionManager.js'; +import {config} from '../../../src/config.js'; +import {gdprDataHandler, uspDataHandler} from '../../../src/consentHandler.js'; +import parse from 'url-parse'; +import {buildAdpodVideoUrl} from '../../../modules/dfpAdpod.js'; +import {expect} from 'chai/index.js'; +import * as utils from '../../../src/utils.js'; +import {server} from '../../mocks/xhr.js'; +import * as adpod from 'modules/adpod.js'; + +describe('dfpAdpod', function () { + let amStub; + let amGetAdUnitsStub; + + before(function () { + let adUnits = [{ + code: 'adUnitCode-1', + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 60, + durationRangeSec: [15, 30], + requireExactDuration: true + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 14542875, + } + } + ] + }]; + + amGetAdUnitsStub = sinon.stub(auctionManager, 'getAdUnits'); + amGetAdUnitsStub.returns(adUnits); + amStub = sinon.stub(auctionManager, 'getBidsReceived'); + }); + + beforeEach(function () { + config.setConfig({ + adpod: { + brandCategoryExclusion: true, + deferCaching: false + } + }); + }) + + afterEach(function() { + config.resetConfig(); + }); + + after(function () { + amGetAdUnitsStub.restore(); + amStub.restore(); + }); + + function getBidsReceived() { + return [ + createBid(10, 'adUnitCode-1', 15, '10.00_395_15s', '123', '395', '10.00'), + createBid(15, 'adUnitCode-1', 15, '15.00_395_15s', '123', '395', '15.00'), + createBid(25, 'adUnitCode-1', 30, '15.00_406_30s', '123', '406', '25.00'), + ] + } + + function createBid(cpm, adUnitCode, durationBucket, priceIndustryDuration, uuid, label, hbpb) { + return { + 'bidderCode': 'appnexus', + 'width': 640, + 'height': 360, + 'statusMessage': 'Bid available', + 'adId': '28f24ced14586c', + 'mediaType': 'video', + 'source': 'client', + 'requestId': '28f24ced14586c', + 'cpm': cpm, + 'creativeId': 97517771, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 3600, + 'adUnitCode': adUnitCode, + 'video': { + 'context': 'adpod', + 'durationBucket': durationBucket + }, + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'vastUrl': 'http://some-vast-url.com', + 'vastImpUrl': 'http://some-vast-imp-url.com', + 'auctionId': 'ec266b31-d652-49c5-8295-e83fafe5532b', + 'responseTimestamp': 1548442460888, + 'requestTimestamp': 1548442460827, + 'bidder': 'appnexus', + 'timeToRespond': 61, + 'pbLg': '5.00', + 'pbMg': '5.00', + 'pbHg': '5.00', + 'pbAg': '5.00', + 'pbDg': '5.00', + 'pbCg': '', + 'size': '640x360', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '28f24ced14586c', + 'hb_pb': hbpb, + 'hb_size': '640x360', + 'hb_source': 'client', + 'hb_format': 'video', + 'hb_pb_cat_dur': priceIndustryDuration, + 'hb_cache_id': uuid + }, + 'customCacheKey': `${priceIndustryDuration}_${uuid}`, + 'meta': { + 'primaryCatId': 'iab-1', + 'adServerCatId': label + }, + 'videoCacheKey': '4cf395af-8fee-4960-af0e-88d44e399f14' + } + } + + it('should return masterTag url', function() { + amStub.returns(getBidsReceived()); + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); + let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gdprDataHandlerStub.returns({ + gdprApplies: true, + consentString: 'consent', + addtlConsent: 'moreConsent' + }); + let url; + parse(buildAdpodVideoUrl({ + code: 'adUnitCode-1', + callback: handleResponse, + params: { + 'iu': 'my/adUnit', + 'description_url': 'someUrl.com', + } + })); + + function handleResponse(err, masterTag) { + if (err) { + return; + } + url = parse(masterTag); + + expect(url.protocol).to.equal('https:'); + expect(url.host).to.equal('securepubads.g.doubleclick.net'); + + const queryParams = utils.parseQS(url.query); + expect(queryParams).to.have.property('correlator'); + expect(queryParams).to.have.property('description_url', 'someUrl.com'); + expect(queryParams).to.have.property('env', 'vp'); + expect(queryParams).to.have.property('gdfp_req', '1'); + expect(queryParams).to.have.property('iu', 'my/adUnit'); + expect(queryParams).to.have.property('output', 'vast'); + expect(queryParams).to.have.property('sz', '640x480'); + expect(queryParams).to.have.property('unviewed_position_start', '1'); + expect(queryParams).to.have.property('url'); + expect(queryParams).to.have.property('cust_params'); + expect(queryParams).to.have.property('gdpr', '1'); + expect(queryParams).to.have.property('gdpr_consent', 'consent'); + expect(queryParams).to.have.property('addtl_consent', 'moreConsent'); + + const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); + expect(custParams).to.have.property('hb_cache_id', '123'); + expect(custParams).to.have.property('hb_pb_cat_dur', '15.00_395_15s,15.00_406_30s,10.00_395_15s'); + uspDataHandlerStub.restore(); + gdprDataHandlerStub.restore(); + } + }); + + it('should return masterTag url with correct custom params when brandCategoryExclusion is false', function() { + config.setConfig({ + adpod: { + brandCategoryExclusion: false, + } + }); + function getBids() { + let bids = [ + createBid(10, 'adUnitCode-1', 15, '10.00_15s', '123', '395', '10.00'), + createBid(15, 'adUnitCode-1', 15, '15.00_15s', '123', '395', '15.00'), + createBid(25, 'adUnitCode-1', 30, '15.00_30s', '123', '406', '25.00'), + ]; + bids.forEach((bid) => { + delete bid.meta; + }); + return bids; + } + amStub.returns(getBids()); + let url; + parse(buildAdpodVideoUrl({ + code: 'adUnitCode-1', + callback: handleResponse, + params: { + 'iu': 'my/adUnit', + 'description_url': 'someUrl.com', + } + })); + + function handleResponse(err, masterTag) { + if (err) { + return; + } + url = parse(masterTag); + expect(url.protocol).to.equal('https:'); + expect(url.host).to.equal('securepubads.g.doubleclick.net'); + + const queryParams = utils.parseQS(url.query); + expect(queryParams).to.have.property('correlator'); + expect(queryParams).to.have.property('description_url', 'someUrl.com'); + expect(queryParams).to.have.property('env', 'vp'); + expect(queryParams).to.have.property('gdfp_req', '1'); + expect(queryParams).to.have.property('iu', 'my/adUnit'); + expect(queryParams).to.have.property('output', 'xml_vast3'); + expect(queryParams).to.have.property('sz', '640x480'); + expect(queryParams).to.have.property('unviewed_position_start', '1'); + expect(queryParams).to.have.property('url'); + expect(queryParams).to.have.property('cust_params'); + + const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); + expect(custParams).to.have.property('hb_cache_id', '123'); + expect(custParams).to.have.property('hb_pb_cat_dur', '10.00_15s,15.00_15s,15.00_30s'); + } + }); + + it('should handle error when cache fails', function() { + config.setConfig({ + adpod: { + brandCategoryExclusion: true, + deferCaching: true + } + }); + amStub.returns(getBidsReceived()); + + parse(buildAdpodVideoUrl({ + code: 'adUnitCode-1', + callback: handleResponse, + params: { + 'iu': 'my/adUnit', + 'description_url': 'someUrl.com', + } + })); + + server.requests[0].respond(503, { + 'Content-Type': 'plain/text', + }, 'The server could not save anything at the moment.'); + + function handleResponse(err, masterTag) { + expect(masterTag).to.be.null; + expect(err).to.be.an('error'); + } + }); +}) diff --git a/test/spec/modules/dianomiBidAdapter_spec.js b/test/spec/modules/dianomiBidAdapter_spec.js index 0838762d750..ef9283d3dad 100644 --- a/test/spec/modules/dianomiBidAdapter_spec.js +++ b/test/spec/modules/dianomiBidAdapter_spec.js @@ -3,6 +3,8 @@ import { assert } from 'chai'; import { spec } from 'modules/dianomiBidAdapter.js'; import { config } from 'src/config.js'; import { createEidsArray } from 'modules/userId/eids.js'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; describe('Dianomi adapter', () => { let bids = []; @@ -250,32 +252,31 @@ describe('Dianomi adapter', () => { { bidId: 'bidId', params: { smartadId: 1234 }, - userIdAsEids: createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE', - }), + userIdAsEids: [ + { + source: 'adserver.org', + uids: [{ id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } }], + }, + { source: 'pubcid.org', uids: [{ id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 }] }, + ], }, ]; let request = JSON.parse( spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data ); - assert.deepEqual(request.user.ext.eids, [ - { - source: 'adserver.org', - uids: [{ id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } }], - }, - { source: 'pubcid.org', uids: [{ id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 }] }, - ]); + assert.deepEqual(request.user.ext.eids, validBidRequests[0].userIdAsEids); }); it('should send currency if defined', () => { - config.setConfig({ currency: { adServerCurrency: 'EUR' } }); + setCurrencyConfig({ adServerCurrency: 'EUR' }) let validBidRequests = [{ params: { smartadId: 1234 } }]; let refererInfo = { page: 'page' }; - let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo }).data); - - assert.deepEqual(request.cur, ['EUR']); + return addFPDToBidderRequest({ refererInfo }).then(res => { + let request = JSON.parse(spec.buildRequests(validBidRequests, res).data); + assert.deepEqual(request.cur, ['EUR']); + setCurrencyConfig({}); + }); }); it('should pass supply chain object', () => { @@ -397,12 +398,18 @@ describe('Dianomi adapter', () => { }); it('should request floor price in adserver currency', () => { - config.setConfig({ currency: { adServerCurrency: 'GBP' } }); - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'GBP'); + setCurrencyConfig({ adServerCurrency: 'GBP' }) + let validBidRequests = [getBidWithFloor()]; + let refererInfo = { page: 'page' }; + return addFPDToBidderRequest({ refererInfo }).then(res => { + let imp = JSON.parse( + spec.buildRequests(validBidRequests, res).data + ).imp[0]; + + assert.equal(imp.bidfloor, undefined); + assert.equal(imp.bidfloorcur, 'GBP'); + setCurrencyConfig({}); + }); }); it('should add correct floor values', () => { diff --git a/test/spec/modules/digitalMatterBidAdapter_spec.js b/test/spec/modules/digitalMatterBidAdapter_spec.js new file mode 100644 index 00000000000..87056588cd9 --- /dev/null +++ b/test/spec/modules/digitalMatterBidAdapter_spec.js @@ -0,0 +1,265 @@ +import {assert, expect} from 'chai'; +import {spec} from 'modules/digitalMatterBidAdapter'; +import {config} from '../../../src/config'; +import {deepClone} from '../../../src/utils'; + +const bid = { + 'adUnitCode': 'adUnitCode', + 'bidId': 'bidId', + 'bidder': 'digitalMatter', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'params': { + 'accountId': '1_demo_1', + 'siteId': '1-demo-1' + } +}; +const bidderRequest = { + ortb2: { + source: { + tid: 'tid-string' + }, + regs: { + ext: { + gdpr: 1 + } + }, + site: { + domain: 'publisher.domain.com', + publisher: { + domain: 'publisher.domain.com' + }, + page: 'https://publisher.domain.com/test.html' + }, + device: { + w: 100, + h: 100, + dnt: 0, + ua: navigator.userAgent, + language: 'en' + } + } +}; + +describe('Digital Matter BidAdapter', function () { + describe('isBidRequestValid', function () { + it('should return true when all required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when media type banner is missing', function () { + let invalidBid = deepClone(bid); + delete invalidBid.mediaTypes.banner; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); + it('should send request with correct structure', function () { + let request = spec.buildRequests([bid], bidderRequest); + + assert.equal(request.method, 'POST'); + assert.equal(request.url, 'https://adx.digitalmatter.services/openrtb2/auction'); + assert.equal(request.options, undefined); + assert.ok(request.data); + }); + + it('should have default request structure', function () { + let keys = 'tid,site,device,imp,test,ext'.split(','); + let request = JSON.parse(spec.buildRequests([bid], bidderRequest).data); + let data = Object.keys(request); + + assert.deepEqual(keys, data); + }); + + it('should send info about device', function () { + config.setConfig({ + device: {w: 1920, h: 1080} + }); + let request = JSON.parse(spec.buildRequests([bid], bidderRequest).data); + + assert.equal(request.device.ua, navigator.userAgent); + assert.equal(request.device.w, 100); + assert.equal(request.device.h, 100); + }); + + it('should send info about the site', function () { + let request = JSON.parse(spec.buildRequests([bid], bidderRequest).data); + + assert.deepEqual(request.site, { + domain: 'publisher.domain.com', + publisher: { + domain: 'publisher.domain.com' + }, + page: 'https://publisher.domain.com/test.html' + }); + }); + + it('should send currency if defined', function () { + config.setConfig({currency: {adServerCurrency: 'EUR'}}); + let request = JSON.parse(spec.buildRequests([bid], bidderRequest).data); + + assert.deepEqual(request.cur, [{adServerCurrency: 'EUR'}]); + }); + + it('should pass supply chain object', function () { + let validBidRequests = { + ...bid, + schain: { + validation: 'strict', + config: { + ver: '1.0' + } + } + }; + + let request = JSON.parse(spec.buildRequests([validBidRequests], bidderRequest).data); + assert.deepEqual(request.source.ext.schain, { + validation: 'strict', + config: { + ver: '1.0' + } + }); + }); + + it('should pass extended ids if exists', function () { + let validBidRequests = { + ...bid, + userIdAsEids: [ + { + source: 'adserver.org', + uids: [ + { + id: 'TTD_ID_FROM_USER_ID_MODULE', + atype: 1, + ext: { + rtiPartner: 'TDID' + } + } + ] + } + ] + }; + + let request = JSON.parse(spec.buildRequests([validBidRequests], bidderRequest).data); + + assert.deepEqual(request.user.ext.eids, validBidRequests.userIdAsEids); + }); + + it('should pass gdpr consent data if gdprApplies', function () { + let consentedBidderRequest = { + ...bidderRequest, + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString' + } + }; + + let request = JSON.parse(spec.buildRequests([bid], consentedBidderRequest).data); + assert.equal(request.user.ext.consent, consentedBidderRequest.gdprConsent.consentString); + assert.equal(request.regs.ext.gdpr, consentedBidderRequest.gdprConsent.gdprApplies); + assert.equal(typeof request.regs.ext.gdpr, 'number'); + }); + }); + + describe('interpretResponse', function () { + it('should return empty array if no body in response', function () { + assert.ok(spec.interpretResponse([])); + }); + + it('should return array with bids if response not empty', function () { + const firstResponse = { + id: 'id_1', + impid: 'impId_1', + bidid: 'bidId_1', + adunitcode: 'adUnitCode_1', + cpm: 0.10, + ad: '

ad', + adomain: [ + 'advertiser.org' + ], + width: 970, + height: 250, + creativeid: 'creativeId_1', + meta: { + advertiserDomains: [ + 'advertiser.org' + ] + } + }; + const secondResponse = { + 'id': 'id_2', + 'impid': 'impId_2', + 'bidid': 'bidId_2', + 'adunitcode': 'adUnitCode_2', + 'cpm': 0.11, + 'ad': '

ad', + 'adomain': [ + 'advertiser.org' + ], + 'width': 970, + 'height': 250, + 'creativeid': 'creativeId_2', + 'meta': { + 'advertiserDomains': [ + 'advertiser.org' + ] + } + }; + const currency = 'EUR'; + + const bids = spec.interpretResponse({ + body: { + id: 'randomId', + cur: currency, + bids: [ + firstResponse, + secondResponse + ] + } + }); + + assert.ok(bids); + assert.deepEqual(bids[0].requestId, firstResponse.bidid); + assert.deepEqual(bids[0].cpm, firstResponse.cpm); + assert.deepEqual(bids[0].creativeId, firstResponse.creativeid); + assert.deepEqual(bids[0].ttl, 300); + assert.deepEqual(bids[0].netRevenue, true); + assert.deepEqual(bids[0].currency, currency); + assert.deepEqual(bids[0].width, firstResponse.width); + assert.deepEqual(bids[0].height, firstResponse.height); + assert.deepEqual(bids[0].dealId, undefined); + assert.deepEqual(bids[0].meta.advertiserDomains, [ 'advertiser.org' ]); + + assert.deepEqual(bids[1].requestId, secondResponse.bidid); + assert.deepEqual(bids[1].cpm, secondResponse.cpm); + assert.deepEqual(bids[1].creativeId, secondResponse.creativeid); + assert.deepEqual(bids[1].ttl, 300); + assert.deepEqual(bids[1].netRevenue, true); + assert.deepEqual(bids[1].currency, currency); + assert.deepEqual(bids[1].width, secondResponse.width); + assert.deepEqual(bids[1].height, secondResponse.height); + assert.deepEqual(bids[1].dealId, undefined); + assert.deepEqual(bids[1].meta.advertiserDomains, [ 'advertiser.org' ]); + }); + }); + + describe('getUserSyncs', function () { + it('handle empty array (e.g. timeout)', function () { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index d148d5062a4..e65243fd15a 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -3,18 +3,36 @@ import { spec, getPmgUID, storage, - getPageTitle, - getPageDescription, - getPageKeywords, - getConnectionDownLink, THIRD_PARTY_COOKIE_ORIGIN, COOKIE_KEY_MGUID, - getCurrentTimeToUTCString, - buildUTMTagData + getCookieTimeToUTCString, + buildUTMTagData, } from 'modules/discoveryBidAdapter.js'; +import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink } from '../../../libraries/fpdUtils/pageInfo.js'; import * as utils from 'src/utils.js'; +import {getHLen} from '../../../libraries/navigatorData/navigatorData.js'; describe('discovery:BidAdapterTests', function () { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(storage, 'getCookie'); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(storage, 'getDataFromLocalStorage'); + sandbox.stub(utils, 'generateUUID').returns('new-uuid'); + sandbox.stub(utils, 'parseUrl').returns({ + search: { + utm_source: 'example.com' + } + }); + sandbox.stub(storage, 'cookiesAreEnabled'); + }) + + afterEach(() => { + sandbox.restore(); + }); + let bidRequestData = { bidderCode: 'discovery', auctionId: 'ff66e39e-4075-4d18-9854-56fde9b879ac', @@ -200,8 +218,8 @@ describe('discovery:BidAdapterTests', function () { }) ).to.equal(true); }); - it('discovery:validate_generated_params', function () { + storage.getCookie.withArgs('_ss_pp_utm').callsFake(() => '{"utm_source":"example.com","utm_medium":"123","utm_campaign":"456"}'); request = spec.buildRequests(bidRequestData.bids, bidRequestData); let req_data = JSON.parse(request.data); expect(req_data.imp).to.have.lengthOf(1); @@ -216,20 +234,6 @@ describe('discovery:BidAdapterTests', function () { describe('discovery: buildRequests', function() { describe('getPmgUID function', function() { - let sandbox; - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - sandbox.stub(storage, 'getCookie'); - sandbox.stub(storage, 'setCookie'); - sandbox.stub(utils, 'generateUUID').returns('new-uuid'); - sandbox.stub(storage, 'cookiesAreEnabled'); - }) - - afterEach(() => { - sandbox.restore(); - }); - it('should generate new UUID and set cookie if not exists', () => { storage.cookiesAreEnabled.callsFake(() => true); storage.getCookie.callsFake(() => null); @@ -252,26 +256,13 @@ describe('discovery:BidAdapterTests', function () { getPmgUID(); expect(storage.setCookie.calledOnce).to.be.false; }); + it('should return other ID from storage and cookie', () => { + spec.buildRequests(bidRequestData.bids, bidRequestData); + expect(storage.getCookie.called).to.be.true; + expect(storage.getDataFromLocalStorage.called).to.be.true; + }); }) describe('buildUTMTagData function', function() { - let sandbox; - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - sandbox.stub(storage, 'getCookie'); - sandbox.stub(storage, 'setCookie'); - sandbox.stub(utils, 'parseUrl').returns({ - search: { - utm_source: 'example.com' - } - }); - sandbox.stub(storage, 'cookiesAreEnabled'); - }) - - afterEach(() => { - sandbox.restore(); - }); - it('should set UTM cookie', () => { storage.cookiesAreEnabled.callsFake(() => true); storage.getCookie.callsFake(() => null); @@ -603,7 +594,7 @@ describe('discovery Bid Adapter Tests', function () { const response = event.data; if (!response.optout && response.mguid) { - storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCookieTimeToUTCString()); } } @@ -646,5 +637,28 @@ describe('discovery Bid Adapter Tests', function () { expect(storage.setCookie.notCalled).to.be.true; }); }); + describe('getHLen', () => { + it('should return the correct length of history when accessible', () => { + const mockWindow = { + top: { + history: { + length: 3 + } + } + }; + const result = getHLen(mockWindow); + expect(result).to.equal(3); + }); + + it('should return undefined when accessing win.top.history.length throws an error', () => { + const mockWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getHLen(mockWindow); + expect(result).be.undefined; + }); + }); }); }); diff --git a/test/spec/modules/djaxBidAdapter_spec.js b/test/spec/modules/djaxBidAdapter_spec.js new file mode 100644 index 00000000000..afa9a36eab7 --- /dev/null +++ b/test/spec/modules/djaxBidAdapter_spec.js @@ -0,0 +1,100 @@ +import { expect } from 'chai'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from '../../../modules/djaxBidAdapter.js'; + +const DOMAIN = 'https://revphpe.djaxbidder.com/header_bidding_vast/'; +const ENDPOINT = DOMAIN + 'www/admin/plugins/Prebid/getAd.php'; + +describe('Djax Adapter', function() { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('should exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValidForBanner', () => { + let bid = { + 'bidder': 'djax', + 'params': { + 'publisherId': 2 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + 'sizes': [[300, 250]], + 'bidId': '26e88c3c703e66', + 'bidderRequestId': '1a8ff729f6c1a3', + 'auctionId': 'cb65d954-ffe1-4f4a-8603-02b521c00333', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequestsForBanner', () => { + let bidRequests = [ + { + 'bidder': 'djax', + 'params': { + 'publisherId': 2 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + 'sizes': [[300, 250]], + 'bidId': '26e88c3c703e66', + 'bidderRequestId': '1a8ff729f6c1a3', + 'auctionId': 'cb65d954-ffe1-4f4a-8603-02b521c00333' + } + ]; + + it('sends bid request to ENDPOINT via POST', () => { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.contain(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + }); + + describe('interpretResponseForBanner', () => { + let bidRequests = [ + { + 'bidder': 'djax', + 'params': { + 'publisherId': 2 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + 'sizes': [[300, 250]], + 'bidId': '26e88c3c703e66', + 'bidderRequestId': '1a8ff729f6c1a3', + 'auctionId': 'cb65d954-ffe1-4f4a-8603-02b521c00333' + } + ]; + + it('handles nobid responses', () => { + var request = spec.buildRequests(bidRequests); + let response = ''; + let result = spec.interpretResponse(response, request[0]); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/docereeAdManagerBidAdapter_spec.js b/test/spec/modules/docereeAdManagerBidAdapter_spec.js index 26b054f4e29..6f7da056681 100644 --- a/test/spec/modules/docereeAdManagerBidAdapter_spec.js +++ b/test/spec/modules/docereeAdManagerBidAdapter_spec.js @@ -1,30 +1,31 @@ import { expect } from 'chai'; -import { spec } from '../../../modules/docereeAdManagerBidAdapter.js'; +import { spec, getPayload, getPageUrl } from '../../../modules/docereeAdManagerBidAdapter.js'; import { config } from '../../../src/config.js'; +import * as utils from '../../../src/utils.js'; describe('docereeadmanager', function () { config.setConfig({ docereeadmanager: { user: { data: { + userId: '', email: '', firstname: '', lastname: '', - mobile: '', specialization: '', - organization: '', hcpid: '', - dob: '', gender: '', city: '', state: '', - country: '', + zipcode: '', hashedhcpid: '', hashedemail: '', hashedmobile: '', - userid: '', - zipcode: '', - userconsent: '', + country: '', + organization: '', + dob: '', + platformUid: '', + mobile: '', }, }, }, @@ -34,6 +35,7 @@ describe('docereeadmanager', function () { bidder: 'docereeadmanager', params: { placementId: 'DOC-19-1', + publisherUrl: 'xxxxxx.com/xxxx', gdpr: '1', gdprconsent: 'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', @@ -122,4 +124,96 @@ describe('docereeadmanager', function () { .empty; }); }); + + describe('getPageUrl', function () { + it('should return an url string', function () { + const result = getPageUrl(); + expect(result).to.equal(utils.getWindowSelf().location.href); + }); + }); + + describe('payload', function () { + it('should return payload with the correct data', function () { + const data = { + userId: 'xxxxx', + email: 'xxxx@mail.com', + firstname: 'Xxxxx', + lastname: 'Xxxxxx', + specialization: 'Xxxxxxxxx', + hcpid: 'xxxxxxx', + gender: 'Xxxx', + city: 'Xxxxx', + state: 'Xxxxxx', + zipcode: 'XXXXXX', + hashedhcpid: 'xxxxxxx', + hashedemail: 'xxxxxxx', + hashedmobile: 'xxxxxxx', + country: 'Xxxxxx', + organization: 'Xxxxxx', + dob: 'xx-xx-xxxx', + platformUid: 'Xx.xxx.xxxxxx', + mobile: 'XXXXXXXXXX', + userconsent: 1 + } + bid = { ...bid, params: { ...bid.params, placementId: 'DOC-19-1' } } + const buildRequests = { + gdprConsent: { + consentString: 'COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprApplies: false + } + } + const payload = getPayload(bid, data, buildRequests); + + const payloadData = payload.data; + expect(payloadData).to.have.all.keys( + 'userid', + 'email', + 'firstname', + 'lastname', + 'specialization', + 'hcpid', + 'gender', + 'city', + 'state', + 'zipcode', + 'pb', + 'adunit', + 'requestId', + 'hashedhcpid', + 'hashedemail', + 'hashedmobile', + 'country', + 'organization', + 'dob', + 'upref', + 'mobile', + 'pageurl', + 'consent' + ); + expect(payloadData.userid).to.equal('Xx.xxx.xxxxxx'); + expect(payloadData.email).to.equal('xxxx@mail.com'); + expect(payloadData.firstname).to.equal('Xxxxx'); + expect(payloadData.lastname).to.equal('Xxxxxx'); + expect(payloadData.specialization).to.equal('Xxxxxxxxx'); + expect(payloadData.hcpid).to.equal('xxxxxxx'); + expect(payloadData.gender).to.equal('Xxxx'); + expect(payloadData.city).to.equal('Xxxxx'); + expect(payloadData.state).to.equal('Xxxxxx'); + expect(payloadData.zipcode).to.equal('XXXXXX'); + expect(payloadData.pb).to.equal(1); + expect(payloadData.upref).to.equal(1); + expect(payloadData.dob).to.equal('xx-xx-xxxx'); + expect(payloadData.organization).to.equal('Xxxxxx'); + expect(payloadData.country).to.equal('Xxxxxx'); + expect(payloadData.hashedmobile).to.equal('xxxxxxx'); + expect(payloadData.hashedemail).to.equal('xxxxxxx'); + expect(payloadData.hashedhcpid).to.equal('xxxxxxx'); + expect(payloadData.requestId).to.equal('testing'); + expect(payloadData.mobile).to.equal('XXXXXXXXXX'); + expect(payloadData.adunit).to.equal('DOC-19-1'); + expect(payloadData.pageurl).to.equal('http://localhost:9876/context.html'); + expect(payloadData.consent.gdprstr).to.equal('COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw'); + expect(payloadData.consent.gdpr).to.equal(0); + }) + }) }); diff --git a/test/spec/modules/dochaseBidAdapter_spec.js b/test/spec/modules/dochaseBidAdapter_spec.js new file mode 100644 index 00000000000..2026f3b5811 --- /dev/null +++ b/test/spec/modules/dochaseBidAdapter_spec.js @@ -0,0 +1,322 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/dochaseBidAdapter.js'; +import * as utils from '../../../src/utils.js'; + +describe('dochase adapter', function () { + let bannerRequest, nativeRequest; + let bannerResponse, nativeResponse, invalidBannerResponse, invalidNativeResponse; + + beforeEach(function () { + bannerRequest = [ + { + bidder: 'dochase', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placement_id: 5550, + bid_floor: 0.5 + } + } + ]; + nativeRequest = [ + { + bidder: 'dochase', + mediaTypes: { + native: { + title: { required: true, len: 100 }, + image: { required: true, sizes: [300, 250] }, + sponsored: { required: false }, + clickUrl: { required: true }, + desc: { required: true }, + icon: { required: false, sizes: [50, 50] }, + cta: { required: false } + } + }, + params: { + placement_id: 5551, + bid_floor: 1 + } + } + ]; + bannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': "", + 'adomain': ['google.com'], + 'iurl': 'https://cdn.dochase.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'dochase', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + nativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': '{\"native\":{\"ver\":\"1.1\",\"assets\": [{\"id\":1,\"required\":1,\"title\":{\"text\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":2,\"required\":1,\"img\":{\"url\":\"https://cdn.dochase.com/1_368852_0.png\",\"w\":500,\"h\":300,\"type\":\"3\"}},{\"id\":3,\"required\":0,\"data\":{\"value\":\"Diabetes In Control. A free weekly diabetes newsletter for Medical Professionals.\"}},{\"id\":4,\"required\":1,\"data\":{\"value\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":6,\"required\":1,\"data\":{\"value\":\"URL\"}}],\"link\":{\"url\":\"https://r.dochase.com/adx-rtb-d/servlet/WebF_AdManager.AdLinkManager?qs=H4sIAAAAAAAAAx2U2QHDIAxDVwKDr3F84P1HqNLPtMSRpSeG9RiPXH+5a474KzO/47YX7UoP50m61fLujlNjb76/8ZiblkimHq5nL/ZRedp3031x1tnk55LjSNN6h9/Zq+qmaLLuWTl74m1ZJKnb+m2OtQm/3L4sb933pM92qMOgjJ41MYmPXKnndRVKs+9bfSEumoZIFpTXuXbCP+WXuzl725E3O+9odi5OJrnBzhwjx9+UnFN3nTNt1/HY5aeljKtvZYpoJHNXr8BWa8ysKQY7ZmNA3DHK2qRwY7+zLu+xm9z5eheJ4Pv2usSptvO3p7JHrnXn0T5yVWdccp9Yz7hhoz2iu2zqsXsGFZ9hh14J6yU4TkJ0BgnOY8tY3tS+n2qsw7xZfKuanSNbAo+9nkJ83i20+FwhfbJeDVOllXsdxmDWauYcSRgS9+yG5qHwUDjAxxA0iZnOjlsnI+y09+ATeTEwbAVGgp0Qu/ceP0kjUvpu1Ty7O9MoegfrmLPxdjUh3mJL+XhARby+Ax8iBckf6BQdn9W+DMlvmlzYLuLlIy7YociFOIvXvEiYYCMboVk8BLHbnw3Zmr5us3xbjtXL67L96F15acJXkM5BOmTaUbBkYGdCI+Et8XmlpbuE3xVQwmxryc2y4wP3ByuuP8GogPZz8OpPaBv8diWWUTrC2nnLhdNUrJRTKc9FepDvwHTDwfbbMCTSb4LhUIFkyFrw/i7GtkPi6NCCai6N47TgNsTnzZWRoVtOSLq7FsLiF29y0Gj0GHVPVYG3QOPS7Swc3UuiFAQZJx3YvpHA2geUgVBASMEL4vcDi2Dw3NPtBSC4EQEvH/uMILu6WyUwraywTeVpoqoHTqOoD84FzReKoWemJy6jyuiBieGlQIe6wY2elTaMOwEUFF5NagzPj6nauc0+aXzQN3Q72hxFAgtfORK60RRAHYZLYymIzSJcXLgRFsqrb1UoD+5Atq7TWojaLTfOyUvH9EeJvZEOilQAXrf/ALoI8ZhABQAA\"},\"imptrackers\":[\"https://i.dochase.com/adx-rtb-d/servlet/WebF_AdManager.ImpCounter?price=${AUCTION_PRICE}&ids=5551,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=https%3A%2F%2Fqa-jboss.audiencelogy.com%2Ftn_native_prod.html\",\"https://i.dochase.com/adx-rtb-d/servlet/WebF_AdManager.ImpTracker?price=${AUCTION_PRICE}&ids=5551,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=\",\"https://rtb-east.dochase.com:9001/beacon?uid=44636f6605b06ec6d4389d6efb7e5054&cc=468132&fccap=5&nid=1\"]}}', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.dochase.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'dochase', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidBannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': 'invalid response', + 'adomain': ['google.com'], + 'iurl': 'https://cdn.dochase.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'dochase', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidNativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': 'invalid response', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.dochase.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'dochase', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + }); + + describe('validations', function () { + it('isBidValid : placement_id is passed', function () { + let bid = { + bidder: 'dochase', + params: { + placement_id: 5550 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(true); + }); + it('isBidValid : placement_id is not passed', function () { + let bid = { + bidder: 'dochase', + params: { + width: 300, + height: 250, + domain: '', + bid_floor: 0.5 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + }); + describe('Validate Banner Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(bannerRequest), + bidRequest = spec.buildRequests(bannerRequest); + expect(bannerRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(bannerRequest); + expect(_Request.url).to.equal('https://rtb.dochaseadx.com/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(bannerRequest[0].bidId); + expect(data[0].placementId).to.equal(5550); + }); + it('Validate bid request : ad size', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].imp[0].banner).to.be.a('object'); + expect(data[0].imp[0].banner.w).to.equal(300); + expect(data[0].imp[0].banner.h).to.equal(250); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(bannerRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate banner response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(bannerRequest); + let bResponse = spec.interpretResponse(bannerResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(bannerResponse.body.seatbid[0].bid[0].impid); + expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('banner'); + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['google.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId); + }); + it('Invalid bid response check ', function () { + let bRequest = spec.buildRequests(bannerRequest); + let response = spec.interpretResponse(invalidBannerResponse, bRequest); + expect(response[0].ad).to.equal('invalid response'); + }); + }); + describe('Validate Native Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(nativeRequest), + bidRequest = spec.buildRequests(nativeRequest); + expect(nativeRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(nativeRequest); + expect(_Request.url).to.equal('https://rtb.dochaseadx.com/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(nativeRequest[0].bidId); + expect(data[0].placementId).to.equal(5551); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(nativeRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate native response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(nativeRequest); + let bResponse = spec.interpretResponse(nativeResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(nativeResponse.body.seatbid[0].bid[0].impid); + // expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + // expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('native'); + expect(bResponse[0].native.clickUrl).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.impressionTrackers).to.be.an('array').with.length.above(0); + expect(bResponse[0].native.title).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.image.url).to.be.a('string').and.not.be.empty; + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['www.diabetesincontrol.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(nativeResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(nativeResponse.body.seatbid[0].bid[0].dealId); + }); + }); + describe('GPP and coppa', function () { + it('Request params check with GPP Consent', function () { + let bidderReq = { gppConsent: { gppString: 'gpp-string-test', applicableSections: [5] } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-string-test'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it('Request params check with GPP Consent read from ortb2', function () { + let bidderReq = { + ortb2: { + regs: { + gpp: 'gpp-test-string', + gpp_sid: [5] + } + } + }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-test-string'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it(' Bid request should have coppa flag if its true', () => { + let bidderReq = { ortb2: { regs: { coppa: 1 } } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.coppa).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/driftpixelBidAdapter_spec.js b/test/spec/modules/driftpixelBidAdapter_spec.js new file mode 100644 index 00000000000..057ca1c7e9f --- /dev/null +++ b/test/spec/modules/driftpixelBidAdapter_spec.js @@ -0,0 +1,447 @@ +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec} from 'modules/driftpixelBidAdapter.js'; +import {deepClone} from 'src/utils'; +import {getBidFloor} from '../../../libraries/xeUtils/bidderUtils.js'; + +const ENDPOINT = 'https://pbjs.driftpixel.live'; + +const defaultRequest = { + tmax: 0, + adUnitCode: 'test', + bidId: '1', + requestId: 'qwerty', + ortb2: { + source: { + tid: 'auctionId' + } + }, + ortb2Imp: { + ext: { + tid: 'tr1', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'driftpixel', + params: { + env: 'driftpixel', + pid: '40', + ext: {} + }, + bidRequestsCount: 1 +}; + +const defaultRequestVideo = deepClone(defaultRequest); +defaultRequestVideo.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } +}; + +const videoBidderRequest = { + bidderCode: 'driftpixel', + bids: [{mediaTypes: {video: {}}, bidId: 'qwerty'}] +}; + +const displayBidderRequest = { + bidderCode: 'driftpixel', + bids: [{bidId: 'qwerty'}] +}; + +describe('driftpixelBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required env param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.env; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required pid param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.pid; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); + + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT + '/bid'); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); + + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('tmax').and.to.equal(defaultRequest.tmax); + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprConsent').and.to.deep.equal({}); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + env: 'driftpixel', + pid: '40' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); + }); + + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); + }); + + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); + + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); + }); + + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }); + }); + + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); + + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); + + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); + + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); + }); + }); + + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; + + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); + + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['driftpixel'] + }, + ext: { + pixels: [ + ['iframe', 'surl1'], + ['image', 'surl2'], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: displayBidderRequest}); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({advertiserDomains: ['driftpixel']}); + }); + + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: displayBidderRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); + + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: videoBidderRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); + }); + + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); + }); + + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = {getFloor: 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when getFloor doesnt return an object', () => { + const bid = {getFloor: () => 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({floor: 'string', currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'EUR'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); + }); +}); diff --git a/test/spec/modules/dsaControl_spec.js b/test/spec/modules/dsaControl_spec.js index 45392d58c04..1744d1d5bab 100644 --- a/test/spec/modules/dsaControl_spec.js +++ b/test/spec/modules/dsaControl_spec.js @@ -108,6 +108,5 @@ describe('DSA transparency', () => { }) }) }) - it('should accept bids regardless of dsa when "required" any other value') }); }); diff --git a/test/spec/modules/dsp_genieeBidAdapter_spec.js b/test/spec/modules/dsp_genieeBidAdapter_spec.js index 94ec1011fbf..b708acffc0b 100644 --- a/test/spec/modules/dsp_genieeBidAdapter_spec.js +++ b/test/spec/modules/dsp_genieeBidAdapter_spec.js @@ -1,6 +1,8 @@ import { expect } from 'chai'; import { spec } from 'modules/dsp_genieeBidAdapter.js'; import { config } from 'src/config'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; describe('Geniee adapter tests', () => { const validBidderRequest = { @@ -38,7 +40,8 @@ describe('Geniee adapter tests', () => { ext: { test: 1 }, - id: 'bid-id' + id: 'bid-id', + secure: 1 } ], test: 1 @@ -73,13 +76,16 @@ describe('Geniee adapter tests', () => { config.resetConfig(); }); it('uncomfortable (currency)', () => { - config.setConfig({ currency: { adServerCurrency: 'TWD' } }); - const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); - expect(request).deep.equal({ - method: 'GET', - url: 'https://rt.gsspat.jp/prebid_uncomfortable', + setCurrencyConfig({ adServerCurrency: 'TWD' }); + return addFPDToBidderRequest(validBidderRequest).then(res => { + const request = spec.buildRequests(validBidderRequest.bids, res); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + setCurrencyConfig({}); + config.resetConfig(); }); - config.resetConfig(); }); }); describe('interpretResponse function test', () => { @@ -121,7 +127,9 @@ describe('Geniee adapter tests', () => { currency: 'JPY', mediaType: 'banner', meta: { - advertiserDomains: ['geniee.co.jp'] + advertiserDomains: ['geniee.co.jp'], + primaryCatId: 'IAB1', + secondaryCatIds: [] }, netRevenue: true, requestId: 'bid-id', diff --git a/test/spec/modules/dspxBidAdapter_spec.js b/test/spec/modules/dspxBidAdapter_spec.js index 841fc087613..ad7bc827837 100644 --- a/test/spec/modules/dspxBidAdapter_spec.js +++ b/test/spec/modules/dspxBidAdapter_spec.js @@ -1,7 +1,9 @@ import { expect } from 'chai'; +import { config } from 'src/config.js'; import { spec } from 'modules/dspxBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { deepClone } from '../../../src/utils'; +import {BANNER} from '../../../src/mediaTypes'; const ENDPOINT_URL = 'https://buyer.dspx.tv/request/'; const ENDPOINT_URL_DEV = 'https://dcbuyer.dspx.tv/request/'; @@ -33,12 +35,19 @@ describe('dspxAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { - 'someIncorrectParam': 0 - }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + const invalidBid = { + bidId: '30b31c1838de1e', + bidder: 'dspx', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + someIncorrectParam: 0 + } + } + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -77,7 +86,26 @@ describe('dspxAdapter', function () { 'sharedid': { 'id': '01EXPPGZ9C8NKG1MTXVHV98505', 'third': '01EXPPGZ9C8NKG1MTXVHV98505' - } + }, + 'pubProvidedId': [{ + 'source': 'puburl2.com', + 'uids': [{ + 'id': 'pubid2' + }, { + 'id': 'pubid2-123' + }] + }, { + 'source': 'puburl.com', + 'uids': [{ + 'id': 'pubid1', + 'atype': 1, + 'ext': { + 'stype': 'ppuid' + } + }] + }], + 'euid': {}, + 'tdid': 'tdid_ID', }, 'crumbs': { 'pubcid': 'e09ab6a3-ae74-4f01-b2e8-81b141d6dc61' @@ -228,12 +256,54 @@ describe('dspxAdapter', function () { } }; + // With ortb2 + var bidderRequestWithORTB = { + refererInfo: { + referer: 'some_referrer.net' + }, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {someData: 'value'}, + gdprApplies: true + }, + ortb2: { + source: {}, + site: { + domain: 'buyer', + publisher: { + domain: 'buyer' + }, + page: 'http://buyer/schain.php?ver=8.5.0-pre:latest-dev-build&pbjs_debug=true', + ref: 'http://buyer/pbjsv/', + content: { + id: 'contentID', + episode: 1, + title: 'contentTitle', + series: 'contentSeries', + season: 'contentSeason 3', + artist: 'contentArtist', + genre: 'rock', + isrc: 'contentIsrc', + url: 'https://content-url.com/', + context: 1, + keywords: 'kw1,kw2,keqword 3', + livestream: 0, + cat: [ + 'IAB1-1', + 'IAB1-2', + 'IAB2-10' + ] + } + }, + } + }; + var request1 = spec.buildRequests([bidRequests[0]], bidderRequest)[0]; it('sends bid request to our endpoint via GET', function () { expect(request1.method).to.equal('GET'); expect(request1.url).to.equal(ENDPOINT_URL); let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); - expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&did_netid=123&did_id5=ID5-ZHMOcvSShIBZiIth_yYh9odjNFxVEmMQ_i5TArPfWw!ID5*dtrjfV5mPLasyya5TW2IE9oVzQZwx7xRPGyAYS4hcWkAAOoxoFef4bIoREpQys8x&did_id5_linktype=2&did_uid2=456&did_sharedid=01EXPPGZ9C8NKG1MTXVHV98505&did_pubcid=e09ab6a3-ae74-4f01-b2e8-81b141d6dc61&did_cpubcid=e09ab6a3-ae74-4f01-b2e8-81b141d6dc61&schain%5Bver%5D=1.0&schain%5Bcomplete%5D=1&schain%5Bnodes%5D%5B0%5D%5Basi%5D=example.com&schain%5Bnodes%5D%5B0%5D%5Bsid%5D=0&schain%5Bnodes%5D%5B0%5D%5Bhp%5D=1&schain%5Bnodes%5D%5B0%5D%5Brid%5D=bidrequestid&schain%5Bnodes%5D%5B0%5D%5Bdomain%5D=example.com&auctionId=1d1a030790a475&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&auctionId=1d1a030790a475&pbcode=testDiv1&media_types%5Bbanner%5D=300x250&schain%5Bver%5D=1.0&schain%5Bcomplete%5D=1&schain%5Bnodes%5D%5B0%5D%5Basi%5D=example.com&schain%5Bnodes%5D%5B0%5D%5Bsid%5D=0&schain%5Bnodes%5D%5B0%5D%5Bhp%5D=1&schain%5Bnodes%5D%5B0%5D%5Brid%5D=bidrequestid&schain%5Bnodes%5D%5B0%5D%5Bdomain%5D=example.com&did_netid=123&did_id5=ID5-ZHMOcvSShIBZiIth_yYh9odjNFxVEmMQ_i5TArPfWw!ID5*dtrjfV5mPLasyya5TW2IE9oVzQZwx7xRPGyAYS4hcWkAAOoxoFef4bIoREpQys8x&did_id5_linktype=2&did_uid2=456&did_sharedid=01EXPPGZ9C8NKG1MTXVHV98505&did_pubcid=e09ab6a3-ae74-4f01-b2e8-81b141d6dc61&did_tdid=tdid_ID&did_ppuid=1%3Apuburl.com%3Apubid1&did_cpubcid=e09ab6a3-ae74-4f01-b2e8-81b141d6dc61'); }); var request2 = spec.buildRequests([bidRequests[1]], bidderRequest)[0]; @@ -281,6 +351,14 @@ describe('dspxAdapter', function () { expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=107&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&pbver=test&pfilter%5Btest%5D=1&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv3&media_types%5Bvideo%5D=640x480&media_types%5Bbanner%5D=300x250&vctx=instream&vpl%5Bmimes%5D%5B0%5D=video%2Fmp4&vpl%5Bprotocols%5D%5B0%5D=1&vpl%5Bprotocols%5D%5B1%5D=2&vpl%5Bplaybackmethod%5D%5B0%5D=2&vpl%5Bskip%5D=1'); }); + var request7 = spec.buildRequests([bidRequests[5]], bidderRequestWithORTB)[0]; + it('ortb2 iab_content test', function () { + expect(request7.method).to.equal('GET'); + expect(request7.url).to.equal('http://localhost'); + let data = request7.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=107&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&pbver=test&pfilter%5Btest%5D=1&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&pfilter%5Biab_content%5D=cat%3AIAB1-1%7CIAB1-2%7CIAB2-10%2Cepisode%3A1%2Ccontext%3A1%2Cid%3AcontentID%2Ctitle%3AcontentTitle%2Cseries%3AcontentSeries%2Cseason%3AcontentSeason%25203%2Cartist%3AcontentArtist%2Cgenre%3Arock%2Cisrc%3AcontentIsrc%2Curl%3Ahttps%253A%252F%252Fcontent-url.com%252F%2Ckeywords%3Akw1%252Ckw2%252Ckeqword%25203&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv3&media_types%5Bvideo%5D=640x480&media_types%5Bbanner%5D=300x250&vctx=instream&vpl%5Bmimes%5D%5B0%5D=video%2Fmp4&vpl%5Bprotocols%5D%5B0%5D=1&vpl%5Bprotocols%5D%5B1%5D=2&vpl%5Bplaybackmethod%5D%5B0%5D=2&vpl%5Bskip%5D=1'); + }); + // bidfloor tests const getFloorResponse = {currency: 'EUR', floor: 5}; let testBidRequest = deepClone(bidRequests[1]); @@ -321,6 +399,109 @@ describe('dspxAdapter', function () { }); }); + describe('google topics handling', () => { + afterEach(() => { + config.resetConfig(); + }); + + const REQPARAMS = { + refererInfo: { + referer: 'some_referrer.net' + }, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {someData: 'value'}, + gdprApplies: true + } + }; + + const defaultRequest = { + 'bidder': 'dspx', + 'params': { + 'placement': '6682', + 'pfilter': { + 'floorprice': 1000000, + 'private_auction': 0, + 'geo': { + 'country': 'DE' + } + }, + 'bcat': 'IAB2,IAB4', + 'dvt': 'desktop' + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e1', + 'bidderRequestId': '22edbae2733bf61', + 'auctionId': '1d1a030790a475', + 'adUnitCode': 'testDiv1', + }; + + it('does pass segtax, segclass, segments for google topics data', () => { + const GOOGLE_TOPICS_DATA = { + ortb2: { + user: { + data: [ + { + ext: { + segtax: 600, + segclass: 'v1', + }, + segment: [ + {id: '717'}, {id: '808'}, + ] + } + ] + }, + }, + } + config.setConfig(GOOGLE_TOPICS_DATA); + const request = spec.buildRequests([defaultRequest], { ...REQPARAMS, ...GOOGLE_TOPICS_DATA })[0]; + expect(request.data).to.contain('segtx=600&segcl=v1&segs=717%2C808'); + }); + + it('does not pass topics params for invalid topics data', () => { + const INVALID_TOPICS_DATA = { + ortb2: { + user: { + data: [ + { + segment: [] + }, + { + segment: [{id: ''}] + }, + { + segment: [{id: null}] + }, + { + segment: [{id: 'dummy'}, {id: '123'}] + }, + { + ext: { + segtax: 600, + segclass: 'v1', + }, + segment: [ + { + name: 'dummy' + } + ] + }, + ] + } + } + }; + + config.setConfig(INVALID_TOPICS_DATA); + let request = spec.buildRequests([defaultRequest], { ...REQPARAMS, ...INVALID_TOPICS_DATA })[0]; + expect(request.data).to.not.contain('segtax'); + expect(request.data).to.not.contain('segclass'); + expect(request.data).to.not.contain('segments'); + }); + }); + describe('interpretResponse', function () { let serverResponse = { 'body': { @@ -427,7 +608,7 @@ describe('dspxAdapter', function () { } }]; let result = spec.interpretResponse(serverResponse, bidRequest[0]); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(Object.keys(result[0])).to.include.members(Object.keys(expectedResponse[0])); expect(result[0].meta.advertiserDomains.length).to.equal(1); expect(result[0].meta.advertiserDomains[0]).to.equal(expectedResponse[0].meta.advertiserDomains[0]); }); @@ -447,7 +628,7 @@ describe('dspxAdapter', function () { } }]; let result = spec.interpretResponse(serverVideoResponse, bidRequest[0]); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[1])); + expect(Object.keys(result[0])).to.include.members(Object.keys(expectedResponse[1])); expect(result[0].meta.advertiserDomains.length).to.equal(0); }); @@ -466,7 +647,7 @@ describe('dspxAdapter', function () { } }]; let result = spec.interpretResponse(serverVideoResponseVastUrl, bidRequest[0]); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[2])); + expect(Object.keys(result[0])).to.include.members(Object.keys(expectedResponse[2])); expect(result[0].meta.advertiserDomains.length).to.equal(0); }); diff --git a/test/spec/modules/e_volutionBidAdapter_spec.js b/test/spec/modules/e_volutionBidAdapter_spec.js index d488048060a..4a97988b128 100644 --- a/test/spec/modules/e_volutionBidAdapter_spec.js +++ b/test/spec/modules/e_volutionBidAdapter_spec.js @@ -1,117 +1,306 @@ -import {expect} from 'chai'; -import {spec} from '../../../modules/e_volutionBidAdapter.js'; +import { expect } from 'chai'; +import { spec } from '../../../modules/e_volutionBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'e_volution'; describe('EvolutionTechBidAdapter', function () { - let bids = [{ - bidId: '23fhj33i987f', - bidder: 'e_volution', - params: { - placementId: 0 - }, - mediaTypes: { - banner: { - sizes: [[300, 250]], + 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 }, - userId: { - id5id: 'id5id' - } - }, { - bidId: '23fhj33i987f', - bidder: 'e_volution', - params: { - placementId: 0 + { + 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: { - video: { - playerSize: [300, 250] + [BANNER]: { + sizes: [[300, 250]] } }, - userId: { - id5id: 'id5id' - } - }, { - bidId: '23fhj33i987f', - bidder: 'e_volution', params: { - placementId: 0 - }, - mediaTypes: { - native: {} - }, - userId: { - id5id: 'id5id' + } - }]; + } const bidderRequest = { - uspConsent: 'uspConsent', - gdprConsent: 'gdprConsent' + 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 placementId parameters present', 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 () { - delete bids[0].params.placementId; - expect(spec.isBidRequestValid(bids[0])).to.be.false; + 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 valid URL', function () { expect(serverRequest.url).to.equal('https://service.e-volution.ai/?c=o&m=multi'); }); - it('Returns valid data if array of bids is valid', function () { + + it('Returns general data valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'ccpa', 'gdpr'); + 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.ccpa).to.be.equal('uspConsent'); - expect(data.gdpr).to.be.equal('gdprConsent'); + 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); + }); - let placement = data['placements'][0]; - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'bidfloor', 'eids'); - expect(placement.placementId).to.equal(0); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.traffic).to.equal('banner'); + 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); - placement = data['placements'][1]; - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'bidfloor', 'eids', 'wPlayer', 'hPlayer', - 'minduration', 'maxduration', 'mimes', 'protocols', 'startdelay', 'placement', 'skip', 'skipafter', 'minbitrate', - 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'); - expect(placement.placementId).to.equal(0); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.traffic).to.equal('video'); + 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; + } + } + }); - placement = data['placements'][2]; - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'bidfloor', 'eids', 'native'); - expect(placement.placementId).to.equal(0); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.traffic).to.equal('native'); + it('Returns valid endpoints', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + endpointId: 'testBanner', + }, + userIdAsEids + } + ]; + + let 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 empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let 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); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; + 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] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { @@ -128,7 +317,8 @@ describe('EvolutionTechBidAdapter', function () { currency: 'USD', dealId: '1', meta: { - adomain: [ 'example.com' ] + advertiserDomains: ['google.com'], + advertiserId: 1234 } }] }; @@ -137,15 +327,16 @@ describe('EvolutionTechBidAdapter', function () { let 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('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.width).to.equal(300); - expect(dataItem.height).to.equal(250); - expect(dataItem.ad).to.equal('Test'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); + 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('USD'); + 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 = { @@ -160,7 +351,8 @@ describe('EvolutionTechBidAdapter', function () { currency: 'USD', dealId: '1', meta: { - adomain: [ 'example.com' ] + advertiserDomains: ['google.com'], + advertiserId: 1234 } }] }; @@ -177,6 +369,7 @@ describe('EvolutionTechBidAdapter', function () { 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 = { @@ -195,7 +388,8 @@ describe('EvolutionTechBidAdapter', function () { netRevenue: true, currency: 'USD', meta: { - adomain: [ 'example.com' ] + advertiserDomains: ['google.com'], + advertiserId: 1234 } }] }; @@ -216,6 +410,7 @@ describe('EvolutionTechBidAdapter', function () { 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 = { @@ -282,18 +477,4 @@ describe('EvolutionTechBidAdapter', function () { expect(serverResponses).to.be.an('array').that.is.empty; }); }); - describe('getUserSyncs', function () { - let userSync = spec.getUserSyncs(); - it('Returns valid URL and type', function () { - if (spec.noSync) { - expect(userSync).to.be.equal(false); - } else { - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.exist; - expect(userSync[0].url).to.exist; - expect(userSync[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://service.e-volution.ai/?c=o&m=sync'); - } - }); - }); }); diff --git a/test/spec/modules/ebdrBidAdapter_spec.js b/test/spec/modules/ebdrBidAdapter_spec.js deleted file mode 100644 index 1c46381500f..00000000000 --- a/test/spec/modules/ebdrBidAdapter_spec.js +++ /dev/null @@ -1,245 +0,0 @@ -import { expect } from 'chai'; -import { spec } from 'modules/ebdrBidAdapter.js'; -import { VIDEO, BANNER } from 'src/mediaTypes.js'; -import * as utils from 'src/utils.js'; - -describe('ebdrBidAdapter', function () { - let bidRequests; - - beforeEach(function () { - bidRequests = [ - { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]], - } - }, - bidder: 'ebdr', - params: { - zoneid: '99999', - bidfloor: '1.00', - IDFA: 'xxx-xxx', - ADID: 'xxx-xxx', - latitude: '34.089811', - longitude: '-118.392805' - }, - bidId: '2c5e8a1a84522d', - bidderRequestId: '1d0c4017f02458', - auctionId: '9adc85ed-43ee-4a78-816b-52b7e578f314' - }, { - adUnitCode: 'div-gpt-ad-1460505748561-1', - mediaTypes: { - video: { - context: 'instream', - playerSize: [300, 250] - } - }, - bidder: 'ebdr', - params: { - zoneid: '99998', - bidfloor: '1.00', - IDFA: 'xxx-xxx', - ADID: 'xxx-xxx', - latitude: '34.089811', - longitude: '-118.392805' - }, - bidId: '23a01e95856577', - bidderRequestId: '1d0c4017f02458', - auctionId: '9adc85ed-43ee-4a78-816b-52b7e578f314' - } - ]; - }); - - describe('spec.isBidRequestValid', function () { - it('should return true when the required params are passed', function () { - const bidRequest = bidRequests[0]; - expect(spec.isBidRequestValid(bidRequest)).to.equal(true); - }); - - it('should return true when the only required param is missing', function () { - const bidRequest = bidRequests[0]; - bidRequest.params = { - zoneid: '99998', - bidfloor: '1.00', - }; - expect(spec.isBidRequestValid(bidRequest)).to.equal(true); - }); - - it('should return true when the "bidfloor" param is missing', function () { - const bidRequest = bidRequests[0]; - bidRequest.params = { - zoneid: '99998', - }; - expect(spec.isBidRequestValid(bidRequest)).to.equal(true); - }); - - it('should return false when no bid params are passed', function () { - const bidRequest = bidRequests[0]; - bidRequest.params = {}; - expect(spec.isBidRequestValid(bidRequest)).to.equal(false); - }); - - it('should return false when a bid request is not passed', function () { - expect(spec.isBidRequestValid()).to.equal(false); - expect(spec.isBidRequestValid({})).to.equal(false); - }); - }); - - describe('spec.buildRequests', function () { - describe('for banner bids', function () { - it('must handle an empty bid size', function () { - bidRequests[0].mediaTypes = { banner: {} }; - const requests = spec.buildRequests(bidRequests); - const bidRequest = {}; - bidRequest['2c5e8a1a84522d'] = { mediaTypes: BANNER, w: null, h: null }; - expect(requests.bids['2c5e8a1a84522d']).to.deep.equals(bidRequest['2c5e8a1a84522d']); - }); - it('should create a single GET', function () { - bidRequests[0].mediaTypes = { banner: {} }; - bidRequests[1].mediaTypes = { banner: {} }; - const requests = spec.buildRequests(bidRequests); - expect(requests.method).to.equal('GET'); - }); - it('must parse bid size from a nested array', function () { - const width = 640; - const height = 480; - const bidRequest = bidRequests[0]; - bidRequest.mediaTypes = { banner: {sizes: [[ width, height ]]} }; - const requests = spec.buildRequests([ bidRequest ]); - const data = {}; - data['2c5e8a1a84522d'] = { mediaTypes: BANNER, w: width, h: height }; - expect(requests.bids['2c5e8a1a84522d']).to.deep.equal(data['2c5e8a1a84522d']); - }); - }); - describe('for video bids', function () { - it('must handle an empty bid size', function () { - bidRequests[1].mediaTypes = { video: {} }; - const requests = spec.buildRequests(bidRequests); - const bidRequest = {}; - bidRequest['23a01e95856577'] = { mediaTypes: VIDEO, w: null, h: null }; - expect(requests.bids['23a01e95856577']).to.deep.equals(bidRequest['23a01e95856577']); - }); - - it('should create a GET request for each bid', function () { - const bidRequest = bidRequests[1]; - const requests = spec.buildRequests([ bidRequest ]); - expect(requests.method).to.equal('GET'); - }); - }); - }); - - describe('spec.interpretResponse', function () { - describe('for video bids', function () { - it('should return no bids if the response is not valid', function () { - const bidRequest = bidRequests[0]; - bidRequest.mediaTypes = { video: {} }; - const bidResponse = spec.interpretResponse({ body: null }, { bidRequest }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return a valid video bid response', function () { - const ebdrReq = {bids: {}}; - bidRequests.forEach(bid => { - let _mediaTypes = (bid.mediaTypes && bid.mediaTypes.video ? VIDEO : BANNER); - ebdrReq.bids[bid.bidId] = {mediaTypes: _mediaTypes, - w: _mediaTypes == BANNER ? bid.mediaTypes[_mediaTypes].sizes[0][0] : bid.mediaTypes[_mediaTypes].playerSize[0], - h: _mediaTypes == BANNER ? bid.mediaTypes[_mediaTypes].sizes[0][1] : bid.mediaTypes[_mediaTypes].playerSize[1] - }; - }); - const serverResponse = {id: '1d0c4017f02458', seatbid: [{bid: [{id: '23a01e95856577', impid: '23a01e95856577', price: 0.81, adid: 'abcde-12345', nurl: 'https://cdn0.bnmla.com/vtest.xml', adm: '\nStatic VASTStatic VAST Tag00:00:15https//www.engagebdr.com/c', adomain: ['advertiserdomain.com'], iurl: '', cid: 'campaign1', crid: 'abcde-12345', w: 300, h: 250}], seat: '19513bcfca8006'}], bidid: '19513bcfca8006', cur: 'USD'}; - const bidResponse = spec.interpretResponse({ body: serverResponse }, ebdrReq); - expect(bidResponse[0]).to.deep.equal({ - requestId: bidRequests[1].bidId, - vastXml: serverResponse.seatbid[0].bid[0].adm, - mediaType: 'video', - creativeId: serverResponse.seatbid[0].bid[0].crid, - cpm: serverResponse.seatbid[0].bid[0].price, - width: serverResponse.seatbid[0].bid[0].w, - height: serverResponse.seatbid[0].bid[0].h, - currency: 'USD', - netRevenue: true, - ttl: 3600, - vastUrl: serverResponse.seatbid[0].bid[0].nurl, - meta: { - advertiserDomains: [ - 'advertiserdomain.com' - ] - } - }); - }); - }); - - describe('for banner bids', function () { - it('should return no bids if the response is not valid', function () { - const bidRequest = bidRequests[0]; - bidRequest.mediaTypes = { banner: {} }; - const bidResponse = spec.interpretResponse({ body: null }, { bidRequest }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response is empty', function () { - const bidRequest = bidRequests[0]; - bidRequest.mediaTypes = { banner: {} }; - const bidResponse = spec.interpretResponse({ body: [] }, { bidRequest }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return valid banner bid responses', function () { - const ebdrReq = {bids: {}}; - bidRequests.forEach(bid => { - let _mediaTypes = (bid.mediaTypes && bid.mediaTypes.video ? VIDEO : BANNER); - ebdrReq.bids[bid.bidId] = {mediaTypes: _mediaTypes, - w: _mediaTypes == BANNER ? bid.mediaTypes[_mediaTypes].sizes[0][0] : bid.mediaTypes[_mediaTypes].playerSize[0], - h: _mediaTypes == BANNER ? bid.mediaTypes[_mediaTypes].sizes[0][1] : bid.mediaTypes[_mediaTypes].playerSize[1] - }; - }); - const serverResponse = {id: '1d0c4017f02458', seatbid: [{bid: [{id: '2c5e8a1a84522d', impid: '2c5e8a1a84522d', price: 0.81, adid: 'abcde-12345', nurl: '', adm: '

', adomain: ['advertiserdomain.com'], iurl: '', cid: 'campaign1', crid: 'abcde-12345', w: 300, h: 250}], seat: '19513bcfca8006'}], bidid: '19513bcfca8006', cur: 'USD', w: 300, h: 250}; - const bidResponse = spec.interpretResponse({ body: serverResponse }, ebdrReq); - expect(bidResponse[0]).to.deep.equal({ - requestId: bidRequests[ 0 ].bidId, - ad: serverResponse.seatbid[0].bid[0].adm, - mediaType: 'banner', - creativeId: serverResponse.seatbid[0].bid[0].crid, - cpm: serverResponse.seatbid[0].bid[0].price, - width: serverResponse.seatbid[0].bid[0].w, - height: serverResponse.seatbid[0].bid[0].h, - currency: 'USD', - netRevenue: true, - ttl: 3600, - meta: { - advertiserDomains: [ - 'advertiserdomain.com' - ] - }, - }); - }); - }); - }); - describe('spec.getUserSyncs', function () { - let syncOptions - beforeEach(function () { - syncOptions = { - enabledBidders: ['ebdr'], // only these bidders are allowed to sync - pixelEnabled: true - } - }); - it('sucess with usersync url', function () { - const serverResponse = {id: '1d0c4017f02458', seatbid: [{bid: [{id: '2c5e8a1a84522d', impid: '2c5e8a1a84522d', price: 0.81, adid: 'abcde-12345', nurl: '', adm: '
', adomain: ['advertiserdomain.com'], iurl: 'https://match.bnmla.com/usersync?sspid=59&redir=', cid: 'campaign1', crid: 'abcde-12345', w: 300, h: 250}], seat: '19513bcfca8006'}], bidid: '19513bcfca8006', cur: 'USD', w: 300, h: 250}; - const result = []; - result.push({type: 'image', url: 'https://match.bnmla.com/usersync?sspid=59&redir='}); - expect(spec.getUserSyncs(syncOptions, { body: serverResponse })).to.deep.equal(result); - }); - - it('sucess without usersync url', function () { - const serverResponse = {id: '1d0c4017f02458', seatbid: [{bid: [{id: '2c5e8a1a84522d', impid: '2c5e8a1a84522d', price: 0.81, adid: 'abcde-12345', nurl: '', adm: '
', adomain: ['advertiserdomain.com'], iurl: '', cid: 'campaign1', crid: 'abcde-12345', w: 300, h: 250}], seat: '19513bcfca8006'}], bidid: '19513bcfca8006', cur: 'USD', w: 300, h: 250}; - const result = []; - expect(spec.getUserSyncs(syncOptions, { body: serverResponse })).to.deep.equal(result); - }); - it('empty response', function () { - const serverResponse = {}; - const result = []; - expect(spec.getUserSyncs(syncOptions, { body: serverResponse })).to.deep.equal(result); - }); - }); -}); diff --git a/test/spec/modules/eclickadsBidAdapter_spec.js b/test/spec/modules/eclickadsBidAdapter_spec.js new file mode 100644 index 00000000000..aadb8f41a64 --- /dev/null +++ b/test/spec/modules/eclickadsBidAdapter_spec.js @@ -0,0 +1,214 @@ +import { expect } from 'chai'; +import { + spec, + ENDPOINT, + BIDDER_CODE, +} from '../../../modules/eclickadsBidAdapter.js'; +import { NATIVE, BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import { deepClone } from '../../../src/utils.js'; +import { config } from '../../../src/config.js'; + +describe('eclickadsBidAdapter', () => { + const bidItem = { + bidder: BIDDER_CODE, + params: { + zid: '7096', + }, + }; + const eclickadsBidderConfigData = { + orig_aid: 'xqf7zdmg7the65ac.1718271138.des', + fosp_aid: '1013000403', + fosp_uid: '7aab24a4663258a2c1d76a08b20f7e6e', + id: '84b2a41c4299bb9b8924423e', + myvne_id: '1013000403', + }; + const bidRequest = { + code: 'test-div', + size: [[320, 85]], + mediaTypes: { + [NATIVE]: { + title: { + required: true, + }, + body: { + required: true, + }, + image: { + required: true, + }, + sponsoredBy: { + required: true, + }, + icon: { + required: false, + }, + }, + }, + ortb2: { + device: { + 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', + }, + site: { + name: 'example', + domain: 'page.example.com', + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + ext: { + data: eclickadsBidderConfigData, + }, + }, + }, + }; + + describe('isBidRequestValid', () => { + it('should return false when atleast one of required params is missing', () => { + const bid = deepClone(bidItem); + delete bid.params.zid; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return true if there is correct required params and mediatype', () => { + bidItem.params.mediaTypes == NATIVE; + expect(spec.isBidRequestValid(bidItem)).to.be.true; + }); + it('should return true if there is no size', () => { + const bid = deepClone(bidItem); + delete bid.params.size; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); + + describe('buildRequests', () => { + const bidList = [bidItem]; + const request = config.runWithBidder(BIDDER_CODE, () => + spec.buildRequests(bidList, bidRequest) + ); + const _data = request.data; + + it('should be create a request to server with POST method, data, valid url', () => { + expect(request).to.be.exist; + expect(_data).to.be.exist; + expect(request.method).to.be.exist; + expect(request.method).equal('POST'); + expect(request.url).to.be.exist; + expect(request.url).equal(ENDPOINT + eclickadsBidderConfigData.fosp_uid); + }); + it('should return valid data format if bid array is valid', () => { + expect(_data).to.be.an('object'); + expect(_data).to.has.all.keys( + 'deviceWidth', + 'deviceHeight', + 'language', + 'host', + 'ua', + 'page', + 'imp', + 'device', + 'myvne_id', + 'orig_aid', + 'fosp_aid', + 'fosp_uid', + 'id' + ); + expect(_data.deviceWidth).to.be.an('number'); + expect(_data.deviceHeight).to.be.an('number'); + expect(_data.device).to.be.an('string'); + expect(_data.language).to.be.an('string'); + expect(_data.host).to.be.an('string').that.is.equal('page.example.com'); + expect(_data.page) + .to.be.an('string') + .that.is.equal('https://page.example.com/here.html'); + expect(_data.imp).to.be.an('array'); + expect(_data.myvne_id).to.be.an('string'); + expect(_data.orig_aid).to.be.an('string'); + expect(_data.fosp_aid).to.be.an('string'); + expect(_data.myvne_id).to.be.an('string'); + expect(_data.fosp_uid).to.be.an('string'); + expect(_data.id).to.be.an('string'); + }); + + it('should return empty array if there is no bidItem passed', () => { + const request = config.runWithBidder(BIDDER_CODE, () => + spec.buildRequests([], bidRequest) + ); + const _data = request.data; + expect(_data.imp).to.be.an('array').that.is.empty; + }); + + it('should return the number of imp equal to the number of bidItem', () => { + expect(_data.imp).to.have.lengthOf(bidList.length); + }); + + it('have to contain required params and correct format for sending to EClickAds', () => { + const item = _data.imp[0]; + expect(item.zid).to.be.an('string'); + }); + }); + + describe('interpretResponse', () => { + const expectedResponse = { + id: '84b2a41c4299bb9b8924423e', + seat: '35809', + seatbid: [ + { + id: 'DBCCDFD5-AACC-424E-8225-4160D35CBE5D', + impid: '35ea1073c745d6c', + adUnitCode: '8871826dc92e', + requestId: '1122839202z3v', + creativeId: '112233ss921v', + netRevenue: true, + currency: ['VND'], + cpm: 0.1844, + ad: 'eclickads_ad_p', + }, + ], + }; + + const response = spec.interpretResponse({ body: expectedResponse }); + + it('should return an array of offers', () => { + expect(response).to.be.an('array'); + }); + + it('should return empty array if there is no offer from server response', () => { + const emptyOfferResponse = deepClone(expectedResponse); + emptyOfferResponse.seatbid = []; + const response = spec.interpretResponse({ body: emptyOfferResponse }); + expect(response).to.be.an('array').that.is.empty; + }); + + it('should return empty array if seatbid from server response is null or missing', () => { + const nullOfferResponse = deepClone(expectedResponse); + nullOfferResponse.seatbid = null; + const response = spec.interpretResponse({ body: nullOfferResponse }); + expect(response).to.be.an('array').that.is.empty; + }); + + it('should return empty array if server response is get error - empty', () => { + const response = spec.interpretResponse({ body: undefined }); + expect(response).to.be.an('array').that.is.empty; + }); + + it('should return correct format, params for each of offers from server response', () => { + const offer = response[0]; + expect(offer.id).to.be.an('string').that.is.not.empty; + expect(offer.impid).to.be.an('string').that.is.not.empty; + expect(offer.requestId).to.be.an('string').that.is.not.empty; + expect(offer.creativeId).to.be.an('string').that.is.not.empty; + expect(offer.netRevenue).to.be.an('boolean'); + expect(offer.ttl).to.be.an('number'); + expect(offer.cpm).to.be.an('number').greaterThan(0); + expect(offer.adserverTargeting).to.be.an('object'); + expect(offer.adserverTargeting['hb_ad_eclickads']).to.be.an('string').that + .is.not.empty; + }); + }); +}); diff --git a/test/spec/modules/edge226BidAdapter_spec.js b/test/spec/modules/edge226BidAdapter_spec.js index 4819d8d4a4e..b1cf07d5bed 100644 --- a/test/spec/modules/edge226BidAdapter_spec.js +++ b/test/spec/modules/edge226BidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/edge226BidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'edge226' +const bidder = 'edge226'; describe('Edge226BidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('Edge226BidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('Edge226BidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('Edge226BidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -73,9 +86,20 @@ describe('Edge226BidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -110,7 +134,9 @@ describe('Edge226BidAdapter', function () { it('Returns general data valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', + expect(data).to.have.all.keys( + 'device', + 'deviceWidth', 'deviceHeight', 'language', 'secure', @@ -120,7 +146,11 @@ describe('Edge226BidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -129,7 +159,7 @@ describe('Edge226BidAdapter', function () { 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('string'); + 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); @@ -145,6 +175,56 @@ describe('Edge226BidAdapter', function () { 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 + } + ]; + + let 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'); @@ -170,8 +250,10 @@ describe('Edge226BidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -186,12 +268,38 @@ describe('Edge226BidAdapter', function () { expect(data.ccpa).to.equal(bidderRequest.uspConsent); expect(data.gdpr).to.not.exist; }); + }); - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { diff --git a/test/spec/modules/ehealthcaresolutionsBidAdapter_spec.js b/test/spec/modules/ehealthcaresolutionsBidAdapter_spec.js new file mode 100644 index 00000000000..c420c387598 --- /dev/null +++ b/test/spec/modules/ehealthcaresolutionsBidAdapter_spec.js @@ -0,0 +1,322 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/ehealthcaresolutionsBidAdapter.js'; +import * as utils from '../../../src/utils.js'; + +describe('ehealthcaresolutions adapter', function () { + let bannerRequest, nativeRequest; + let bannerResponse, nativeResponse, invalidBannerResponse, invalidNativeResponse; + + beforeEach(function () { + bannerRequest = [ + { + bidder: 'ehealthcaresolutions', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placement_id: 111520, + bid_floor: 0.5 + } + } + ]; + nativeRequest = [ + { + bidder: 'ehealthcaresolutions', + mediaTypes: { + native: { + title: { required: true, len: 100 }, + image: { required: true, sizes: [300, 250] }, + sponsored: { required: false }, + clickUrl: { required: true }, + desc: { required: true }, + icon: { required: false, sizes: [50, 50] }, + cta: { required: false } + } + }, + params: { + placement_id: 111519, + bid_floor: 1 + } + } + ]; + bannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': "", + 'adomain': ['google.com'], + 'iurl': 'https://cdn.ehealthcaresolutions.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'ehealthcaresolutions', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + nativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': '{\"native\":{\"ver\":\"1.1\",\"assets\": [{\"id\":1,\"required\":1,\"title\":{\"text\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":2,\"required\":1,\"img\":{\"url\":\"https://cdn.ehealthcaresolutions.com/1_368852_0.png\",\"w\":500,\"h\":300,\"type\":\"3\"}},{\"id\":3,\"required\":0,\"data\":{\"value\":\"Diabetes In Control. A free weekly diabetes newsletter for Medical Professionals.\"}},{\"id\":4,\"required\":1,\"data\":{\"value\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":6,\"required\":1,\"data\":{\"value\":\"URL\"}}],\"link\":{\"url\":\"https://r.ehealthcaresolutions.com/adx-rtb-d/servlet/WebF_AdManager.AdLinkManager?qs=H4sIAAAAAAAAAx2U2QHDIAxDVwKDr3F84P1HqNLPtMSRpSeG9RiPXH+5a474KzO/47YX7UoP50m61fLujlNjb76/8ZiblkimHq5nL/ZRedp3031x1tnk55LjSNN6h9/Zq+qmaLLuWTl74m1ZJKnb+m2OtQm/3L4sb933pM92qMOgjJ41MYmPXKnndRVKs+9bfSEumoZIFpTXuXbCP+WXuzl725E3O+9odi5OJrnBzhwjx9+UnFN3nTNt1/HY5aeljKtvZYpoJHNXr8BWa8ysKQY7ZmNA3DHK2qRwY7+zLu+xm9z5eheJ4Pv2usSptvO3p7JHrnXn0T5yVWdccp9Yz7hhoz2iu2zqsXsGFZ9hh14J6yU4TkJ0BgnOY8tY3tS+n2qsw7xZfKuanSNbAo+9nkJ83i20+FwhfbJeDVOllXsdxmDWauYcSRgS9+yG5qHwUDjAxxA0iZnOjlsnI+y09+ATeTEwbAVGgp0Qu/ceP0kjUvpu1Ty7O9MoegfrmLPxdjUh3mJL+XhARby+Ax8iBckf6BQdn9W+DMlvmlzYLuLlIy7YociFOIvXvEiYYCMboVk8BLHbnw3Zmr5us3xbjtXL67L96F15acJXkM5BOmTaUbBkYGdCI+Et8XmlpbuE3xVQwmxryc2y4wP3ByuuP8GogPZz8OpPaBv8diWWUTrC2nnLhdNUrJRTKc9FepDvwHTDwfbbMCTSb4LhUIFkyFrw/i7GtkPi6NCCai6N47TgNsTnzZWRoVtOSLq7FsLiF29y0Gj0GHVPVYG3QOPS7Swc3UuiFAQZJx3YvpHA2geUgVBASMEL4vcDi2Dw3NPtBSC4EQEvH/uMILu6WyUwraywTeVpoqoHTqOoD84FzReKoWemJy6jyuiBieGlQIe6wY2elTaMOwEUFF5NagzPj6nauc0+aXzQN3Q72hxFAgtfORK60RRAHYZLYymIzSJcXLgRFsqrb1UoD+5Atq7TWojaLTfOyUvH9EeJvZEOilQAXrf/ALoI8ZhABQAA\"},\"imptrackers\":[\"https://i.ehealthcaresolutions.com/adx-rtb-d/servlet/WebF_AdManager.ImpCounter?price=${AUCTION_PRICE}&ids=111519,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=https%3A%2F%2Fqa-jboss.audiencelogy.com%2Ftn_native_prod.html\",\"https://i.ehealthcaresolutions.com/adx-rtb-d/servlet/WebF_AdManager.ImpTracker?price=${AUCTION_PRICE}&ids=111519,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=\",\"https://rtb-east.ehealthcaresolutions.com:9001/beacon?uid=44636f6605b06ec6d4389d6efb7e5054&cc=468132&fccap=5&nid=1\"]}}', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.ehealthcaresolutions.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'ehealthcaresolutions', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidBannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': 'invalid response', + 'adomain': ['google.com'], + 'iurl': 'https://cdn.ehealthcaresolutions.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'ehealthcaresolutions', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidNativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': 'invalid response', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.ehealthcaresolutions.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'ehealthcaresolutions', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + }); + + describe('validations', function () { + it('isBidValid : placement_id is passed', function () { + let bid = { + bidder: 'ehealthcaresolutions', + params: { + placement_id: 111520 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(true); + }); + it('isBidValid : placement_id is not passed', function () { + let bid = { + bidder: 'ehealthcaresolutions', + params: { + width: 300, + height: 250, + domain: '', + bid_floor: 0.5 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + }); + describe('Validate Banner Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(bannerRequest), + bidRequest = spec.buildRequests(bannerRequest); + expect(bannerRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(bannerRequest); + expect(_Request.url).to.equal('https://rtb.ehealthcaresolutions.com/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(bannerRequest[0].bidId); + expect(data[0].placementId).to.equal(111520); + }); + it('Validate bid request : ad size', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].imp[0].banner).to.be.a('object'); + expect(data[0].imp[0].banner.w).to.equal(300); + expect(data[0].imp[0].banner.h).to.equal(250); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(bannerRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate banner response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(bannerRequest); + let bResponse = spec.interpretResponse(bannerResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(bannerResponse.body.seatbid[0].bid[0].impid); + expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('banner'); + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['google.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId); + }); + it('Invalid bid response check ', function () { + let bRequest = spec.buildRequests(bannerRequest); + let response = spec.interpretResponse(invalidBannerResponse, bRequest); + expect(response[0].ad).to.equal('invalid response'); + }); + }); + describe('Validate Native Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(nativeRequest), + bidRequest = spec.buildRequests(nativeRequest); + expect(nativeRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(nativeRequest); + expect(_Request.url).to.equal('https://rtb.ehealthcaresolutions.com/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(nativeRequest[0].bidId); + expect(data[0].placementId).to.equal(111519); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(nativeRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate native response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(nativeRequest); + let bResponse = spec.interpretResponse(nativeResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(nativeResponse.body.seatbid[0].bid[0].impid); + // expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + // expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('native'); + expect(bResponse[0].native.clickUrl).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.impressionTrackers).to.be.an('array').with.length.above(0); + expect(bResponse[0].native.title).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.image.url).to.be.a('string').and.not.be.empty; + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['www.diabetesincontrol.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(nativeResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(nativeResponse.body.seatbid[0].bid[0].dealId); + }); + }); + describe('GPP and coppa', function () { + it('Request params check with GPP Consent', function () { + let bidderReq = { gppConsent: { gppString: 'gpp-string-test', applicableSections: [5] } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-string-test'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it('Request params check with GPP Consent read from ortb2', function () { + let bidderReq = { + ortb2: { + regs: { + gpp: 'gpp-test-string', + gpp_sid: [5] + } + } + }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-test-string'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it(' Bid request should have coppa flag if its true', () => { + let bidderReq = { ortb2: { regs: { coppa: 1 } } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.coppa).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index e1f2394ab27..30bfabb6f50 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -1,630 +1,7 @@ import {createEidsArray} from 'modules/userId/eids.js'; -import {expect} from 'chai'; -// Note: In unit tets cases for bidders, call the createEidsArray function over userId object that is used for calling fetchBids -// this way the request will stay consistent and unit test cases will not need lots of changes. - -describe('eids array generation for known sub-modules', function() { - it('pubCommonId', function() { - const userId = { - pubcid: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'pubcid.org', - uids: [{id: 'some-random-id-value', atype: 1}] - }); - }); - - it('unifiedId: ext generation', function() { - const userId = { - tdid: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'adserver.org', - uids: [{id: 'some-random-id-value', atype: 1, ext: { rtiPartner: 'TDID' }}] - }); - }); - - it('unifiedId: ext generation with provider', function() { - const userId = { - tdid: {'id': 'some-sample_id', 'ext': {'provider': 'some.provider.com'}} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'adserver.org', - uids: [{id: 'some-sample_id', atype: 1, ext: { rtiPartner: 'TDID', provider: 'some.provider.com' }}] - }); - }); - - describe('id5Id', function() { - it('does not include an ext if not provided', function() { - const userId = { - id5id: { - uid: 'some-random-id-value' - } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'id5-sync.com', - uids: [{ id: 'some-random-id-value', atype: 1 }] - }); - }); - - it('includes ext if provided', function() { - const userId = { - id5id: { - uid: 'some-random-id-value', - ext: { - linkType: 0 - } - } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'id5-sync.com', - uids: [{ - id: 'some-random-id-value', - atype: 1, - ext: { - linkType: 0 - } - }] - }); - }); - }); - - it('parrableId', function() { - const userId = { - parrableId: { - eid: 'some-random-id-value' - } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'parrable.com', - uids: [{id: 'some-random-id-value', atype: 1}] - }); - }); - - it('merkleId (legacy) - supports single id', function() { - const userId = { - merkleId: { - id: 'some-random-id-value', keyID: 1 - } - }; - const newEids = createEidsArray(userId); - - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'merkleinc.com', - uids: [{ - id: 'some-random-id-value', - atype: 3, - ext: { keyID: 1 } - }] - }); - }); - - it('merkleId supports multiple source providers', function() { - const userId = { - merkleId: [{ - id: 'some-random-id-value', ext: { enc: 1, keyID: 16, idName: 'pamId', ssp: 'ssp1' } - }, { - id: 'another-random-id-value', - ext: { - enc: 1, - idName: 'pamId', - third: 4, - ssp: 'ssp2' - } - }] - } - - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(2); - expect(newEids[0]).to.deep.equal({ - source: 'ssp1.merkleinc.com', - uids: [{id: 'some-random-id-value', - atype: 3, - ext: { - enc: 1, - keyID: 16, - idName: 'pamId', - ssp: 'ssp1' - } - }] - }); - expect(newEids[1]).to.deep.equal({ - source: 'ssp2.merkleinc.com', - uids: [{id: 'another-random-id-value', - atype: 3, - ext: { - third: 4, - enc: 1, - idName: 'pamId', - ssp: 'ssp2' - } - }] - }); - }); - - it('identityLink', function() { - const userId = { - idl_env: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveramp.com', - uids: [{id: 'some-random-id-value', atype: 3}] - }); - }); - - it('liveIntentId; getValue call and ext', function() { - const userId = { - lipb: { - lipbid: 'some-random-id-value', - segments: ['s1', 's2'] - } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveintent.com', - uids: [{id: 'some-random-id-value', atype: 3}], - ext: {segments: ['s1', 's2']} - }); - }); - - it('bidswitch', function() { - const userId = { - bidswitch: {'id': 'sample_id'} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'bidswitch.net', - uids: [{ - id: 'sample_id', - atype: 3 - }] - }); - }); - - it('bidswitch with ext', function() { - const userId = { - bidswitch: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'bidswitch.net', - uids: [{ - id: 'sample_id', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - - it('medianet', function() { - const userId = { - medianet: {'id': 'sample_id'} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'media.net', - uids: [{ - id: 'sample_id', - atype: 3 - }] - }); - }); - - it('medianet with ext', function() { - const userId = { - medianet: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'media.net', - uids: [{ - id: 'sample_id', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - - it('sovrn', function() { - const userId = { - sovrn: {'id': 'sample_id'} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveintent.sovrn.com', - uids: [{ - id: 'sample_id', - atype: 3 - }] - }); - }); - - it('sovrn with ext', function() { - const userId = { - sovrn: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveintent.sovrn.com', - uids: [{ - id: 'sample_id', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - - it('magnite', function() { - const userId = { - magnite: {'id': 'sample_id'} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'rubiconproject.com', - uids: [{ - id: 'sample_id', - atype: 3 - }] - }); - }); - - it('magnite with ext', function() { - const userId = { - magnite: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'rubiconproject.com', - uids: [{ - id: 'sample_id', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - - it('index', function() { - const userId = { - index: {'id': 'sample_id'} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveintent.indexexchange.com', - uids: [{ - id: 'sample_id', - atype: 3 - }] - }); - }); - - it('index with ext', function() { - const userId = { - index: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveintent.indexexchange.com', - uids: [{ - id: 'sample_id', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - - it('openx', function () { - const userId = { - openx: { 'id': 'sample_id' } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'openx.net', - uids: [{ - id: 'sample_id', - atype: 3 - }] - }); - }); - - it('openx with ext', function () { - const userId = { - openx: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'openx.net', - uids: [{ - id: 'sample_id', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - - it('pubmatic', function() { - const userId = { - pubmatic: {'id': 'sample_id'} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'pubmatic.com', - uids: [{ - id: 'sample_id', - atype: 3 - }] - }); - }); - - it('pubmatic with ext', function() { - const userId = { - pubmatic: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'pubmatic.com', - uids: [{ - id: 'sample_id', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - - it('liveIntentId; getValue call and NO ext', function() { - const userId = { - lipb: { - lipbid: 'some-random-id-value' - } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveintent.com', - uids: [{id: 'some-random-id-value', atype: 3}] - }); - }); - - it('britepoolId', function() { - const userId = { - britepoolid: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'britepool.com', - uids: [{id: 'some-random-id-value', atype: 3}] - }); - }); - - it('lotamePanoramaId', function () { - const userId = { - lotamePanoramaId: 'some-random-id-value', - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'crwdcntrl.net', - uids: [{ id: 'some-random-id-value', atype: 1 }], - }); - }); - - it('criteo', function() { - const userId = { - criteoId: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'criteo.com', - uids: [{id: 'some-random-id-value', atype: 1}] - }); - }); - - it('tapadId', function() { - const userId = { - tapadId: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'tapad.com', - uids: [{id: 'some-random-id-value', atype: 1}] - }); - }); - - it('deepintentId', function() { - const userId = { - deepintentId: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'deepintent.com', - uids: [{id: 'some-random-id-value', atype: 3}] - }); - }); - - it('NetId', function() { - const userId = { - netId: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'netid.de', - uids: [{id: 'some-random-id-value', atype: 1}] - }); - }); - - it('zeotapIdPlus', function() { - const userId = { - IDP: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'zeotap.com', - uids: [{ - id: 'some-random-id-value', - atype: 1 - }] - }); - }); - - it('hadronId', function() { - const userId = { - hadronId: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'audigent.com', - uids: [{ - id: 'some-random-id-value', - atype: 1 - }] - }); - }); - - it('quantcastId', function() { - const userId = { - quantcastId: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'quantcast.com', - uids: [{ - id: 'some-random-id-value', - atype: 1 - }] - }); - }); - - it('uid2', function() { - const userId = { - uid2: {'id': 'Sample_AD_Token'} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'uidapi.com', - uids: [{ - id: 'Sample_AD_Token', - atype: 3 - }] - }); - }); - - it('uid2 with ext', function() { - const userId = { - uid2: {'id': 'Sample_AD_Token', 'ext': {'provider': 'some.provider.com'}} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'uidapi.com', - uids: [{ - id: 'Sample_AD_Token', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - - it('euid', function() { - const userId = { - euid: {'id': 'Sample_AD_Token'} - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'euid.eu', - uids: [{ - id: 'Sample_AD_Token', - atype: 3 - }] - }); - }); - - it('kpuid', function() { - const userId = { - kpuid: 'Sample_Token' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'kpuid.com', - uids: [{ - id: 'Sample_Token', - atype: 3 - }] - }); - }); - - it('tncid', function() { - const userId = { - tncid: 'TEST_TNCID' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'thenewco.it', - uids: [{ - id: 'TEST_TNCID', - atype: 3 - }] - }); - }); - - it('pubProvidedId', function() { +describe('eids array generation for known sub-modules', function () { + it('pubProvidedId', function () { const userId = { pubProvidedId: [{ source: 'example.com', @@ -659,148 +36,10 @@ describe('eids array generation for known sub-modules', function() { }] }); }); - - it('amxId', () => { - const id = 'c4bcadb0-124f-4468-a91a-d3d44cf311c5' - const userId = { - amxId: id - }; - - const [eid] = createEidsArray(userId); - expect(eid).to.deep.equal({ - source: 'amxdt.net', - uids: [{ - atype: 1, - id, - }] - }); - }); - - it('qid', function() { - const userId = { - qid: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'adquery.io', - uids: [{ - id: 'some-random-id-value', - atype: 1 - }] - }); - }); - - it('operaId', function() { - const userId = { - operaId: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 't.adx.opera.com', - uids: [{ - id: 'some-random-id-value', - atype: 1 - }] - }); - }); - - it('33acrossId', function() { - const userId = { - '33acrossId': { - envelope: 'some-random-id-value' - } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: '33across.com', - uids: [{ - id: 'some-random-id-value', - atype: 1 - }] - }); - }); - - it('czechAdId', () => { - const id = 'some-random-id-value' - const userId = { czechAdId: id }; - const [eid] = createEidsArray(userId); - expect(eid).to.deep.equal({ - source: 'czechadid.cz', - uids: [{ id: 'some-random-id-value', atype: 1 }] - }); - }); - - describe('ftrackId', () => { - it('should return the correct EID schema', () => { - // This is the schema returned from the ftrack decode() method - expect(createEidsArray({ - ftrackId: { - uid: 'test-device-id', - ext: { - DeviceID: 'test-device-id', - SingleDeviceID: 'test-single-device-id', - HHID: 'test-household-id' - } - }, - foo: { - bar: 'baz' - }, - lorem: { - ipsum: '' - } - })).to.deep.equal([{ - source: 'flashtalking.com', - uids: [{ - atype: 1, - id: 'test-device-id', - ext: { - DeviceID: 'test-device-id', - SingleDeviceID: 'test-single-device-id', - HHID: 'test-household-id' - } - }] - }]); - }); - }); - - describe('imuid', function() { - it('should return the correct EID schema with imuid', function() { - const userId = { - imuid: 'testimuid' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'intimatemerger.com', - uids: [{ - id: 'testimuid', - atype: 1 - }] - }); - }); - - it('should return the correct EID schema with imppid', function() { - const userId = { - imppid: 'imppid-value-imppid-value-imppid-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'ppid.intimatemerger.com', - uids: [{ - id: 'imppid-value-imppid-value-imppid-value', - atype: 1 - }] - }); - }); - }); }); describe('Negative case', function () { - it('eids array generation for UN-known sub-module', function() { + it('eids array generation for UN-known sub-module', function () { // UnknownCommonId const userId = { unknowncid: 'some-random-id-value' @@ -809,7 +48,7 @@ describe('Negative case', function () { expect(newEids.length).to.equal(0); }); - it('eids array generation for known sub-module with non-string value', function() { + it('eids array generation for known sub-module with non-string value', function () { // pubCommonId let userId = { pubcid: undefined diff --git a/test/spec/modules/eightPodAnalyticsAdapter_spec.js b/test/spec/modules/eightPodAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..55a09db03b4 --- /dev/null +++ b/test/spec/modules/eightPodAnalyticsAdapter_spec.js @@ -0,0 +1,187 @@ +import analyticsAdapter, { storage, queue, trackEvent } from 'modules/eightPodAnalyticsAdapter.js'; +import { expect } from 'chai'; +import adapterManager from 'src/adapterManager.js'; +import eightPodAnalytics from 'modules/eightPodAnalyticsAdapter.js'; +import { EVENTS } from '../../../src/constants.js'; + +const { + BID_WON +} = EVENTS; + +describe('eightPodAnalyticAdapter', function() { + let sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + adapterManager.enableAnalytics({ + provider: 'eightPod' + }); + }); + + afterEach(function() { + sandbox.restore(); + analyticsAdapter.disableAnalytics(); + }); + + describe('setup page', function() { + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let addEventListenerSpy; + + beforeEach(function() { + localStorageIsEnabledStub = sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + getDataFromLocalStorageStub = sandbox.stub( + storage, + 'getDataFromLocalStorage' + ); + addEventListenerSpy = sandbox.spy(window, 'addEventListener'); + }); + + afterEach(function() { + getDataFromLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); + addEventListenerSpy.restore(); + }); + + it('should subscribe on messageEvents', function() { + getDataFromLocalStorageStub.returns(JSON.stringify([])); + sandbox.spy(eightPodAnalytics, 'eventSubscribe'); + sandbox.spy(eightPodAnalytics, 'getEventFromLocalStorage'); + + analyticsAdapter.setupPage(); + + sandbox.assert.callCount(analyticsAdapter.eventSubscribe, 0); + sandbox.assert.callCount(analyticsAdapter.getEventFromLocalStorage, 1); + }); + + it('should receive saved events list', function() { + const eventList = [1, 2, 3]; + getDataFromLocalStorageStub.returns(JSON.stringify(eventList)); + sandbox.spy(eightPodAnalytics, 'eventSubscribe'); + + analyticsAdapter.setupPage(); + expect(queue).to.deep.equal(eventList) + }); + }); + + describe('track event', function() { + let setupPageStub; + + beforeEach(function() { + setupPageStub = sandbox.stub(eightPodAnalytics, 'setupPage'); + eightPodAnalytics.resetContext(); + }); + + afterEach(function() { + setupPageStub.restore(); + }); + + it('should NOT call setup page and get context', function() { + eightPodAnalytics.track({ + eventType: 'wrong_event_type', + }) + + sandbox.assert.callCount(setupPageStub, 0); + expect(analyticsAdapter.getContext()).to.deep.equal({}) + }); + + it('should call setup page and get context', function() { + eightPodAnalytics.track({ + eventType: BID_WON, + args: { + adUnitCode: 'adUnitCode', + bidder: 'eightPod', + creativeId: 'creativeId', + seatBidId: 'seatBidId', + cid: 'campaignId', + params: [ + { + publisherId: 'publisherId', + placementId: 'placementId', + } + ] + } + }) + + sandbox.assert.callCount(setupPageStub, 1); + expect(analyticsAdapter.getContext()).to.deep.equal({ + adUnitCode: { + bidId: 'seatBidId', + campaignId: 'campaignId', + placementId: 'placementId', + publisherId: 'publisherId', + variantId: 'creativeId' + } + }) + }); + }); + + describe('trackEvent', function() { + let getContextStub, getTimeStub; + const adUnitCode = 'adUnitCode'; + + beforeEach(function() { + getContextStub = sandbox.stub(eightPodAnalytics, 'getContext'); + getTimeStub = sandbox.stub(Date.prototype, 'getTime').returns(1234); + eightPodAnalytics.resetQueue(); + eightPodAnalytics.resetContext(); + }); + + afterEach(function() { + getContextStub.restore(); + getTimeStub.restore(); + }); + + it('should add event to the queue', function() { + getContextStub.returns({adUnitCode: {}}); + + const event1 = { + detail: { + type: 'Counter', + name: 'next_slide', + payload: { + from: '1.1', + to: '2.2', + value: 3 + } + } + } + const result1 = { + context: {}, + eventType: 'Counter', + eventClass: 'adunit', + timestamp: 1234, + eventName: 'next_slide', + payload: { + from: '1.1', + to: '2.2', + value: 3 + } + } + + const event2 = { + detail: { + type: 'Counter', + name: 'pod_impression', + payload: { + value: 2 + } + } + } + const result2 = { + context: {}, + eventType: 'Counter', + eventClass: 'adunit', + timestamp: 1234, + eventName: 'pod_impression', + payload: { + value: 2 + } + } + + trackEvent(event1, adUnitCode) + expect(queue).to.deep.equal([result1]); + trackEvent(event2, adUnitCode); + expect(queue).to.deep.equal([result1, result2]); + }); + }); +}); diff --git a/test/spec/modules/eightPodBidAdapter_spec.js b/test/spec/modules/eightPodBidAdapter_spec.js new file mode 100644 index 00000000000..7d55997d8cd --- /dev/null +++ b/test/spec/modules/eightPodBidAdapter_spec.js @@ -0,0 +1,203 @@ +import { expect } from 'chai' +import { spec, getPageKeywords, parseUserAgent } from 'modules/eightPodBidAdapter' +import 'modules/priceFloors.js' +import { config } from 'src/config.js' +import { newBidder } from 'src/adapters/bidderFactory' +import * as utils from '../../../src/utils'; +import sinon from 'sinon'; + +describe('eightPodBidAdapter', 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 validBid = { + bidder: 'eightPod', + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + params: { + placementId: 'placementId1', + }, + } + const invalidBid = { + bidder: 'eightPod', + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + } + + beforeEach(() => { + config.resetConfig() + }) + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(validBid)).to.equal(true) + }) + + it('should return false when required params found and invalid bid', function () { + expect(spec.isBidRequestValid(invalidBid)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + let bidRequests, bidderRequest + beforeEach(function () { + bidRequests = [ + { + bidder: 'eightPod', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + params: { + placementId: 'placementId1', + } + } + ] + bidderRequest = { + refererInfo: {}, + ortb2: { + device: { + ua: 'ua', + language: 'en', + dnt: 1, + js: 1, + } + } } + }) + + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], bidderRequest) + expect(bidRequest).to.be.an('array') + expect(bidRequest.length).to.equal(0) + }) + + it('should return a valid bid request object', function () { + const request = spec.buildRequests(bidRequests, bidderRequest) + + expect(request).to.be.an('array') + expect(request[0].data).to.be.an('object') + expect(request[0].method).to.equal('POST') + expect(request[0].url).to.not.equal('') + expect(request[0].url).to.not.equal(undefined) + expect(request[0].url).to.not.equal(null) + }) + }) + + 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.callCount).to.equal(0) + }) + + it('Should trigger pixel if bid nurl', function() { + spec.onBidWon({ + burl: 'https://example.com/some-tracker' + }); + expect(utils.triggerPixel.callCount).to.equal(1) + }) + }) + + describe('getPageKeywords function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document keywords if available', function() { + const keywordsContent = 'keyword1,keyword2,keyword3'; + const fakeTopDocument = { + querySelector: sandbox.stub() + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + + const result = getPageKeywords({ top: fakeTopWindow }); + expect(result).to.equal(keywordsContent); + }); + + it('should return the current document keywords if top document is not accessible', function() { + const keywordsContent = 'keyword1,keyword2,keyword3'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }); + + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + + const result = getPageKeywords(fakeWindow); + expect(result).to.equal(keywordsContent); + }); + + it('should return an empty string if no keywords meta tag is found', function() { + sandbox.stub(document, 'querySelector').withArgs('meta[name="keywords"]').returns(null); + + const result = getPageKeywords(); + expect(result).to.equal(''); + }); + }); + + describe('parseUserAgent function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the platform and version IOS', function() { + const uaStub = sandbox.stub(window.navigator, 'userAgent'); + uaStub.value('Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1'); + + const result = parseUserAgent(); + expect(result.platform).to.equal('ios'); + expect(result.version).to.equal('iphone'); + expect(result.device).to.equal('16.6'); + }); + + it('should return the platform and version android', function() { + const uaStub = sandbox.stub(window.navigator, 'userAgent'); + uaStub.value('Mozilla/5.0 (Linux; Android 5.0.1; SM-G920V Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.137 Mobile Safari/537.36'); + + const result = parseUserAgent(); + expect(result.platform).to.equal('android'); + expect(result.version).to.equal('5.0'); + expect(result.device).to.equal(''); + }) + }) +}) diff --git a/test/spec/modules/emtvBidAdapter_spec.js b/test/spec/modules/emtvBidAdapter_spec.js index 4f95a0cc094..e68a65a04d6 100644 --- a/test/spec/modules/emtvBidAdapter_spec.js +++ b/test/spec/modules/emtvBidAdapter_spec.js @@ -8,6 +8,16 @@ const adUrl = 'https://us-east-ep.engagemedia.tv/pbjs'; const syncUrl = 'https://cs.engagemedia.tv'; describe('EMTVBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -18,8 +28,9 @@ describe('EMTVBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -32,8 +43,9 @@ describe('EMTVBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -55,8 +67,9 @@ describe('EMTVBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -75,10 +88,22 @@ describe('EMTVBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' - } + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } + }, + timeout: 500 }; describe('isBidRequestValid', function () { @@ -113,6 +138,7 @@ describe('EMTVBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -121,7 +147,11 @@ describe('EMTVBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -130,7 +160,7 @@ describe('EMTVBidAdapter', function () { 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('string'); + 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); @@ -146,6 +176,56 @@ describe('EMTVBidAdapter', function () { 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 + } + ]; + + let 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'); @@ -171,8 +251,10 @@ describe('EMTVBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -187,12 +269,38 @@ describe('EMTVBidAdapter', function () { 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -396,5 +504,17 @@ describe('EMTVBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal(`${syncUrl}/image?pbjs=1&ccpa_consent=1---&coppa=0`) }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal(`${syncUrl}/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0`) + }); }); }); diff --git a/test/spec/modules/eplanningAnalyticsAdapter_spec.js b/test/spec/modules/eplanningAnalyticsAdapter_spec.js deleted file mode 100644 index dddc248b409..00000000000 --- a/test/spec/modules/eplanningAnalyticsAdapter_spec.js +++ /dev/null @@ -1,164 +0,0 @@ -import eplAnalyticsAdapter from 'modules/eplanningAnalyticsAdapter.js'; -import {includes} from 'src/polyfill.js'; -import { expect } from 'chai'; -import { parseUrl } from 'src/utils.js'; -import { server } from 'test/mocks/xhr.js'; -import { EVENTS } from 'src/constants.js'; - -let adapterManager = require('src/adapterManager').default; -let events = require('src/events'); - -describe('eplanning analytics adapter', function () { - beforeEach(function () { - sinon.stub(events, 'getEvents').returns([]); - }); - - afterEach(function () { - events.getEvents.restore(); - eplAnalyticsAdapter.disableAnalytics(); - }); - - describe('track', function () { - it('builds and sends auction data', function () { - sinon.spy(eplAnalyticsAdapter, 'track'); - - let auctionTimestamp = 1496510254313; - let pauctionId = '5018eb39-f900-4370-b71e-3bb5b48d324f'; - let initOptions = { - host: 'https://ads.ar.e-planning.net/hba/1/', - ci: '12345' - }; - let pbidderCode = 'adapter'; - - const bidRequest = { - bidderCode: pbidderCode, - auctionId: pauctionId, - bidderRequestId: '1a6fc81528d0f6', - bids: [{ - bidder: pbidderCode, - placementCode: 'container-1', - bidId: '208750227436c1', - bidderRequestId: '1a6fc81528d0f6', - auctionId: pauctionId, - startTime: 1509369418389, - sizes: [[300, 250]], - }], - auctionStart: 1509369418387, - timeout: 3000, - start: 1509369418389 - }; - - const bidResponse = { - bidderCode: pbidderCode, - adId: '208750227436c1', - cpm: 0.015, - auctionId: pauctionId, - responseTimestamp: 1509369418832, - requestTimestamp: 1509369418389, - bidder: pbidderCode, - timeToRespond: 443, - size: '300x250', - width: 300, - height: 250, - }; - - let bidTimeout = [ - { - bidId: '208750227436c1', - bidder: pbidderCode, - auctionId: pauctionId - } - ]; - - adapterManager.registerAnalyticsAdapter({ - code: 'eplanning', - adapter: eplAnalyticsAdapter - }); - - adapterManager.enableAnalytics({ - provider: 'eplanning', - options: initOptions - }); - - // Emit the events with the "real" arguments - - // Step 1: Send auction init event - events.emit(EVENTS.AUCTION_INIT, { - auctionId: pauctionId, - timestamp: auctionTimestamp - }); - - // Step 2: Send bid requested event - events.emit(EVENTS.BID_REQUESTED, bidRequest); - - // Step 3: Send bid response event - events.emit(EVENTS.BID_RESPONSE, bidResponse); - - // Step 4: Send bid time out event - events.emit(EVENTS.BID_TIMEOUT, bidTimeout); - - // Step 5: Send auction bid won event - events.emit(EVENTS.BID_WON, { - adId: 'adIdData', - ad: 'adContent', - auctionId: pauctionId, - width: 300, - height: 250 - }); - - // Step 6: Send auction end event - events.emit(EVENTS.AUCTION_END, { auctionId: pauctionId }); - - // Step 7: Find the request data sent (filtering other hosts) - let requests = server.requests.filter(req => { - return req.url.indexOf(initOptions.host) > -1; - }); - expect(requests.length).to.equal(1); - - expect(includes([initOptions.host + initOptions.ci], requests[0].url)); - expect(includes(['https://ads.ar.e-planning.net/hba/1/12345?d='], requests[0].url)); - - let info = requests[0].url; - let purl = parseUrl(info); - let eplData = JSON.parse(decodeURIComponent(purl.search.d)); - - // Step 8 check that 6 events were sent - expect(eplData.length).to.equal(6); - - // Step 9 verify that we only receive the parameters we need - let expectedEventValues = [ - // AUCTION INIT - { - ec: EVENTS.AUCTION_INIT, - p: {auctionId: pauctionId, time: auctionTimestamp}}, - // BID REQ - { - ec: EVENTS.BID_REQUESTED, - p: {auctionId: pauctionId, time: 1509369418389, bidder: pbidderCode, bids: [{time: 1509369418389, sizes: [[300, 250]], bidder: pbidderCode, placementCode: 'container-1', auctionId: pauctionId}]}}, - // BID RESP - { - ec: EVENTS.BID_RESPONSE, - p: {auctionId: pauctionId, bidder: pbidderCode, cpm: 0.015, size: '300x250', time: 1509369418832}}, - // BID T.O. - { - ec: EVENTS.BID_TIMEOUT, - p: [{auctionId: pauctionId, bidder: pbidderCode}]}, - // BID WON - { - ec: EVENTS.BID_WON, - p: {auctionId: pauctionId, size: '300x250'}}, - // AUCTION END - { - ec: EVENTS.AUCTION_END, - p: {auctionId: pauctionId}} - ]; - - for (let evid = 0; evid < eplData.length; evid++) { - expect(eplData[evid]).to.deep.equal(expectedEventValues[evid]); - } - - // Step 10 check that the host to send the ajax request is configurable via options - expect(eplAnalyticsAdapter.context.host).to.equal(initOptions.host); - }); - }); -}); diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index a381d7644a1..efb2f4a4cbb 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -8,6 +8,7 @@ import {hook} from '../../../src/hook.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; import { makeSlot } from '../integration/faker/googletag.js'; import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; +import { internal, resetWinDimensions } from '../../../src/utils.js'; describe('E-Planning Adapter', function () { const adapter = newBidder('spec'); @@ -53,6 +54,68 @@ describe('E-Planning Adapter', function () { 'adUnitCode': ADUNIT_CODE2, 'sizes': [[300, 250], [300, 600]], }; + const validBidWithSchain = { + 'bidder': 'eplanning', + 'bidId': BID_ID2, + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE2, + 'sizes': [[300, 250], [300, 600]], + 'schain': { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'directseller.com', + sid: '00001', + rid: 'BidRequest1', + hp: 1, + name: 'publisher', + domain: 'publisher.com' + } + ] + } + }; + const validBidWithSchainNodes = { + 'bidder': 'eplanning', + 'bidId': BID_ID2, + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE2, + 'sizes': [[300, 250], [300, 600]], + 'schain': { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'directseller.com', + sid: '00001', + rid: 'BidRequest1', + hp: 1, + name: 'publisher', + domain: 'publisher.com' + }, + { + asi: 'reseller.com', + sid: 'aaaaa', + rid: 'BidRequest2', + hp: 1, + name: 'publisher2', + domain: 'publisher2.com' + }, + { + asi: 'reseller3.com', + sid: 'aaaaab', + rid: 'BidRequest3', + hp: 1, + name: 'publisher3', + domain: 'publisher3.com' + } + ] + } + }; const ML = '1'; const validBidMappingLinear = { 'bidder': 'eplanning', @@ -563,7 +626,7 @@ describe('E-Planning Adapter', function () { describe('buildRequests', function () { let bidRequests = [validBid]; let sandbox; - let getWindowSelfStub; + let getWindowTopStub; let innerWidth; beforeEach(() => { $$PREBID_GLOBAL$$.bidderSettings = { @@ -572,8 +635,9 @@ describe('E-Planning Adapter', function () { } }; sandbox = sinon.sandbox.create(); - getWindowSelfStub = sandbox.stub(utils, 'getWindowSelf'); - getWindowSelfStub.returns(createWindow(800)); + getWindowTopStub = sandbox.stub(internal, 'getWindowTop'); + getWindowTopStub.returns(createWindow(800)); + resetWinDimensions(); }); afterEach(() => { @@ -585,6 +649,9 @@ describe('E-Planning Adapter', function () { const win = {}; win.self = win; win.innerWidth = innerWidth; + win.location = { + href: 'location' + }; return win; }; @@ -727,7 +794,18 @@ describe('E-Planning Adapter', function () { expect(data.vctx).to.equal(2); expect(data.vv).to.equal(3); }); - + it('should return sch parameter', function () { + let bidRequests = [validBidWithSchain], schainExpected, schain; + schain = validBidWithSchain.schain; + schainExpected = schain.ver + ',' + schain.complete + '!' + schain.nodes.map(node => node.asi + ',' + node.sid + ',' + node.hp + ',' + node.rid + ',' + node.name + ',' + node.domain).join('!'); + const data = spec.buildRequests(bidRequests, bidderRequest).data; + expect(data.sch).to.deep.equal(schainExpected); + }); + it('should not return sch parameter', function () { + let bidRequests = [validBidWithSchainNodes]; + const data = spec.buildRequests(bidRequests, bidderRequest).data; + expect(data.sch).to.equal(undefined); + }); it('should return correct e parameter with linear mapping attribute with more than one adunit', function () { let bidRequestsML = [validBidMappingLinear]; const NEW_CODE = ADUNIT_CODE + '2'; @@ -883,7 +961,8 @@ describe('E-Planning Adapter', function () { it('should return the e parameter with a value according to the sizes in order corresponding to the desktop priority list of the ad units', function () { let bidRequestsPrioritySizes = [validBidExistingSizesInPriorityListForDesktop]; // overwrite default innerWdith for tests with a larger one we consider "Desktop" or NOT Mobile - getWindowSelfStub.returns(createWindow(1025)); + getWindowTopStub.returns(createWindow(1025)); + resetWinDimensions(); const e = spec.buildRequests(bidRequestsPrioritySizes, bidderRequest).data.e; expect(e).to.equal('300x250_0:300x250,300x600,970x250'); }); diff --git a/test/spec/modules/equativBidAdapter_spec.js b/test/spec/modules/equativBidAdapter_spec.js new file mode 100644 index 00000000000..8744ec8f0a9 --- /dev/null +++ b/test/spec/modules/equativBidAdapter_spec.js @@ -0,0 +1,949 @@ +import { converter, spec, storage } from 'modules/equativBidAdapter.js'; +import * as utils from '../../../src/utils.js'; + +describe('Equativ bid adapter tests', () => { + let sandBox; + + beforeEach(() => { + sandBox = sinon.createSandbox(); + sandBox.stub(utils, 'logError'); + sandBox.stub(utils, 'logWarn'); + }); + + afterEach(() => sandBox.restore()); + + const DEFAULT_BANNER_BID_REQUESTS = [ + { + adUnitCode: 'eqtv_42', + bidId: 'abcd1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bidder: 'equativ', + params: { + networkId: 111, + }, + requestId: 'efgh5678', + ortb2Imp: { + ext: { + tid: 'zsfgzzg', + }, + }, + } + ]; + + const DEFAULT_VIDEO_BID_REQUESTS = [ + { + adUnitCode: 'eqtv_43', + bidId: 'efgh5678', + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + pos: 3, + skip: 1, + linearity: 1, + minduration: 10, + maxduration: 30, + minbitrate: 300, + maxbitrate: 600, + w: 640, + h: 480, + playbackmethod: [1], + api: [3], + mimes: ['video/x-flv', 'video/mp4'], + // protocols: [2, 3], // used in older adapter ... including as comment for reference + startdelay: 42, + battr: [13, 14], + placement: 1, + }, + }, + bidder: 'equativ', + params: { + networkId: 111, + }, + requestId: 'abcd1234', + ortb2Imp: { + ext: { + 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 + } + }], + context: 1, + eventtrackers: [{ + event: 1, + methods: [1, 2] + }], + plcmttype: 1, + privacy: 1, + ver: '1.2', + }; + const DEFAULT_NATIVE_BID_REQUESTS = [ + { + adUnitCode: 'equativ_native_42', + bidId: 'equativ_native_bidid_42', + mediaTypes: { + native: { + ortb: { + ...nativeOrtbRequest + } + }, + }, + nativeOrtbRequest, + bidder: 'equativ', + params: { + networkId: 111, + }, + requestId: 'equativ_native_reqid_42', + ortb2Imp: { + ext: { + tid: 'equativ_native_tid_42', + }, + }, + } + ]; + + const DEFAULT_MULTI_IMP_BID_REQUESTS = [ + { + adUnitCode: 'eqtv_42', + bidId: 'abcd1234', + mediaTypes: { + banner: DEFAULT_BANNER_BID_REQUESTS[0].mediaTypes.banner, + video: DEFAULT_VIDEO_BID_REQUESTS[0].mediaTypes.video, + native: DEFAULT_NATIVE_BID_REQUESTS[0].mediaTypes.native, + }, + nativeOrtbRequest, + bidder: 'equativ', + params: { + networkId: 111, + }, + requestId: 'efgh5678', + ortb2Imp: { + ext: { + tid: 'zsfgzzg', + }, + }, + getFloor: ({ mediaType, size }) => { + if ((mediaType === 'banner' && size[0] === 300 && size[1] === 250) || mediaType === 'native') { + return { floor: 1.1 }; + } + return { floor: 0.9 }; + } + } + ]; + + const DEFAULT_BANNER_BIDDER_REQUEST = { + bidderCode: 'equativ', + bids: DEFAULT_BANNER_BID_REQUESTS, + }; + + const DEFAULT_VIDEO_BIDDER_REQUEST = { + bidderCode: 'equativ', + bids: DEFAULT_VIDEO_BID_REQUESTS, + }; + + const DEFAULT_NATIVE_BIDDER_REQUEST = { + bidderCode: 'equativ', + bids: DEFAULT_NATIVE_BID_REQUESTS, + }; + + const DEFAULT_MULTI_IMP_BIDDER_REQUEST = { + bidderCode: 'equativ', + bids: DEFAULT_MULTI_IMP_BID_REQUESTS, + }; + + const SAMPLE_RESPONSE = { + body: { + id: '12h712u7-k22g-8124-ab7a-h268s22dy271', + seatbid: [ + { + bid: [ + { + id: '1bh7jku7-ko2g-8654-ab72-h268shvwy271', + impid: 'r12gwgf231', + price: 0.6565, + adm: '

AD

', + adomain: ['abc.com'], + cid: '1242512', + crid: '535231', + w: 300, + h: 600, + mtype: 1, + cat: ['IAB19', 'IAB19-1'], + cattax: 1, + }, + ], + seat: '4212', + }, + ], + cur: 'USD', + statuscode: 0, + }, + }; + + describe('buildRequests', () => { + it('should build correct requests using ORTB converter', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + ); + const dataFromConverter = converter.toORTB({ + bidderRequest: DEFAULT_BANNER_BIDDER_REQUEST, + bidRequests: DEFAULT_BANNER_BID_REQUESTS, + }); + expect(request[0]).to.deep.equal({ + data: { + ...dataFromConverter, + id: request[0].data.id, + imp: [ + { + ...dataFromConverter.imp[0], + id: request[0].data.imp[0].id, + } + ], + }, + method: 'POST', + url: 'https://ssb-global.smartadserver.com/api/bid?callerId=169', + }); + }); + + it('should generate a 14-char id for each imp object', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + ); + + request[0].data.imp.forEach(imp => { + expect(imp.id).to.have.lengthOf(14); + }); + }); + + it('should add ext.bidder to imp object when siteId is defined', () => { + const bidRequests = [ + { ...DEFAULT_BANNER_BID_REQUESTS[0], params: { siteId: 123 } }, + ]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.imp[0].ext.bidder).to.deep.equal({ + siteId: 123, + }); + }); + + it('should add ext.bidder to imp object when pageId is defined', () => { + const bidRequests = [ + { ...DEFAULT_BANNER_BID_REQUESTS[0], params: { pageId: 123 } }, + ]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.imp[0].ext.bidder).to.deep.equal({ + pageId: 123, + }); + }); + + it('should add ext.bidder to imp object when formatId is defined', () => { + const bidRequests = [ + { ...DEFAULT_BANNER_BID_REQUESTS[0], params: { formatId: 123 } }, + ]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.imp[0].ext.bidder).to.deep.equal({ + formatId: 123, + }); + }); + + it('should not add ext.bidder to imp object when siteId, pageId, formatId are not defined', () => { + const bidRequests = [{ ...DEFAULT_BANNER_BID_REQUESTS[0], params: {} }]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.imp[0].ext.bidder).to.be.undefined; + }); + + it('should add site.publisher.id param', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + expect(request.data.site.publisher.id).to.equal(111); + }); + + it('should pass ortb2.site.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BANNER_BID_REQUESTS[0], + ortb2: { + site: { + publisher: { + id: 98, + } + } + } + }]; + delete bidRequests[0].params; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.site.publisher.id).to.equal(98); + }); + + it('should pass networkId as site.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BANNER_BID_REQUESTS[0], + ortb2: { + site: { + publisher: {} + } + } + }]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.site.publisher.id).to.equal(111); + }); + + it('should pass ortb2.app.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BANNER_BID_REQUESTS[0], + ortb2: { + app: { + publisher: { + id: 27, + } + } + } + }]; + delete bidRequests[0].params; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.app.publisher.id).to.equal(27); + }); + + it('should pass networkId as app.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BANNER_BID_REQUESTS[0], + ortb2: { + app: { + publisher: {} + } + } + }]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.app.publisher.id).to.equal(111); + }); + + it('should pass ortb2.dooh.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BANNER_BID_REQUESTS[0], + ortb2: { + dooh: { + publisher: { + id: 35, + } + } + } + }]; + delete bidRequests[0].params; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.dooh.publisher.id).to.equal(35); + }); + + it('should pass networkId as dooh.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BANNER_BID_REQUESTS[0], + ortb2: { + dooh: { + publisher: {} + } + } + }]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.dooh.publisher.id).to.equal(111); + }); + + it('should not send floor by default', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + expect(request.data.imp[0]).to.not.have.property('bidfloor'); + }); + + it('should send secure connection', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + expect(request.data.imp[0]).to.have.property('secure').that.eq(1); + }); + + it('should have tagid', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + expect(request.data.imp[0]).to.have.property('tagid').that.eq(DEFAULT_BANNER_BID_REQUESTS[0].adUnitCode); + }); + + it('should remove dt', () => { + const bidRequests = [ + { ...DEFAULT_BANNER_BID_REQUESTS[0], ortb2Imp: { dt: 1728377558235 } } + ]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.imp[0]).to.not.have.property('dt'); + }); + + it('should read and send pid as buyeruid', () => { + const localStorageData = { + 'eqt_pid': '7789746781' + }; + const getDataFromLocalStorage = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorage.callsFake(name => localStorageData[name]); + + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + + expect(request.data.user).to.have.property('buyeruid').that.eq(localStorageData['eqt_pid']); + + getDataFromLocalStorage.restore(); + }); + + it('should not send buyeruid', () => { + const getDataFromLocalStorage = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorage.callsFake(() => null); + + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + + expect(request.data).to.not.have.property('user'); + + getDataFromLocalStorage.restore(); + }); + + it('should pass buyeruid defined in config', () => { + const getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.callsFake(() => undefined); + + const bidRequest = { + ...DEFAULT_BANNER_BIDDER_REQUEST, + ortb2: { + user: { + buyeruid: 'buyeruid-provided-by-publisher' + } + } + }; + const request = spec.buildRequests([ DEFAULT_BANNER_BID_REQUESTS[0] ], bidRequest)[0]; + + expect(request.data.user.buyeruid).to.deep.eq(bidRequest.ortb2.user.buyeruid); + + getDataFromLocalStorageStub.restore(); + }); + + it('should build a video request properly under normal circumstances', () => { + // ASSEMBLE + if (FEATURES.VIDEO) { + // ACT + const request = spec.buildRequests(DEFAULT_VIDEO_BID_REQUESTS, {})[0].data; + + // ASSERT + expect(request.imp[0]).to.have.property('video'); + + const videoObj = request.imp[0].video; + + expect(videoObj).to.have.property('api').and.to.deep.equal([3]); + expect(videoObj).to.have.property('battr').and.to.deep.equal([13, 14]); + expect(videoObj).to.have.property('linearity').and.to.equal(1); + expect(videoObj).to.have.property('mimes').and.to.deep.equal(['video/x-flv', 'video/mp4']); + expect(videoObj).to.have.property('minbitrate').and.to.equal(300); + expect(videoObj).to.have.property('maxbitrate').and.to.equal(600); + expect(videoObj).to.have.property('minduration').and.to.equal(10); + expect(videoObj).to.have.property('maxduration').and.to.equal(30); + expect(videoObj).to.have.property('placement').and.to.equal(1); + expect(videoObj).to.have.property('playbackmethod').and.to.deep.equal([1]); + expect(videoObj).to.have.property('pos').and.to.equal(3); + expect(videoObj).to.have.property('skip').and.to.equal(1); + expect(videoObj).to.have.property('startdelay').and.to.equal(42); + expect(videoObj).to.have.property('w').and.to.equal(640); + expect(videoObj).to.have.property('h').and.to.equal(480); + expect(videoObj).not.to.have.property('ext'); + } + }); + + it('should read and pass ortb2Imp.rwdd', () => { + // ASSEMBLE + if (FEATURES.VIDEO) { + const bidRequestsWithOrtb2ImpRwdd = [ + { + ...DEFAULT_VIDEO_BID_REQUESTS[0], + ortb2Imp: { + rwdd: 1 + } + } + ]; + // ACT + const request = spec.buildRequests(bidRequestsWithOrtb2ImpRwdd, {})[0].data; + + // ASSERT + expect(request.imp[0]).to.have.property('rwdd').and.to.equal(1); + } + }); + + it('should read mediaTypes.video.ext.rewarded and pass as rwdd', () => { + // ASSEMBLE + if (FEATURES.VIDEO) { + const bidRequestsWithExtReworded = [ + { + ...DEFAULT_VIDEO_BID_REQUESTS[0], + mediaTypes: { + video: { + ext: { + rewarded: 1 + } + } + } + } + ]; + // ACT + const request = spec.buildRequests(bidRequestsWithExtReworded, {})[0].data; + + // ASSERT + expect(request.imp[0]).to.have.property('rwdd').and.to.equal(1); + } + }); + + it('should prioritize ortb2Imp.rwdd over mediaTypes.video.ext.rewarded', () => { + // ASSEMBLE + if (FEATURES.VIDEO) { + const bidRequestsWithBothRewordedParams = [ + { + ...DEFAULT_VIDEO_BID_REQUESTS[0], + mediaTypes: { + video: { + ext: { + rewarded: 1 + } + } + }, + ortb2Imp: { + rwdd: 2 + } + } + ]; + // ACT + const request = spec.buildRequests(bidRequestsWithBothRewordedParams, {})[0].data; + + // ASSERT + expect(request.imp[0]).to.have.property('rwdd').and.to.equal(2); + } + }); + + it('should warn about missing required properties for video requests', () => { + // ASSEMBLE + const missingRequiredVideoRequest = DEFAULT_VIDEO_BID_REQUESTS[0]; + + // removing required properties + delete missingRequiredVideoRequest.mediaTypes.video.mimes; + delete missingRequiredVideoRequest.mediaTypes.video.placement; + + const bidRequests = [ missingRequiredVideoRequest ]; + const bidderRequest = { ...DEFAULT_VIDEO_BIDDER_REQUEST, bids: bidRequests }; + + // ACT + spec.buildRequests(bidRequests, bidderRequest); + + // ASSERT + expect(utils.logWarn.callCount).to.equal(2); + expect(utils.logWarn.getCall(0).args[0]).to.satisfy(arg => arg.includes('"mediaTypes.video.mimes" is missing')); + expect(utils.logWarn.getCall(1).args[0]).to.satisfy(arg => arg.includes('"mediaTypes.video.placement" is missing')); + }); + + it('should not send a video request when it has an empty body and no other impressions with any media types are defined', () => { + // ASSEMBLE + const emptyVideoRequest = { + ...DEFAULT_VIDEO_BID_REQUESTS[0], + mediaTypes: { + video: {} + } + }; + const bidRequests = [ emptyVideoRequest ]; + const bidderRequest = { ...DEFAULT_VIDEO_BIDDER_REQUEST, bids: bidRequests }; + + // ACT + const request = spec.buildRequests(bidRequests, bidderRequest); + + // ASSERT + expect(utils.logError.calledOnce).to.equal(true); + expect(utils.logError.args[0][0]).to.satisfy(arg => arg.includes('No request')); + expect(request).to.be.undefined; + }); + + it('should build a native request properly under normal circumstances', () => { + if (FEATURES.NATIVE) { + // ACT + const request = spec.buildRequests(DEFAULT_NATIVE_BID_REQUESTS, {})[0].data; + + // ASSERT + expect(request.imp[0]).to.have.property('native'); + + const nativeObj = request.imp[0].native; + expect(nativeObj).to.have.property('ver').and.to.equal('1.2'); + expect(nativeObj).to.have.property('request').and.to.be.a('string'); + + const requestObj = JSON.parse(nativeObj.request); + expect(requestObj).to.have.property('assets').and.to.be.an('array'); + expect(requestObj).to.have.property('eventtrackers').and.to.be.an('array'); + expect(requestObj).to.have.property('plcmttype').and.to.equal(1); + expect(requestObj).to.have.property('privacy').and.to.equal(1); + expect(requestObj).to.have.property('ver').and.to.equal('1.2'); + } + }); + + it('should not send a native request when it has an empty body and no other impressions with any media types are defined', () => { + if (FEATURES.NATIVE) { + // ASSEMBLE + const emptyNativeRequest = { + ...DEFAULT_NATIVE_BID_REQUESTS[0], + mediaTypes: { + native: {} + } + }; + const bidRequests = [ emptyNativeRequest ]; + const bidderRequest = { ...DEFAULT_NATIVE_BIDDER_REQUEST, bids: bidRequests }; + + // ACT + const request = spec.buildRequests(bidRequests, bidderRequest); + + // ASSERT + expect(utils.logError.calledOnce).to.equal(true); + expect(utils.logError.args[0][0]).to.satisfy(arg => arg.includes('No request')); + expect(request).to.be.undefined; + } + }); + + it('should warn about missing "assets" property for native requests', () => { + if (FEATURES.NATIVE) { + // ASSEMBLE + const missingRequiredNativeRequest = utils.deepClone(DEFAULT_NATIVE_BID_REQUESTS[0]); + + // removing just "assets" for this test + delete missingRequiredNativeRequest.nativeOrtbRequest.assets; + const bidRequests = [ missingRequiredNativeRequest ]; + const bidderRequest = { ...DEFAULT_NATIVE_BIDDER_REQUEST, bids: bidRequests }; + + // this value comes from native.js, part of the ortbConverter library + const warningMsgFromLibrary = 'mediaTypes.native is set, but no assets were specified. Native request skipped.' + + // ACT + spec.buildRequests(bidRequests, bidderRequest); + + // ASSERT + expect(utils.logWarn.getCall(0).args[0]).to.satisfy(arg => arg.includes(warningMsgFromLibrary)); + } + }); + + it('should warn about other missing required properties for native requests', () => { + if (FEATURES.NATIVE) { + // ASSEMBLE + const missingRequiredNativeRequest = utils.deepClone(DEFAULT_NATIVE_BID_REQUESTS[0]); + + // ortbConverter library will warn about missing assets; we supply warnings for these properties here + delete missingRequiredNativeRequest.nativeOrtbRequest.assets; + delete missingRequiredNativeRequest.mediaTypes.native.ortb.eventtrackers; + delete missingRequiredNativeRequest.mediaTypes.native.ortb.plcmttype; + delete missingRequiredNativeRequest.mediaTypes.native.ortb.privacy; + + const bidRequests = [ missingRequiredNativeRequest ]; + const bidderRequest = { ...DEFAULT_NATIVE_BIDDER_REQUEST, bids: bidRequests }; + + // ACT + spec.buildRequests(bidRequests, bidderRequest); + + // ASSERT + expect(utils.logWarn.callCount).to.equal(4); // the first message, regarding missing assets, is supplied by the ortbConverter library + expect(utils.logWarn.getCall(0).args[0]).to.satisfy(arg => arg.includes('no assets were specified')); + expect(utils.logWarn.getCall(1).args[0]).to.satisfy(arg => arg.includes('"mediaTypes.native.ortb.privacy" is missing')); + expect(utils.logWarn.getCall(2).args[0]).to.satisfy(arg => arg.includes('"mediaTypes.native.ortb.plcmttype" is missing')); + expect(utils.logWarn.getCall(3).args[0]).to.satisfy(arg => arg.includes('"mediaTypes.native.ortb.eventtrackers" is missing')); + } + }); + + it('should split banner sizes per floor', () => { + const bids = [ + { + ...DEFAULT_BANNER_BID_REQUESTS[0], + getFloor: ({ size }) => ({ floor: size[0] * size[1] / 100_000 }) + } + ]; + + const request = spec.buildRequests( + bids, + { ...DEFAULT_BANNER_BIDDER_REQUEST, bids } + ); + + 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.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.banner.format).to.have.lengthOf(1); + expect(secondImp.banner.format[0]).to.deep.equal({ w: 300, h: 600 }); + }); + + it('should group media types per floor', () => { + if (FEATURES.NATIVE) { + const request = spec.buildRequests( + DEFAULT_MULTI_IMP_BID_REQUESTS, + DEFAULT_MULTI_IMP_BIDDER_REQUEST + ); + + const firstImp = request[0].data.imp[0]; + expect(firstImp.bidfloor).to.equal(1.1); + expect(firstImp.banner.format).to.have.lengthOf(1); + expect(firstImp.banner.format[0]).to.deep.equal({ w: 300, h: 250 }); + expect(firstImp).to.have.property('native'); + expect(firstImp).to.not.have.property('video'); + + const secondImp = request[0].data.imp[1]; + expect(secondImp.bidfloor).to.equal(0.9); + expect(secondImp.banner.format).to.have.lengthOf(1); + expect(secondImp.banner.format[0]).to.deep.equal({ w: 300, h: 600 }); + expect(secondImp).to.not.have.property('native'); + expect(secondImp).to.have.property('video'); + } + }) + }); + + describe('getUserSyncs', () => { + let setDataInLocalStorageStub; + + beforeEach(() => setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage')); + + afterEach(() => setDataInLocalStorageStub.restore()); + + it('should return empty array if iframe sync not enabled', () => { + const syncs = spec.getUserSyncs({}, SAMPLE_RESPONSE); + expect(syncs).to.deep.equal([]); + }); + + it('should retrieve and save user pid', (done) => { + spec.getUserSyncs( + { iframeEnabled: true }, + SAMPLE_RESPONSE, + { gdprApplies: true, vendorData: { vendor: { consents: {} } } } + ); + + window.dispatchEvent(new MessageEvent('message', { + data: { + action: 'getConsent', + pid: '7767825890726' + }, + origin: 'https://apps.smartadserver.com', + source: window + })); + + setTimeout(() => { + expect(setDataInLocalStorageStub.calledOnce).to.be.true; + expect(setDataInLocalStorageStub.calledWith('eqt_pid', '7767825890726')).to.be.true; + done(); + }); + }); + + it('should not save user pid coming from incorrect origin', (done) => { + spec.getUserSyncs( + { iframeEnabled: true }, + SAMPLE_RESPONSE, + { gdprApplies: true, vendorData: { vendor: { consents: {} } } } + ); + + window.dispatchEvent(new MessageEvent('message', { + data: { + action: 'getConsent', + pid: '7767825890726' + }, + origin: 'https://another-origin.com', + source: window + })); + + setTimeout(() => { + expect(setDataInLocalStorageStub.notCalled).to.be.true; + done(); + }); + }); + + it('should not save empty pid', (done) => { + spec.getUserSyncs( + { iframeEnabled: true }, + SAMPLE_RESPONSE, + { gdprApplies: true, vendorData: { vendor: { consents: {} } } } + ); + + window.dispatchEvent(new MessageEvent('message', { + data: { + action: 'getConsent', + pid: '' + }, + origin: 'https://apps.smartadserver.com', + source: window + })); + + setTimeout(() => { + expect(setDataInLocalStorageStub.notCalled).to.be.true; + done(); + }); + }); + + it('should return array including iframe cookie sync object (gdprApplies=true)', () => { + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + SAMPLE_RESPONSE, + { gdprApplies: true } + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0]).to.deep.equal({ + type: 'iframe', + url: 'https://apps.smartadserver.com/diff/templates/asset/csync.html?nwid=111&gdpr=1&' + }); + }); + + it('should return array including iframe cookie sync object (gdprApplies=false)', () => { + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + SAMPLE_RESPONSE, + { gdprApplies: false } + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0]).to.deep.equal({ + type: 'iframe', + url: 'https://apps.smartadserver.com/diff/templates/asset/csync.html?nwid=111&gdpr=0&' + }); + }); + }); + + describe('interpretResponse', () => { + it('should return data returned by ORTB converter', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + const bids = spec.interpretResponse(SAMPLE_RESPONSE, request); + expect(bids).to.deep.equal( + converter.fromORTB({ + request: request.data, + response: SAMPLE_RESPONSE.body, + }) + ); + }); + + it('should not fail if bidRequest.data.imp is undefined', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + delete request.data.imp; + expect(spec.interpretResponse(SAMPLE_RESPONSE, request)).to.not.throw; + }); + + it('should not fail if serverResponse.body.seatbid is undefined', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + )[0]; + const response = utils.deepClone(SAMPLE_RESPONSE); + delete response.body.seatbid; + expect(spec.interpretResponse(response, request)).to.not.throw; + }); + }); + + describe('isBidRequestValid', () => { + it('should return true if params.networkId is set', () => { + const bidRequest = { + params: { + networkId: 123, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true if ortb2.site.publisher.id is set', () => { + const bidRequest = { + ortb2: { + site: { + publisher: { + id: 123, + }, + }, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true if ortb2.app.publisher.id is set', () => { + const bidRequest = { + ortb2: { + app: { + publisher: { + id: 123, + }, + }, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true if ortb2.dooh.publisher.id is set', () => { + const bidRequest = { + ortb2: { + dooh: { + publisher: { + id: 123, + }, + }, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false if networkId is not set', () => { + const bidRequest = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); +}); diff --git a/test/spec/modules/escalaxBidAdapter_spec.js b/test/spec/modules/escalaxBidAdapter_spec.js new file mode 100644 index 00000000000..6238e6cb208 --- /dev/null +++ b/test/spec/modules/escalaxBidAdapter_spec.js @@ -0,0 +1,306 @@ +import { expect } from 'chai'; +import { spec } from 'modules/escalaxBidAdapter'; +import 'modules/priceFloors.js'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { config } from '../../../src/config.js'; +import { addFPDToBidderRequest } from '../../helpers/fpd.js'; + +import 'src/prebid.js'; +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagementTcf.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; + +const SIMPLE_BID_REQUEST = { + bidder: 'escalax', + params: { + sourceId: 'sourceId', + accountId: 'accountId', + }, + mediaTypes: { + banner: { + sizes: [ + [320, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1234567890123-0', + transactionId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + bidId: 'abcdef1234567890', + bidderRequestId: '1234567890abcdef', + auctionId: 'abcdef1234567890', + sizes: [[300, 250], [160, 600]], + gdprConsent: { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', + }, +} + +const BANNER_BID_REQUEST = { + bidder: 'escalax', + params: { + sourceId: 'sourceId', + accountId: 'accountId', + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + code: 'banner_example', + timeout: 1000, +} + +const VIDEO_BID_REQUEST = { + placementCode: '/DfpAccount1/slotVideo', + bidId: 'test-bid-id-2', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + bidder: 'escalax', + params: { + sourceId: '123', + accountId: '123', + }, + adUnitCode: '/adunit-code/test-path', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, +} + +const NATIVE_BID_REQUEST = { + code: 'native_example', + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + } + } + }, + bidder: 'escalax', + params: { + sourceId: 'sourceId', + accountId: 'accountId', + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, + uspConsent: 'uspConsent' +}; + +const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + } +}; + +const gdprConsent = { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', +} + +describe('escalaxAdapter', 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('with user privacy regulations', function () { + it('should send the Coppa "required" flag set to "1" in the request', async function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); + expect(serverRequest.data.regs.coppa).to.equal(1); + config.getConfig.restore(); + }); + + it('should send the GDPR Consent data in the request', async function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], await addFPDToBidderRequest({ + ...bidderRequest, + gdprConsent + })); + expect(serverRequest.data.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(serverRequest.data.user.ext.consent).to.equal('CONSENT'); + }); + + it('should send the CCPA data in the request', async function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], await addFPDToBidderRequest({...bidderRequest, ...{uspConsent: '1YYY'}})); + expect(serverRequest.data.regs.ext.us_privacy).to.equal('1YYY'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(true); + }); + + it('should return false when sourceId/accountId is missing', function () { + let localbid = Object.assign({}, BANNER_BID_REQUEST); + delete localbid.params.sourceId; + delete localbid.params.accountId; + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(false); + }); + }); + + describe('build request', function () { + it('should return an empty array when no bid requests', async function () { + const bidRequest = spec.buildRequests([], await addFPDToBidderRequest(bidderRequest)); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); + }); + + it('should return a valid bid request object', async function () { + const request = spec.buildRequests([SIMPLE_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); + expect(request).to.not.equal('array'); + expect(request.data).to.be.an('object'); + expect(request.method).to.equal('POST'); + expect(request.url).to.not.equal(''); + expect(request.url).to.not.equal(undefined); + expect(request.url).to.not.equal(null); + + expect(request.data.site).to.have.property('page'); + expect(request.data.site).to.have.property('domain'); + expect(request.data).to.have.property('id'); + expect(request.data).to.have.property('imp'); + expect(request.data).to.have.property('device'); + }); + + it('should return a valid bid BANNER request object', async function () { + const request = spec.buildRequests([BANNER_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].banner).to.exist; + expect(request.data.imp[0].banner.format[0].w).to.be.an('number'); + expect(request.data.imp[0].banner.format[0].h).to.be.an('number'); + }); + + if (FEATURES.VIDEO) { + it('should return a valid bid VIDEO request object', async function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].video).to.exist; + expect(request.data.imp[0].video.w).to.be.an('number'); + expect(request.data.imp[0].video.h).to.be.an('number'); + }); + } + + it('should return a valid bid NATIVE request object', async function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], await addFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0]).to.be.an('object'); + }); + }) + + describe('interpretResponse', function () { + let bidRequests, bidderRequest; + beforeEach(function () { + bidRequests = [{ + 'bidId': '28ffdk2B952532', + 'bidder': 'escalax', + 'userId': { + 'freepassId': { + 'userIp': '172.21.0.1', + 'userId': '123', + 'commonId': 'commonIdValue' + } + }, + 'adUnitCode': 'adunit-code', + 'params': { + 'publisherId': 'publisherIdValue' + } + }]; + bidderRequest = {}; + }); + + it('Empty response must return empty array', function () { + const emptyResponse = null; + let response = spec.interpretResponse(emptyResponse, BANNER_BID_REQUEST); + + expect(response).to.be.an('array').that.is.empty; + }) + + it('Should interpret banner response', function () { + const serverResponse = { + body: { + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'impid': '28ffdk2B952532', + 'price': 97, + 'adm': '', + 'w': 300, + 'h': 250, + 'crid': 'creative0' + }] + }] + } + }; + it('should interpret server response', async function () { + const bidRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse(serverResponse, bidRequest); + expect(bids).to.be.an('array'); + const bid = bids[0]; + expect(bid).to.be.an('object'); + expect(bid.currency).to.equal('USD'); + expect(bid.cpm).to.equal(97); + expect(bid.ad).to.equal(ad) + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('creative0'); + }); + }) + }); +}); diff --git a/test/spec/modules/eskimiBidAdapter_spec.js b/test/spec/modules/eskimiBidAdapter_spec.js index d01240c86ab..a452f115767 100644 --- a/test/spec/modules/eskimiBidAdapter_spec.js +++ b/test/spec/modules/eskimiBidAdapter_spec.js @@ -1,5 +1,5 @@ -import { expect } from 'chai'; -import { spec } from 'modules/eskimiBidAdapter.js'; +import {expect} from 'chai'; +import {spec} from 'modules/eskimiBidAdapter.js'; import * as utils from 'src/utils'; const BANNER_BID = { @@ -33,7 +33,7 @@ const VIDEO_BID = { playbackmethod: [2, 4, 6], playerSize: [[1024, 768]], protocols: [3, 4, 7, 8, 10], - placement: 1, + plcmt: 1, minduration: 0, maxduration: 60, startdelay: 0 @@ -165,8 +165,8 @@ describe('Eskimi bid adapter', function () { it('should properly forward ORTB blocking params', function () { let bid = utils.deepClone(BANNER_BID); bid = utils.mergeDeep(bid, { - params: { bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example'] }, - mediaTypes: { banner: { battr: [1] } } + params: {bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example']}, + mediaTypes: {banner: {battr: [1]}} }); let [request] = spec.buildRequests([bid], BIDDER_REQUEST); @@ -222,7 +222,7 @@ describe('Eskimi bid adapter', function () { mimes: ['video/mp4', 'video/x-flv'], playbackmethod: [3, 4], protocols: [5, 6], - placement: 1, + plcmt: 1, minduration: 0, maxduration: 60, w: 1024, @@ -253,7 +253,7 @@ describe('Eskimi bid adapter', function () { const [request] = spec.buildRequests([bid], BIDDER_REQUEST); const response = utils.deepClone(BANNER_BID_RESPONSE); - const bids = spec.interpretResponse({ body: response }, request); + const bids = spec.interpretResponse({body: response}, request); expect(bids).to.be.an('array').that.is.not.empty; expect(bids[0].mediaType).to.equal('banner'); @@ -274,7 +274,7 @@ describe('Eskimi bid adapter', function () { const bid = utils.deepClone(BANNER_BID); let request = spec.buildRequests([bid], BIDDER_REQUEST)[0]; - const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, { 'body': {} }); + const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, {'body': {}}); const bids = spec.interpretResponse(EMPTY_RESP, request); expect(bids).to.be.empty; }); @@ -285,7 +285,7 @@ describe('Eskimi bid adapter', function () { const bid = utils.deepClone(VIDEO_BID); const [request] = spec.buildRequests([bid], BIDDER_REQUEST); - const bids = spec.interpretResponse({ body: VIDEO_BID_RESPONSE }, request); + const bids = spec.interpretResponse({body: VIDEO_BID_RESPONSE}, request); expect(bids).to.be.an('array').that.is.not.empty; expect(bids[0].mediaType).to.equal('video'); diff --git a/test/spec/modules/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 9ad2b69e89c..211e08458a8 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -1,13 +1,13 @@ -import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; +import {attachIdSystem, coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; import {euidIdSubmodule} from 'modules/euidIdSystem.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'src/prebid.js'; -import * as utils from 'src/utils.js'; import {apiHelpers, cookieHelpers, runAuction, setGdprApplies} from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; -import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {uninstall as uninstallTcfControl} from 'modules/tcfControl.js'; import {server} from 'test/mocks/xhr'; +import {createEidsArray} from '../../../modules/userId/eids.js'; let expect = require('chai').expect; @@ -49,7 +49,7 @@ describe('EUID module', function() { const configureEuidCstgResponse = (httpStatus, response) => server.respondWith('POST', cstgApiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); before(function() { - uninstallGdprEnforcement(); + uninstallTcfControl(); hook.ready(); suiteSandbox = sinon.sandbox.create(); if (typeof window.crypto.subtle === 'undefined') { @@ -122,10 +122,11 @@ describe('EUID module', function() { expectToken(bid, initialToken); }) - it('When an expired token is provided in config, it calls the API.', function() { + it('When an expired token is provided in config, it calls the API.', async function () { setGdprApplies(true); const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({euidToken})); + await runAuction(); expect(server.requests[0]?.url).to.have.string('https://prod.euid.eu/'); }); @@ -161,4 +162,24 @@ describe('EUID module', function() { expectOptout(bid, optoutToken); }); } + + describe('eid', () => { + before(() => { + attachIdSystem(euidIdSubmodule); + }); + it('euid', function() { + const userId = { + euid: {'id': 'Sample_AD_Token'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'euid.eu', + uids: [{ + id: 'Sample_AD_Token', + atype: 3 + }] + }); + }); + }) }); diff --git a/test/spec/modules/exadsBidAdapter_spec.js b/test/spec/modules/exadsBidAdapter_spec.js new file mode 100644 index 00000000000..ca24dad3959 --- /dev/null +++ b/test/spec/modules/exadsBidAdapter_spec.js @@ -0,0 +1,632 @@ +import { expect } from 'chai'; +import { spec, imps } from 'modules/exadsBidAdapter.js'; +import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; + +describe('exadsBidAdapterTest', function () { + const bidder = 'exads'; + + const partners = { + ORTB_2_4: 'ortb_2_4' + }; + + const imageBanner = { + mediaTypes: { + banner: { + sizes: [300, 250] + } + }, + bidder: bidder, + params: { + zoneId: 5147485, + fid: '829a896f011475d505a0d89cfdd1af8d9cdb07ff', + partner: partners.ORTB_2_4, + siteId: '12345', + siteName: 'your-site.com', + catIab: ['IAB25-3'], + userIp: '0.0.0.0', + userId: '', + country: 'IRL', + impressionId: '123456', + keywords: 'lifestyle, humour', + bidfloor: 0.00000011, + bidfloorcur: 'EUR', + bcat: ['IAB25', 'IAB7-39', 'IAB8-18', 'IAB8-5', 'IAB9-9'], + badv: ['first.com', 'second.com'], + mimes: ['image/jpg'], + endpoint: 'test.com', + dsa: { + dsarequired: 3, + pubrender: 0, + datatopub: 2 + }, + } + }; + + const native = { + mediaTypes: { + native: { + ortb: { + assets: [{ + id: 3, + required: 1, + title: { + len: 124 + } + }, + { + id: 2, + data: { + type: 1, + len: 50 + } + }, + { + id: 1, + required: 1, + img: { + type: 3, + w: 300, + h: 300, + } + }] + } + }, + }, + bidder: bidder, + params: { + zoneId: 5147485, + fid: '829a896f011475d505a0d89cfdd1af8d9cdb07ff', + partner: partners.ORTB_2_4, + siteId: '12345', + siteName: 'your-site.com', + catIab: ['IAB25-3'], + userIp: '0.0.0.0', + userId: '', + country: 'IRL', + impressionId: '123456', + keywords: 'lifestyle, humour', + bidfloor: 0.00000011, + bidfloorcur: 'EUR', + native: { + plcmtcnt: 4, + }, + dsa: { + pubrender: 0, + datatopub: 2 + }, + endpoint: 'test.com' + } + }; + + const instream = { + mediaTypes: { + video: { + mimes: ['video/mp4'], + protocols: [3, 6], + } + }, + bidder: bidder, + params: { + zoneId: 5147485, + fid: '829a896f011475d505a0d89cfdd1af8d9cdb07ff', + partner: partners.ORTB_2_4, + siteId: '12345', + siteName: 'your-site.com', + catIab: ['IAB25-3'], + userIp: '0.0.0.0', + userId: '', + country: 'IRL', + impressionId: '123456', + keywords: 'lifestyle, humour', + bidfloor: 0.00000011, + bidfloorcur: 'EUR', + imp: { + ext: { + video_cta: 0 + } + }, + dsa: { + datatopub: 2 + }, + endpoint: 'test.com', + } + }; + + describe('while validating bid request', function () { + it('should check the validity of bidRequest with all mandatory params for banner ad-format', function () { + expect(spec.isBidRequestValid(imageBanner)).to.equal(true); + }); + + it('should check the validity of a bidRequest with all mandatory params for native ad-format', function () { + expect(spec.isBidRequestValid(native)); + }); + + it('should check the validity of a bidRequest with all mandatory params for instream ad-format', function () { + expect(spec.isBidRequestValid(instream)).to.equal(true); + }); + + it('should check the validity of a bidRequest with wrong partner', function () { + expect(spec.isBidRequestValid({ + ...imageBanner, + params: { + ...imageBanner.params, + partner: 'not_ortb_2_4' + } + })).to.eql(false); + }); + + it('should check the validity of a bidRequest without params', function () { + expect(spec.isBidRequestValid({ + bidder: bidder, + params: { } + })).to.equal(false); + }); + }); + + describe('while building bid request for banner ad-format', function () { + const bidRequests = [imageBanner]; + + it('should make a bidRequest by HTTP method', function () { + const requests = spec.buildRequests(bidRequests, {}); + requests.forEach(function(requestItem) { + expect(requestItem.method).to.equal('POST'); + }); + }); + }); + + describe('while building bid request for native ad-format', function () { + const bidRequests = [native]; + + it('should make a bidRequest by HTTP method', function () { + const requests = spec.buildRequests(bidRequests, {}); + requests.forEach(function(requestItem) { + expect(requestItem.method).to.equal('POST'); + }); + }); + }); + + describe('while building bid request for instream ad-format', function () { + const bidRequests = [instream]; + + it('should make a bidRequest by HTTP method', function () { + const requests = spec.buildRequests(bidRequests, {}); + requests.forEach(function(requestItem) { + expect(requestItem.method).to.equal('POST'); + }); + }); + }); + + describe('while interpreting bid response', function () { + beforeEach(() => { + imps.set('270544423272657', { adPartner: 'ortb_2_4', mediaType: null }); + }); + + it('should test the banner interpretResponse', function () { + const serverResponse = { + body: { + 'id': '2d2a496527398e', + 'seatbid': [ + { + 'bid': [ + { + 'id': '8f7fa506af97bc193e7bf099d8ed6930bd50aaf1', + 'impid': '270544423272657', + 'price': 0.0045000000000000005, + 'adm': '\n\n', + 'ext': { + 'btype': 1, + 'asset_mime_type': [ + 'image/jpeg', + 'image/jpg' + ] + }, + 'nurl': 'http://your-ad-network.com/', + 'cid': '6260389', + 'crid': '89453173', + 'adomain': [ + 'test.com' + ], + 'w': 300, + 'h': 250, + 'attr': [ + 12 + ] + } + ] + } + ], + 'cur': 'USD' + } + }; + + const bidResponses = spec.interpretResponse(serverResponse, { + data: JSON.stringify({ + 'id': '2d2a496527398e', + 'at': 1, + 'imp': [ + { + 'id': '270544423272657', + 'bidfloor': 1.1e-7, + 'bidfloorcur': 'EUR', + 'banner': { + 'w': 300, + 'h': 250 + } + } + ], + 'site': { + 'id': '12345', + 'domain': 'your-ad-network.com', + 'cat': [ + 'IAB25-3' + ], + 'page': 'https://your-ad-network.com/prebidJS-client-RTB-banner.html', + 'keywords': 'lifestyle, humour' + }, + 'device': { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'ip': '95.233.216.174', + 'geo': { + 'country': 'ITA' + }, + 'language': 'en', + 'os': 'MacOS', + 'js': 0, + 'ext': { + 'remote_addr': '', + 'x_forwarded_for': '', + 'accept_language': 'en-GB' + } + }, + 'user': { + 'id': '' + }, + 'ext': { + 'sub': 0 + } + }) + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + + const bid = serverResponse.body.seatbid[0].bid[0]; + const bidResponse = bidResponses[0]; + + expect(bidResponse.mediaType).to.equal(BANNER); + expect(bidResponse.width).to.equal(bid.w); + expect(bidResponse.height).to.equal(bid.h); + }); + + it('should test the native interpretResponse', function () { + const serverResponse = { + body: { + 'id': '21dea1fc6c3e1b', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'cedc93987cd4a1e08fdfe97de97482d1ecc503ee', + 'impid': '270544423272657', + 'price': 0.0045000000000000005, + 'adm': '{"native":{"link":{"url":"https:\\/\\/your-ad-network.com"},"eventtrackers":[{"event":1,"method":1,"url":"https:\\/\\/your-ad-network.com"}],"assets":[{"id":1,"title":{"text":"Title"}},{"id":2,"data":{"value":"Description"}},{"id":3,"img":{"url":"https:\\/\\/your-ad-network.com\\/32167\\/f85ee87ea23.jpg"}}]}}', + 'ext': { + 'btype': 1, + 'asset_mime_type': [ + 'image/jpeg', + 'image/jpg' + ] + }, + 'nurl': 'http://your-ad-network.com', + 'cid': '6260393', + 'crid': '89453189', + 'adomain': [ + 'test.com' + ], + 'w': 300, + 'h': 300, + 'attr': [] + } + ] + } + ], + 'cur': 'USD' + } + }; + + const bidResponses = spec.interpretResponse(serverResponse, { + data: JSON.stringify({ + 'id': '21dea1fc6c3e1b', + 'at': 1, + 'imp': [ + { + 'id': '270544423272657', + 'bidfloor': 1.1e-7, + 'bidfloorcur': 'EUR', + 'native': { + 'request': '{"native":{"ver":"1.2","context":1,"contextsubtype":10,"plcmttype":4,"plcmtcnt":4,"assets":[{"id":1,"required":1,"title":{"len":124}},{"id":2,"data":{"type":1,"len":50}},{"id":3,"required":1,"img":{"type":3,"w":300,"h":300,"wmin":300,"hmin":300}}]}}', + 'ver': '1.2' + } + } + ], + 'site': { + 'id': '12345', + 'domain': 'your-ad-network.com', + 'cat': [ + 'IAB25-3' + ], + 'page': 'https://your-ad-network.com/prebidJS-client-RTB-native.html' + }, + 'device': { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'ip': '95.233.216.174', + 'geo': { + 'country': 'ITA' + }, + 'language': 'en', + 'os': 'MacOS', + 'js': 0, + 'ext': { + 'remote_addr': '', + 'x_forwarded_for': '', + 'accept_language': 'en-GB' + } + }, + 'user': { + 'id': '' + }, + 'ext': { + 'sub': 0 + } + }) + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + + const bidResponse = bidResponses[0]; + + expect(bidResponse.mediaType).to.equal(NATIVE); + }); + + it('should test the InStream Video interpretResponse', function () { + const serverResponse = { + body: { + 'id': '2218abc7ebca97', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'd2d2063517b126252f56e22767c53f936ff40411', + 'impid': '270544423272657', + 'price': 0.12474000000000002, + 'adm': '\n\n \n \n your-ad-network.com\n \n \n \n \n \n \n 00:00:20.32\n \n \n \n \n \n \n \n \n \n \n \n \n test.com\n \n \n \n \n \n \n \n \n\n', + 'ext': { + 'btype': 1, + 'asset_mime_type': [ + 'video/mp4' + ] + }, + 'nurl': 'http://your-ad-network.com', + 'cid': '6260395', + 'crid': '89453191', + 'adomain': [ + 'test.com' + ], + 'w': 0, + 'h': 0, + 'attr': [] + } + ] + } + ], + 'cur': 'USD' + } + }; + + const bidResponses = spec.interpretResponse(serverResponse, { + data: JSON.stringify({ + 'id': '2218abc7ebca97', + 'at': 1, + 'imp': [ + { + 'id': '270544423272657', + 'video': { + 'mimes': [ + 'video/mp4' + ] + }, + 'protocols': [ + 3, + 6 + ], + 'ext': { + 'video_cta': 0 + } + } + ], + 'site': { + 'id': '12345', + 'domain': 'your-ad-network.com', + 'cat': [ + 'IAB25-3' + ], + 'page': 'https://your-ad-network.com/prebidJS-client-RTB-InStreamVideo.html', + 'keywords': 'lifestyle, humour' + }, + 'device': { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'ip': '95.233.216.174', + 'geo': { + 'country': 'ITA' + }, + 'language': 'en', + 'os': 'MacOS', + 'js': 0, + 'ext': { + 'remote_addr': '', + 'x_forwarded_for': '', + 'accept_language': 'en-GB' + } + }, + 'user': { + 'id': '' + }, + 'ext': { + 'sub': 0 + } + }) + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + + const bidResponse = bidResponses[0]; + + expect(bidResponse.mediaType).to.equal(VIDEO); + }); + }); + + describe('checking dsa information', function() { + it('should add dsa information to the request via bidderRequest.params.dsa', function () { + const bidRequests = [imageBanner]; + + const requests = spec.buildRequests(bidRequests, {}); + + requests.forEach(function(requestItem) { + const payload = JSON.parse(requestItem.data); + + expect(payload.regs.ext.dsa).to.exist; + expect(payload.regs.ext.dsa.dsarequired).to.equal(3); + expect(payload.regs.ext.dsa.pubrender).to.equal(0); + expect(payload.regs.ext.dsa.datatopub).to.equal(2); + }); + }); + + it('should test the dsa interpretResponse', function () { + const dsaResponse = { + 'behalf': '...', + 'paid': '...', + 'transparency': [ + { + 'params': [ + 2 + ] + } + ], + 'adrender': 0 + }; + + const serverResponse = { + body: { + 'id': '2d2a496527398e', + 'seatbid': [ + { + 'bid': [ + { + 'id': '8f7fa506af97bc193e7bf099d8ed6930bd50aaf1', + 'impid': '270544423272657', + 'price': 0.0045000000000000005, + 'adm': '\n\n', + 'ext': { + 'btype': 1, + 'asset_mime_type': [ + 'image/jpeg', + 'image/jpg' + ], + 'dsa': dsaResponse + }, + 'nurl': 'http://your-ad-network.com/', + 'cid': '6260389', + 'crid': '89453173', + 'adomain': [ + 'test.com' + ], + 'w': 300, + 'h': 250, + 'attr': [ + 12 + ] + } + ] + } + ], + 'cur': 'USD' + } + }; + + const bidResponses = spec.interpretResponse(serverResponse, { + data: JSON.stringify({ + 'id': '2d2a496527398e', + 'at': 1, + 'imp': [ + { + 'id': '270544423272657', + 'bidfloor': 1.1e-7, + 'bidfloorcur': 'EUR', + 'banner': { + 'w': 300, + 'h': 250 + } + } + ], + 'site': { + 'id': '12345', + 'domain': 'your-ad-network.com', + 'cat': [ + 'IAB25-3' + ], + 'page': 'https://your-ad-network.com/prebidJS-client-RTB-banner.html', + 'keywords': 'lifestyle, humour' + }, + 'device': { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'ip': '95.233.216.174', + 'geo': { + 'country': 'ITA' + }, + 'language': 'en', + 'os': 'MacOS', + 'js': 0, + 'ext': { + 'remote_addr': '', + 'x_forwarded_for': '', + 'accept_language': 'en-GB' + } + }, + 'user': { + 'id': '' + }, + 'ext': { + 'sub': 0 + }, + 'regs': { + 'ext': { + 'dsa': { + 'dsarequired': 3, + 'pubrender': 0, + 'datatopub': 2 + } + } + } + }) + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + const bidResponse = bidResponses[0]; + expect(bidResponse.meta).to.exist; + expect(bidResponse.meta.dsa).to.exist; + expect(bidResponse.meta.dsa).equal(dsaResponse); + }); + }); + + describe('on getting the win event', function() { + it('should not create nurl request if bid is undefined', function() { + const result = spec.onBidWon({}); + expect(result).to.be.undefined; + }); + }); + + describe('checking timeut', function () { + it('should exists and be a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + }); +}); diff --git a/test/spec/modules/excoBidAdapter_spec.js b/test/spec/modules/excoBidAdapter_spec.js new file mode 100644 index 00000000000..904ca67f19a --- /dev/null +++ b/test/spec/modules/excoBidAdapter_spec.js @@ -0,0 +1,214 @@ +import { expect } from 'chai'; +import { spec as adapter, SID, ENDPOINT, BIDDER_CODE } from 'modules/excoBidAdapter'; +import { BANNER } from '../../../src/mediaTypes'; +import { config } from '../../../src/config'; +import sinon from 'sinon'; + +describe('ExcoBidAdapter', function () { + const BID = { + bidId: '1731e91fa1236fd', + adUnitCode: '300x250', + bidder: BIDDER_CODE, + params: { + accountId: 'accountId', + publisherId: 'publisherId', + tagId: 'tagId', + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + transactionId: 'transactionId', + sizes: [[300, 250]], + bidderRequestId: '1677eaa35e64f46', + auctionId: 'auctionId', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }; + + const BIDDER_REQUEST = { + bidderCode: BIDDER_CODE, + auctionId: 'auctionId', + bidderRequestId: '1677eaa35e64f46', + bids: [BID], + 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', + }, + ortb2: { + site: { + content: { + language: 'en', + }, + }, + device: { + w: 1309, + h: 1305, + dnt: 0, + ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + language: 'en', + sua: { + source: 1, + platform: { brand: 'Windows' }, + browsers: [ + { brand: 'Not(A:Brand', version: ['99'] }, + { brand: 'Google Chrome', version: ['133'] }, + { brand: 'Chromium', version: ['133'] }, + ], + mobile: 0, + }, + } + }, + }; + + let BUILT_REQ = null; + + describe('isBidRequestValid', function () { + it('should return false if accountId is missing', function () { + const bid = { ...BID, params: { publisherId: 'publisherId', tagId: 'tagId' } }; + expect(adapter.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false if publisherId is missing', function () { + const bid = { ...BID, params: { accountId: 'accountId', tagId: 'tagId' } }; + expect(adapter.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false if tagId is missing', function () { + const bid = { ...BID, params: { accountId: 'accountId', publisherId: 'publisherId' } }; + expect(adapter.isBidRequestValid(bid)).to.be.false; + }); + + it('should return true if all required params are present', function () { + expect(adapter.isBidRequestValid(BID)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let sandbox; + before(function () { + sandbox = sinon.createSandbox(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build request', function () { + config.setConfig({ + bidderTimeout: 3000, + enableTIDs: true, + }); + + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + + const req = requests[0]; + expect(req.method).to.equal('POST'); + expect(req.url).to.equal(ENDPOINT); + + const ext = req.data.ext[BIDDER_CODE] || {}; + expect(ext.pbversion).to.equal('$prebid.version$'); + expect(ext.sid).to.equal(SID); + + BUILT_REQ = req; + }); + + after(function () { + sandbox.restore(); + }); + }); + + describe('interpretResponse', function () { + const SERVER_RESPONSE = { + body: { + id: 'b1ec10d0-a1af-44e5-8a85-cb7b1652fe81', + seatbid: [ + { + bid: [ + { + id: 'b7b6eddb-9924-425e-aa52-5eba56689abe', + impid: BID.bidId, + cpm: 10.56, + ad: '', + lurl: 'https://ads-ssp-stg.hit.buzz/loss?loss=${AUCTION_LOSS}&min_to_win=${AUCTION_MIN_TO_WIN}', + adomain: ['crest.com'], + iurl: 'https://thetradedesk-t-general.s3.amazonaws.com/AdvertiserLogos/qrr9d3g.png', + crid: 'h6bvt3rl', + w: 300, + h: 250, + mtype: 2, + creativeId: 'h6bvt3rl', + netRevenue: true, + mediaType: BANNER, + ext: { + advid: 37981, + bidtype: 1, + dspid: 377, + origbidcpm: 0, + origbidcur: 'USD', + wDSPByrId: '3000', + }, + currency: 'USD', + }, + ], + }, + ], + } + }; + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, BUILT_REQ); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + seatBidId: 'b7b6eddb-9924-425e-aa52-5eba56689abe', + mediaType: BANNER, + requestId: BID.bidId, + cpm: 10.56, + width: 300, + height: 250, + adapterCode: BIDDER_CODE, + bidderCode: BIDDER_CODE, + creativeId: 'h6bvt3rl', + creative_id: 'h6bvt3rl', + ttl: 3000, + eventtrackers: [], + meta: { + advertiserDomains: [ + 'crest.com' + ], + mediaType: BANNER + }, + ad: '', + netRevenue: true, + currency: 'USD', + vastXml: undefined, + adUrl: undefined, + }); + }); + + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null, BUILT_REQ); + expect(responses).to.have.length(0); + }); + + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({ price: 1, ad: '' }, BUILT_REQ); + expect(responses).to.have.length(0); + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({ + price: null, + ad: 'great ad', + }, BUILT_REQ); + expect(responses).to.have.length(0); + }); + }); +}); diff --git a/test/spec/modules/fanAdapter_spec.js b/test/spec/modules/fanAdapter_spec.js new file mode 100644 index 00000000000..010ba91339d --- /dev/null +++ b/test/spec/modules/fanAdapter_spec.js @@ -0,0 +1,315 @@ +import * as ajax from 'src/ajax.js'; +import { expect } from 'chai'; +import { spec } from 'modules/fanAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from 'src/mediaTypes.js'; + +describe('Freedom Ad Network Bid Adapter', function () { + describe('Test isBidRequestValid', function () { + it('undefined bid should return false', function () { + expect(spec.isBidRequestValid()).to.be.false; + }); + + it('null bid should return false', function () { + expect(spec.isBidRequestValid(null)).to.be.false; + }); + + it('bid.params should be set', function () { + expect(spec.isBidRequestValid({})).to.be.false; + }); + + it('bid.params.placementId should be set', function () { + expect(spec.isBidRequestValid({ + params: { foo: 'bar' } + })).to.be.false; + }); + + it('valid bid should return true', function () { + expect(spec.isBidRequestValid({ + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'e6203f1e-bd6d-4f42-9895-d1a19cdb83c8' + } + })).to.be.true; + }); + }); + + describe('Test buildRequests', function () { + const bidderRequest = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + auctionStart: Date.now(), + bidderCode: 'myBidderCode', + bidderRequestId: '15246a574e859f', + refererInfo: { + page: 'http://example.com', + stack: ['http://example.com'] + }, + gdprConsent: { + gdprApplies: true, + consentString: 'IwuyYwpjmnsauyYasIUWwe' + }, + uspConsent: 'Oush3@jmUw82has', + timeout: 3000 + }; + + it('build request object', function () { + const bidRequests = [ + { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '8064026a1776', + bidder: 'freedomadnetwork', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: { sizes: [[300, 250]] } + }, + params: { + placementId: 'e6203f1e-bd6d-4f42-9895-d1a19cdb83c8' + } + }, + { + adUnitCode: 'test-native', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '8064026a1777', + bidder: 'freedomadnetwork', + bidderRequestId: '15246a574e859f', + mediaTypes: { + native: { + title: { + required: true, + len: 20, + }, + image: { + required: true, + sizes: [300, 250], + aspect_ratios: [{ + ratio_width: 1, + ratio_height: 1 + }] + }, + icon: { + required: true, + sizes: [60, 60], + aspect_ratios: [{ + ratio_width: 1, + ratio_height: 1 + }] + }, + sponsoredBy: { + required: true, + len: 20 + }, + body: { + required: true, + len: 140 + }, + cta: { + required: true, + len: 20, + } + } + }, + params: { + placementId: '3f50a79e-5582-4e5c-b1f4-9dcc1c82cece' + } + }, + { + adUnitCode: 'test-native2', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '8064026a1778', + bidder: 'freedomadnetwork', + bidderRequestId: '15246a574e859f', + mediaTypes: { + native: { + title: {}, + image: {}, + icon: {}, + sponsoredBy: {}, + body: {}, + cta: {} + } + }, + params: { + placementId: '2015defc-19db-4cf6-926d-d2d0d32122fa', + } + }, + { + adUnitCode: 'test-native3', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '8064026a1779', + bidder: 'freedomadnetwork', + bidderRequestId: '15246a574e859f', + mediaTypes: { + native: {}, + }, + params: { + placementId: '8064026a-9932-45ae-b804-03491302ad88' + } + } + ]; + + let reqs; + + expect(function () { + reqs = spec.buildRequests(bidRequests, bidderRequest); + }).to.not.throw(); + + expect(reqs).to.be.an('array').that.have.lengthOf(bidRequests.length); + + for (let i = 0, len = reqs.length; i < len; i++) { + const req = reqs[i]; + const bidRequest = bidRequests[i]; + + expect(req.method).to.equal('POST'); + expect(req.url).to.equal('https://srv.freedomadnetwork.com/pb/req'); + + expect(req.options).to.be.an('object'); + expect(req.options.contentType).to.contain('application/json'); + expect(req.options.customHeaders).to.be.an('object'); + + expect(req.originalBidRequest).to.equal(bidRequest); + + var data = JSON.parse(req.data); + expect(data.id).to.equal(bidRequest.bidId); + expect(data.placements[0]).to.equal(bidRequest.params.placementId); + } + }); + }); + + describe('Test adapter request', function () { + const adapter = newBidder(spec); + + it('adapter.callBids exists and is a function', function () { + expect(adapter.callBids).to.be.a('function'); + }); + }); + + describe('Test response interpretResponse', function () { + it('Test main interpretResponse', function () { + const serverResponse = { + body: [{ + id: '8064026a1776', + bidid: '78e10bd4-aa67-40a6-b282-0f2697251eb3', + impid: '88faf7e7-bef8-43a5-9ef3-73db10c2af6b', + userId: '944c9c880be09af1e90da1f883538607', + cpm: 17.76, + currency: 'USD', + width: 300, + height: 250, + ttl: 60, + netRevenue: false, + crid: '03f3ed6f-1a9e-4276-8ad7-0dc5efae289e', + payload: '', + trackers: [], + mediaType: 'native', + domains: ['foo.com'], + }] + }; + + const bidResponses = spec.interpretResponse(serverResponse, { + originalBidRequest: { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '8064026a1776', + bidder: 'freedomadnetwork', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: { sizes: [[300, 250]] } + }, + params: { + placementId: 'e6203f1e-bd6d-4f42-9895-d1a19cdb83c8' + } + } + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + + const bid = serverResponse.body[0]; + const bidResponse = bidResponses[0]; + + expect(bidResponse.requestId).to.equal(bid.id); + expect(bidResponse.bidid).to.equal(bid.bidid); + expect(bidResponse.impid).to.equal(bid.impid); + expect(bidResponse.userId).to.equal(bid.userId); + expect(bidResponse.cpm).to.equal(bid.cpm); + expect(bidResponse.currency).to.equal(bid.currency); + expect(bidResponse.width).to.equal(bid.width); + expect(bidResponse.height).to.equal(bid.height); + expect(bidResponse.ad).to.equal(bid.payload); + expect(bidResponse.ttl).to.equal(bid.ttl); + expect(bidResponse.creativeId).to.equal(bid.crid); + expect(bidResponse.netRevenue).to.equal(bid.netRevenue); + expect(bidResponse.trackers).to.equal(bid.trackers); + expect(bidResponse.meta.mediaType).to.equal(bid.mediaType); + expect(bidResponse.meta.advertiserDomains).to.equal(bid.domains); + }); + + it('Test empty server response', function () { + const bidResponses = spec.interpretResponse({}, {}); + + expect(bidResponses).to.be.an('array').that.is.empty; + }); + + it('Test empty bid response', function () { + const bidResponses = spec.interpretResponse({ body: [] }, {}); + + expect(bidResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('Test getUserSyncs', function () { + it('getUserSyncs should return empty', function () { + const serverResponse = {}; + const syncOptions = {} + const userSyncPixels = spec.getUserSyncs(syncOptions, [serverResponse]) + expect(userSyncPixels).to.have.lengthOf(0); + }); + }); + + describe('Test onTimeout', function () { + it('onTimeout should not throw', function () { + expect(spec.onTimeout()).to.not.throw; + }); + }); + + describe('Test onBidWon', function () { + let sandbox, ajaxStub; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + ajaxStub = sandbox.stub(ajax, 'ajax'); + }); + + afterEach(function () { + sandbox.restore(); + ajaxStub.restore(); + }); + + const bid = { + bidid: '78e10bd4-aa67-40a6-b282-0f2697251eb3', + impid: '88faf7e7-bef8-43a5-9ef3-73db10c2af6b', + cpm: 17.76, + trackers: ['foo.com'], + } + + it('onBidWon empty bid should not throw', function () { + expect(spec.onBidWon({})).to.not.throw; + expect(ajaxStub.calledOnce).to.equal(true); + }); + + it('onBidWon valid bid should not throw', function () { + expect(spec.onBidWon(bid)).to.not.throw; + expect(ajaxStub.calledOnce).to.equal(true); + }); + }); + + describe('Test onSetTargeting', function () { + it('onSetTargeting should not throw', function () { + expect(spec.onSetTargeting()).to.not.throw; + }); + }); +}); diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js deleted file mode 100644 index 8ab11171121..00000000000 --- a/test/spec/modules/fledgeForGpt_spec.js +++ /dev/null @@ -1,177 +0,0 @@ -import {onAuctionConfigFactory, setPAAPIConfigFactory, slotConfigurator} from 'modules/fledgeForGpt.js'; -import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; -import 'modules/appnexusBidAdapter.js'; -import 'modules/rubiconBidAdapter.js'; -import {deepSetValue} from '../../../src/utils.js'; -import {config} from 'src/config.js'; - -describe('fledgeForGpt module', () => { - let sandbox, fledgeAuctionConfig; - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - fledgeAuctionConfig = { - seller: 'bidder', - mock: 'config' - }; - }); - afterEach(() => { - sandbox.restore(); - }); - - describe('slotConfigurator', () => { - let mockGptSlot, setGptConfig; - beforeEach(() => { - mockGptSlot = { - setConfig: sinon.stub(), - getAdUnitPath: () => 'mock/gpt/au' - }; - sandbox.stub(gptUtils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot); - setGptConfig = slotConfigurator(); - }); - it('should set GPT slot config', () => { - setGptConfig('au', [fledgeAuctionConfig]); - sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au'); - sinon.assert.calledWith(mockGptSlot.setConfig, { - componentAuction: [{ - configKey: 'bidder', - auctionConfig: fledgeAuctionConfig, - }] - }); - }); - - describe('when reset = true', () => { - it('should reset GPT slot config', () => { - setGptConfig('au', [fledgeAuctionConfig]); - mockGptSlot.setConfig.resetHistory(); - gptUtils.getGptSlotForAdUnitCode.resetHistory(); - setGptConfig('au', [], true); - sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au'); - sinon.assert.calledWith(mockGptSlot.setConfig, { - componentAuction: [{ - configKey: 'bidder', - auctionConfig: null - }] - }); - }); - - it('should reset only sellers with no fresh config', () => { - setGptConfig('au', [{seller: 's1'}, {seller: 's2'}]); - mockGptSlot.setConfig.resetHistory(); - setGptConfig('au', [{seller: 's1'}], true); - sinon.assert.calledWith(mockGptSlot.setConfig, { - componentAuction: [{ - configKey: 's1', - auctionConfig: {seller: 's1'} - }, { - configKey: 's2', - auctionConfig: null - }] - }) - }); - - it('should not reset sellers that were already reset', () => { - setGptConfig('au', [{seller: 's1'}]); - setGptConfig('au', [], true); - mockGptSlot.setConfig.resetHistory(); - setGptConfig('au', [], true); - sinon.assert.notCalled(mockGptSlot.setConfig); - }) - - it('should keep track of configuration history by slot', () => { - setGptConfig('au1', [{seller: 's1'}]); - setGptConfig('au1', [{seller: 's2'}], false); - setGptConfig('au2', [{seller: 's3'}]); - mockGptSlot.setConfig.resetHistory(); - setGptConfig('au1', [], true); - sinon.assert.calledWith(mockGptSlot.setConfig, { - componentAuction: [{ - configKey: 's1', - auctionConfig: null - }, { - configKey: 's2', - auctionConfig: null - }] - }); - }) - }); - }); - describe('onAuctionConfig', () => { - [ - 'fledgeForGpt', - 'paapi.gpt' - ].forEach(namespace => { - describe(`using ${namespace} config`, () => { - Object.entries({ - 'omitted': [undefined, true], - 'enabled': [true, true], - 'disabled': [false, false] - }).forEach(([t, [autoconfig, shouldSetConfig]]) => { - describe(`when autoconfig is ${t}`, () => { - beforeEach(() => { - const cfg = {}; - deepSetValue(cfg, `${namespace}.autoconfig`, autoconfig); - config.setConfig(cfg); - }); - afterEach(() => { - config.resetConfig(); - }); - - it(`should ${shouldSetConfig ? '' : 'NOT'} set GPT slot configuration`, () => { - const auctionConfig = {componentAuctions: [{seller: 'mock1'}, {seller: 'mock2'}]}; - const setGptConfig = sinon.stub(); - const markAsUsed = sinon.stub(); - onAuctionConfigFactory(setGptConfig)('aid', {au1: auctionConfig, au2: null}, markAsUsed); - if (shouldSetConfig) { - sinon.assert.calledWith(setGptConfig, 'au1', auctionConfig.componentAuctions); - sinon.assert.calledWith(setGptConfig, 'au2', []); - sinon.assert.calledWith(markAsUsed, 'au1'); - } else { - sinon.assert.notCalled(setGptConfig); - sinon.assert.notCalled(markAsUsed); - } - }); - }) - }) - }) - }) - }); - describe('setPAAPIConfigForGpt', () => { - let getPAAPIConfig, setGptConfig, setPAAPIConfigForGPT; - beforeEach(() => { - getPAAPIConfig = sinon.stub(); - setGptConfig = sinon.stub(); - setPAAPIConfigForGPT = setPAAPIConfigFactory(getPAAPIConfig, setGptConfig); - }); - - Object.entries({ - missing: null, - empty: {} - }).forEach(([t, configs]) => { - it(`does not set GPT slot config when config is ${t}`, () => { - getPAAPIConfig.returns(configs); - setPAAPIConfigForGPT('mock-filters'); - sinon.assert.calledWith(getPAAPIConfig, 'mock-filters'); - sinon.assert.notCalled(setGptConfig); - }) - }); - - it('sets GPT slot config for each ad unit that has PAAPI config, and resets the rest', () => { - const cfg = { - au1: { - componentAuctions: [{seller: 's1'}, {seller: 's2'}] - }, - au2: { - componentAuctions: [{seller: 's3'}] - }, - au3: null - } - getPAAPIConfig.returns(cfg); - setPAAPIConfigForGPT('mock-filters'); - sinon.assert.calledWith(getPAAPIConfig, 'mock-filters'); - Object.entries(cfg).forEach(([au, config]) => { - sinon.assert.calledWith(setGptConfig, au, config?.componentAuctions ?? [], true); - }) - }); - }) -}); diff --git a/test/spec/modules/flippBidAdapter_spec.js b/test/spec/modules/flippBidAdapter_spec.js index 518052ad91e..9602a156bed 100644 --- a/test/spec/modules/flippBidAdapter_spec.js +++ b/test/spec/modules/flippBidAdapter_spec.js @@ -99,6 +99,14 @@ describe('flippAdapter', function () { 'requestId': '237f4d1a293f99', 'cpm': 1.11, 'creative': 'Returned from server', + }, + 'contents': { + 'data': { + 'customData': { + 'compactHeight': 600, + 'standardHeight': 1800 + } + } } }] }, @@ -114,7 +122,7 @@ describe('flippAdapter', function () { cpm: 1.11, netRevenue: true, width: 300, - height: 600, + height: 1800, creativeId: 262838368, ttl: 30, ad: 'Returned from server', diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index ff6f8562a4e..3335e8b23cc 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -26,30 +26,30 @@ describe('fluctAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return true when dfpUnitCode is not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { tagId: '10000:100000001', groupId: '1000000002', }; - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(spec.isBidRequestValid(invalidBid)).to.equal(true); }); it('should return false when groupId is not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { dfpUnitCode: '/1000/dfp_unit_code', tagId: '10000:100000001', }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -402,6 +402,36 @@ describe('fluctAdapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(request.data.regs.coppa).to.eql(1); }); + + it('includes data.regs.gpp.string and data.regs.gpp.sid if bidderRequest.gppConsent exists', function () { + const request = spec.buildRequests( + bidRequests, + Object.assign({}, bidderRequest, { + gppConsent: { + gppString: 'gpp-consent-string', + applicableSections: [1, 2, 3], + }, + }), + )[0]; + expect(request.data.regs.gpp.string).to.eql('gpp-consent-string'); + expect(request.data.regs.gpp.sid).to.eql([1, 2, 3]); + }); + + it('includes data.regs.gpp.string and data.regs.gpp.sid if bidderRequest.ortb2.regs.gpp exists', function () { + const request = spec.buildRequests( + bidRequests, + Object.assign({}, bidderRequest, { + ortb2: { + regs: { + gpp: 'gpp-consent-string', + gpp_sid: [1, 2, 3], + }, + }, + }), + )[0]; + expect(request.data.regs.gpp.string).to.eql('gpp-consent-string'); + expect(request.data.regs.gpp.sid).to.eql([1, 2, 3]); + }); }); describe('should interpretResponse', function() { diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index 90ebe0b80ee..94b7f04b637 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -41,12 +41,12 @@ describe('freewheelSSP BidAdapter Test', () => { }); it('should return false when required params are not passed', () => { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { wrong: 'missing zone id' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -73,12 +73,12 @@ describe('freewheelSSP BidAdapter Test', () => { }); it('should return false when required params are not passed', () => { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { wrong: 'missing zone id' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js index ecd610a12fb..9d8c8b40bfc 100644 --- a/test/spec/modules/ftrackIdSystem_spec.js +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -3,10 +3,10 @@ import * as utils from 'src/utils.js'; import { uspDataHandler } from 'src/adapterManager.js'; import { loadExternalScript } from 'src/adloader.js'; import { getGlobal } from 'src/prebidGlobal.js'; -import { init, setSubmoduleRegistry } from 'modules/userId/index.js'; +import {attachIdSystem, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {createEidsArray} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; -let expect = require('chai').expect; +import 'src/prebid.js'; let server; @@ -105,41 +105,28 @@ describe('FTRACK ID System', () => { }); describe(`ftrackIdSubmodule.isThereConsent():`, () => { - let uspDataHandlerStub; - beforeEach(() => { - uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); - }); - - afterEach(() => { - uspDataHandlerStub.restore(); - }); - describe(`returns 'false' if:`, () => { it(`GDPR: if gdprApplies is truthy`, () => { - expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 1})).to.not.be.ok; - expect(ftrackIdSubmodule.isThereConsent({gdprApplies: true})).to.not.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdpr: {gdprApplies: 1}})).to.not.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdpr: {gdprApplies: true}})).to.not.be.ok; }); it(`US_PRIVACY version 1: if 'Opt Out Sale' is 'Y'`, () => { - uspDataHandlerStub.returns('1YYY'); - expect(ftrackIdSubmodule.isThereConsent({})).to.not.be.ok; + expect(ftrackIdSubmodule.isThereConsent({usp: '1YYY'})).to.not.be.ok; }); }); describe(`returns 'true' if`, () => { it(`GDPR: if gdprApplies is undefined, false or 0`, () => { - expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 0})).to.be.ok; - expect(ftrackIdSubmodule.isThereConsent({gdprApplies: false})).to.be.ok; - expect(ftrackIdSubmodule.isThereConsent({gdprApplies: null})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdpr: {gdprApplies: 0}})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdpr: {gdprApplies: false}})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdpr: {gdprApplies: null}})).to.be.ok; expect(ftrackIdSubmodule.isThereConsent({})).to.be.ok; }); it(`US_PRIVACY version 1: if 'Opt Out Sale' is not 'Y' ('N','-')`, () => { - uspDataHandlerStub.returns('1NNN'); - expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok; - - uspDataHandlerStub.returns('1---'); - expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({usp: '1NNN'})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({usp: '1---'})).to.be.ok; }); }); }); @@ -380,10 +367,10 @@ describe('FTRACK ID System', () => { } }); - getGlobal().getUserIdsAsync().then(ids => { - expect(ids).to.deep.equal({ + return getGlobal().getUserIdsAsync().then(ids => { + expect(ids.ftrackId).to.deep.equal({ uid: 'device_test_id', - ftrackId: { + ext: { HHID: 'household_test_id', DeviceID: 'device_test_id', SingleDeviceID: 'single_device_test_id' @@ -394,7 +381,7 @@ describe('FTRACK ID System', () => { }); describe('pbjs.getUserIds()', () => { - it('should return the IDs in the correct schema', () => { + it('should return the IDs in the correct schema', async () => { config.setConfig({ userSync: { auctionDelay: 10, @@ -414,6 +401,8 @@ describe('FTRACK ID System', () => { } }); + await getGlobal().getUserIdsAsync(); + expect(getGlobal().getUserIds()).to.deep.equal({ ftrackId: { uid: 'device_test_id', @@ -428,7 +417,7 @@ describe('FTRACK ID System', () => { }); describe('pbjs.getUserIdsAsEids()', () => { - it('should return the correct EIDs schema ', () => { + it('should return the correct EIDs schema ', async () => { // Pass all three IDs config.setConfig({ userSync: { @@ -449,6 +438,8 @@ describe('FTRACK ID System', () => { } }); + await getGlobal().getUserIdsAsync(); + expect(getGlobal().getUserIdsAsEids()).to.deep.equal([{ source: 'flashtalking.com', uids: [{ @@ -464,7 +455,7 @@ describe('FTRACK ID System', () => { }); describe('by ID type:', () => { - it('- DeviceID', () => { + it('- DeviceID', async () => { // Pass DeviceID only config.setConfig({ userSync: { @@ -483,6 +474,8 @@ describe('FTRACK ID System', () => { } }); + await getGlobal().getUserIdsAsync(); + expect(getGlobal().getUserIdsAsEids()).to.deep.equal([{ source: 'flashtalking.com', uids: [{ @@ -495,7 +488,7 @@ describe('FTRACK ID System', () => { }]); }); - it('- HHID', () => { + it('- HHID', async () => { // Pass HHID only config.setConfig({ userSync: { @@ -514,6 +507,8 @@ describe('FTRACK ID System', () => { } }); + await getGlobal().getUserIdsAsync(); + expect(getGlobal().getUserIdsAsEids()).to.deep.equal([{ source: 'flashtalking.com', uids: [{ @@ -526,7 +521,7 @@ describe('FTRACK ID System', () => { }]); }); - it('- SingleDeviceID', () => { + it('- SingleDeviceID', async () => { // Pass SingleDeviceID only config.setConfig({ userSync: { @@ -545,6 +540,8 @@ describe('FTRACK ID System', () => { } }); + await getGlobal().getUserIdsAsync(); + expect(getGlobal().getUserIdsAsEids()).to.deep.equal([{ source: 'flashtalking.com', uids: [{ @@ -558,5 +555,40 @@ describe('FTRACK ID System', () => { }); }); }); + }); + describe('eid', () => { + before(() => { + attachIdSystem(ftrackIdSubmodule); + }); + it('should return the correct EID schema', () => { + // This is the schema returned from the ftrack decode() method + expect(createEidsArray({ + ftrackId: { + uid: 'test-device-id', + ext: { + DeviceID: 'test-device-id', + SingleDeviceID: 'test-single-device-id', + HHID: 'test-household-id' + } + }, + foo: { + bar: 'baz' + }, + lorem: { + ipsum: '' + } + })).to.deep.equal([{ + source: 'flashtalking.com', + uids: [{ + atype: 1, + id: 'test-device-id', + ext: { + DeviceID: 'test-device-id', + SingleDeviceID: 'test-single-device-id', + HHID: 'test-household-id' + } + }] + }]); + }); }) }); diff --git a/test/spec/modules/gameraRtdProvider_spec.js b/test/spec/modules/gameraRtdProvider_spec.js new file mode 100644 index 00000000000..63029d85545 --- /dev/null +++ b/test/spec/modules/gameraRtdProvider_spec.js @@ -0,0 +1,223 @@ +import { submodule } from 'src/hook.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +import * as utils from 'src/utils.js'; +import { subModuleObj } from 'modules/gameraRtdProvider.js'; + +describe('gameraRtdProvider', function () { + let logErrorSpy; + + beforeEach(function () { + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(function () { + logErrorSpy.restore(); + }); + + describe('subModuleObj', function () { + it('should have the correct module name', function () { + expect(subModuleObj.name).to.equal('gamera'); + }); + + it('successfully instantiates and returns true', function () { + expect(subModuleObj.init()).to.equal(true); + }); + }); + + describe('getBidRequestData', function () { + const reqBidsConfigObj = { + adUnits: [{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123', + } + } + }, + bids: [{ bidder: 'test' }] + }], + ortb2Fragments: { + global: { + site: { + name: 'example', + domain: 'page.example.com', + // OpenRTB 2.5 spec / Content Taxonomy + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + keywords: 'power tools, drills', + search: 'drill', + content: { + userrating: '4', + data: [{ + name: 'www.dataprovider1.com', // who resolved the segments + ext: { + segtax: 7, // taxonomy used to encode the segments + cids: ['iris_c73g5jq96mwso4d8'] + }, + // the bare minimum are the IDs. These IDs are the ones from the new IAB Content Taxonomy v3 + segment: [{ id: '687' }, { id: '123' }] + }] + }, + ext: { + data: { // fields that aren't part of openrtb 2.6 + pageType: 'article', + category: 'repair' + } + } + }, + // this is where the user data is placed + user: { + keywords: 'a,b', + data: [{ + name: 'dataprovider.com', + ext: { + segtax: 4 + }, + segment: [{ + id: '1' + }] + }], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + } + } + }; + + let callback; + + beforeEach(function () { + callback = sinon.spy(); + window.gamera = undefined; + }); + + it('should queue command when gamera.getPrebidSegments is not available', function () { + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + + expect(window.gamera).to.exist; + expect(window.gamera.cmd).to.be.an('array'); + expect(window.gamera.cmd.length).to.equal(1); + expect(callback.called).to.be.false; + + // our callback should be executed if command queue is flushed + window.gamera.cmd.forEach(command => command()); + expect(callback.calledOnce).to.be.true; + }); + + it('should call enrichAuction directly when gamera.getPrebidSegments is available', function () { + window.gamera = { + getPrebidSegments: () => ({}) + }; + + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + + expect(callback.calledOnce).to.be.true; + }); + + it('should handle errors gracefully', function () { + window.gamera = { + getPrebidSegments: () => { + throw new Error('Test error'); + } + }; + + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + + expect(logErrorSpy.calledWith('gameraRtdProvider', 'Error getting segments:')).to.be.true; + expect(callback.calledOnce).to.be.true; + }); + + describe('segment enrichment', function () { + const mockSegments = { + user: { + data: [{ + name: 'gamera.ai', + ext: { + segtax: 4, + }, + segment: [{ id: 'user-1' }] + }] + }, + site: { + keywords: 'gamera,article,keywords', + content: { + data: [{ + name: 'gamera.ai', + ext: { + segtax: 7, + }, + segment: [{ id: 'site-1' }] + }] + } + }, + adUnits: { + 'test-div': { + key: 'value', + ext: { + data: { + gameraSegment: 'ad-1', + } + } + } + } + }; + + beforeEach(function () { + window.gamera = { + getPrebidSegments: () => mockSegments + }; + }); + + it('should enrich ortb2Fragments with user data', function () { + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + + expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.deep.include(mockSegments.user.data[0]); + + // check if existing attributes are not overwritten + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].ext.segtax).to.equal(4); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment[0].id).to.equal('1'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.keywords).to.equal('a,b'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.ext.data.registered).to.equal(true); + }); + + it('should enrich ortb2Fragments with site data', function () { + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data).to.deep.include(mockSegments.site.content.data[0]); + expect(reqBidsConfigObj.ortb2Fragments.global.site.keywords).to.equal('gamera,article,keywords'); + + // check if existing attributes are not overwritten + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].ext.segtax).to.equal(7); + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].segment[0].id).to.equal('687'); + expect(reqBidsConfigObj.ortb2Fragments.global.site.ext.data.category).to.equal('repair'); + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.userrating).to.equal('4'); + }); + + it('should enrich adUnits with segment data', function () { + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + + expect(reqBidsConfigObj.adUnits[0].ortb2Imp.key).to.equal('value'); + expect(reqBidsConfigObj.adUnits[0].ortb2Imp.ext.data.gameraSegment).to.equal('ad-1'); + + // check if existing attributes are not overwritten + expect(reqBidsConfigObj.adUnits[0].ortb2Imp.ext.data.adUnitSpecificAttribute).to.equal('123'); + expect(reqBidsConfigObj.adUnits[0].ortb2Imp.ext.data.pbadslot).to.equal('homepage-top-rect'); + }); + }); + }); +}); diff --git a/test/spec/modules/gammaBidAdapter_spec.js b/test/spec/modules/gammaBidAdapter_spec.js index f3a28c08576..2c83c3912e3 100644 --- a/test/spec/modules/gammaBidAdapter_spec.js +++ b/test/spec/modules/gammaBidAdapter_spec.js @@ -28,9 +28,9 @@ describe('gammaBidAdapter', function() { }); it('should return false when require params are not passed', () => { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when params not passed correctly', () => { diff --git a/test/spec/modules/gamoshiBidAdapter_spec.js b/test/spec/modules/gamoshiBidAdapter_spec.js index 984830f67d4..8f9818ed901 100644 --- a/test/spec/modules/gamoshiBidAdapter_spec.js +++ b/test/spec/modules/gamoshiBidAdapter_spec.js @@ -379,7 +379,7 @@ describe('GamoshiAdapter', () => { const bidRequestWithVideo = utils.deepClone(bidRequest); bidRequestWithVideo.params.video = { - placement: 1, + plcmt: 1, minduration: 1, } @@ -398,7 +398,7 @@ describe('GamoshiAdapter', () => { expect(response.data.imp[0].video.mimes).to.equal(bidRequestWithVideo.mediaTypes.video.mimes); expect(response.data.imp[0].video.skip).to.not.exist; - expect(response.data.imp[0].video.placement).to.equal(1); + expect(response.data.imp[0].video.plcmt).to.equal(1); expect(response.data.imp[0].video.minduration).to.equal(1); expect(response.data.imp[0].video.playbackmethod).to.equal(1); expect(response.data.imp[0].video.startdelay).to.equal(1); @@ -408,7 +408,7 @@ describe('GamoshiAdapter', () => { playerSize: [302, 252], mimes: ['video/mpeg'], skip: 1, - placement: 1, + plcmt: 1, minduration: 1, playbackmethod: 1, startdelay: 1, @@ -431,7 +431,7 @@ describe('GamoshiAdapter', () => { context: 'instream', mimes: ['video/mpeg'], skip: 1, - placement: 1, + plcmt: 1, minduration: 1, playbackmethod: 1, startdelay: 1, @@ -440,6 +440,7 @@ describe('GamoshiAdapter', () => { let response = spec.buildRequests([bidRequestWithVideo], bidRequest)[0]; expect(response.data.imp[0].video.ext.context).to.equal('instream'); bidRequestWithVideo.mediaTypes.video.context = 'outstream'; + bidRequestWithVideo.mediaTypes.video.context = 'outstream'; const bidRequestWithPosEquals1 = utils.deepClone(bidRequestWithVideo); bidRequestWithPosEquals1.mediaTypes.video.context = 'outstream'; @@ -460,7 +461,7 @@ describe('GamoshiAdapter', () => { context: 'instream', mimes: ['video/mpeg'], skip: 1, - placement: 1, + plcmt: 1, minduration: 1, playbackmethod: 1, startdelay: 1, diff --git a/test/spec/modules/genericAnalyticsAdapter_spec.js b/test/spec/modules/genericAnalyticsAdapter_spec.js index 2d9c7b4ae45..8ec61b70810 100644 --- a/test/spec/modules/genericAnalyticsAdapter_spec.js +++ b/test/spec/modules/genericAnalyticsAdapter_spec.js @@ -120,7 +120,7 @@ describe('Generic analytics', () => { recv = arg; }); events.emit(BID_RESPONSE, {i: 1}); - expect(recv).to.eql([{eventType: BID_RESPONSE, args: {i: 1}}]); + sinon.assert.match(recv, [sinon.match({eventType: BID_RESPONSE, args: {i: 1}})]) }); it('should not cause infinite recursion, if handler triggers more events', () => { @@ -265,7 +265,7 @@ describe('Generic analytics', () => { handler([payload, {}]); sinon.assert.calledWith(ajax, url, sinon.match.any, sinon.match(data => sinon.match(payload).test(parse(data))), - {method} + {method, keepalive: true} ); }); @@ -275,7 +275,7 @@ describe('Generic analytics', () => { handler(payload); sinon.assert.calledWith(ajax, url, sinon.match.any, sinon.match(data => sinon.match(payload).test(parse(data))), - {method} + {method, keepalive: true} ); }); }); diff --git a/test/spec/modules/geoedgeRtdProvider_spec.js b/test/spec/modules/geoedgeRtdProvider_spec.js index ecc24bce6b5..5f3f2a5b1b8 100644 --- a/test/spec/modules/geoedgeRtdProvider_spec.js +++ b/test/spec/modules/geoedgeRtdProvider_spec.js @@ -143,6 +143,14 @@ describe('Geoedge RTD module', function () { const hasCurrency = dict['%_hbCurrency!'] === bid.currency; expect(hasCpm && hasCurrency); }); + it('return a dictionary of macros replaced with values from overrides object if provided', function () { + const bid = mockBid('testBidder'); + window.grumi.overrides = { site: 'test-overrides' }; + const overrides = window.grumi.overrides; + const dict = getMacros(bid, key); + const siteOveridden = dict['%%SITE%%'] === overrides.site; + expect(siteOveridden); + }); }); describe('onBidResponseEvent', function () { const bidFromA = mockBid('bidderA'); diff --git a/test/spec/modules/geolocationRtdProvider_spec.js b/test/spec/modules/geolocationRtdProvider_spec.js index 20f3b2f91dd..764d378742d 100644 --- a/test/spec/modules/geolocationRtdProvider_spec.js +++ b/test/spec/modules/geolocationRtdProvider_spec.js @@ -2,7 +2,7 @@ import {expect} from 'chai'; import {geolocationSubmodule} from 'modules/geolocationRtdProvider.js'; import * as activityRules from 'src/activities/rules.js'; import 'src/prebid.js'; -import {GreedyPromise} from '../../../src/utils/promise.js'; +import {PbPromise} from '../../../src/utils/promise.js'; import {ACTIVITY_TRANSMIT_PRECISE_GEO} from '../../../src/activities/activities.js'; describe('Geolocation RTD Provider', function () { @@ -43,7 +43,7 @@ describe('Geolocation RTD Provider', function () { }); describe('Geolocation supported', function() { - let clock, rtdConfig, permState, onDone; + let clock, rtdConfig, permState, permGiven, onDone; beforeEach(() => { onDone = sinon.stub(); @@ -51,12 +51,16 @@ describe('Geolocation RTD Provider', function () { rtdConfig = {params: {}}; clock = sandbox.useFakeTimers(11000); sandbox.stub(navigator.geolocation, 'getCurrentPosition').value((cb) => { - // eslint-disable-next-line standard/no-callback-literal cb({coords: {latitude: 1, longitude: 2}, timestamp: 1000}); }); - sandbox.stub(navigator.permissions, 'query').value(() => GreedyPromise.resolve({ - state: permState, - })); + permGiven = new Promise((resolve) => { + sandbox.stub(navigator.permissions, 'query').value(() => { + permGiven = Promise.resolve({ + state: permState, + }) + return permGiven; + }); + }) geolocationSubmodule.init(rtdConfig); }); @@ -75,9 +79,10 @@ describe('Geolocation RTD Provider', function () { rtdConfig.params.requestPermission = requestPermission; }); - it(`should set geolocation`, () => { + it(`should set geolocation`, async () => { const requestBidObject = {ortb2Fragments: {global: {}}}; geolocationSubmodule.getBidRequestData(requestBidObject, onDone, rtdConfig); + await permGiven; clock.tick(300); expect(onDone.called).to.be.true; expect(requestBidObject.ortb2Fragments.global.device.geo).to.eql({ diff --git a/test/spec/modules/globalsunBidAdapter_spec.js b/test/spec/modules/globalsunBidAdapter_spec.js index 0d17c25363d..f8d6e2b710d 100644 --- a/test/spec/modules/globalsunBidAdapter_spec.js +++ b/test/spec/modules/globalsunBidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/globalsunBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'globalsun' +const bidder = 'globalsun'; describe('GlobalsunBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('GlobalsunBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('GlobalsunBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('GlobalsunBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -73,9 +86,20 @@ describe('GlobalsunBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -112,6 +136,7 @@ describe('GlobalsunBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -120,7 +145,11 @@ describe('GlobalsunBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -129,7 +158,7 @@ describe('GlobalsunBidAdapter', function () { 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('string'); + 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); @@ -145,6 +174,56 @@ describe('GlobalsunBidAdapter', function () { 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 + } + ]; + + let 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'); @@ -170,8 +249,10 @@ describe('GlobalsunBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -186,12 +267,38 @@ describe('GlobalsunBidAdapter', function () { 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -395,5 +502,17 @@ describe('GlobalsunBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://cs.globalsun.io/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.globalsun.io/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/gmosspBidAdapter_spec.js b/test/spec/modules/gmosspBidAdapter_spec.js index 8c3aa6c94cb..77644b136db 100644 --- a/test/spec/modules/gmosspBidAdapter_spec.js +++ b/test/spec/modules/gmosspBidAdapter_spec.js @@ -27,10 +27,10 @@ describe('GmosspAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/gnetBidAdapter_spec.js b/test/spec/modules/gnetBidAdapter_spec.js index f1af3b71103..8e2cfadc96b 100644 --- a/test/spec/modules/gnetBidAdapter_spec.js +++ b/test/spec/modules/gnetBidAdapter_spec.js @@ -32,10 +32,10 @@ describe('gnetAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/goldbachBidAdapter_spec.js b/test/spec/modules/goldbachBidAdapter_spec.js index 93956d2caf9..744379efd19 100644 --- a/test/spec/modules/goldbachBidAdapter_spec.js +++ b/test/spec/modules/goldbachBidAdapter_spec.js @@ -1,16 +1,424 @@ import { expect } from 'chai'; +import sinon from 'sinon'; import { spec } from 'modules/goldbachBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import * as bidderFactory from 'src/adapters/bidderFactory.js'; import { auctionManager } from 'src/auctionManager.js'; import { deepClone } from 'src/utils.js'; -import { config } from 'src/config.js'; +import { VIDEO } from 'src/mediaTypes.js'; +import * as ajaxLib from 'src/ajax.js'; -const ENDPOINT = 'https://ib.adnxs.com/ut/v3/prebid'; -const PRICING_ENDPOINT = 'https://templates.da-services.ch/01_universal/burda_prebid/1.0/json/sizeCPMMapping.json'; +const BIDDER_NAME = 'goldbach' +const ENDPOINT = 'https://goldlayer-api.prod.gbads.net/bid/pbjs'; -describe('GoldbachXandrAdapter', function () { +/* Eids */ +let eids = [ + { + source: 'goldbach.com', + uids: [ + { + id: '0d862e87-14e9-47a4-9e9b-886b7d7a9d1b', + atype: 1, + ext: { stype: 'ppuid' } + } + ] + }, + { + source: 'niceid.live', + uids: [ + { + id: '0d862e87-14e9-47a4-9e9b-886b7d7a9d1a', + atype: 1, + ext: { stype: 'ppuid' } + } + ] + }, + { + source: 'otherid.live', + uids: [ + { + id: '0d862e87-14e9-47a4-9e9b-886b7d7a9d1a', + atype: 1, + ext: { stype: 'other-id' } + } + ] + } +]; + +const validNativeAd = { + link: { + url: 'https://example.com/cta', + }, + imptrackers: [ + 'https://example.com/impression1', + 'https://example.com/impression2', + ], + assets: [ + { + id: 1, + title: { + text: 'Amazing Product - Don’t Miss Out!', + }, + }, + { + id: 2, + img: { + url: 'https://example.com/main-image.jpg', + w: 300, + h: 250, + }, + }, + { + id: 3, + img: { + url: 'https://example.com/icon-image.jpg', + w: 50, + h: 50, + }, + }, + { + id: 4, + data: { + value: 'This is the description of the product. Its so good youll love it!', + }, + }, + { + id: 5, + data: { + value: 'Sponsored by ExampleBrand', + }, + }, + { + id: 6, + data: { + value: 'Shop Now', + }, + }, + ], +}; + +/* Ortb2 bid information */ +let ortb2 = { + device: { + ip: '133.713.371.337', + connectiontype: 6, + w: 1512, + h: 982, + ifa: '23575619-ef35-4908-b468-ffc4000cdf07', + 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', + geo: {lat: 47.318054, lon: 8.582883, zip: '8700'} + }, + site: { + domain: 'publisher-page.ch', + page: 'https://publisher-page.ch/home', + publisher: { domain: 'publisher-page.ch' }, + ref: 'https://publisher-page.ch/home' + }, + user: { + ext: { + eids: eids + } + } +}; + +/* Minimal bidderRequest */ +let validBidderRequest = { + auctionId: '7570fb24-810d-4c26-9f9c-acd0b6977f60', + start: 1731680672810, + auctionStart: 1731680672808, + ortb2: ortb2, + bidderCode: BIDDER_NAME, + gdprConsent: { + gdprApplies: true, + consentString: 'trust-me-i-consent' + }, + timeout: 300 +}; + +/* Minimal validBidRequests */ +let validBidRequests = [ + { + bidder: BIDDER_NAME, + adUnitCode: 'au-1', + adUnitId: 'c3400db6-c4c5-465e-bf67-1545751944b7', + auctionId: '7570fb24-810d-4c26-9f9c-acd0b6977f60', + bidId: '3d52a1909b972a', + bidderRequestId: '2b63a1826ab946', + userIdAsEids: eids, + ortb2: ortb2, + mediaTypes: { + banner: { + sizes: [[300, 50], [300, 250], [300, 600], [320, 50], [320, 480], [320, 64], [320, 160], [320, 416], [336, 280]] + } + }, + sizes: [[300, 50], [300, 250], [300, 600], [320, 50], [320, 480], [320, 64], [320, 160], [320, 416], [336, 280]], + params: { + publisherId: 'de-publisher.ch-ios', + slotId: '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test', + customTargeting: { + language: 'de' + } + } + }, + { + bidder: BIDDER_NAME, + adUnitCode: 'au-2', + adUnitId: 'c3400db6-c4c5-465e-bf67-1545751944b8', + auctionId: '7570fb24-810d-4c26-9f9c-acd0b6977f60', + bidId: '3d52a1909b972b', + bidderRequestId: '2b63a1826ab946', + userIdAsEids: eids, + ortb2: ortb2, + mediaTypes: { + video: { + sizes: [[640, 480]] + } + }, + sizes: [[640, 480]], + params: { + publisherId: 'de-publisher.ch-ios', + slotId: '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test/video', + video: { + maxduration: 30, + }, + customTargeting: { + language: 'de' + } + } + }, + { + bidder: BIDDER_NAME, + adUnitCode: 'au-3', + adUnitId: 'c3400db6-c4c5-465e-bf67-1545751944b9', + auctionId: '7570fb24-810d-4c26-9f9c-acd0b6977f60', + bidId: '3d52a1909b972c', + bidderRequestId: '2b63a1826ab946', + userIdAsEids: eids, + ortb2: ortb2, + mediaTypes: { + native: { + title: { + required: true, + len: 50 + }, + image: { + required: true, + sizes: [300, 157] + }, + icon: { + required: true, + sizes: [30, 30] + }, + body: { + required: true, + len: 150 + }, + cta: { + required: true, + len: 15 + }, + sponsoredBy: { + required: true, + len: 25 + }, + } + }, + params: { + publisherId: 'de-publisher.ch-ios', + slotId: '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test/native', + customTargeting: { + language: 'de' + } + } + } +]; + +/* Creative request send to server */ +let validCreativeRequest = { + mock: false, + debug: false, + timestampStart: 1731680672811, + timestampEnd: 1731680675811, + config: { + publisher: { + id: 'de-20minuten.ch', + }, + }, + gdpr: {}, + contextInfo: { + contentUrl: 'http://127.0.0.1:5500/sample-request.html', + }, + appInfo: { + id: '127.0.0.1:5500', + }, + userInfo: { + 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', + ifa: '23575619-ef35-4908-b468-ffc4000cdf07', + ppid: [ + { + source: 'oneid.live', + id: '0d862e87-14e9-47a4-9e9b-886b7d7a9d1b', + }, + { + source: 'goldbach.com', + id: 'aa07ead5044f47bb28894ffa0346ed2c', + }, + ], + }, + slots: [ + { + id: '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test', + sizes: [ + [300, 50], + [300, 250], + [300, 600], + [320, 50], + [320, 480], + [320, 64], + [320, 160], + [320, 416], + [336, 280], + ], + targetings: { + gpsenabled: 'false', + fr: 'false', + pagetype: 'story', + darkmode: 'false', + userloggedin: 'false', + iosbuild: '24110', + language: 'de', + storyId: '103211763', + connection: 'wifi', + }, + }, + { + id: '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test/video', + sizes: [[640, 480]], + targetings: { + gpsenabled: 'false', + fr: 'false', + pagetype: 'story', + darkmode: 'false', + userloggedin: 'false', + iosbuild: '24110', + language: 'de', + storyId: '103211763', + connection: 'wifi', + duration: 'XL', + }, + }, + ], + targetings: { + long: 8.582883, + lat: 47.318054, + connection: '4G', + zip: '8700', + }, +}; + +/* Creative response received from server */ +let validCreativeResponse = { + creatives: { + '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test': [ + { + cpm: 32.2, + currency: 'USD', + width: 1, + height: 1, + creativeId: '1', + ttl: 3600, + mediaType: 'native', + netRevenue: true, + contextType: 'native', + ad: JSON.stringify(validNativeAd), + meta: { + advertiserDomains: ['example.com'], + mediaType: 'native' + } + }, + { + cpm: 21.9, + currency: 'USD', + width: 300, + height: 50, + creativeId: '2', + ttl: 3600, + mediaType: 'banner', + netRevenue: true, + contextType: 'banner', + ad: 'banner-ad', + meta: { + advertiserDomains: ['example.com'], + mediaType: 'banner' + } + } + ], + '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test/video': [ + { + cpm: 44.2, + currency: 'USD', + width: 1, + height: 1, + creativeId: '3', + ttl: 3600, + mediaType: 'video', + netRevenue: true, + contextType: 'video_preroll', + ad: 'video-ad', + meta: { + advertiserDomains: ['example.com'], + mediaType: 'video' + } + } + ], + '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test/native': [ + { + cpm: 10.2, + currency: 'USD', + width: 1, + height: 1, + creativeId: '4', + ttl: 3600, + mediaType: 'native', + netRevenue: true, + contextType: 'native', + ad: JSON.stringify(validNativeAd), + meta: { + advertiserDomains: ['example.com'], + mediaType: 'native' + } + } + ], + } +}; + +/* composed request */ +let validRequest = { + url: ENDPOINT, + method: 'POST', + data: validCreativeRequest, + options: { + contentType: 'application/json', + withCredentials: false + }, + bidderRequest: { + ...validBidderRequest, + bids: validBidRequests + } +} + +describe('GoldbachBidAdapter', function () { const adapter = newBidder(spec); + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(ajaxLib, 'ajax'); + sinon.stub(Math, 'random').returns(0); + }); + + afterEach(() => { + ajaxStub.restore(); + Math.random.restore(); + }); describe('inherited functions', function () { it('exists and is a function', function () { @@ -20,58 +428,30 @@ describe('GoldbachXandrAdapter', function () { describe('isBidRequestValid', function () { let bid = { - 'bidder': 'goldbach', - 'params': { - 'placementId': '10433394' + bidder: BIDDER_NAME, + params: { + publisherId: 'de-publisher.ch-ios', }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + adUnitCode: '/46753895/publisher.ch/inside-full-content-pos1/pbjs-test', + sizes: [[300, 250], [300, 600]] }; it('should return true when required params found', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return true when required params found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { - 'member': '1234', - 'invCode': 'ABCD' - }; - - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { - 'placementId': 0 + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { + publisherId: undefined }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); describe('buildRequests', function () { let getAdUnitsStub; - let bidRequests = [ - { - 'bidder': 'goldbach', - 'params': { - 'placementId': '10433394' - }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'transactionId': '04f2659e-c005-4eb1-a57c-fa93145e3843' - } - ]; beforeEach(function() { getAdUnitsStub = sinon.stub(auctionManager, 'getAdUnits').callsFake(function() { @@ -83,1258 +463,353 @@ describe('GoldbachXandrAdapter', function () { getAdUnitsStub.restore(); }); - it('should parse out private sizes', function () { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { - placementId: '10433394', - privateSizes: [300, 250] - } - } - ); + it('should use defined endpoint', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].private_sizes).to.exist; - expect(payload.tags[0].private_sizes).to.deep.equal([{width: 300, height: 250}]); - }); - - it('should add publisher_id in request', function() { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { - placementId: '10433394', - publisherId: '1231234' - } - }); - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].publisher_id).to.exist; - expect(payload.tags[0].publisher_id).to.deep.equal(1231234); - expect(payload.publisher_id).to.exist; - expect(payload.publisher_id).to.deep.equal(1231234); + const requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests.length).to.equal(1); + expect(requests[0].url).to.equal(ENDPOINT); }) - it('should add source and verison to the tag', function () { - const request = spec.buildRequests(bidRequests)[1]; - const payload = JSON.parse(request.data); - expect(payload.sdk).to.exist; - expect(payload.sdk).to.deep.equal({ - source: 'pbjs', - version: '$prebid.version$' - }); - }); - - it('should populate the ad_types array on all requests', function () { - let adUnits = [{ - code: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bids: [{ - bidder: 'goldbach', - params: { - placementId: '10433394' - } - }], - transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' - }]; - - ['banner', 'video', 'native'].forEach(type => { - getAdUnitsStub.callsFake(function(...args) { - return adUnits; - }); - - const bidRequest = Object.assign({}, bidRequests[0]); - bidRequest.mediaTypes = {}; - bidRequest.mediaTypes[type] = {}; - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].ad_types).to.deep.equal([type]); - - if (type === 'banner') { - delete adUnits[0].mediaTypes; - } - }); - }); - - it('should not populate the ad_types array when adUnit.mediaTypes is undefined', function() { - const bidRequest = Object.assign({}, bidRequests[0]); - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].ad_types).to.not.exist; - }); - - it('should populate the ad_types array on outstream requests', function () { - const bidRequest = Object.assign({}, bidRequests[0]); - bidRequest.mediaTypes = {}; - bidRequest.mediaTypes.video = {context: 'outstream'}; - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].ad_types).to.deep.equal(['video']); - expect(payload.tags[0].hb_source).to.deep.equal(1); - }); - - it('sends bid request to ENDPOINT via POST', function () { - const request = spec.buildRequests(bidRequests)[1]; - expect(request.url).to.equal(ENDPOINT); - expect(request.method).to.equal('POST'); - }); - - it('sends bid request to ENDPOINT via GET', function () { - const request = spec.buildRequests(bidRequests)[0]; - expect(request.url).to.equal(PRICING_ENDPOINT); - expect(request.method).to.equal('GET'); - }); + it('should parse all bids to valid slots', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - it('should attach valid video params to the tag', function () { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { - placementId: '10433394', - video: { - id: 123, - minduration: 100, - foobar: 'invalid' - } - } - } - ); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - expect(payload.tags[0].video).to.deep.equal({ - id: 123, - minduration: 100 - }); - expect(payload.tags[0].hb_source).to.deep.equal(1); + expect(payload.slots).to.exist; + expect(Array.isArray(payload.slots)).to.be.true; + expect(payload.slots.length).to.equal(3); + expect(payload.slots[0].id).to.equal(bidRequests[0].params.slotId); + expect(Array.isArray(payload.slots[0].sizes)).to.be.true; + expect(payload.slots[0].sizes.length).to.equal(bidRequests[0].sizes.length); + expect(payload.slots[1].id).to.equal(bidRequests[1].params.slotId); + expect(Array.isArray(payload.slots[1].sizes)).to.be.true; }); - it('should include ORTB video values when video params were not set', function() { - let bidRequest = deepClone(bidRequests[0]); - bidRequest.params = { - placementId: '1234235', - video: { - skippable: true, - playback_method: ['auto_play_sound_off', 'auto_play_sound_unknown'], - context: 'outstream' - } - }; - bidRequest.mediaTypes = { - video: { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'], - skip: 0, - minduration: 5, - api: [1, 5, 6], - playbackmethod: [2, 4] - } - }; - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].video).to.deep.equal({ - minduration: 5, - playback_method: 2, - skippable: true, - context: 4 - }); - expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) - }); + it('should parse all video bids to valid video slots (use video sizes)', function () { + let bidRequests = validBidRequests.map(request => Object.assign({}, [])); + let bidderRequest = deepClone(validBidderRequest); - it('should add video property when adUnit includes a renderer', function () { - const videoData = { + const requests = spec.buildRequests([{ + ...bidRequests[1], + sizes: [], mediaTypes: { - video: { - context: 'outstream', - mimes: ['video/mp4'] - } - }, - params: { - placementId: '10433394', - video: { - skippable: true, - playback_method: ['auto_play_sound_off'] + [VIDEO]: { + sizes: [[640, 480]] } } - }; + }], bidderRequest); + const payload = requests[0].data; - let bidRequest1 = deepClone(bidRequests[0]); - bidRequest1 = Object.assign({}, bidRequest1, videoData, { - renderer: { - url: 'https://test.renderer.url', - render: function () {} - } - }); - - let bidRequest2 = deepClone(bidRequests[0]); - bidRequest2.adUnitCode = 'adUnit_code_2'; - bidRequest2 = Object.assign({}, bidRequest2, videoData); - - const request = spec.buildRequests([bidRequest1, bidRequest2])[1]; - const payload = JSON.parse(request.data); - expect(payload.tags[0].video).to.deep.equal({ - skippable: true, - playback_method: 2, - custom_renderer_present: true - }); - expect(payload.tags[1].video).to.deep.equal({ - skippable: true, - playback_method: 2 - }); + expect(payload.slots.length).to.equal(1); + expect(payload.slots[0].sizes.length).to.equal(1); + expect(payload.slots[0].sizes[0][0]).to.equal(640); + expect(payload.slots[0].sizes[0][1]).to.equal(480); }); - it('should attach valid user params to the tag', function () { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { - placementId: '10433394', - user: { - externalUid: '123', - segments: [123, { id: 987, value: 876 }], - foobar: 'invalid' - } - } - } - ); + it('should set timestamps on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - expect(payload.user).to.exist; - expect(payload.user).to.deep.equal({ - external_uid: '123', - segments: [{id: 123}, {id: 987, value: 876}] - }); + expect(payload.timestampStart).to.exist; + expect(payload.timestampStart).to.be.greaterThan(1) + expect(payload.timestampEnd).to.exist; + expect(payload.timestampEnd).to.be.greaterThan(1) }); - it('should attach reserve param when either bid param or getFloor function exists', function () { - let getFloorResponse = { currency: 'USD', floor: 3 }; - let request, payload = null; - let bidRequest = deepClone(bidRequests[0]); - - // 1 -> reserve not defined, getFloor not defined > empty - request = spec.buildRequests([bidRequest])[1]; - payload = JSON.parse(request.data); - - expect(payload.tags[0].reserve).to.not.exist; - - // 2 -> reserve is defined, getFloor not defined > reserve is used - bidRequest.params = { - 'placementId': '10433394', - 'reserve': 0.5 - }; - request = spec.buildRequests([bidRequest])[1]; - payload = JSON.parse(request.data); - - expect(payload.tags[0].reserve).to.exist.and.to.equal(0.5); + it('should set config on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - // 3 -> reserve is defined, getFloor is defined > getFloor is used - bidRequest.getFloor = () => getFloorResponse; + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - request = spec.buildRequests([bidRequest])[1]; - payload = JSON.parse(request.data); - - expect(payload.tags[0].reserve).to.exist.and.to.equal(3); + expect(payload.config.publisher.id).to.equal(bidRequests[0].params.publisherId); }); - it('should duplicate adpod placements into batches and set correct maxduration', function() { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { placementId: '14542875' } - }, - { - mediaTypes: { - video: { - context: 'adpod', - playerSize: [640, 480], - adPodDurationSec: 300, - durationRangeSec: [15, 30], - } - } - } - ); - - const request = spec.buildRequests([bidRequest])[1]; - const payload1 = JSON.parse(request[0].data); - const payload2 = JSON.parse(request[1].data); + it('should set config on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - // 300 / 15 = 20 total - expect(payload1.tags.length).to.equal(15); - expect(payload2.tags.length).to.equal(5); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - expect(payload1.tags[0]).to.deep.equal(payload1.tags[1]); - expect(payload1.tags[0].video.maxduration).to.equal(30); - - expect(payload2.tags[0]).to.deep.equal(payload1.tags[1]); - expect(payload2.tags[0].video.maxduration).to.equal(30); + expect(payload.config.publisher.id).to.equal(bidRequests[0].params.publisherId); }); - it('should round down adpod placements when numbers are uneven', function() { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { placementId: '14542875' } - }, - { - mediaTypes: { - video: { - context: 'adpod', - playerSize: [640, 480], - adPodDurationSec: 123, - durationRangeSec: [45], - } - } - } - ); + it('should set gdpr on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - expect(payload.tags.length).to.equal(2); - }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - it('should duplicate adpod placements when requireExactDuration is set', function() { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { placementId: '14542875' } - }, - { - mediaTypes: { - video: { - context: 'adpod', - playerSize: [640, 480], - adPodDurationSec: 300, - durationRangeSec: [15, 30], - requireExactDuration: true, - } - } - } - ); - - // 20 total placements with 15 max impressions = 2 requests - const request = spec.buildRequests([bidRequest])[1]; - expect(request.length).to.equal(2); - - // 20 spread over 2 requests = 15 in first request, 5 in second - const payload1 = JSON.parse(request[0].data); - const payload2 = JSON.parse(request[1].data); - expect(payload1.tags.length).to.equal(15); - expect(payload2.tags.length).to.equal(5); - - // 10 placements should have max/min at 15 - // 10 placemenst should have max/min at 30 - const payload1tagsWith15 = payload1.tags.filter(tag => tag.video.maxduration === 15); - const payload1tagsWith30 = payload1.tags.filter(tag => tag.video.maxduration === 30); - expect(payload1tagsWith15.length).to.equal(10); - expect(payload1tagsWith30.length).to.equal(5); - - // 5 placemenst with min/max at 30 were in the first request - // so 5 remaining should be in the second - const payload2tagsWith30 = payload2.tags.filter(tag => tag.video.maxduration === 30); - expect(payload2tagsWith30.length).to.equal(5); + expect(payload.gdpr).to.exist; + expect(payload.gdpr.consent).to.equal(bidderRequest.gdprConsent.gdprApplies); + expect(payload.gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); }); - it('should set durations for placements when requireExactDuration is set and numbers are uneven', function() { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { placementId: '14542875' } - }, - { - mediaTypes: { - video: { - context: 'adpod', - playerSize: [640, 480], - adPodDurationSec: 105, - durationRangeSec: [15, 30, 60], - requireExactDuration: true, - } - } - } - ); - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - expect(payload.tags.length).to.equal(7); - - const tagsWith15 = payload.tags.filter(tag => tag.video.maxduration === 15); - const tagsWith30 = payload.tags.filter(tag => tag.video.maxduration === 30); - const tagsWith60 = payload.tags.filter(tag => tag.video.maxduration === 60); - expect(tagsWith15.length).to.equal(3); - expect(tagsWith30.length).to.equal(3); - expect(tagsWith60.length).to.equal(1); - }); + it('should set contextInfo on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - it('should break adpod request into batches', function() { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { placementId: '14542875' } - }, - { - mediaTypes: { - video: { - context: 'adpod', - playerSize: [640, 480], - adPodDurationSec: 225, - durationRangeSec: [5], - } - } - } - ); - - const request = spec.buildRequests([bidRequest])[1]; - const payload1 = JSON.parse(request[0].data); - const payload2 = JSON.parse(request[1].data); - const payload3 = JSON.parse(request[2].data); - - expect(payload1.tags.length).to.equal(15); - expect(payload2.tags.length).to.equal(15); - expect(payload3.tags.length).to.equal(15); - }); - - // it('should contain hb_source value for adpod', function() { - // let bidRequest = Object.assign({}, - // bidRequests[0], - // { - // params: { placementId: '14542875' } - // }, - // { - // mediaTypes: { - // video: { - // context: 'adpod', - // playerSize: [640, 480], - // adPodDurationSec: 300, - // durationRangeSec: [15, 30], - // } - // } - // } - // ); - // const request = spec.buildRequests([bidRequest])[1]; - // const payload = JSON.parse(request.data); - // expect(payload.tags[0].hb_source).to.deep.equal(7); - // }); - - it('should contain hb_source value for other media', function() { - let bidRequest = Object.assign({}, - bidRequests[0], - { - mediaType: 'banner', - params: { - sizes: [[300, 250], [300, 600]], - placementId: 13144370 - } - } - ); - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - expect(payload.tags[0].hb_source).to.deep.equal(1); - }); - - it('adds brand_category_exclusion to request when set', function() { - let bidRequest = Object.assign({}, bidRequests[0]); - sinon - .stub(config, 'getConfig') - .withArgs('adpod.brandCategoryExclusion') - .returns(true); - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.brand_category_uniqueness).to.equal(true); - - config.getConfig.restore(); - }); - - it('should attach native params to the request', function () { - let bidRequest = Object.assign({}, - bidRequests[0], - { - mediaType: 'native', - nativeParams: { - title: {required: true}, - body: {required: true}, - body2: {required: true}, - image: {required: true, sizes: [100, 100]}, - icon: {required: true}, - cta: {required: false}, - rating: {required: true}, - sponsoredBy: {required: true}, - privacyLink: {required: true}, - displayUrl: {required: true}, - address: {required: true}, - downloads: {required: true}, - likes: {required: true}, - phone: {required: true}, - price: {required: true}, - salePrice: {required: true} - } - } - ); - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].native.layouts[0]).to.deep.equal({ - title: {required: true}, - description: {required: true}, - desc2: {required: true}, - main_image: {required: true, sizes: [{ width: 100, height: 100 }]}, - icon: {required: true}, - ctatext: {required: false}, - rating: {required: true}, - sponsored_by: {required: true}, - privacy_link: {required: true}, - displayurl: {required: true}, - address: {required: true}, - downloads: {required: true}, - likes: {required: true}, - phone: {required: true}, - price: {required: true}, - saleprice: {required: true}, - privacy_supported: true - }); - expect(payload.tags[0].hb_source).to.equal(1); - }); - - it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () { - let bidRequest = Object.assign({}, - bidRequests[0], - { - mediaType: 'native', - nativeParams: { - image: { required: true } - } - } - ); - bidRequest.sizes = [[150, 100], [300, 250]]; - - let request = spec.buildRequests([bidRequest])[1]; - let payload = JSON.parse(request.data); - expect(payload.tags[0].sizes).to.deep.equal([{width: 150, height: 100}, {width: 300, height: 250}]); - - delete bidRequest.sizes; - - request = spec.buildRequests([bidRequest])[1]; - payload = JSON.parse(request.data); - - expect(payload.tags[0].sizes).to.deep.equal([{width: 1, height: 1}]); - }); - - it('should convert keyword params to proper form and attaches to request', function () { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { - placementId: '10433394', - keywords: { - single: 'val', - singleArr: ['val'], - singleArrNum: [5], - multiValMixed: ['value1', 2, 'value3'], - singleValNum: 123, - emptyStr: '', - emptyArr: [''], - badValue: {'foo': 'bar'} // should be dropped - } - } - } - ); - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].keywords).to.deep.equal([{ - 'key': 'single', - 'value': ['val'] - }, { - 'key': 'singleArr', - 'value': ['val'] - }, { - 'key': 'singleArrNum', - 'value': ['5'] - }, { - 'key': 'multiValMixed', - 'value': ['value1', '2', 'value3'] - }, { - 'key': 'singleValNum', - 'value': ['123'] - }, { - 'key': 'emptyStr' - }, { - 'key': 'emptyArr' - }]); - }); - - it('should add payment rules to the request', function () { - let bidRequest = Object.assign({}, - bidRequests[0], - { - params: { - placementId: '10433394', - usePaymentRule: true - } - } - ); - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].use_pmt_rule).to.equal(true); - }); - - it('should add gpid to the request', function () { - let testGpid = '/12345/my-gpt-tag-0'; - let bidRequest = deepClone(bidRequests[0]); - bidRequest.ortb2Imp = { ext: { data: { pbadslot: testGpid } } }; - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - - expect(payload.tags[0].gpid).to.exist.and.equal(testGpid) - }); - - it('should add gdpr consent information to the request', function () { - let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; - let bidderRequest = { - 'bidderCode': 'goldbach', - 'auctionId': '1d1a030790a475', - 'bidderRequestId': '22edbae2733bf6', - 'timeout': 3000, - 'gdprConsent': { - consentString: consentString, - gdprApplies: true, - addtlConsent: '1~7.12.35.62.66.70.89.93.108' - } - }; - bidderRequest.bids = bidRequests; - - const request = spec.buildRequests(bidRequests, bidderRequest)[1]; - expect(request.options).to.deep.equal({withCredentials: true}); - const payload = JSON.parse(request.data); - - expect(payload.gdpr_consent).to.exist; - expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString); - expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true; - expect(payload.gdpr_consent.addtl_consent).to.exist.and.to.deep.equal([7, 12, 35, 62, 66, 70, 89, 93, 108]); - }); - - it('should add us privacy string to payload', function() { - let consentString = '1YA-'; - let bidderRequest = { - 'bidderCode': 'goldbach', - 'auctionId': '1d1a030790a475', - 'bidderRequestId': '22edbae2733bf6', - 'timeout': 3000, - 'uspConsent': consentString - }; - bidderRequest.bids = bidRequests; - - const request = spec.buildRequests(bidRequests, bidderRequest)[1]; - const payload = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - expect(payload.us_privacy).to.exist; - expect(payload.us_privacy).to.exist.and.to.equal(consentString); + expect(payload.contextInfo.contentUrl).to.exist; + expect(payload.contextInfo.contentUrl).to.equal(bidderRequest.ortb2.site.page); }); - it('supports sending hybrid mobile app parameters', function () { - let appRequest = Object.assign({}, - bidRequests[0], - { - params: { - placementId: '10433394', - app: { - id: 'B1O2W3M4AN.com.prebid.webview', - geo: { - lat: 40.0964439, - lng: -75.3009142 - }, - device_id: { - idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', // Apple advertising identifier - aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', // Android advertising identifier - md5udid: '5756ae9022b2ea1e47d84fead75220c8', // MD5 hash of the ANDROID_ID - sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', // SHA1 hash of the ANDROID_ID - windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' // Windows advertising identifier - } - } - } - } - ); - const request = spec.buildRequests([appRequest])[1]; - const payload = JSON.parse(request.data); - expect(payload.app).to.exist; - expect(payload.app).to.deep.equal({ - appid: 'B1O2W3M4AN.com.prebid.webview' - }); - expect(payload.device.device_id).to.exist; - expect(payload.device.device_id).to.deep.equal({ - aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', - idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', - md5udid: '5756ae9022b2ea1e47d84fead75220c8', - sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', - windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' - }); - expect(payload.device.geo).to.exist; - expect(payload.device.geo).to.deep.equal({ - lat: 40.0964439, - lng: -75.3009142 - }); - }); + it('should set appInfo on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - it('should add referer info to payload', function () { - const bidRequest = Object.assign({}, bidRequests[0]) - const bidderRequest = { - refererInfo: { - topmostLocation: 'https://example.com/page.html', - reachedTop: true, - numIframes: 2, - stack: [ - 'https://example.com/page.html', - 'https://example.com/iframe1.html', - 'https://example.com/iframe2.html' - ] - } - } - const request = spec.buildRequests([bidRequest], bidderRequest)[1]; - const payload = JSON.parse(request.data); - - expect(payload.referrer_detection).to.exist; - expect(payload.referrer_detection).to.deep.equal({ - rd_ref: 'https%3A%2F%2Fexample.com%2Fpage.html', - rd_top: true, - rd_ifs: 2, - rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') - }); - }); - - it('should populate schain if available', function () { - const bidRequest = Object.assign({}, bidRequests[0], { - schain: { - ver: '1.0', - complete: 1, - nodes: [ - { - 'asi': 'blob.com', - 'sid': '001', - 'hp': 1 - } - ] - } - }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - expect(payload.schain).to.deep.equal({ - ver: '1.0', - complete: 1, - nodes: [ - { - 'asi': 'blob.com', - 'sid': '001', - 'hp': 1 - } - ] - }); + expect(payload.appInfo.id).to.exist; + expect(payload.appInfo.id).to.equal(bidderRequest.ortb2.site.domain); }); - it('should populate coppa if set in config', function () { - let bidRequest = Object.assign({}, bidRequests[0]); - sinon.stub(config, 'getConfig') - .withArgs('coppa') - .returns(true); + it('should set userInfo on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - expect(payload.user.coppa).to.equal(true); - - config.getConfig.restore(); + expect(payload.userInfo).to.exist; + expect(payload.userInfo.ua).to.equal(bidderRequest.ortb2.device.ua); + expect(payload.userInfo.ip).to.equal(bidderRequest.ortb2.device.ip); + expect(payload.userInfo.ifa).to.equal(bidderRequest.ortb2.device.ifa); + expect(Array.isArray(payload.userInfo.ppid)).to.be.true; + expect(payload.userInfo.ppid.length).to.equal(2); }); - it('should set the X-Is-Test customHeader if test flag is enabled', function () { - let bidRequest = Object.assign({}, bidRequests[0]); - sinon.stub(config, 'getConfig') - .withArgs('apn_test') - .returns(true); + it('should set mapped general targetings on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); - const request = spec.buildRequests([bidRequest])[1]; - expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1}); + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; - config.getConfig.restore(); + expect(payload.slots[0].targetings['duration']).to.not.exist; + expect(payload.slots[1].targetings['duration']).to.exist; + expect(payload.targetings['duration']).to.not.exist; + expect(payload.targetings['lat']).to.exist; + expect(payload.targetings['long']).to.exist; + expect(payload.targetings['zip']).to.exist; + expect(payload.targetings['connection']).to.exist; }); - it('should always set withCredentials: true on the request.options', function () { - let bidRequest = Object.assign({}, bidRequests[0]); - const request = spec.buildRequests([bidRequest])[1]; - expect(request.options.withCredentials).to.equal(true); - }); + it('should set mapped video duration targetings on request', function () { + let bidRequests = deepClone(validBidRequests); + let videoRequest = deepClone(validBidRequests[1]); + let bidderRequest = deepClone(validBidderRequest); - it('should set simple domain variant if purpose 1 consent is not given', function () { - let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; - let bidderRequest = { - 'bidderCode': 'goldbach', - 'auctionId': '1d1a030790a475', - 'bidderRequestId': '22edbae2733bf6', - 'timeout': 3000, - 'gdprConsent': { - consentString: consentString, - gdprApplies: true, - apiVersion: 2, - vendorData: { - purpose: { - consents: { - 1: false - } - } - } - } - }; - bidderRequest.bids = bidRequests; - - const request = spec.buildRequests(bidRequests, bidderRequest)[1]; - expect(request.url).to.equal('https://ib.adnxs-simple.com/ut/v3/prebid'); - }); - - it('should populate eids when supported userIds are available', function () { - const bidRequest = Object.assign({}, bidRequests[0], { - userId: { - tdid: 'sample-userid', - uid2: { id: 'sample-uid2-value' }, - criteoId: 'sample-criteo-userid', - netId: 'sample-netId-userid', - idl_env: 'sample-idl-userid' - } - }); - - const request = spec.buildRequests([bidRequest])[1]; - const payload = JSON.parse(request.data); - expect(payload.eids).to.deep.include({ - source: 'adserver.org', - id: 'sample-userid', - rti_partner: 'TDID' - }); - - expect(payload.eids).to.deep.include({ - source: 'criteo.com', - id: 'sample-criteo-userid', - }); - - expect(payload.eids).to.deep.include({ - source: 'netid.de', - id: 'sample-netId-userid', - }); - - expect(payload.eids).to.deep.include({ - source: 'liveramp.com', - id: 'sample-idl-userid' - }); - - expect(payload.eids).to.deep.include({ - source: 'uidapi.com', - id: 'sample-uid2-value', - rti_partner: 'UID2' - }); - }); - - it('should populate iab_support object at the root level if omid support is detected', function () { - // with bid.params.frameworks - let bidRequest_A = Object.assign({}, bidRequests[0], { + bidRequests.push({ + ...videoRequest, params: { - frameworks: [1, 2, 5, 6], + ...videoRequest.params, video: { - frameworks: [1, 2, 5, 6] + maxduration: 10 } } - }); - let request = spec.buildRequests([bidRequest_A])[1]; - let payload = JSON.parse(request.data); - expect(payload.iab_support).to.be.an('object'); - expect(payload.iab_support).to.deep.equal({ - omidpn: 'Appnexus', - omidpv: '$prebid.version$' - }); - expect(payload.tags[0].banner_frameworks).to.be.an('array'); - expect(payload.tags[0].banner_frameworks).to.deep.equal([1, 2, 5, 6]); - expect(payload.tags[0].video_frameworks).to.be.an('array'); - expect(payload.tags[0].video_frameworks).to.deep.equal([1, 2, 5, 6]); - expect(payload.tags[0].video.frameworks).to.not.exist; - - // without bid.params.frameworks - const bidRequest_B = Object.assign({}, bidRequests[0]); - request = spec.buildRequests([bidRequest_B])[1]; - payload = JSON.parse(request.data); - expect(payload.iab_support).to.not.exist; - expect(payload.tags[0].banner_frameworks).to.not.exist; - expect(payload.tags[0].video_frameworks).to.not.exist; - - // with video.frameworks but it is not an array - const bidRequest_C = Object.assign({}, bidRequests[0], { + }) + + bidRequests.push({ + ...videoRequest, params: { + ...videoRequest.params, video: { - frameworks: "'1', '2', '3', '6'" + maxduration: 35 } } + }) + + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; + + expect(payload.slots[0].targetings['duration']).to.not.exist; + expect(payload.slots[1].targetings['duration']).to.exist; + expect(payload.slots[1].targetings['duration']).to.equal('XL'); + expect(payload.slots[3].targetings['duration']).to.equal('M'); + expect(payload.slots[4].targetings['duration']).to.equal('XXL'); + }); + + it('should set mapped connection targetings on request', function () { + let bidRequests = deepClone(validBidRequests); + let bidderRequest = deepClone(validBidderRequest); + + const bidderRequestEthernet = deepClone(bidderRequest); + bidderRequestEthernet.ortb2.device.connectiontype = 1; + const payloadEthernet = spec.buildRequests(bidRequests, bidderRequestEthernet)[0].data; + + const bidderRequestWifi = deepClone(bidderRequest); + bidderRequestWifi.ortb2.device.connectiontype = 2; + const payloadWifi = spec.buildRequests(bidRequests, bidderRequestWifi)[0].data; + + const bidderRequest2G = deepClone(bidderRequest); + bidderRequest2G.ortb2.device.connectiontype = 4; + const payload2G = spec.buildRequests(bidRequests, bidderRequest2G)[0].data; + + const bidderRequest3G = deepClone(bidderRequest); + bidderRequest3G.ortb2.device.connectiontype = 5; + const payload3G = spec.buildRequests(bidRequests, bidderRequest3G)[0].data; + + const bidderRequest4G = deepClone(bidderRequest); + bidderRequest4G.ortb2.device.connectiontype = 6; + const payload4G = spec.buildRequests(bidRequests, bidderRequest4G)[0].data; + + const bidderRequestNoConnection = deepClone(bidderRequest); + bidderRequestNoConnection.ortb2.device.connectiontype = undefined; + const payloadNoConnection = spec.buildRequests(bidRequests, bidderRequestNoConnection)[0].data; + + expect(payloadEthernet.targetings['connection']).to.equal('ethernet'); + expect(payloadWifi.targetings['connection']).to.equal('wifi'); + expect(payload2G.targetings['connection']).to.equal('2G'); + expect(payload3G.targetings['connection']).to.equal('3G'); + expect(payload4G.targetings['connection']).to.equal('4G'); + expect(payloadNoConnection.targetings['connection']).to.equal(undefined); + }); + + it('should create a request with minimal information', function () { + let bidderRequest = Object.assign({}, validBidderRequest); + let bidRequests = validBidRequests.map(request => Object.assign({}, request)); + + // Removing usable bidderRequest values + bidderRequest.gdprConsent = undefined; + bidderRequest.ortb2.device.connectiontype = undefined; + bidderRequest.ortb2.device.geo = undefined; + bidderRequest.ortb2.device.ip = undefined; + bidderRequest.ortb2.device.ifa = undefined; + bidderRequest.ortb2.device.ua = undefined; + + // Removing usable bidRequests values + bidRequests = bidRequests.map(request => { + request.ortb2.device.connectiontype = undefined; + request.ortb2.device.geo = undefined; + request.ortb2.device.ip = undefined; + request.ortb2.device.ifa = undefined; + request.ortb2.device.ua = undefined; + request.userIdAsEids = undefined; + request.params = { + publisherId: 'de-publisher.ch-ios' + }; + return request; }); - request = spec.buildRequests([bidRequest_C])[1]; - payload = JSON.parse(request.data); - expect(payload.iab_support).to.not.exist; - expect(payload.tags[0].banner_frameworks).to.not.exist; - expect(payload.tags[0].video_frameworks).to.not.exist; + + const requests = spec.buildRequests(bidRequests, bidderRequest); + const payload = requests[0].data; + + // bidderRequest mappings + expect(payload.gdpr).to.exist; + expect(payload.gdpr.consent).to.not.exist; + expect(payload.gdpr.consentString).to.not.exist; + expect(payload.userInfo).to.exist; + expect(payload.userInfo.ua).to.exist; + expect(payload.userInfo.ip).to.not.exist; + expect(payload.userInfo.ifa).to.not.exist; + expect(payload.userInfo.ppid.length).to.equal(0); + expect(payload.targetings).to.exist; + expect(payload.targetings['connection']).to.not.exist; + expect(payload.targetings['lat']).to.not.exist; + expect(payload.targetings['long']).to.not.exist; + expect(payload.targetings['zip']).to.not.exist; + + // bidRequests mapping + expect(payload.slots).to.exist; + expect(payload.slots.length).to.equal(3); + expect(payload.slots[0].targetings).to.exist + expect(payload.slots[1].targetings).to.exist }); - }) + }); describe('interpretResponse', function () { - let response = { - 'version': '3.0.0', - 'tags': [ - { - 'uuid': '3db3773286ee59', - 'tag_id': 10433394, - 'auction_id': '4534722592064951574', - 'nobid': false, - 'no_ad_url': 'https://lax1-ib.adnxs.com/no-ad', - 'timeout_ms': 10000, - 'ad_profile_id': 27079, - 'ads': [ - { - 'content_source': 'rtb', - 'ad_type': 'banner', - 'buyer_member_id': 958, - 'creative_id': 29681110, - 'media_type_id': 1, - 'media_subtype_id': 1, - 'cpm': 0.5, - 'cpm_publisher_currency': 0.5, - 'publisher_currency_code': '$', - 'client_initiated_ad_counting': true, - 'viewability': { - 'config': '' - }, - 'rtb': { - 'banner': { - 'content': '', - 'width': 300, - 'height': 250 - }, - 'trackers': [ - { - 'impression_urls': [ - 'https://lax1-ib.adnxs.com/impression' - ], - 'video_events': {} - } - ] - } - } - ] - } - ] - }; - - it('should get correct bid response', function () { - let expectedResponse = [ - { - 'requestId': '3db3773286ee59', - 'cpm': 0.5, - 'creativeId': 29681110, - 'dealId': undefined, - 'width': 300, - 'height': 250, - 'ad': '', - 'mediaType': 'banner', - 'currency': 'USD', - 'ttl': 300, - 'netRevenue': true, - 'adUnitCode': 'code', - 'appnexus': { - 'buyerMemberId': 958 - } - } - ]; - let bidderRequest = { - bids: [{ - bidId: '3db3773286ee59', - adUnitCode: 'code' - }] - } - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); - }); + it('should map response to valid bids (amount)', function () { + let request = deepClone(validRequest); + let bidResponse = deepClone({body: validCreativeResponse}); - it('handles nobid responses', function () { - let response = { - 'version': '0.0.1', - 'tags': [{ - 'uuid': '84ab500420319d', - 'tag_id': 5976557, - 'auction_id': '297492697822162468', - 'nobid': true - }] - }; - let bidderRequest; + const response = spec.interpretResponse(bidResponse, request); - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(result.length).to.equal(0); + expect(response).to.exist; + expect(response.length).to.equal(3); + expect(response.filter(bid => bid.requestId === validBidRequests[0].bidId).length).to.equal(1) + expect(response.filter(bid => bid.requestId === validBidRequests[1].bidId).length).to.equal(1) }); - it('handles outstream video responses', function () { - let response = { - 'tags': [{ - 'uuid': '84ab500420319d', - 'ads': [{ - 'ad_type': 'video', - 'cpm': 0.500000, - 'notify_url': 'imptracker.com', - 'rtb': { - 'video': { - 'content': '' - } - }, - 'javascriptTrackers': '' - }] - }] - }; - let bidderRequest = { - bids: [{ - bidId: '84ab500420319d', - adUnitCode: 'code', - mediaTypes: { - video: { - context: 'outstream' - } - } - }] - } + it('should attach a custom video renderer ', function () { + let request = deepClone(validRequest); + let bidResponse = deepClone({body: validCreativeResponse}); + bidResponse.body.creatives[validBidRequests[1].params.slotId][0].mediaType = 'video'; + bidResponse.body.creatives[validBidRequests[1].params.slotId][0].vastXml = ''; + bidResponse.body.creatives[validBidRequests[1].params.slotId][0].contextType = 'video_outstream'; - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(result[0]).to.have.property('vastXml'); - expect(result[0]).to.have.property('vastImpUrl'); - expect(result[0]).to.have.property('mediaType', 'video'); - }); + const response = spec.interpretResponse(bidResponse, request); - it('handles instream video responses', function () { - let response = { - 'tags': [{ - 'uuid': '84ab500420319d', - 'ads': [{ - 'ad_type': 'video', - 'cpm': 0.500000, - 'notify_url': 'imptracker.com', - 'rtb': { - 'video': { - 'asset_url': 'https://sample.vastURL.com/here/vid' - } - }, - 'javascriptTrackers': '' - }] - }] - }; - let bidderRequest = { - bids: [{ - bidId: '84ab500420319d', - adUnitCode: 'code', - mediaTypes: { - video: { - context: 'instream' - } - } - }] - } - - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(result[0]).to.have.property('vastUrl'); - expect(result[0]).to.have.property('vastImpUrl'); - expect(result[0]).to.have.property('mediaType', 'video'); + expect(response).to.exist; + expect(response.filter(bid => !!bid.renderer).length).to.equal(1); }); - it('handles adpod responses', function () { - let response = { - 'tags': [{ - 'uuid': '84ab500420319d', - 'ads': [{ - 'ad_type': 'video', - 'brand_category_id': 10, - 'cpm': 0.500000, - 'notify_url': 'imptracker.com', - 'rtb': { - 'video': { - 'asset_url': 'https://sample.vastURL.com/here/adpod', - 'duration_ms': 30000, - } - }, - 'viewability': { - 'config': '' - } - }] - }] - }; + it('should not attach a custom video renderer when VAST url/xml is missing', function () { + let request = deepClone(validRequest); + let bidResponse = deepClone({body: validCreativeResponse}); + bidResponse.body.creatives[validBidRequests[1].params.slotId][0].mediaType = 'video'; + bidResponse.body.creatives[validBidRequests[1].params.slotId][0].contextType = 'video_outstream'; - let bidderRequest = { - bids: [{ - bidId: '84ab500420319d', - adUnitCode: 'code', - mediaTypes: { - video: { - context: 'adpod' - } - } - }] - }; + const response = spec.interpretResponse(bidResponse, request); - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(result[0]).to.have.property('vastUrl'); - expect(result[0].video.context).to.equal('adpod'); - expect(result[0].video.durationSeconds).to.equal(30); + expect(response).to.exist; + expect(response.filter(bid => !!bid.renderer).length).to.equal(0); }); + }); - it('handles native responses', function () { - let response1 = deepClone(response); - response1.tags[0].ads[0].ad_type = 'native'; - response1.tags[0].ads[0].rtb.native = { - 'title': 'Native Creative', - 'desc': 'Cool description great stuff', - 'desc2': 'Additional body text', - 'ctatext': 'Do it', - 'sponsored': 'AppNexus', - 'icon': { - 'width': 0, - 'height': 0, - 'url': 'https://cdn.adnxs.com/icon.png' - }, - 'main_img': { - 'width': 2352, - 'height': 1516, - 'url': 'https://cdn.adnxs.com/img.png' - }, - 'link': { - 'url': 'https://www.appnexus.com', - 'fallback_url': '', - 'click_trackers': ['https://nym1-ib.adnxs.com/click'] - }, - 'impression_trackers': ['https://example.com'], - 'rating': '5', - 'displayurl': 'https://AppNexus.com/?url=display_url', - 'likes': '38908320', - 'downloads': '874983', - 'price': '9.99', - 'saleprice': 'FREE', - 'phone': '1234567890', - 'address': '28 W 23rd St, New York, NY 10010', - 'privacy_link': 'https://appnexus.com/?url=privacy_url', - 'javascriptTrackers': '' - }; - let bidderRequest = { - bids: [{ - bidId: '3db3773286ee59', - adUnitCode: 'code' - }] - } - - let result = spec.interpretResponse({ body: response1 }, {bidderRequest}); - expect(result[0].native.title).to.equal('Native Creative'); - expect(result[0].native.body).to.equal('Cool description great stuff'); - expect(result[0].native.cta).to.equal('Do it'); - expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + describe('sendLogs', function () { + it('should not send logs when percentage is not met', function () { + Math.random.returns(1); + spec.onTimeout([]); + expect(ajaxStub.calledOnce).to.be.false; }); + }); - it('supports configuring outstream renderers', function () { - const outstreamResponse = deepClone(response); - outstreamResponse.tags[0].ads[0].rtb.video = {}; - outstreamResponse.tags[0].ads[0].renderer_url = 'renderer.js'; - - const bidderRequest = { - bids: [{ - bidId: '3db3773286ee59', - renderer: { - options: { - adText: 'configured' - } - }, - mediaTypes: { - video: { - context: 'outstream' - } - } - }] - }; - - const result = spec.interpretResponse({ body: outstreamResponse }, {bidderRequest}); - expect(result[0].renderer.config).to.deep.equal( - bidderRequest.bids[0].renderer.options - ); + describe('onTimeout', function () { + it('should send logs on timeout', function () { + spec.onTimeout([]); + expect(ajaxStub.calledOnce).to.be.true; }); + }); - it('should add deal_priority and deal_code', function() { - let responseWithDeal = deepClone(response); - responseWithDeal.tags[0].ads[0].ad_type = 'video'; - responseWithDeal.tags[0].ads[0].deal_priority = 5; - responseWithDeal.tags[0].ads[0].deal_code = '123'; - responseWithDeal.tags[0].ads[0].rtb.video = { - duration_ms: 1500, - player_width: 640, - player_height: 340, - }; - - let bidderRequest = { - bids: [{ - bidId: '3db3773286ee59', - adUnitCode: 'code', - mediaTypes: { - video: { - context: 'adpod' - } - } - }] - } - let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); - expect(Object.keys(result[0].appnexus)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); - expect(result[0].video.dealTier).to.equal(5); + describe('onBidWon', function () { + it('should send logs on won', function () { + spec.onBidWon([]); + expect(ajaxStub.calledOnce).to.be.true; }); + }); - it('should add advertiser id', function() { - let responseAdvertiserId = deepClone(response); - responseAdvertiserId.tags[0].ads[0].advertiser_id = '123'; - - let bidderRequest = { - bids: [{ - bidId: '3db3773286ee59', - adUnitCode: 'code' - }] - } - let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); - expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); + describe('onSetTargeting', function () { + it('should send logs on targeting', function () { + spec.onSetTargeting([]); + expect(ajaxStub.calledOnce).to.be.true; }); + }); - it('should add advertiserDomains', function() { - let responseAdvertiserId = deepClone(response); - responseAdvertiserId.tags[0].ads[0].adomain = ['123']; + describe('onBidderError', function () { + it('should send logs on bidder error', function () { + spec.onBidderError([]); + expect(ajaxStub.calledOnce).to.be.true; + }); + }); - let bidderRequest = { - bids: [{ - bidId: '3db3773286ee59', - adUnitCode: 'code' - }] - } - let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); - expect(Object.keys(result[0].meta)).to.include.members(['advertiserDomains']); - expect(Object.keys(result[0].meta.advertiserDomains)).to.deep.equal([]); + describe('onAdRenderSucceeded', function () { + it('should send logs on render succeeded', function () { + spec.onAdRenderSucceeded([]); + expect(ajaxStub.calledOnce).to.be.true; }); }); }); diff --git a/test/spec/modules/gptPreAuction_spec.js b/test/spec/modules/gptPreAuction_spec.js index fa2236f77c6..989a5f376bb 100644 --- a/test/spec/modules/gptPreAuction_spec.js +++ b/test/spec/modules/gptPreAuction_spec.js @@ -2,10 +2,16 @@ import { appendGptSlots, appendPbAdSlot, _currentConfig, - makeBidRequestsHook + makeBidRequestsHook, + getAuctionsIdsFromTargeting, + getSegments, + getSignals, + getSignalsArrayByAuctionsIds, + getSignalsIntersection } from 'modules/gptPreAuction.js'; import { config } from 'src/config.js'; import { makeSlot } from '../integration/faker/googletag.js'; +import { taxonomies } from '../../../libraries/gptUtils/gptUtils.js'; describe('GPT pre-auction module', () => { let sandbox; @@ -25,6 +31,87 @@ describe('GPT pre-auction module', () => { makeSlot({ code: 'slotCode4', divId: 'div5' }) ]; + const mockTargeting = {'/123456/header-bid-tag-0': {'hb_deal_rubicon': '1234', 'hb_deal': '1234', 'hb_pb': '0.53', 'hb_adid': '148018fe5e', 'hb_bidder': 'rubicon', 'foobar': '300x250', 'hb_pb_rubicon': '0.53', 'hb_adid_rubicon': '148018fe5e', 'hb_bidder_rubicon': 'rubicon', 'hb_deal_appnexus': '4321', 'hb_pb_appnexus': '0.1', 'hb_adid_appnexus': '567891011', 'hb_bidder_appnexus': 'appnexus'}} + + const mockAuctionManager = { + findBidByAdId(adId) { + const bidsMap = { + '148018fe5e': { + auctionId: mocksAuctions[0].auctionId + }, + '567891011': { + auctionId: mocksAuctions[1].auctionId + }, + }; + return bidsMap[adId]; + }, + index: { + getAuction({ auctionId }) { + return mocksAuctions.find(auction => auction.auctionId === auctionId); + } + } + } + + const mocksAuctions = [ + { + auctionId: '1111', + getFPD: () => ({ + global: { + user: { + data: [{ + name: 'dataprovider.com', + ext: { + segtax: 4 + }, + segment: [{ + id: '1' + }, { + id: '2' + }] + }], + } + } + }) + }, + { + auctionId: '234234', + getFPD: () => ({ + global: { + user: { + data: [{ + name: 'dataprovider.com', + ext: { + segtax: 4 + }, + segment: [{ + id: '2' + }] + }] + } + } + }), + }, { + auctionId: '234324234', + getFPD: () => ({ + global: { + user: { + data: [{ + name: 'dataprovider.com', + ext: { + segtax: 4 + }, + segment: [{ + id: '2' + }, { + id: '3' + }] + }] + } + } + }) + }, + ] + describe('appendPbAdSlot', () => { // sets up our document body to test the pbAdSlot dom actions against document.body.innerHTML = '
test1
' + @@ -122,6 +209,17 @@ describe('GPT pre-auction module', () => { expect(adUnit.ortb2Imp.ext.data.adserver).to.deep.equal({ name: 'gam', adslot: '/12345/slotCode2' }); }); + it('returns full ad unit path even if mcmEnabled is true', () => { + config.setConfig({ gptPreAuction: { enabled: true, mcmEnabled: true } }); + window.googletag.pubads().setSlots([ + makeSlot({ code: '/12345,21212/slot', divId: 'div1' }), + ]); + const adUnit = {code: '/12345,21212/slot'}; + expect(appendGptSlots([adUnit])).to.eql({ + '/12345,21212/slot': '/12345,21212/slot' + }) + }) + it('will not trim child id if mcmEnabled is not set to true', () => { window.googletag.pubads().setSlots([ makeSlot({ code: '/12345,21212/slotCode1', divId: 'div1' }), @@ -188,7 +286,7 @@ describe('GPT pre-auction module', () => { customGptSlotMatching: false, customPbAdSlot: false, customPreAuction: false, - useDefaultPreAuction: false + useDefaultPreAuction: true }); }); }); @@ -372,6 +470,33 @@ describe('GPT pre-auction module', () => { expect(returnedAdUnits).to.deep.equal(expectedAdUnits); }); + it('should pass full slot path to customPreAuction when mcmEnabled is true', () => { + const customPreAuction = sinon.stub(); + config.setConfig({ + gptPreAuction: { + enabled: true, + mcmEnabled: true, + customPreAuction + } + }); + window.googletag.pubads().setSlots([ + makeSlot({ code: '/12345,21212/slot', divId: 'div1' }), + ]); + const adUnit = {code: '/12345,21212/slot'}; + makeBidRequestsHook(sinon.stub(), [adUnit]); + sinon.assert.calledWith(customPreAuction, adUnit, '/12345/slot', adUnit.code); + }); + + it('should not choke if gpt is not available', () => { + config.setConfig({ + gptPreAuction: { + enabled: true + } + }); + sandbox.stub(window, 'googletag').value(null); + makeBidRequestsHook(sinon.stub(), [{}]); + }) + it('should use useDefaultPreAuction logic', () => { config.setConfig({ gptPreAuction: { @@ -453,5 +578,70 @@ describe('GPT pre-auction module', () => { runMakeBidRequests(testAdUnits); expect(returnedAdUnits).to.deep.equal(expectedAdUnits); }); + + it('sets gpid when mcmEnabled: true', () => { + config.setConfig({ gptPreAuction: { enabled: true, mcmEnabled: true } }); + window.googletag.pubads().setSlots([ + makeSlot({ code: '/12345,21212/slot', divId: 'div1' }), + ]); + const adUnit = {code: '/12345,21212/slot'}; + makeBidRequestsHook(sinon.stub(), [adUnit]); + expect(adUnit.ortb2Imp.ext.gpid).to.eql('/12345/slot'); + }); + }); + + describe('pps gpt config', () => { + it('should parse segments from fpd', () => { + const twoSegments = getSegments(mocksAuctions[0].getFPD().global, ['user.data'], 4); + expect(JSON.stringify(twoSegments)).to.equal(JSON.stringify(['1', '2'])); + const zeroSegments = getSegments(mocksAuctions[0].getFPD().global, ['user.data'], 6); + expect(zeroSegments).to.length(0); + }); + + it('should return signals from fpd', () => { + const signals = getSignals(mocksAuctions[0].getFPD().global); + const expectedSignals = [{ taxonomy: taxonomies[0], values: ['1', '2'] }]; + expect(signals).to.eql(expectedSignals); + }); + + it('should properly get auctions ids from targeting', () => { + const auctionsIds = getAuctionsIdsFromTargeting(mockTargeting, mockAuctionManager); + expect(auctionsIds).to.eql([mocksAuctions[0].auctionId, mocksAuctions[1].auctionId]) + }); + + it('should filter out adIds that do not map to any auction', () => { + const auctionsIds = getAuctionsIdsFromTargeting({ + ...mockTargeting, + 'au': {'hb_adid': 'missing'}, + }, mockAuctionManager); + expect(auctionsIds).to.eql([mocksAuctions[0].auctionId, mocksAuctions[1].auctionId]); + }) + + it('should properly return empty array of auction ids for invalid targeting', () => { + let auctionsIds = getAuctionsIdsFromTargeting({}, mockAuctionManager); + expect(Array.isArray(auctionsIds)).to.equal(true); + expect(auctionsIds).to.length(0); + auctionsIds = getAuctionsIdsFromTargeting({'/123456/header-bid-tag-0/bg': {'invalidContent': '123'}}, mockAuctionManager); + expect(Array.isArray(auctionsIds)).to.equal(true); + expect(auctionsIds).to.length(0); + }); + + it('should properly get signals from auctions', () => { + const signals = getSignalsArrayByAuctionsIds(['1111', '234234', '234324234'], mockAuctionManager.index); + const intersection = getSignalsIntersection(signals); + const expectedResult = { IAB_AUDIENCE_1_1: { values: ['2'] }, IAB_CONTENT_2_2: { values: [] } }; + expect(JSON.stringify(intersection)).to.be.equal(JSON.stringify(expectedResult)); + }); + + it('should return empty signals array for empty auctions ids array', () => { + const signals = getSignalsArrayByAuctionsIds([], mockAuctionManager.index); + expect(Array.isArray(signals)).to.equal(true); + expect(signals).to.length(0); + }); + + it('should return properly formatted object for getSignalsIntersection invoked with empty array', () => { + const signals = getSignalsIntersection([]); + expect(Object.keys(signals)).to.contain.members(taxonomies); + }); }); }); diff --git a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js index 918da50d8bc..7eeaa7a6c67 100644 --- a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js +++ b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js @@ -7,7 +7,7 @@ import { generateUUID } from '../../../src/utils.js'; import * as utils from 'src/utils.js'; -import {expect} from 'chai'; +import { expect } from 'chai'; import sinon from 'sinon'; const events = require('src/events'); @@ -65,17 +65,17 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { greenbidsAnalyticsAdapter.disableAnalytics(); }); - describe('#getCachedAuction()', function() { - const existing = {timeoutBids: [{}]}; + describe('#getCachedAuction()', function () { + const existing = { timeoutBids: [{}] }; greenbidsAnalyticsAdapter.cachedAuctions['test_auction_id'] = existing; - it('should get the existing cached object if it exists', function() { + it('should get the existing cached object if it exists', function () { const result = greenbidsAnalyticsAdapter.getCachedAuction('test_auction_id'); expect(result).to.equal(existing); }); - it('should create a new object and store it in the cache on cache miss', function() { + it('should create a new object and store it in the cache on cache miss', function () { const result = greenbidsAnalyticsAdapter.getCachedAuction('no_such_id'); expect(result).to.deep.include({ @@ -84,7 +84,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }); }); - describe('when formatting JSON payload sent to backend', function() { + describe('when formatting JSON payload sent to backend', function () { const receivedBids = [ { auctionId: auctionId, @@ -106,7 +106,8 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { timeToRespond: 100, cpm: 0.08, currency: 'USD', - ad: 'fake ad2' + ad: 'fake ad2', + params: {'placement ID': 12784} }, { auctionId: auctionId, @@ -149,16 +150,16 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }); } - describe('#createCommonMessage', function() { - it('should correctly serialize some common fields', function() { + describe('#createCommonMessage', function () { + it('should correctly serialize some common fields', function () { const message = greenbidsAnalyticsAdapter.createCommonMessage(auctionId); assertHavingRequiredMessageFields(message); }); }); - describe('#serializeBidResponse', function() { - it('should handle BID properly with timeout false and hasBid true', function() { + describe('#serializeBidResponse', function () { + it('should handle BID properly with timeout false and hasBid true', function () { const result = greenbidsAnalyticsAdapter.serializeBidResponse(receivedBids[0], BIDDER_STATUS.BID); expect(result).to.include({ @@ -168,7 +169,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }); }); - it('should handle NO_BID properly and set hasBid to false', function() { + it('should handle NO_BID properly and set hasBid to false', function () { const result = greenbidsAnalyticsAdapter.serializeBidResponse(noBids[0], BIDDER_STATUS.NO_BID); expect(result).to.include({ @@ -178,7 +179,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }); }); - it('should handle TIMEOUT properly and set isTimeout to true', function() { + it('should handle TIMEOUT properly and set isTimeout to true', function () { const result = greenbidsAnalyticsAdapter.serializeBidResponse(noBids[0], BIDDER_STATUS.TIMEOUT); expect(result).to.include({ @@ -189,8 +190,8 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }); }); - describe('#addBidResponseToMessage()', function() { - it('should add a bid response in the output message, grouped by adunit_id and bidder', function() { + describe('#addBidResponseToMessage()', function () { + it('should add a bid response in the output message, grouped by adunit_id and bidder', function () { const message = { adUnits: [ { @@ -208,14 +209,15 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { bidder: 'greenbids', isTimeout: false, hasBid: false, + params: {} } ] }); }); }); - describe('#createBidMessage()', function() { - it('should format auction message sent to the backend', function() { + describe('#createBidMessage()', function () { + it('should format auction message sent to the backend', function () { const args = { auctionId: auctionId, timestamp: 1234567890, @@ -258,10 +260,9 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { noBids: noBids }; - sinon.stub(greenbidsAnalyticsAdapter, 'getCachedAuction').returns({timeoutBids: timeoutBids}); + sinon.stub(greenbidsAnalyticsAdapter, 'getCachedAuction').returns({ timeoutBids: timeoutBids }); const result = greenbidsAnalyticsAdapter.createBidMessage(args, timeoutBids); greenbidsAnalyticsAdapter.getCachedAuction.restore(); - assertHavingRequiredMessageFields(result); expect(result).to.deep.include({ auctionElapsed: 100, @@ -278,12 +279,18 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { { bidder: 'greenbids', isTimeout: false, - hasBid: true + hasBid: true, + params: {}, + cpm: 0.1, + currency: 'USD', }, { bidder: 'greenbidsx', isTimeout: false, - hasBid: true + hasBid: true, + params: { 'placement ID': 12784 }, + cpm: 0.08, + currency: 'USD', } ] }, @@ -312,7 +319,10 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { { bidder: 'greenbids', isTimeout: true, - hasBid: true + hasBid: true, + cpm: 0.09, + currency: 'USD', + params: {} } ] } @@ -321,8 +331,8 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }); }); - describe('#handleBidTimeout()', function() { - it('should cached the timeout bid as BID_TIMEOUT event was triggered', function() { + describe('#handleBidTimeout()', function () { + it('should cached the timeout bid as BID_TIMEOUT event was triggered', function () { greenbidsAnalyticsAdapter.cachedAuctions['test_timeout_auction_id'] = { 'timeoutBids': [] }; const args = [{ auctionId: 'test_timeout_auction_id', @@ -369,28 +379,28 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { events.getEvents.restore(); }); - it('should call handleAuctionInit as AUCTION_INIT trigger event', function() { + it('should call handleAuctionInit as AUCTION_INIT trigger event', function () { sinon.spy(greenbidsAnalyticsAdapter, 'handleAuctionInit'); - events.emit(constants.EVENTS.AUCTION_INIT, {auctionId: 'auctionId'}); + events.emit(constants.EVENTS.AUCTION_INIT, { auctionId: 'auctionId' }); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionInit, 1); greenbidsAnalyticsAdapter.handleAuctionInit.restore(); }); - it('should call handleBidTimeout as BID_TIMEOUT trigger event', function() { + it('should call handleBidTimeout as BID_TIMEOUT trigger event', function () { sinon.spy(greenbidsAnalyticsAdapter, 'handleBidTimeout'); - events.emit(constants.EVENTS.BID_TIMEOUT, {auctionId: 'auctionId'}); + events.emit(constants.EVENTS.BID_TIMEOUT, { auctionId: 'auctionId' }); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleBidTimeout, 1); greenbidsAnalyticsAdapter.handleBidTimeout.restore(); }); - it('should call handleAuctionEnd as AUCTION_END trigger event', function() { + it('should call handleAuctionEnd as AUCTION_END trigger event', function () { sinon.spy(greenbidsAnalyticsAdapter, 'handleAuctionEnd'); - events.emit(constants.EVENTS.AUCTION_END, {auctionId: 'auctionId'}); + events.emit(constants.EVENTS.AUCTION_END, { auctionId: 'auctionId' }); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionEnd, 1); greenbidsAnalyticsAdapter.handleAuctionEnd.restore(); }); - it('should call handleBillable as BILLABLE_EVENT trigger event', function() { + it('should call handleBillable as BILLABLE_EVENT trigger event', function () { sinon.spy(greenbidsAnalyticsAdapter, 'handleBillable'); events.emit(constants.EVENTS.BILLABLE_EVENT, { type: 'auction', @@ -403,34 +413,34 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }); }); - describe('isSampled', function() { - it('should return true for invalid sampling rates', function() { + describe('isSampled', function () { + it('should return true for invalid sampling rates', function () { expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', -1, 0.0)).to.be.true; expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 1.2, 0.0)).to.be.true; }); - it('should return determinist falsevalue for valid sampling rate given the predifined id and rate', function() { + it('should return determinist falsevalue for valid sampling rate given the predifined id and rate', function () { expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001, 0.0)).to.be.false; }); - it('should return determinist true value for valid sampling rate given the predifined id and rate', function() { + it('should return determinist true value for valid sampling rate given the predifined id and rate', function () { expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999, 0.0)).to.be.true; }); - it('should return determinist true value for valid sampling rate given the predifined id and rate when we split to non exploration first', function() { + it('should return determinist true value for valid sampling rate given the predifined id and rate when we split to non exploration first', function () { expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999, 0.0, 1.0)).to.be.true; }); - it('should return determinist false value for valid sampling rate given the predifined id and rate when we split to non exploration first', function() { + it('should return determinist false value for valid sampling rate given the predifined id and rate when we split to non exploration first', function () { expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001, 0.0, 1.0)).to.be.false; }); }); - describe('isSampled when analytic isforced', function() { + describe('isSampled when analytic isforced', function () { before(() => { sinon.stub(utils, 'getParameterByName').callsFake(par => par === 'greenbids_force_sampling' ? true : undefined); }); - it('should return determinist true when sampling flag activated', function() { + it('should return determinist true when sampling flag activated', function () { expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001, 0.0)).to.be.true; }); after(() => { diff --git a/test/spec/modules/greenbidsBidAdapter_specs.js b/test/spec/modules/greenbidsBidAdapter_specs.js new file mode 100644 index 00000000000..014c3545cad --- /dev/null +++ b/test/spec/modules/greenbidsBidAdapter_specs.js @@ -0,0 +1,1051 @@ +import { expect } from 'chai'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from 'modules/greenbidsBidAdapter.js'; +const ENDPOINT = 'https://d.greenbids.ai/hb/bid-request'; +const AD_SCRIPT = '"'; + +describe('greenbidsBidAdapter', () => { + const adapter = newBidder(spec); + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'greenbids', + 'params': { + 'placementId': 4242 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + let bidNonGbCompatible = { + 'bidder': 'greenbids', + }; + expect(spec.isBidRequestValid(bidNonGbCompatible)).to.equal(false); + }); + + it('should return false when the placement is not a number', function () { + let bidNonGbCompatible = { + 'bidder': 'greenbids', + 'params': { + 'placementId': 'toto' + }, + }; + expect(spec.isBidRequestValid(bidNonGbCompatible)).to.equal(false); + }); + }) + describe('buildRequests', function () { + it('should send bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should not send auctionId in bid request ', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].auctionId).to.not.exist + }); + + it('should send US Privacy to endpoint', function () { + let usPrivacy = 'OHHHFCP1' + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': usPrivacy + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.us_privacy).to.exist; + expect(payload.us_privacy).to.equal(usPrivacy); + }); + + it('should send GPP values to endpoint when available and valid', function () { + let consentString = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'; + let applicableSectionIds = [7, 8]; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': consentString, + 'applicableSections': applicableSectionIds + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(consentString); + expect(payload.gpp.applicableSectionIds).to.have.members(applicableSectionIds); + }); + + it('should send default GPP values to endpoint when available but invalid', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': undefined, + 'applicableSections': ['a'] + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(''); + expect(payload.gpp.applicableSectionIds).to.have.members([]); + }); + + it('should not set the GPP object in the request sent to the endpoint when not present', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.not.exist; + }); + + it('should send GDPR to endpoint', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + page: 'https://example.com/page.html', + reachedTop: true, + numIframes: 2 + } + } + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.referrer).to.exist; + expect(payload.referrer).to.deep.equal('https://example.com/page.html') + }); + + const originalConnection = window.navigator.connection; + const mockConnection = { downlink: 10 }; + + const setNavigatorConnection = (connection) => { + Object.defineProperty(window.navigator, 'connection', { + value: connection, + configurable: true, + }); + }; + + try { + setNavigatorConnection(mockConnection); + + const requestWithConnection = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithConnection = JSON.parse(requestWithConnection.data); + + expect(payloadWithConnection.networkBandwidth).to.exist; + expect(payloadWithConnection.networkBandwidth).to.deep.equal(mockConnection.downlink.toString()); + + setNavigatorConnection(undefined); + + const requestWithoutConnection = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutConnection = JSON.parse(requestWithoutConnection.data); + + expect(payloadWithoutConnection.networkBandwidth).to.exist; + expect(payloadWithoutConnection.networkBandwidth).to.deep.equal(''); + } finally { + setNavigatorConnection(originalConnection); + } + + it('should add pageReferrer info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageReferrer).to.exist; + expect(payload.pageReferrer).to.deep.equal(document.referrer); + }); + + it('should add width info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceWidth = screen.width + + expect(payload.deviceWidth).to.exist; + expect(payload.deviceWidth).to.deep.equal(deviceWidth); + }); + + it('should add height info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceHeight = screen.height + + expect(payload.deviceHeight).to.exist; + expect(payload.deviceHeight).to.deep.equal(deviceHeight); + }); + + it('should add pixelRatio info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const pixelRatio = window.top.devicePixelRatio + + expect(payload.devicePixelRatio).to.exist; + expect(payload.devicePixelRatio).to.deep.equal(pixelRatio); + }); + + it('should add screenOrientation info to payload', function () { + const originalScreenOrientation = window.top.screen.orientation; + + const mockScreenOrientation = (type) => { + Object.defineProperty(window.top.screen, 'orientation', { + value: { type }, + configurable: true, + }); + }; + + try { + const mockType = 'landscape-primary'; + mockScreenOrientation(mockType); + + const requestWithOrientation = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithOrientation = JSON.parse(requestWithOrientation.data); + + expect(payloadWithOrientation.screenOrientation).to.exist; + expect(payloadWithOrientation.screenOrientation).to.deep.equal(mockType); + + mockScreenOrientation(undefined); + + const requestWithoutOrientation = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutOrientation = JSON.parse(requestWithoutOrientation.data); + + expect(payloadWithoutOrientation.screenOrientation).to.not.exist; + } finally { + Object.defineProperty(window.top.screen, 'orientation', { + value: originalScreenOrientation, + configurable: true, + }); + } + }); + + it('should add historyLength info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.historyLength).to.exist; + expect(payload.historyLength).to.deep.equal(window.top.history.length); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add viewportWidth info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportWidth).to.exist; + expect(payload.viewportWidth).to.deep.equal(window.top.visualViewport.width); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add ortb2 device data to payload', function () { + const ortb2DeviceBidderRequest = { + ...bidderRequestDefault, + ...{ + ortb2: { + device: { + 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' }, + }, + }, + }, + }; + const request = spec.buildRequests(bidRequests, ortb2DeviceBidderRequest); + const payload = JSON.parse(request.data); + + 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 () { + it('should add pageTitle info to payload based on document title', function () { + const testText = 'This is a title'; + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload based on open-graph title', function () { + const testText = 'This is a title from open-graph'; + sandbox.stub(window.top.document, 'title').value(''); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.have.length(300); + }); + + it('should add pageTitle info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback title'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + }); + + describe('pageDescription', function () { + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description from open-graph'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.have.length(300); + }); + + it('should add pageDescription info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback description'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add timeToFirstByte info to payload for Navigation Timing V2', function () { + // Mock `performance` object with Navigation Timing V2 data + const mockPerformance = { + getEntriesByType: () => [ + { requestStart: 100, responseStart: 150 }, + ], + }; + + // Override the global performance object + const originalPerformance = window.performance; + window.performance = mockPerformance; + + // Execute the code + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + // Calculate expected TTFB for V2 + const ttfbExpected = Math.round( + mockPerformance.getEntriesByType('navigation')[0].responseStart - + mockPerformance.getEntriesByType('navigation')[0].requestStart + ).toString(); + + // Assertions + expect(payload.timeToFirstByte).to.exist; + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpected); + + // Restore the original performance object + window.performance = originalPerformance; + }); + + it('should add timeToFirstByte info to payload for Navigation Timing V1', function () { + // Mock `performance` object with Navigation Timing V1 data + const mockPerformance = { + timing: { + requestStart: 100, + responseStart: 150, + }, + getEntriesByType: () => [], + }; + + // Override the global performance object + const originalPerformance = window.performance; + window.performance = mockPerformance; + + // Execute the code + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + // Calculate expected TTFB for V1 + const ttfbExpected = ( + mockPerformance.timing.responseStart - mockPerformance.timing.requestStart + ).toString(); + + // Assertions + expect(payload.timeToFirstByte).to.exist; + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpected); + + // Restore the original performance object + window.performance = originalPerformance; + }); + + it('should send GDPR to endpoint with 11 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': false, + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(11); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR TCF2 to endpoint with 12 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 22 status', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': undefined, + 'vendorData': undefined, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(22); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 0 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': false, + 'vendorData': { + 'hasGlobalScope': false + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 0 status when gdprApplies = false (vendorData = undefined)', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': false, + 'vendorData': undefined, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 12 status when apiVersion = 0', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 0 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(0); + }); + + 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 + }] + } + }); + + const request = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.schain).to.exist; + expect(payload.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + }); + }); + + it('should add userAgentClientHints info to payload if available', function () { + const sua = { + source: 2, + platform: { + brand: 'macOS', + version: ['12', '4', '0'] + }, + browsers: [ + { + brand: 'Chromium', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Google Chrome', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Not;A=Brand', + version: ['99', '0', '0', '0'] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + + const bidRequest = Object.assign({}, bidRequests[0], { + ortb2: { + device: { + sua: sua + } + } + }); + + const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(requestWithUserAgentClientHints.data); + + expect(payload.userAgentClientHints).to.exist; + expect(payload.userAgentClientHints).to.deep.equal(sua); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).userAgentClientHints).to.not.exist; + }); + + it('should use good mediaTypes banner sizes', function () { + const mediaTypesBannerSize = { + 'mediaTypes': { + 'banner': { + 'sizes': [300, 250] + } + } + }; + checkMediaTypesSizes(mediaTypesBannerSize, '300x250'); + }); + }); + + describe('Global Placement Id', function () { + let bidRequests = [ + { + 'bidder': 'greenbids', + 'params': { + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + }, + { + 'bidder': 'greenbids', + 'params': { + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1f', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ef', + 'deviceWidth': 1680 + } + ]; + + it('should add gpid if ortb2Imp.ext.gpid is present and is non empty', function () { + const updatedBidRequests = bidRequests.map(function (bidRequest, index) { + return { + ...bidRequest, + ortb2Imp: { + ext: { + gpid: '1111/home-left-' + index + } + } + }; + } + ); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].gpid).to.equal('1111/home-left-0'); + expect(payload.data[1].gpid).to.equal('1111/home-left-1'); + }); + + it('should not add gpid if ortb2Imp.ext.gpid is present but empty', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + gpid: '' + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + payload.data.forEach(bid => { + expect(bid).not.to.have.property('gpid'); + }); + }); + + it('should not add gpid if ortb2Imp.ext.gpid is not present', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + payload.data.forEach(bid => { + expect(bid).not.to.have.property('gpid'); + }); + }); + + it('should add dsa info to payload if available', function () { + const bidRequestWithDsa = Object.assign({}, bidderRequestDefault, { + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + } + } + } + }); + + const requestWithDsa = spec.buildRequests(bidRequests, bidRequestWithDsa); + const payload = JSON.parse(requestWithDsa.data); + + expect(payload.dsa).to.exist; + expect(payload.dsa).to.deep.equal( + { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + ); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).dsa).to.not.exist; + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid responses', function () { + let bids = { + 'body': { + 'responses': [{ + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 250, + 'bidId': '3ede2a3fa0db94', + 'ttl': 360, + 'width': 300, + 'creativeId': 'er2ee', + 'placementId': 4242 + }, { + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 200, + 'bidId': '4fef3b4gb1ec15', + 'ttl': 360, + 'width': 350, + 'creativeId': 'fs3ff', + 'placementId': 4242, + 'dealId': 'ABC_123', + 'ext': { + 'dsa': { + 'behalf': 'some-behalf', + 'paid': 'some-paid', + 'transparency': [{ + 'domain': 'test.com', + 'dsaparams': [1, 2, 3] + }], + 'adrender': 1 + } + } + }] + } + }; + let expectedResponse = [ + { + 'cpm': 0.5, + 'width': 300, + 'height': 250, + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [] + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '3ede2a3fa0db94', + 'creativeId': 'er2ee', + 'placementId': 4242 + }, { + 'cpm': 0.5, + 'width': 350, + 'height': 200, + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [], + dsa: { + behalf: 'some-behalf', + paid: 'some-paid', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }], + adrender: 1 + } + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '4fef3b4gb1ec15', + 'creativeId': 'fs3ff', + 'placementId': 4242, + 'dealId': 'ABC_123' + } + ] + ; + + let result = spec.interpretResponse(bids); + expect(result).to.eql(expectedResponse); + }); + + it('handles nobid responses', function () { + let bids = { + 'body': { + 'responses': [] + } + }; + + let result = spec.interpretResponse(bids); + expect(result.length).to.equal(0); + }); + }); +}); + +let bidderRequestDefault = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 +}; + +let 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/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index efd7b06685f..18f1f7aa7c8 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -30,12 +30,12 @@ describe('TheMediaGrid Adapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'uid': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -950,6 +950,33 @@ describe('TheMediaGrid Adapter', function () { expect(payload.tmax).to.equal(null); }) + it('should add ORTB2 device data to the request', function () { + const bidderRequestWithDevice = { + ...bidderRequest, + ...{ + ortb2: { + device: { + 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', + }, + }, + }, + }; + + const [request] = spec.buildRequests([bidRequests[0]], bidderRequestWithDevice); + const payload = parseRequest(request.data); + + expect(payload.device).to.deep.equal(bidderRequestWithDevice.ortb2.device); + }); + describe('floorModule', function () { const floorTestData = { 'currency': 'USD', diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index 1c5940c06a3..9318b33697d 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -44,9 +44,9 @@ describe('gumgumAdapter', function () { }); it('should return true when required params found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'inSlot': '789' }; @@ -54,33 +54,33 @@ describe('gumgumAdapter', function () { }); it('should return true when inslot sends sizes and trackingid', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'inSlot': '789', 'sizes': [[0, 1], [2, 3], [4, 5], [6, 7]] }; - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(spec.isBidRequestValid(invalidBid)).to.equal(true); }); it('should return false when no unit type is specified', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placementId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when bidfloor is not a number', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'inSlot': '789', 'bidfloor': '0.50' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false if invalid request id is found', function () { @@ -100,6 +100,29 @@ describe('gumgumAdapter', function () { describe('buildRequests', function () { let sizesArray = [[300, 250], [300, 600]]; + const bidderRequest = { + ortb2: { + site: { + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }], + url: 'http://pub.com/news', + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + id: 'p10000', + domain: 'pub.com' + } + } + } + }; + let bidRequests = [ { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', @@ -259,19 +282,21 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data).to.have.property('iriscat'); }); - + it('should set the irisid param when found iris_c73g5jq96mwso4d8', function() { + const request = { ...bidRequests[0], params: { irisid: 'abc123' } }; + const bidRequest = spec.buildRequests([request], bidderRequest)[0]; + expect(bidRequest.data).to.have.property('irisid', 'iris_c73g5jq96mwso4d8'); + }); + it('should set the curl param if present', function() { + const request = { ...bidRequests[0] }; + const bidRequest = spec.buildRequests([request], bidderRequest)[0]; + expect(bidRequest.data).to.have.property('curl', 'http://pub.com/news'); + }); it('should not set the iriscat param when not found', function () { const request = { ...bidRequests[0] } const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data).to.not.have.property('iriscat'); }); - - it('should set the irisid param when found', function () { - const request = { ...bidRequests[0], params: { irisid: 'abc123' } } - const bidRequest = spec.buildRequests([request])[0]; - expect(bidRequest.data).to.have.property('irisid'); - }); - it('should not set the irisid param when not found', function () { const request = { ...bidRequests[0] } const bidRequest = spec.buildRequests([request])[0]; @@ -285,10 +310,38 @@ describe('gumgumAdapter', function () { }); it('should set the global placement id (gpid) if in adserver property', function () { - const req = { ...bidRequests[0], ortb2Imp: { ext: { data: { adserver: { name: 'test', adslot: 123456 } } } } } + const req = { ...bidRequests[0], + ortb2Imp: { + ext: { + gpid: '/17037559/jeusol/jeusol_D_1', + data: { + adserver: { + name: 'test', + adslot: 123456 + } + } + } + } } const bidRequest = spec.buildRequests([req])[0]; expect(bidRequest.data).to.have.property('gpid'); - expect(bidRequest.data.gpid).to.equal(123456); + expect(bidRequest.data.gpid).to.equal('/17037559/jeusol/jeusol_D_1'); + }); + it('should set ae value to 1 for PAAPI', function () { + const req = { ...bidRequests[0], + ortb2Imp: { + ext: { + ae: 1, + data: { + adserver: { + name: 'test', + adslot: 123456 + } + } + } + } } + const bidRequest = spec.buildRequests([req])[0]; + expect(bidRequest.data).to.have.property('ae'); + expect(bidRequest.data.ae).to.equal(true); }); it('should set the global placement id (gpid) if in pbadslot property', function () { @@ -469,7 +522,12 @@ describe('gumgumAdapter', function () { startdelay: 1, placement: 123456, plcmt: 3, - protocols: [1, 2] + protocols: [1, 2], + skip: 1, + api: [1, 2], + mimes: ['video/mp4', 'video/webm'], + playbackmethod: [1, 2], + playbackend: 2 }; const request = Object.assign({}, bidRequests[0]); delete request.params; @@ -491,6 +549,11 @@ describe('gumgumAdapter', function () { expect(bidRequest.data.pr).to.eq(videoVals.protocols.join(',')); expect(bidRequest.data.viw).to.eq(videoVals.playerSize[0].toString()); expect(bidRequest.data.vih).to.eq(videoVals.playerSize[1].toString()); + expect(bidRequest.data.skip).to.eq(videoVals.skip); + expect(bidRequest.data.api).to.eq(videoVals.api.join(',')); + expect(bidRequest.data.mimes).to.eq(videoVals.mimes.join(',')); + expect(bidRequest.data.pbm).to.eq(videoVals.playbackmethod.join(',')); + expect(bidRequest.data.pbe).to.eq(videoVals.playbackend); }); it('should add parameters associated with invideo if invideo request param is found', function () { const inVideoVals = { @@ -502,7 +565,12 @@ describe('gumgumAdapter', function () { startdelay: 1, placement: 123456, plcmt: 3, - protocols: [1, 2] + protocols: [1, 2], + skip: 1, + api: [1, 2], + mimes: ['video/mp4', 'video/webm'], + playbackmethod: [6], + playbackend: 1 }; const request = Object.assign({}, bidRequests[0]); delete request.params; @@ -524,6 +592,11 @@ describe('gumgumAdapter', function () { expect(bidRequest.data.pr).to.eq(inVideoVals.protocols.join(',')); expect(bidRequest.data.viw).to.eq(inVideoVals.playerSize[0].toString()); expect(bidRequest.data.vih).to.eq(inVideoVals.playerSize[1].toString()); + expect(bidRequest.data.skip).to.eq(inVideoVals.skip); + expect(bidRequest.data.api).to.eq(inVideoVals.api.join(',')); + expect(bidRequest.data.mimes).to.eq(inVideoVals.mimes.join(',')); + expect(bidRequest.data.pbm).to.eq(inVideoVals.playbackmethod.join(',')); + expect(bidRequest.data.pbe).to.eq(inVideoVals.playbackend); }); it('should not add additional parameters depending on params field', function () { const request = spec.buildRequests(bidRequests)[0]; @@ -737,6 +810,59 @@ describe('gumgumAdapter', function () { expect(bidRequest.data.pu.includes('ggad')).to.be.false; expect(bidRequest.data.pu.includes('ggdeal')).to.be.false; }); + + it('should handle ORTB2 device data', function () { + const ortb2 = { + device: { + 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'}, + ip: '127.0.0.1', + ipv6: '51dc:5e20:fd6a:c955:66be:03b4:dfa3:35b2', + }, + }; + + const bidRequest = spec.buildRequests(bidRequests, { ortb2 })[0]; + + expect(bidRequest.data.dnt).to.equal(ortb2.device.dnt); + expect(bidRequest.data.ua).to.equal(ortb2.device.ua); + expect(bidRequest.data.lang).to.equal(ortb2.device.language); + expect(bidRequest.data.dt).to.equal(ortb2.device.devicetype); + expect(bidRequest.data.make).to.equal(ortb2.device.make); + expect(bidRequest.data.model).to.equal(ortb2.device.model); + expect(bidRequest.data.os).to.equal(ortb2.device.os); + expect(bidRequest.data.osv).to.equal(ortb2.device.osv); + expect(bidRequest.data.foddid).to.equal(ortb2.device.ext.fiftyonedegrees_deviceId); + expect(bidRequest.data.ip).to.equal(ortb2.device.ip); + expect(bidRequest.data.ipv6).to.equal(ortb2.device.ipv6); + }); + + it('should set tId from ortb2Imp.ext.tid if available', function () { + const ortb2Imp = { ext: { tid: 'test-tid-1' } }; + const request = { ...bidRequests[0], ortb2Imp }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.tId).to.equal('test-tid-1'); + }); + + it('should set tId from bidderRequest.ortb2.source.tid if ortb2Imp.ext.tid is not available', function () { + const ortb2 = { source: { tid: 'test-tid-2' } }; + const fakeBidRequest = { ortb2 }; + const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; + expect(bidRequest.data.tId).to.equal('test-tid-2'); + }); + + it('should set tId to an empty string if neither ortb2Imp.ext.tid nor bidderRequest.ortb2.source.tid are available', function () { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.tId).to.equal(''); + }) }) describe('interpretResponse', function () { diff --git a/test/spec/modules/h12mediaBidAdapter_spec.js b/test/spec/modules/h12mediaBidAdapter_spec.js index 9861069f260..154da869b38 100644 --- a/test/spec/modules/h12mediaBidAdapter_spec.js +++ b/test/spec/modules/h12mediaBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import {spec} from 'modules/h12mediaBidAdapter'; import {newBidder} from 'src/adapters/bidderFactory'; -import * as utils from 'src/utils'; +import {clearCache} from '../../../libraries/boundingClientRect/boundingClientRect'; describe('H12 Media Adapter', function () { const DEFAULT_CURRENCY = 'USD'; @@ -158,6 +158,7 @@ describe('H12 Media Adapter', function () { left: 10, top: 10, }); + clearCache(); }); afterEach(function () { diff --git a/test/spec/modules/hadronIdSystem_spec.js b/test/spec/modules/hadronIdSystem_spec.js index 85c8cc11c9e..70aaf06bcc8 100644 --- a/test/spec/modules/hadronIdSystem_spec.js +++ b/test/spec/modules/hadronIdSystem_spec.js @@ -1,12 +1,15 @@ -import { hadronIdSubmodule, storage } from 'modules/hadronIdSystem.js'; -import { server } from 'test/mocks/xhr.js'; -import * as utils from 'src/utils.js'; +import {hadronIdSubmodule, storage, LS_TAM_KEY} from 'modules/hadronIdSystem.js'; +import {server} from 'test/mocks/xhr.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; describe('HadronIdSystem', function () { - describe('getId', function() { + const HADRON_TEST = 'tstCachedHadronId1'; + describe('getId', function () { let getDataFromLocalStorageStub; - beforeEach(function() { + beforeEach(function () { getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); @@ -14,42 +17,47 @@ describe('HadronIdSystem', function () { getDataFromLocalStorageStub.restore(); }); - it('gets a hadronId', function() { + it('gets a cached hadronid', function () { const config = { params: {} }; - const callbackSpy = sinon.spy(); - const callback = hadronIdSubmodule.getId(config).callback; - callback(callbackSpy); - const request = server.requests[0]; - expect(request.url).to.match(/^https:\/\/id\.hadron\.ad\.gt\/api\/v1\/pbhid/); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({ id: { hadronId: 'testHadronId1' } }); - }); - - it('gets a cached hadronid', function() { - const config = { - params: {} - }; - getDataFromLocalStorageStub.withArgs('auHadronId').returns('tstCachedHadronId1'); - + getDataFromLocalStorageStub.withArgs(LS_TAM_KEY).returns(HADRON_TEST); const result = hadronIdSubmodule.getId(config); - expect(result).to.deep.equal({ id: { hadronId: 'tstCachedHadronId1' } }); + expect(result).to.deep.equal({id: HADRON_TEST}); }); - it('allows configurable id url', function() { + it('allows configurable id url', function () { const config = { params: { url: 'https://hadronid.publync.com' } }; + getDataFromLocalStorageStub.withArgs(LS_TAM_KEY).returns(null); const callbackSpy = sinon.spy(); const callback = hadronIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; expect(request.url).to.match(/^https:\/\/hadronid\.publync\.com\//); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({ id: { hadronId: 'testHadronId1' } }); }); }); + + describe('eids', () => { + before(() => { + attachIdSystem(hadronIdSubmodule); + }); + it('hadronId', function () { + const userId = { + hadronId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'audigent.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + }) }); diff --git a/test/spec/modules/hadronRtdProvider_spec.js b/test/spec/modules/hadronRtdProvider_spec.js index b9e07c97f84..46877f246b5 100644 --- a/test/spec/modules/hadronRtdProvider_spec.js +++ b/test/spec/modules/hadronRtdProvider_spec.js @@ -1,15 +1,20 @@ -// TODO: this and hadronRtdProvider_spec are a copy-paste of each other - import {config} from 'src/config.js'; -import {HALOID_LOCAL_NAME, RTD_LOCAL_NAME, addRealTimeData, getRealTimeData, hadronSubmodule, storage} from 'modules/hadronRtdProvider.js'; +import { + HADRONID_LOCAL_NAME, + RTD_LOCAL_NAME, + addRealTimeData, + getRealTimeData, + hadronSubmodule, + storage +} from 'modules/hadronRtdProvider.js'; import {server} from 'test/mocks/xhr.js'; const responseHeader = {'Content-Type': 'application/json'}; -describe('hadronRtdProvider', function() { +describe('hadronRtdProvider', function () { let getDataFromLocalStorageStub; - beforeEach(function() { + beforeEach(function () { config.resetConfig(); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); @@ -18,19 +23,19 @@ describe('hadronRtdProvider', function() { getDataFromLocalStorageStub.restore(); }); - describe('hadronSubmodule', function() { + describe('hadronSubmodule', function () { it('successfully instantiates', function () { - expect(hadronSubmodule.init()).to.equal(true); + expect(hadronSubmodule.init()).to.equal(true); }); }); - describe('Add Real-Time Data', function() { - it('merges ortb2 data', function() { + describe('Add Real-Time Data', function () { + it('merges ortb2 data', function () { let rtdConfig = {}; const setConfigUserObj1 = { name: 'www.dataprovider1.com', - ext: { taxonomyname: 'iab_audience_taxonomy' }, + ext: {taxonomyname: 'iab_audience_taxonomy'}, segment: [{ id: '1776' }] @@ -38,7 +43,7 @@ describe('hadronRtdProvider', function() { const setConfigUserObj2 = { name: 'www.dataprovider2.com', - ext: { taxonomyname: 'iab_audience_taxonomy' }, + ext: {taxonomyname: 'iab_audience_taxonomy'}, segment: [{ id: '1914' }] @@ -125,12 +130,12 @@ describe('hadronRtdProvider', function() { expect(ortb2Config.site.content.data).to.deep.include.members([setConfigSiteObj1, rtdSiteObj1]); }); - it('merges ortb2 data without duplication', function() { + it('merges ortb2 data without duplication', function () { let rtdConfig = {}; const userObj1 = { name: 'www.dataprovider1.com', - ext: { taxonomyname: 'iab_audience_taxonomy' }, + ext: {taxonomyname: 'iab_audience_taxonomy'}, segment: [{ id: '1776' }] @@ -138,7 +143,7 @@ describe('hadronRtdProvider', function() { const userObj2 = { name: 'www.dataprovider2.com', - ext: { taxonomyname: 'iab_audience_taxonomy' }, + ext: {taxonomyname: 'iab_audience_taxonomy'}, segment: [{ id: '1914' }] @@ -197,12 +202,12 @@ describe('hadronRtdProvider', function() { expect(ortb2Config.site.content.data).to.have.lengthOf(1); }); - it('merges bidder-specific ortb2 data', function() { + it('merges bidder-specific ortb2 data', function () { let rtdConfig = {}; const configUserObj1 = { name: 'www.dataprovider1.com', - ext: { segtax: 3 }, + ext: {segtax: 3}, segment: [{ id: '1776' }] @@ -210,7 +215,7 @@ describe('hadronRtdProvider', function() { const configUserObj2 = { name: 'www.dataprovider2.com', - ext: { segtax: 3 }, + ext: {segtax: 3}, segment: [{ id: '1914' }] @@ -218,7 +223,7 @@ describe('hadronRtdProvider', function() { const configUserObj3 = { name: 'www.dataprovider1.com', - ext: { segtax: 3 }, + ext: {segtax: 3}, segment: [{ id: '2003' }] @@ -374,12 +379,12 @@ describe('hadronRtdProvider', function() { expect(ortb2Config.site.content.data).to.deep.include.members([configSiteObj2, rtdSiteObj2]); }); - it('merges bidder-specific ortb2 data without duplication', function() { + it('merges bidder-specific ortb2 data without duplication', function () { let rtdConfig = {}; const userObj1 = { name: 'www.dataprovider1.com', - ext: { segtax: 3 }, + ext: {segtax: 3}, segment: [{ id: '1776' }] @@ -387,7 +392,7 @@ describe('hadronRtdProvider', function() { const userObj2 = { name: 'www.dataprovider2.com', - ext: { segtax: 3 }, + ext: {segtax: 3}, segment: [{ id: '1914' }] @@ -395,7 +400,7 @@ describe('hadronRtdProvider', function() { const userObj3 = { name: 'www.dataprovider1.com', - ext: { segtax: 3 }, + ext: {segtax: 3}, segment: [{ id: '2003' }] @@ -503,10 +508,10 @@ describe('hadronRtdProvider', function() { expect(ortb2Config.site.content.data).to.have.lengthOf(2); }); - it('allows publisher defined rtd ortb2 logic', function() { + it('allows publisher defined rtd ortb2 logic', function () { const rtdConfig = { params: { - handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + handleRtd: function (bidConfig, rtd, rtdConfig, pbConfig) { if (rtd.ortb2.user.data[0].segment[0].id == '1776') { pbConfig.setConfig({ortb2: rtd.ortb2}); } else { @@ -520,7 +525,7 @@ describe('hadronRtdProvider', function() { const rtdUserObj1 = { name: 'www.dataprovider.com', - ext: { taxonomyname: 'iab_audience_taxonomy' }, + ext: {taxonomyname: 'iab_audience_taxonomy'}, segment: [{ id: '1776' }] @@ -566,10 +571,10 @@ describe('hadronRtdProvider', function() { expect(config.getConfig().ortb2).to.deep.equal({}); }); - it('allows publisher defined adunit logic', function() { + it('allows publisher defined adunit logic', function () { const rtdConfig = { params: { - handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + handleRtd: function (bidConfig, rtd, rtdConfig, pbConfig) { var adUnits = bidConfig.adUnits; for (var i = 0; i < adUnits.length; i++) { var adUnit = adUnits[i]; @@ -631,8 +636,8 @@ describe('hadronRtdProvider', function() { }); }); - describe('Get Real-Time Data', function() { - it('gets rtd from local storage cache', function() { + describe('Get Real-Time Data', function () { + it('gets rtd from local storage cache', function () { const rtdConfig = { params: { segmentCache: true @@ -667,12 +672,12 @@ describe('hadronRtdProvider', function() { }; getDataFromLocalStorageStub.withArgs(RTD_LOCAL_NAME).returns(JSON.stringify(cachedRtd)); - - getRealTimeData(bidConfig, () => {}, rtdConfig, {}); - expect(bidConfig.ortb2Fragments.global.user.data).to.deep.include.members([rtdUserObj1]); + getRealTimeData(bidConfig, () => { + expect(bidConfig.ortb2Fragments.global.user.data).to.deep.include.members([rtdUserObj1]); + }, rtdConfig, {}); }); - it('gets real-time data via async request', function() { + it('gets real-time data via async request', function () { const setConfigSiteObj1 = { name: 'www.audigent.com', ext: { @@ -737,17 +742,15 @@ describe('hadronRtdProvider', function() { } }; - getDataFromLocalStorageStub.withArgs(HALOID_LOCAL_NAME).returns('testHadronId1'); - getRealTimeData(bidConfig, () => {}, rtdConfig, {}); - - let request = server.requests[0]; - let postData = JSON.parse(request.requestBody); - expect(postData.config).to.have.deep.property('publisherId', 'testPub1'); - expect(postData.userIds).to.have.deep.property('hadronId', 'testHadronId1'); - - request.respond(200, responseHeader, JSON.stringify(data)); - - expect(bidConfig.ortb2Fragments.global.user.data).to.deep.include.members([rtdUserObj1]); + getDataFromLocalStorageStub.withArgs(HADRONID_LOCAL_NAME).returns('testHadronId1'); + getRealTimeData(bidConfig, () => { + let request = server.requests[0]; + let postData = JSON.parse(request.requestBody); + expect(postData.config).to.have.deep.property('publisherId', 'testPub1'); + expect(postData.userIds).to.have.deep.property('hadronId', 'testHadronId1'); + request.respond(200, responseHeader, JSON.stringify(data)); + expect(bidConfig.ortb2Fragments.global.user.data).to.deep.include.members([rtdUserObj1]); + }, rtdConfig, {}); }); }); }); diff --git a/test/spec/modules/holidBidAdapter_spec.js b/test/spec/modules/holidBidAdapter_spec.js index ef0283d0f2c..671427d3475 100644 --- a/test/spec/modules/holidBidAdapter_spec.js +++ b/test/spec/modules/holidBidAdapter_spec.js @@ -1,10 +1,10 @@ -import { expect } from 'chai' -import { spec } from 'modules/holidBidAdapter.js' +import { expect } from 'chai'; +import { spec } from 'modules/holidBidAdapter.js'; describe('holidBidAdapterTests', () => { const bidderRequest = { bidderRequestId: 'test-id' - } + }; const bidRequestData = { bidder: 'holid', @@ -32,59 +32,60 @@ describe('holidBidAdapterTests', () => { w: 1860, } } - } + }; describe('isBidRequestValid', () => { - const bid = JSON.parse(JSON.stringify(bidRequestData)) + const bid = JSON.parse(JSON.stringify(bidRequestData)); it('should return true', () => { - expect(spec.isBidRequestValid(bid)).to.equal(true) - }) + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); it('should return false when required params are not passed', () => { - const bid = JSON.parse(JSON.stringify(bidRequestData)) - delete bid.params.adUnitID + const bid = JSON.parse(JSON.stringify(bidRequestData)); + delete bid.params.adUnitID; - expect(spec.isBidRequestValid(bid)).to.equal(false) - }) - }) + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); describe('buildRequests', () => { - const bid = JSON.parse(JSON.stringify(bidRequestData)) - const request = spec.buildRequests([bid], bidderRequest) - const payload = JSON.parse(request[0].data) + const bid = JSON.parse(JSON.stringify(bidRequestData)); + const request = spec.buildRequests([bid], bidderRequest); + const payload = JSON.parse(request[0].data); it('should include id in request', () => { - expect(payload.id).to.equal('test-id') - }) + expect(payload.id).to.equal('test-id'); + }); it('should include ext in imp', () => { expect(payload.imp[0].ext).to.deep.equal({ prebid: { storedrequest: { id: '12345' } }, - }) - }) + }); + }); it('should include ext in request', () => { expect(payload.ext).to.deep.equal({ prebid: { storedrequest: { id: '12345' } }, - }) - }) + }); + }); it('should include banner format in imp', () => { expect(payload.imp[0].banner).to.deep.equal({ format: [{ w: 300, h: 250 }], - }) - }) + }); + }); it('should include ortb2 first party data', () => { - expect(payload.device.w).to.equal(1860) - expect(payload.device.h).to.equal(410) - expect(payload.user.ext.consent).to.equal('G4ll0p1ng_Un1c0rn5') - expect(payload.regs.gdpr).to.equal(1) - }) - }) + expect(payload.device.w).to.equal(1860); + expect(payload.device.h).to.equal(410); + expect(payload.user.ext.consent).to.equal('G4ll0p1ng_Un1c0rn5'); + expect(payload.regs.gdpr).to.equal(1); + }); + }); describe('interpretResponse', () => { + // Add impid: 'bid-id' so requestId matches bidRequestData.bidId const serverResponse = { body: { id: 'test-id', @@ -94,6 +95,7 @@ describe('holidBidAdapterTests', () => { bid: [ { id: 'testbidid', + impid: 'bid-id', price: 0.4, adm: 'test-ad', adid: 789456, @@ -105,40 +107,37 @@ describe('holidBidAdapterTests', () => { }, ], }, - } + }; - const interpretedResponse = spec.interpretResponse( - serverResponse, - bidRequestData - ) + const interpretedResponse = spec.interpretResponse(serverResponse, bidRequestData); it('should interpret response', () => { - expect(interpretedResponse[0].requestId).to.equal(bidRequestData.bidId) + expect(interpretedResponse[0].requestId).to.equal(bidRequestData.bidId); expect(interpretedResponse[0].cpm).to.equal( serverResponse.body.seatbid[0].bid[0].price - ) + ); expect(interpretedResponse[0].ad).to.equal( serverResponse.body.seatbid[0].bid[0].adm - ) + ); expect(interpretedResponse[0].creativeId).to.equal( serverResponse.body.seatbid[0].bid[0].crid - ) + ); expect(interpretedResponse[0].width).to.equal( serverResponse.body.seatbid[0].bid[0].w - ) + ); expect(interpretedResponse[0].height).to.equal( serverResponse.body.seatbid[0].bid[0].h - ) - expect(interpretedResponse[0].currency).to.equal(serverResponse.body.cur) - }) - }) + ); + expect(interpretedResponse[0].currency).to.equal(serverResponse.body.cur); + }); + }); describe('getUserSyncs', () => { it('should return user sync', () => { const optionsType = { iframeEnabled: true, pixelEnabled: true, - } + }; const serverResponse = [ { body: { @@ -150,12 +149,14 @@ describe('holidBidAdapterTests', () => { }, }, }, - ] + ]; const gdprConsent = { gdprApplies: 1, consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig', - } - const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb' + }; + const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb'; + + // Updated 'usp_consent' to 'us_privacy' to match adapter code const expectedUserSyncs = [ { type: 'image', @@ -163,52 +164,53 @@ describe('holidBidAdapterTests', () => { }, { type: 'iframe', - url: 'https://null.holid.io/sync.html?bidders=%5B%22test%20seat%201%22%2C%22test%20seat%202%22%5D&gdpr=1&gdpr_consent=dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig&usp_consent=mkjvbiniwot4827obfoy8sdg8203gb&type=iframe', + url: 'https://null.holid.io/sync.html?bidders=%5B%22test%20seat%201%22%2C%22test%20seat%202%22%5D&gdpr=1&gdpr_consent=dkj49Sjmfjuj34as%3A12jaf90123hufabidfy9u23brfpoig&us_privacy=mkjvbiniwot4827obfoy8sdg8203gb&type=iframe', }, - ] + ]; const userSyncs = spec.getUserSyncs( optionsType, serverResponse, gdprConsent, uspConsent - ) + ); - expect(userSyncs).to.deep.equal(expectedUserSyncs) - }) + expect(userSyncs).to.deep.equal(expectedUserSyncs); + }); it('should return base user syncs when responsetimemillis is not defined', () => { const optionsType = { iframeEnabled: true, pixelEnabled: true, - } + }; const serverResponse = [ { body: { ext: {}, }, }, - ] + ]; const gdprConsent = { gdprApplies: 1, consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig', - } - const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb' + }; + const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb'; + const expectedUserSyncs = [ { type: 'image', url: 'https://track.adform.net/Serving/TrackPoint/?pm=2992097&lid=132720821', - } - ] + }, + ]; const userSyncs = spec.getUserSyncs( optionsType, serverResponse, gdprConsent, uspConsent - ) + ); - expect(userSyncs).to.deep.equal(expectedUserSyncs) - }) - }) -}) + expect(userSyncs).to.deep.equal(expectedUserSyncs); + }); + }); +}); diff --git a/test/spec/modules/humansecurityRtdProvider_spec.js b/test/spec/modules/humansecurityRtdProvider_spec.js new file mode 100644 index 00000000000..627c3a6c1e4 --- /dev/null +++ b/test/spec/modules/humansecurityRtdProvider_spec.js @@ -0,0 +1,210 @@ +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; + +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js'; +import * as refererDetection from '../../../src/refererDetection.js'; + +import { __TEST__ } from '../../../modules/humansecurityRtdProvider.js'; + +const { + SUBMODULE_NAME, + SCRIPT_URL, + main, + load, + onImplLoaded, + onImplMessage, + onGetBidRequestData +} = __TEST__; + +describe('humansecurity RTD module', function () { + let sandbox; + + const stubUuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; + const sonarStubId = `sonar_${stubUuid}`; + const stubWindow = { [sonarStubId]: undefined }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(utils, 'getWindowSelf').returns(stubWindow); + sandbox.stub(utils, 'generateUUID').returns(stubUuid); + sandbox.stub(refererDetection, 'getRefererInfo').returns({ domain: 'example.com' }); + }); + afterEach(function() { + sandbox.restore(); + }); + + describe('Initialization step', function () { + let sandbox2; + let connectSpy; + beforeEach(function() { + sandbox2 = sinon.sandbox.create(); + connectSpy = sandbox.spy(); + // Once the impl script is loaded, it registers the API using session ID + sandbox2.stub(stubWindow, sonarStubId).value({ connect: connectSpy }); + }); + afterEach(function () { + sandbox2.restore(); + }); + + it('should accept valid configurations', function () { + // Default configuration - empty + expect(() => load({})).to.not.throw(); + expect(() => load({ params: {} })).to.not.throw(); + // Configuration with clientId + expect(() => load({ params: { clientId: 'customer123' } })).to.not.throw(); + }); + + it('should throw an Error on invalid configuration', function () { + expect(() => load({ params: { clientId: 123 } })).to.throw(); + expect(() => load({ params: { clientId: 'abc.def' } })).to.throw(); + expect(() => load({ params: { clientId: '1' } })).to.throw(); + expect(() => load({ params: { clientId: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' } })).to.throw(); + }); + + it('should insert implementation script', () => { + load({ }); + + expect(loadExternalScriptStub.calledOnce).to.be.true; + + const args = loadExternalScriptStub.getCall(0).args; + expect(args[0]).to.be.equal(`${SCRIPT_URL}?r=example.com`); + expect(args[2]).to.be.equal(SUBMODULE_NAME); + expect(args[3]).to.be.equal(onImplLoaded); + expect(args[4]).to.be.equal(null); + expect(args[5]).to.be.deep.equal({ 'data-sid': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' }); + }); + + it('should insert external script element with "customerId" info from config', () => { + load({ params: { clientId: 'customer123' } }); + + expect(loadExternalScriptStub.calledOnce).to.be.true; + + const args = loadExternalScriptStub.getCall(0).args; + expect(args[0]).to.be.equal(`${SCRIPT_URL}?r=example.com&c=customer123`); + }); + + it('should connect to the implementation script once it loads', function () { + load({ }); + + expect(loadExternalScriptStub.calledOnce).to.be.true; + expect(connectSpy.calledOnce).to.be.true; + + const args = connectSpy.getCall(0).args; + expect(args[0]).to.haveOwnProperty('cmd'); // pbjs global + expect(args[0]).to.haveOwnProperty('que'); + expect(args[1]).to.be.equal(onImplMessage); + }); + }); + + describe('Bid enrichment step', function () { + const hmnsData = { 'v1': 'sometoken' }; + + let sandbox2; + let callbackSpy; + let reqBidsConfig; + beforeEach(function() { + sandbox2 = sinon.sandbox.create(); + callbackSpy = sandbox2.spy(); + reqBidsConfig = { ortb2Fragments: { bidder: {}, global: {} } }; + }); + afterEach(function () { + sandbox2.restore(); + }); + + it('should add empty device.ext.hmns to global ortb2 when data is yet to be received from the impl script', () => { + load({ }); + + onGetBidRequestData(reqBidsConfig, callbackSpy, { params: {} }, {}); + + expect(callbackSpy.calledOnce).to.be.true; + expect(reqBidsConfig.ortb2Fragments.global).to.have.own.property('device'); + expect(reqBidsConfig.ortb2Fragments.global.device).to.have.own.property('ext'); + expect(reqBidsConfig.ortb2Fragments.global.device.ext).to.have.own.property('hmns').which.is.an('object').that.deep.equals({}); + }); + + it('should add the default device.ext.hmns to global ortb2 when no "hmns" data was yet received', () => { + load({ }); + + onImplMessage({ type: 'info', data: 'not a hmns message' }); + onGetBidRequestData(reqBidsConfig, callbackSpy, { params: {} }, {}); + + expect(callbackSpy.calledOnce).to.be.true; + expect(reqBidsConfig.ortb2Fragments.global).to.have.own.property('device'); + expect(reqBidsConfig.ortb2Fragments.global.device).to.have.own.property('ext'); + expect(reqBidsConfig.ortb2Fragments.global.device.ext).to.have.own.property('hmns').which.is.an('object').that.deep.equals({}); + }); + + it('should add device.ext.hmns with received tokens to global ortb2 when the data was received', () => { + load({ }); + + onImplMessage({ type: 'hmns', data: hmnsData }); + onGetBidRequestData(reqBidsConfig, callbackSpy, { params: {} }, {}); + + expect(callbackSpy.calledOnce).to.be.true; + expect(reqBidsConfig.ortb2Fragments.global).to.have.own.property('device'); + expect(reqBidsConfig.ortb2Fragments.global.device).to.have.own.property('ext'); + expect(reqBidsConfig.ortb2Fragments.global.device.ext).to.have.own.property('hmns').which.is.an('object').that.deep.equals(hmnsData); + }); + + it('should update device.ext.hmns with new data', () => { + load({ }); + + onImplMessage({ type: 'hmns', data: { 'v1': 'should be overwritten' } }); + onImplMessage({ type: 'hmns', data: hmnsData }); + onGetBidRequestData(reqBidsConfig, callbackSpy, { params: {} }, {}); + + expect(callbackSpy.calledOnce).to.be.true; + expect(reqBidsConfig.ortb2Fragments.global).to.have.own.property('device'); + expect(reqBidsConfig.ortb2Fragments.global.device).to.have.own.property('ext'); + expect(reqBidsConfig.ortb2Fragments.global.device.ext).to.have.own.property('hmns').which.is.an('object').that.deep.equals(hmnsData); + }); + }); + + describe('Sumbodule execution', function() { + let sandbox2; + let submoduleStub; + beforeEach(function() { + sandbox2 = sinon.sandbox.create(); + submoduleStub = sandbox2.stub(hook, 'submodule'); + }); + afterEach(function () { + sandbox2.restore(); + }); + + function getModule() { + main(); + + expect(submoduleStub.calledOnceWith('realTimeData')).to.equal(true); + + const submoduleDef = submoduleStub.getCall(0).args[1]; + expect(submoduleDef).to.be.an('object'); + expect(submoduleDef).to.have.own.property('name', SUBMODULE_NAME); + expect(submoduleDef).to.have.own.property('init').that.is.a('function'); + expect(submoduleDef).to.have.own.property('getBidRequestData').that.is.a('function'); + + return submoduleDef; + } + + it('should register humansecurity RTD submodule provider', function () { + getModule(); + }); + + it('should refuse initialization on invalid customer id configuration', function () { + const { init } = getModule(); + expect(init({ params: { clientId: 123 } })).to.equal(false); + expect(loadExternalScriptStub.notCalled).to.be.true; + }); + + it('should commence initialization on valid initialization', function () { + const { init } = getModule(); + expect(init({ params: { clientId: 'customer123' } })).to.equal(true); + expect(loadExternalScriptStub.calledOnce).to.be.true; + }); + + it('should commence initialization on default initialization', function () { + const { init } = getModule(); + expect(init({ })).to.equal(true); + expect(loadExternalScriptStub.calledOnce).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/id5AnalyticsAdapter_spec.js b/test/spec/modules/id5AnalyticsAdapter_spec.js index c9d21daa4e0..7616052dbe7 100644 --- a/test/spec/modules/id5AnalyticsAdapter_spec.js +++ b/test/spec/modules/id5AnalyticsAdapter_spec.js @@ -102,7 +102,7 @@ describe('ID5 analytics adapter', () => { server.respond(); // Why 3? 1: config, 2: tcfEnforcement, 3: auctionEnd - // tcfEnforcement? yes, gdprEnforcement module emits in reaction to auctionEnd + // tcfEnforcement? yes, tcfControl module emits in reaction to auctionEnd expect(server.requests).to.have.length(3); const body1 = JSON.parse(server.requests[1].requestBody); diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index fd5d24295f5..48ff5223ff5 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -1,29 +1,29 @@ import * as id5System from '../../../modules/id5IdSystem.js'; import { + attachIdSystem, coreStorage, getConsentHash, init, - requestBidsHook, - setSubmoduleRegistry + setSubmoduleRegistry, + startAuctionHook } from '../../../modules/userId/index.js'; import {config} from '../../../src/config.js'; import * as events from '../../../src/events.js'; -import { EVENTS } from '../../../src/constants.js'; +import {EVENTS} from '../../../src/constants.js'; import * as utils from '../../../src/utils.js'; -import {uspDataHandler, gppDataHandler} from '../../../src/adapterManager.js'; import '../../../src/prebid.js'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {server} from '../../mocks/xhr.js'; import {expect} from 'chai'; -import {GreedyPromise} from '../../../src/utils/promise.js'; - -const IdFetchFlow = id5System.IdFetchFlow; +import {PbPromise} from '../../../src/utils/promise.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; describe('ID5 ID System', function () { const ID5_MODULE_NAME = 'id5Id'; const ID5_EIDS_NAME = ID5_MODULE_NAME.toLowerCase(); const ID5_SOURCE = 'id5-sync.com'; + const TRUE_LINK_SOURCE = 'true-link-id5-sync.com'; const ID5_TEST_PARTNER_ID = 173; const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_TEST_PARTNER_ID}.json`; const ID5_API_CONFIG_URL = `https://id5-sync.com/api/config/prebid`; @@ -46,10 +46,8 @@ describe('ID5 ID System', function () { const EUID_STORED_ID = 'EUID_1'; const EUID_SOURCE = 'uidapi.com'; const ID5_STORED_OBJ_WITH_EUID = { - 'universal_uid': ID5_STORED_ID, - 'signature': ID5_STORED_SIGNATURE, + ...ID5_STORED_OBJ, 'ext': { - 'linkType': ID5_STORED_LINK_TYPE, 'euid': { 'source': EUID_SOURCE, 'uids': [{ @@ -59,6 +57,11 @@ describe('ID5 ID System', function () { } } }; + const TRUE_LINK_STORED_ID = 'TRUE_LINK_1'; + const ID5_STORED_OBJ_WITH_TRUE_LINK = { + ...ID5_STORED_OBJ, + publisherTrueLinkId: TRUE_LINK_STORED_ID + }; const ID5_RESPONSE_ID = 'newid5id'; const ID5_RESPONSE_SIGNATURE = 'abcdef'; const ID5_RESPONSE_LINK_TYPE = 2; @@ -70,6 +73,63 @@ describe('ID5 ID System', function () { 'linkType': ID5_RESPONSE_LINK_TYPE } }; + const IDS_ID5ID = { + eid: { + source: 'id5-sync.com', + uids: [{ + id: 'ID5ID-value', + atype: 1, + ext: { + linkType: 1, + pba: 12 + } + }] + } + }; + const IDS_TRUE_LINK_ID = { + eid: { + source: 'true-link-id5-sync.com', + inserter: 'id5-sync.com', + matcher: 'id5-sync.com', + mm: 1, + uids: [{ + id: 'truelink-id-value', + atype: 1 + }] + } + }; + + const IDS_EUID = { + eid: { + source: EUID_SOURCE, + inserter: 'id5-sync.com', + matcher: 'id5-sync.com', + mm: 2, + uids: [{ + atype: 3, + id: 'euid-value', + ext: { + provider: 'id5-sync.com' + } + }] + } + }; + + const ID5_STORED_OBJ_WITH_IDS_ID5ID_ONLY = { + ...ID5_STORED_OBJ, + ids: { + id5id: IDS_ID5ID + } + }; + + const ID5_STORED_OBJ_WITH_IDS_ALL = { + ...ID5_STORED_OBJ, + ids: { + id5id: IDS_ID5ID, + trueLinkId: IDS_TRUE_LINK_ID, + euid: IDS_EUID + } + }; const ALLOWED_ID5_VENDOR_DATA = { purpose: { consents: { @@ -81,10 +141,19 @@ describe('ID5 ID System', function () { 131: true } } - } + }; const HEADERS_CONTENT_TYPE_JSON = { 'Content-Type': 'application/json' + }; + + function expDaysStr(expDays) { + return (new Date(Date.now() + (1000 * 60 * 60 * 24 * expDays))).toUTCString(); + } + + function storeInStorage(key, value, expDays) { + id5System.storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays)); + id5System.storage.setDataInLocalStorage(`${key}`, value); } function getId5FetchConfig(partner = ID5_TEST_PARTNER_ID, storageName = id5System.ID5_STORAGE_NAME, storageType = 'html5') { @@ -98,18 +167,7 @@ describe('ID5 ID System', function () { type: storageType, expires: 90 } - } - } - - function getId5ValueConfig(value) { - return { - name: ID5_MODULE_NAME, - value: { - id5id: { - uid: value - } - } - } + }; } function getUserSyncConfig(userIds) { @@ -118,17 +176,13 @@ describe('ID5 ID System', function () { userIds: userIds, syncDelay: 0 } - } + }; } function getFetchLocalStorageConfig() { return getUserSyncConfig([getId5FetchConfig()]); } - function getValueConfig(value) { - return getUserSyncConfig([getId5ValueConfig(value)]); - } - function getAdUnitMock(code = 'adUnit-code') { return { code, @@ -139,13 +193,23 @@ describe('ID5 ID System', function () { } function callSubmoduleGetId(config, consentData, cacheIdObj) { - return new GreedyPromise((resolve) => { + return new PbPromise((resolve) => { id5System.id5IdSubmodule.getId(config, consentData, cacheIdObj).callback((response) => { resolve(response); }); }); } + function wrapAsyncExpects(done, expectsFn) { + return function () { + try { + expectsFn(); + } catch (err) { + done(err); + } + }; + } + class XhrServerMock { currentRequestIdx = 0; server; @@ -167,6 +231,7 @@ describe('ID5 ID System', function () { const configRequest = await this.expectFirstRequest(); expect(configRequest.url).is.eq(ID5_API_CONFIG_URL); expect(configRequest.method).is.eq('POST'); + expect(configRequest.withCredentials).is.eq(true); return configRequest; } @@ -184,8 +249,8 @@ describe('ID5 ID System', function () { } async #waitOnRequest(index) { - const server = this.server - return new GreedyPromise((resolve) => { + const server = this.server; + return new PbPromise((resolve) => { const waitForCondition = () => { if (server.requests && server.requests.length > index) { resolve(server.requests[index]); @@ -205,6 +270,7 @@ describe('ID5 ID System', function () { before(() => { hook.ready(); + id5System.id5IdSubmodule._reset(); }); describe('Check for valid publisher config', function () { @@ -214,31 +280,37 @@ describe('ID5 ID System', function () { expect(id5System.id5IdSubmodule.getId({})).is.eq(undefined); // valid params, invalid id5System.storage - expect(id5System.id5IdSubmodule.getId({ params: { partner: 123 } })).to.be.eq(undefined); - expect(id5System.id5IdSubmodule.getId({ params: { partner: 123 }, storage: {} })).to.be.eq(undefined); - expect(id5System.id5IdSubmodule.getId({ params: { partner: 123 }, storage: { name: '' } })).to.be.eq(undefined); - expect(id5System.id5IdSubmodule.getId({ params: { partner: 123 }, storage: { type: '' } })).to.be.eq(undefined); + expect(id5System.id5IdSubmodule.getId({params: {partner: 123}})).to.be.eq(undefined); + expect(id5System.id5IdSubmodule.getId({params: {partner: 123}, storage: {}})).to.be.eq(undefined); + expect(id5System.id5IdSubmodule.getId({params: {partner: 123}, storage: {name: ''}})).to.be.eq(undefined); + expect(id5System.id5IdSubmodule.getId({params: {partner: 123}, storage: {type: ''}})).to.be.eq(undefined); // valid id5System.storage, invalid params - expect(id5System.id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, })).to.be.eq(undefined); - expect(id5System.id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { } })).to.be.eq(undefined); - expect(id5System.id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 'abc' } })).to.be.eq(undefined); + expect(id5System.id5IdSubmodule.getId({storage: {name: 'name', type: 'html5'}})).to.be.eq(undefined); + expect(id5System.id5IdSubmodule.getId({storage: {name: 'name', type: 'html5'}, params: {}})).to.be.eq(undefined); + expect(id5System.id5IdSubmodule.getId({ + storage: {name: 'name', type: 'html5'}, + params: {partner: 'abc'} + })).to.be.eq(undefined); }); it('should warn with non-recommended id5System.storage params', function () { const logWarnStub = sinon.stub(utils, 'logWarn'); - id5System.id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 123 } }); + id5System.id5IdSubmodule.getId({storage: {name: 'name', type: 'html5'}, params: {partner: 123}}); expect(logWarnStub.calledOnce).to.be.true; logWarnStub.restore(); - id5System.id5IdSubmodule.getId({ storage: { name: id5System.ID5_STORAGE_NAME, type: 'cookie', }, params: { partner: 123 } }); + id5System.id5IdSubmodule.getId({ + storage: {name: id5System.ID5_STORAGE_NAME, type: 'cookie'}, + params: {partner: 123} + }); expect(logWarnStub.calledOnce).to.be.true; logWarnStub.restore(); }); }); - describe('Check for valid consent', function() { + describe('Check for valid consent', function () { const dataConsentVals = [ [{purpose: {consents: {1: false}}}, {vendor: {consents: {131: true}}}, ' no purpose consent'], [{purpose: {consents: {1: true}}}, {vendor: {consents: {131: false}}}, ' no vendor consent'], @@ -250,45 +322,44 @@ describe('ID5 ID System', function () { [{purpose: {consents: {1: true}}}, {vendor: {consents: {31: true}}}, ' incorrect vendor consent'] ]; - dataConsentVals.forEach(function([purposeConsent, vendorConsent, caseName]) { - it('should fail with invalid consent because of ' + caseName, function() { + dataConsentVals.forEach(function ([purposeConsent, vendorConsent, caseName]) { + it('should fail with invalid consent because of ' + caseName, function () { const dataConsent = { gdprApplies: true, consentString: 'consentString', vendorData: { purposeConsent, vendorConsent } - } + }; expect(id5System.id5IdSubmodule.getId(config)).is.eq(undefined); - expect(id5System.id5IdSubmodule.getId(config, dataConsent)).is.eq(undefined); + expect(id5System.id5IdSubmodule.getId(config, {gdpr: dataConsent})).is.eq(undefined); const cacheIdObject = 'cacheIdObject'; expect(id5System.id5IdSubmodule.extendId(config)).is.eq(undefined); - expect(id5System.id5IdSubmodule.extendId(config, dataConsent, cacheIdObject)).is.eq(cacheIdObject); + expect(id5System.id5IdSubmodule.extendId(config, {gdpr: dataConsent}, cacheIdObject)).is.eq(cacheIdObject); }); }); }); describe('Xhr Requests from getId()', function () { - const responseHeader = HEADERS_CONTENT_TYPE_JSON - let gppStub + const responseHeader = HEADERS_CONTENT_TYPE_JSON; + let gppStub; beforeEach(function () { }); afterEach(function () { - uspDataHandler.reset() - gppStub?.restore() + gppStub?.restore(); }); it('should call the ID5 server and handle a valid response', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const config = getId5FetchConfig(); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(config, undefined, undefined); - const fetchRequest = await xhrServerMock.expectFetchRequest() + const fetchRequest = await xhrServerMock.expectFetchRequest(); expect(fetchRequest.url).to.contain(ID5_ENDPOINT); expect(fetchRequest.withCredentials).is.true; @@ -298,31 +369,31 @@ describe('ID5 ID System', function () { expect(requestBody.o).is.eq('pbjs'); expect(requestBody.pd).is.undefined; expect(requestBody.s).is.undefined; - expect(requestBody.provider).is.undefined + expect(requestBody.provider).is.undefined; expect(requestBody.v).is.eq('$prebid.version$'); expect(requestBody.gdpr).is.eq(0); expect(requestBody.gdpr_consent).is.undefined; expect(requestBody.us_privacy).is.undefined; - expect(requestBody.storage).is.deep.eq(config.storage) + expect(requestBody.storage).is.deep.eq(config.storage); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); const submoduleResponse = await submoduleResponsePromise; - expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + expect(submoduleResponse).is.eql(ID5_JSON_RESPONSE); }); it('should call the ID5 server with gdpr data ', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const consentData = { gdprApplies: true, consentString: 'consentString', vendorData: ALLOWED_ID5_VENDOR_DATA - } + }; // Trigger the fetch but we await on it later - const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); + const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), {gdpr: consentData}, undefined); - const fetchRequest = await xhrServerMock.expectFetchRequest() + const fetchRequest = await xhrServerMock.expectFetchRequest(); const requestBody = JSON.parse(fetchRequest.requestBody); expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); expect(requestBody.gdpr).to.eq(1); @@ -331,44 +402,43 @@ describe('ID5 ID System', function () { fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); const submoduleResponse = await submoduleResponsePromise; - expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + expect(submoduleResponse).is.eql(ID5_JSON_RESPONSE); }); it('should call the ID5 server without gdpr data when gdpr not applies ', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const consentData = { gdprApplies: false, consentString: 'consentString' - } + }; // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); - const fetchRequest = await xhrServerMock.expectFetchRequest() + const fetchRequest = await xhrServerMock.expectFetchRequest(); const requestBody = JSON.parse(fetchRequest.requestBody); expect(requestBody.gdpr).to.eq(0); - expect(requestBody.gdpr_consent).is.undefined + expect(requestBody.gdpr_consent).is.undefined; fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); const submoduleResponse = await submoduleResponsePromise; - expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + expect(submoduleResponse).is.eql(ID5_JSON_RESPONSE); }); it('should call the ID5 server with us privacy consent', async function () { const usPrivacyString = '1YN-'; - uspDataHandler.setConsentData(usPrivacyString) - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const consentData = { gdprApplies: true, consentString: 'consentString', vendorData: ALLOWED_ID5_VENDOR_DATA - } + }; // Trigger the fetch but we await on it later - const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); + const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), {gdpr: consentData, usp: usPrivacyString}, undefined); - const fetchRequest = await xhrServerMock.expectFetchRequest() + const fetchRequest = await xhrServerMock.expectFetchRequest(); const requestBody = JSON.parse(fetchRequest.requestBody); expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); expect(requestBody.us_privacy).to.eq(usPrivacyString); @@ -376,16 +446,16 @@ describe('ID5 ID System', function () { fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); const submoduleResponse = await submoduleResponsePromise; - expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + expect(submoduleResponse).is.eql(ID5_JSON_RESPONSE); }); it('should call the ID5 server with no signature field when no stored object', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); - const fetchRequest = await xhrServerMock.expectFetchRequest() + const fetchRequest = await xhrServerMock.expectFetchRequest(); const requestBody = JSON.parse(fetchRequest.requestBody); expect(requestBody.s).is.undefined; @@ -394,7 +464,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server for config with submodule config object', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const id5FetchConfig = getId5FetchConfig(); id5FetchConfig.params.extraParam = { x: 'X', @@ -402,22 +472,25 @@ describe('ID5 ID System', function () { a: 1, b: '3' } - } + }; // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(id5FetchConfig, undefined, undefined); const configRequest = await xhrServerMock.expectConfigRequest(); const requestBody = JSON.parse(configRequest.requestBody); - expect(requestBody).is.deep.eq(id5FetchConfig) + expect(requestBody).is.deep.eq({ + ...id5FetchConfig, + bounce: true + }); - const fetchRequest = await xhrServerMock.respondWithConfigAndExpectNext(configRequest) + const fetchRequest = await xhrServerMock.respondWithConfigAndExpectNext(configRequest); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); await submoduleResponsePromise; }); it('should call the ID5 server for config with partner id being a string', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const id5FetchConfig = getId5FetchConfig(); id5FetchConfig.params.partner = '173'; @@ -425,18 +498,18 @@ describe('ID5 ID System', function () { const submoduleResponsePromise = callSubmoduleGetId(id5FetchConfig, undefined, undefined); const configRequest = await xhrServerMock.expectConfigRequest(); - const requestBody = JSON.parse(configRequest.requestBody) - expect(requestBody.params.partner).is.eq(173) + const requestBody = JSON.parse(configRequest.requestBody); + expect(requestBody.params.partner).is.eq(173); - const fetchRequest = await xhrServerMock.respondWithConfigAndExpectNext(configRequest) + const fetchRequest = await xhrServerMock.respondWithConfigAndExpectNext(configRequest); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); await submoduleResponsePromise; }); it('should call the ID5 server for config with overridden url', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const id5FetchConfig = getId5FetchConfig(); - id5FetchConfig.params.configUrl = 'http://localhost/x/y/z' + id5FetchConfig.params.configUrl = 'http://localhost/x/y/z'; // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(id5FetchConfig, undefined, undefined); @@ -444,13 +517,13 @@ describe('ID5 ID System', function () { const configRequest = await xhrServerMock.expectFirstRequest(); expect(configRequest.url).is.eq('http://localhost/x/y/z'); - const fetchRequest = await xhrServerMock.respondWithConfigAndExpectNext(configRequest) + const fetchRequest = await xhrServerMock.respondWithConfigAndExpectNext(configRequest); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); await submoduleResponsePromise; }); it('should call the ID5 server with additional data when provided', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); @@ -472,18 +545,18 @@ describe('ID5 ID System', function () { expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); expect(requestBody.o).is.eq('pbjs'); expect(requestBody.v).is.eq('$prebid.version$'); - expect(requestBody.arg1).is.eq('123') + expect(requestBody.arg1).is.eq('123'); expect(requestBody.arg2).is.deep.eq({ x: '1', y: 2 - }) + }); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); await submoduleResponsePromise; }); it('should call the ID5 server with extensions', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); @@ -498,8 +571,8 @@ describe('ID5 ID System', function () { method: 'GET' } }); - expect(extensionsRequest.url).is.eq(ID5_EXTENSIONS_ENDPOINT) - expect(extensionsRequest.method).is.eq('GET') + expect(extensionsRequest.url).is.eq(ID5_EXTENSIONS_ENDPOINT); + expect(extensionsRequest.method).is.eq('GET'); extensionsRequest.respond(200, responseHeader, JSON.stringify({ lb: 'ex' @@ -511,14 +584,14 @@ describe('ID5 ID System', function () { expect(requestBody.v).is.eq('$prebid.version$'); expect(requestBody.extensions).is.deep.eq({ lb: 'ex' - }) + }); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); await submoduleResponsePromise; }); it('should call the ID5 server with extensions fetched using method POST', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); @@ -537,15 +610,15 @@ describe('ID5 ID System', function () { } } }); - expect(extensionsRequest.url).is.eq(ID5_EXTENSIONS_ENDPOINT) - expect(extensionsRequest.method).is.eq('POST') - const extRequestBody = JSON.parse(extensionsRequest.requestBody) + expect(extensionsRequest.url).is.eq(ID5_EXTENSIONS_ENDPOINT); + expect(extensionsRequest.method).is.eq('POST'); + const extRequestBody = JSON.parse(extensionsRequest.requestBody); expect(extRequestBody).is.deep.eq({ x: '1', y: 2 - }) + }); extensionsRequest.respond(200, responseHeader, JSON.stringify({ - lb: 'post', + lb: 'post' })); const fetchRequest = await xhrServerMock.expectNextRequest(); @@ -562,12 +635,12 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with signature field from stored object', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); - const fetchRequest = await xhrServerMock.expectFetchRequest() + const fetchRequest = await xhrServerMock.expectFetchRequest(); const requestBody = JSON.parse(fetchRequest.requestBody); expect(requestBody.s).is.eq(ID5_STORED_SIGNATURE); @@ -576,7 +649,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with pd field when pd config is set', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; const id5Config = getId5FetchConfig(); @@ -594,7 +667,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with no pd field when pd config is not set', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const id5Config = getId5FetchConfig(); id5Config.params.pd = undefined; @@ -610,9 +683,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with nb=1 when no stored value exists and reset after', async function () { - const xhrServerMock = new XhrServerMock(server) - const TEST_PARTNER_ID = 189; - coreStorage.removeDataFromLocalStorage(id5System.nbCacheName(TEST_PARTNER_ID)); + const xhrServerMock = new XhrServerMock(server); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -622,34 +693,34 @@ describe('ID5 ID System', function () { expect(requestBody.nbPage).is.eq(1); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - await submoduleResponsePromise; + const response = await submoduleResponsePromise; - expect(id5System.getNbFromCache(TEST_PARTNER_ID)).is.eq(0); + expect(response.nbPage).is.undefined; }); it('should call the ID5 server with incremented nb when stored value exists and reset after', async function () { const xhrServerMock = new XhrServerMock(server); const TEST_PARTNER_ID = 189; const config = getId5FetchConfig(TEST_PARTNER_ID); - id5System.storeNbInCache(TEST_PARTNER_ID, 1); + const storedObj = {...ID5_STORED_OBJ, nbPage: 1}; // Trigger the fetch but we await on it later - const submoduleResponsePromise = callSubmoduleGetId(config, undefined, ID5_STORED_OBJ); + const submoduleResponsePromise = callSubmoduleGetId(config, undefined, storedObj); const fetchRequest = await xhrServerMock.expectFetchRequest(); const requestBody = JSON.parse(fetchRequest.requestBody); expect(requestBody.nbPage).is.eq(2); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - await submoduleResponsePromise; + const response = await submoduleResponsePromise; - expect(id5System.getNbFromCache(TEST_PARTNER_ID)).is.eq(0); + expect(response.nbPage).is.undefined; }); it('should call the ID5 server with ab_testing object when abTesting is turned on', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const id5Config = getId5FetchConfig(); - id5Config.params.abTesting = {enabled: true, controlGroupPct: 0.234} + id5Config.params.abTesting = {enabled: true, controlGroupPct: 0.234}; // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); @@ -664,9 +735,9 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server without ab_testing object when abTesting is turned off', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const id5Config = getId5FetchConfig(); - id5Config.params.abTesting = {enabled: false, controlGroupPct: 0.55} + id5Config.params.abTesting = {enabled: false, controlGroupPct: 0.55}; // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); @@ -680,7 +751,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server without ab_testing when when abTesting is not set', async function () { - const xhrServerMock = new XhrServerMock(server) + const xhrServerMock = new XhrServerMock(server); const id5Config = getId5FetchConfig(); // Trigger the fetch but we await on it later @@ -694,46 +765,7 @@ describe('ID5 ID System', function () { await submoduleResponsePromise; }); - it('should store the privacy object from the ID5 server response', async function () { - const xhrServerMock = new XhrServerMock(server) - - // Trigger the fetch but we await on it later - const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); - - const privacy = { - jurisdiction: 'gdpr', - id5_consent: true - }; - - const fetchRequest = await xhrServerMock.expectFetchRequest(); - const responseObject = utils.deepClone(ID5_JSON_RESPONSE); - responseObject.privacy = privacy; - - fetchRequest.respond(200, responseHeader, JSON.stringify(responseObject)); - await submoduleResponsePromise; - - expect(id5System.getFromLocalStorage(id5System.ID5_PRIVACY_STORAGE_NAME)).is.eq(JSON.stringify(privacy)); - coreStorage.removeDataFromLocalStorage(id5System.ID5_PRIVACY_STORAGE_NAME); - }); - - it('should not store a privacy object if not part of ID5 server response', async function () { - const xhrServerMock = new XhrServerMock(server); - coreStorage.removeDataFromLocalStorage(id5System.ID5_PRIVACY_STORAGE_NAME); - - // Trigger the fetch but we await on it later - const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); - - const fetchRequest = await xhrServerMock.expectFetchRequest(); - const responseObject = utils.deepClone(ID5_JSON_RESPONSE); - responseObject.privacy = undefined; - - fetchRequest.respond(200, responseHeader, JSON.stringify(responseObject)); - await submoduleResponsePromise; - - expect(id5System.getFromLocalStorage(id5System.ID5_PRIVACY_STORAGE_NAME)).is.null; - }); - - describe('with successful external module call', function() { + describe('with successful external module call', function () { const MOCK_RESPONSE = { ...ID5_JSON_RESPONSE, universal_uid: 'my_mock_reponse' @@ -743,7 +775,8 @@ describe('ID5 ID System', function () { beforeEach(() => { window.id5Prebid = { integration: { - fetchId5Id: function() {} + fetchId5Id: function () { + } } }; mockId5ExternalModule = sinon.stub(window.id5Prebid.integration, 'fetchId5Id') @@ -755,7 +788,7 @@ describe('ID5 ID System', function () { delete window.id5Prebid; }); - it('should retrieve the response from the external module interface', async function() { + it('should retrieve the response from the external module interface', async function () { const xhrServerMock = new XhrServerMock(server); const config = getId5FetchConfig(); config.params.externalModuleUrl = 'https://test-me.test'; @@ -772,8 +805,8 @@ describe('ID5 ID System', function () { }); }); - describe('with failing external module loading', function() { - it('should fallback to regular logic if external module fails to load', async function() { + describe('with failing external module loading', function () { + it('should fallback to regular logic if external module fails to load', async function () { const xhrServerMock = new XhrServerMock(server); const config = getId5FetchConfig(); config.params.externalModuleUrl = 'https://test-me.test'; // Fails by loading this fake URL @@ -791,14 +824,13 @@ describe('ID5 ID System', function () { }); it('should pass gpp_string and gpp_sid to ID5 server', function () { - let xhrServerMock = new XhrServerMock(server) - gppStub = sinon.stub(gppDataHandler, 'getConsentData'); - gppStub.returns({ + let xhrServerMock = new XhrServerMock(server); + const gppData = { ready: true, gppString: 'GPP_STRING', applicableSections: [2] - }); - let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + }; + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), {gpp: gppData}, ID5_STORED_OBJ); return xhrServerMock.expectFetchRequest() .then(fetchRequest => { @@ -806,7 +838,7 @@ describe('ID5 ID System', function () { expect(requestBody.gpp_string).is.equal('GPP_STRING'); expect(requestBody.gpp_sid).contains(2); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - return submoduleResponse + return submoduleResponse; }); }); @@ -823,7 +855,39 @@ describe('ID5 ID System', function () { id5System.storage.getCookie.callsFake(() => ' Not JSON '); id5System.id5IdSubmodule.getId(getId5FetchConfig()); }); - }) + }); + + it('should pass true link info to ID5 server even when true link is not booted', function () { + let xhrServerMock = new XhrServerMock(server); + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.true_link).is.eql({booted: false}); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse; + }); + }); + + it('should pass full true link info to ID5 server when true link is booted', function () { + let xhrServerMock = new XhrServerMock(server); + let trueLinkResponse = {booted: true, redirected: true, id: 'TRUE_LINK_ID'}; + window.id5Bootstrap = { + getTrueLinkInfo: function () { + return trueLinkResponse; + } + }; + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.true_link).is.eql(trueLinkResponse); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse; + }); + }); }); describe('Local storage', () => { @@ -839,9 +903,9 @@ describe('ID5 ID System', function () { [true, 1], [false, 0] ].forEach(([isEnabled, expectedValue]) => { - it(`should check localStorage availability and log in request. Available=${isEnabled}`, async function() { - const xhrServerMock = new XhrServerMock(server) - id5System.storage.localStorageIsEnabled.callsFake(() => isEnabled) + it(`should check localStorage availability and log in request. Available=${isEnabled}`, async function () { + const xhrServerMock = new XhrServerMock(server); + id5System.storage.localStorageIsEnabled.callsFake(() => isEnabled); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -852,7 +916,7 @@ describe('ID5 ID System', function () { fetchRequest.respond(200, HEADERS_CONTENT_TYPE_JSON, JSON.stringify(ID5_JSON_RESPONSE)); const submoduleResponse = await submoduleResponsePromise; - expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + expect(submoduleResponse).is.eql(ID5_JSON_RESPONSE); }); }); }); @@ -867,140 +931,118 @@ describe('ID5 ID System', function () { sinon.stub(events, 'getEvents').returns([]); coreStorage.removeDataFromLocalStorage(id5System.ID5_STORAGE_NAME); coreStorage.removeDataFromLocalStorage(`${id5System.ID5_STORAGE_NAME}_last`); - coreStorage.removeDataFromLocalStorage(id5System.nbCacheName(ID5_TEST_PARTNER_ID)); - coreStorage.setDataInLocalStorage(id5System.ID5_STORAGE_NAME + '_cst', getConsentHash()) + coreStorage.setDataInLocalStorage(id5System.ID5_STORAGE_NAME + '_cst', getConsentHash()); adUnits = [getAdUnitMock()]; }); afterEach(function () { events.getEvents.restore(); coreStorage.removeDataFromLocalStorage(id5System.ID5_STORAGE_NAME); coreStorage.removeDataFromLocalStorage(`${id5System.ID5_STORAGE_NAME}_last`); - coreStorage.removeDataFromLocalStorage(id5System.nbCacheName(ID5_TEST_PARTNER_ID)); - coreStorage.removeDataFromLocalStorage(id5System.ID5_STORAGE_NAME + '_cst') + coreStorage.removeDataFromLocalStorage(id5System.ID5_STORAGE_NAME + '_cst'); sandbox.restore(); }); - it('should add stored ID from cache to bids', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); - - init(config); - setSubmoduleRegistry([id5System.id5IdSubmodule]); - config.setConfig(getFetchLocalStorageConfig()); - - requestBidsHook(function () { - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id.uid).is.equal(ID5_STORED_ID); - expect(bid.userIdAsEids[0]).is.deep.equal({ - source: ID5_SOURCE, - uids: [{ - id: ID5_STORED_ID, - atype: 1, - ext: { - linkType: ID5_STORED_LINK_TYPE - } - }] + describe('when old request stored', function () { + it('should add stored ID from cache to bids', function (done) { + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + + init(config); + setSubmoduleRegistry([id5System.id5IdSubmodule]); + config.setConfig(getFetchLocalStorageConfig()); + + startAuctionHook(wrapAsyncExpects(done, () => { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); + expect(bid.userId.id5id.uid).is.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[0]).is.eql({ + source: ID5_SOURCE, + uids: [{ + id: ID5_STORED_ID, + atype: 1, + ext: { + linkType: ID5_STORED_LINK_TYPE + } + }] + }); }); }); - }); - done(); - }, {adUnits}); - }); - - it('should add stored EUID from cache to bids', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_EUID), 1); + done(); + }), {adUnits}); + }); - init(config); - setSubmoduleRegistry([id5System.id5IdSubmodule]); - config.setConfig(getFetchLocalStorageConfig()); - - requestBidsHook(function () { - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property(`userId.euid`); - expect(bid.userId.euid.uid).is.equal(EUID_STORED_ID); - expect(bid.userIdAsEids[0].uids[0].id).is.equal(ID5_STORED_ID); - expect(bid.userIdAsEids[1]).is.deep.equal({ - source: EUID_SOURCE, - uids: [{ - id: EUID_STORED_ID, - atype: 3, - ext: { - provider: ID5_SOURCE - } - }] - }) + it('should add stored EUID from cache to bids', function (done) { + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_EUID), 1); + + init(config); + setSubmoduleRegistry([id5System.id5IdSubmodule]); + config.setConfig(getFetchLocalStorageConfig()); + + startAuctionHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.euid`); + expect(bid.userId.euid.uid).is.equal(EUID_STORED_ID); + expect(bid.userIdAsEids[0].uids[0].id).is.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[1]).is.eql({ + source: EUID_SOURCE, + uids: [{ + id: EUID_STORED_ID, + atype: 3, + ext: { + provider: ID5_SOURCE + } + }] + }); + }); }); - }); - done(); - }, {adUnits}); - }); + done(); + }, {adUnits}); + }); - it('should add config value ID to bids', function (done) { - init(config); - setSubmoduleRegistry([id5System.id5IdSubmodule]); - config.setConfig(getValueConfig(ID5_STORED_ID)); - - requestBidsHook(function () { - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id.uid).is.equal(ID5_STORED_ID); - expect(bid.userIdAsEids[0]).is.deep.equal({ - source: ID5_SOURCE, - uids: [{id: ID5_STORED_ID, atype: 1}] + it('should add stored TRUE_LINK_ID from cache to bids', function (done) { + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_TRUE_LINK), 1); + + init(config); + setSubmoduleRegistry([id5System.id5IdSubmodule]); + config.setConfig(getFetchLocalStorageConfig()); + + startAuctionHook(wrapAsyncExpects(done, function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.trueLinkId`); + expect(bid.userId.trueLinkId.uid).is.equal(TRUE_LINK_STORED_ID); + expect(bid.userIdAsEids[1]).is.eql({ + source: TRUE_LINK_SOURCE, + uids: [{ + id: TRUE_LINK_STORED_ID, + atype: 1 + }] + }); }); }); - }); - done(); - }, {adUnits}); - }); - - it('should set nb=1 in cache when no stored nb value exists and cached ID', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); - coreStorage.removeDataFromLocalStorage(id5System.nbCacheName(ID5_TEST_PARTNER_ID)); - - init(config); - setSubmoduleRegistry([id5System.id5IdSubmodule]); - config.setConfig(getFetchLocalStorageConfig()); - - requestBidsHook((adUnitConfig) => { - expect(id5System.getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(1); - done() - }, {adUnits}); - }); - - it('should increment nb in cache when stored nb value exists and cached ID', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); - id5System.storeNbInCache(ID5_TEST_PARTNER_ID, 1); - - init(config); - setSubmoduleRegistry([id5System.id5IdSubmodule]); - config.setConfig(getFetchLocalStorageConfig()); - - requestBidsHook(() => { - expect(id5System.getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(2); - done() - }, {adUnits}); + done(); + }), {adUnits}); + }); }); it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { - const xhrServerMock = new XhrServerMock(server) - const initialLocalStorageValue = JSON.stringify(ID5_STORED_OBJ); - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, initialLocalStorageValue, 1); - id5System.storeInLocalStorage(`${id5System.ID5_STORAGE_NAME}_last`, id5System.expDaysStr(-1), 1); + const xhrServerMock = new XhrServerMock(server); + let storedObject = ID5_STORED_OBJ; + storedObject.nbPage = 1; + const initialLocalStorageValue = JSON.stringify(storedObject); + storeInStorage(id5System.ID5_STORAGE_NAME, initialLocalStorageValue, 1); + storeInStorage(`${id5System.ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); - id5System.storeNbInCache(ID5_TEST_PARTNER_ID, 1); let id5Config = getFetchLocalStorageConfig(); id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; + id5Config.userSync.auctionDelay = 0; // do not trigger callback before auction init(config); setSubmoduleRegistry([id5System.id5IdSubmodule]); config.setConfig(id5Config); - return new Promise((resolve) => { - requestBidsHook(() => { - resolve() + startAuctionHook(() => { + resolve(); }, {adUnits}); }).then(() => { expect(xhrServerMock.hasReceivedAnyRequest()).is.false; @@ -1010,19 +1052,143 @@ describe('ID5 ID System', function () { const requestBody = JSON.parse(request.requestBody); expect(requestBody.s).is.eq(ID5_STORED_SIGNATURE); expect(requestBody.nbPage).is.eq(2); - expect(id5System.getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); request.respond(200, HEADERS_CONTENT_TYPE_JSON, JSON.stringify(ID5_JSON_RESPONSE)); + }); + }); - return new Promise(function (resolve) { - (function waitForCondition() { - if (id5System.getFromLocalStorage(id5System.ID5_STORAGE_NAME) !== initialLocalStorageValue) return resolve(); - setTimeout(waitForCondition, 30); - })(); - }) - }).then(() => { - expect(decodeURIComponent(id5System.getFromLocalStorage(id5System.ID5_STORAGE_NAME))).is.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(id5System.getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); - }) + describe('when request with "ids" object stored', function () { + it('should add stored ID from cache to bids - from ids', function (done) { + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_IDS_ID5ID_ONLY), 1); + + init(config); + setSubmoduleRegistry([id5System.id5IdSubmodule]); + config.setConfig(getFetchLocalStorageConfig()); + const id5IdEidUid = IDS_ID5ID.eid.uids[0]; + startAuctionHook(wrapAsyncExpects(done, () => { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); + expect(bid.userId.id5id).is.eql({ + uid: id5IdEidUid.id, + ext: id5IdEidUid.ext + }); + expect(bid.userIdAsEids[0]).is.eql({ + source: IDS_ID5ID.eid.source, + uids: [{ + id: id5IdEidUid.id, + atype: id5IdEidUid.atype, + ext: id5IdEidUid.ext + }] + }); + }); + }); + done(); + }), {adUnits}); + }); + it('should add stored EUID from cache to bids - from ids', function (done) { + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify({ + ...ID5_STORED_OBJ, + ids: { + id5id: IDS_ID5ID, + euid: IDS_EUID + } + }), 1); + + init(config); + setSubmoduleRegistry([id5System.id5IdSubmodule]); + config.setConfig(getFetchLocalStorageConfig()); + + startAuctionHook(wrapAsyncExpects(done, () => { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.euid`); + expect(bid.userId.euid).is.eql({ + uid: IDS_EUID.eid.uids[0].id, + ext: IDS_EUID.eid.uids[0].ext + }); + expect(bid.userIdAsEids[0]).is.eql(IDS_ID5ID.eid); + expect(bid.userIdAsEids[1]).is.eql(IDS_EUID.eid); + }); + }); + done(); + }), {adUnits}); + }); + + it('should add stored TRUE_LINK_ID from cache to bids - from ids', function (done) { + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify({ + ...ID5_STORED_OBJ, + ids: { + id5id: IDS_ID5ID, + trueLinkId: IDS_TRUE_LINK_ID + } + }), 1); + + init(config); + setSubmoduleRegistry([id5System.id5IdSubmodule]); + config.setConfig(getFetchLocalStorageConfig()); + + startAuctionHook(wrapAsyncExpects(done, function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.trueLinkId`); + expect(bid.userId.trueLinkId.uid).is.eql(IDS_TRUE_LINK_ID.eid.uids[0].id); + expect(bid.userIdAsEids[1]).is.eql(IDS_TRUE_LINK_ID.eid); + }); + }); + done(); + }), {adUnits}); + }); + + it('should add other id from cache to bids', function (done) { + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify({ + ...ID5_STORED_OBJ, + ids: { + id5id: IDS_ID5ID, + otherId: { + pbid: { + uid: 'other-id-value' + }, + eid: { + source: 'other-id.com', + inserter: 'id5-sync.com', + uids: [{ + id: 'other-id-value', + atype: 2, + ext: { + provider: 'id5-sync.com' + } + }], + + } + } + } + }), 1); + + init(config); + setSubmoduleRegistry([id5System.id5IdSubmodule]); + config.setConfig(getFetchLocalStorageConfig()); + + startAuctionHook(wrapAsyncExpects(done, function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.otherId`); + expect(bid.userId.otherId.uid).is.eql('other-id-value'); + expect(bid.userIdAsEids[1]).is.eql({ + source: 'other-id.com', + inserter: 'id5-sync.com', + uids: [{ + id: 'other-id-value', + atype: 2, + ext: { + provider: 'id5-sync.com' + } + }] + }); + }); + }); + done(); + }), {adUnits}); + }); }); }); @@ -1030,18 +1196,46 @@ describe('ID5 ID System', function () { const expectedDecodedObject = {id5id: {uid: ID5_STORED_ID, ext: {linkType: ID5_STORED_LINK_TYPE}}}; it('should properly decode from a stored object', function () { - expect(id5System.id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).is.deep.equal(expectedDecodedObject); + expect(id5System.id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).is.eql(expectedDecodedObject); }); it('should return undefined if passed a string', function () { expect(id5System.id5IdSubmodule.decode('somestring', getId5FetchConfig())).is.eq(undefined); }); it('should decode euid from a stored object with EUID', function () { - expect(id5System.id5IdSubmodule.decode(ID5_STORED_OBJ_WITH_EUID, getId5FetchConfig()).euid).is.deep.equal({ + expect(id5System.id5IdSubmodule.decode(ID5_STORED_OBJ_WITH_EUID, getId5FetchConfig()).euid).is.eql({ 'source': EUID_SOURCE, 'uid': EUID_STORED_ID, 'ext': {'provider': ID5_SOURCE} }); }); + it('should decode trueLinkId from a stored object with trueLinkId', function () { + expect(id5System.id5IdSubmodule.decode(ID5_STORED_OBJ_WITH_TRUE_LINK, getId5FetchConfig()).trueLinkId).is.eql({ + 'uid': TRUE_LINK_STORED_ID + }); + }); + + it('should decode id5id from a stored object with ids', function () { + expect(id5System.id5IdSubmodule.decode(ID5_STORED_OBJ_WITH_IDS_ID5ID_ONLY, getId5FetchConfig()).id5id).is.eql({ + uid: IDS_ID5ID.eid.uids[0].id, + ext: IDS_ID5ID.eid.uids[0].ext + }); + }); + + it('should decode all ids from a stored object with ids', function () { + let decoded = id5System.id5IdSubmodule.decode(ID5_STORED_OBJ_WITH_IDS_ALL, getId5FetchConfig()); + expect(decoded.id5id).is.eql({ + uid: IDS_ID5ID.eid.uids[0].id, + ext: IDS_ID5ID.eid.uids[0].ext + }); + expect(decoded.trueLinkId).is.eql({ + uid: IDS_TRUE_LINK_ID.eid.uids[0].id, + ext: IDS_TRUE_LINK_ID.eid.uids[0].ext + }); + expect(decoded.euid).is.eql({ + uid: IDS_EUID.eid.uids[0].id, + ext: IDS_EUID.eid.uids[0].ext + }); + }); }); describe('A/B Testing', function () { @@ -1084,13 +1278,13 @@ describe('ID5 ID System', function () { it('should not set abTestingControlGroup extension when A/B testing is off', function () { const decoded = id5System.id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOff); + expect(decoded).is.eql(expectedDecodedObjectWithIdAbOff); }); it('should set abTestingControlGroup to false when A/B testing is on but in normal group', function () { storedObject.ab_testing = {result: 'normal'}; const decoded = id5System.id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOn); + expect(decoded).is.eql(expectedDecodedObjectWithIdAbOn); }); it('should not expose ID when everyone is in control group', function () { @@ -1100,16 +1294,57 @@ describe('ID5 ID System', function () { 'linkType': 0 }; const decoded = id5System.id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).is.deep.equal(expectedDecodedObjectWithoutIdAbOn); + expect(decoded).is.eql(expectedDecodedObjectWithoutIdAbOn); }); it('should log A/B testing errors', function () { storedObject.ab_testing = {result: 'error'}; const decoded = id5System.id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOff); + expect(decoded).is.eql(expectedDecodedObjectWithIdAbOff); sinon.assert.calledOnce(logErrorSpy); }); }); }); }); + describe('eid', () => { + before(() => { + attachIdSystem(id5System); + }); + it('does not include an ext if not provided', function () { + const userId = { + id5id: { + uid: 'some-random-id-value' + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'id5-sync.com', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + + it('includes ext if provided', function () { + const userId = { + id5id: { + uid: 'some-random-id-value', + ext: { + linkType: 0 + } + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'id5-sync.com', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + linkType: 0 + } + }] + }); + }); + }); }); diff --git a/test/spec/modules/idImportLibrary_spec.js b/test/spec/modules/idImportLibrary_spec.js index d5b3e32546d..6045eb0bda0 100644 --- a/test/spec/modules/idImportLibrary_spec.js +++ b/test/spec/modules/idImportLibrary_spec.js @@ -4,6 +4,10 @@ import * as idImportlibrary from 'modules/idImportLibrary.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; import {config} from 'src/config.js'; import {hook} from '../../../src/hook.js'; +import * as activities from '../../../src/activities/rules.js'; +import { ACTIVITY_ENRICH_UFPD } from '../../../src/activities/activities.js'; +import { CONF_DEFAULT_FULL_BODY_SCAN, CONF_DEFAULT_INPUT_SCAN } from '../../../modules/idImportLibrary.js'; + var expect = require('chai').expect; const mockMutationObserver = { @@ -86,6 +90,16 @@ describe('IdImportLibrary Tests', function () { idImportlibrary.setConfig(config); expect(config.inputscan).to.be.equal(true); }); + it('results when activity is not allowed', function () { + sandbox.stub(activities, 'isActivityAllowed').callsFake((activity) => { + return !(activity === ACTIVITY_ENRICH_UFPD); + }); + let config = { 'url': 'URL', 'debounce': 0 }; + idImportlibrary.setConfig(config); + sinon.assert.called(utils.logError); + expect(config.inputscan).to.be.not.equal(CONF_DEFAULT_INPUT_SCAN); + expect(config.fullscan).to.be.not.equal(CONF_DEFAULT_FULL_BODY_SCAN); + }); }); describe('Test with email is found', function () { let mutationObserverStub; diff --git a/test/spec/modules/idWardRtdProvider_spec.js b/test/spec/modules/idWardRtdProvider_spec.js deleted file mode 100644 index d1601f058ff..00000000000 --- a/test/spec/modules/idWardRtdProvider_spec.js +++ /dev/null @@ -1,116 +0,0 @@ -import {config} from 'src/config.js'; -import {getRealTimeData, idWardRtdSubmodule, storage} from 'modules/idWardRtdProvider.js'; - -describe('idWardRtdProvider', function() { - let getDataFromLocalStorageStub; - - const testReqBidsConfigObj = { - adUnits: [ - { - bids: ['bid1', 'bid2'] - } - ] - }; - - const onDone = function() { return true }; - - const cmoduleConfig = { - 'name': 'idWard', - 'params': { - 'cohortStorageKey': 'cohort_ids' - } - } - - beforeEach(function() { - config.resetConfig(); - getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage') - }); - - afterEach(function () { - getDataFromLocalStorageStub.restore(); - }); - - describe('idWardRtdSubmodule', function() { - it('successfully instantiates', function () { - expect(idWardRtdSubmodule.init()).to.equal(true); - }); - }); - - describe('Get Real-Time Data', function() { - it('gets rtd from local storage', function() { - const rtdConfig = { - params: { - cohortStorageKey: 'cohort_ids', - segtax: 503 - } - }; - - const bidConfig = { - ortb2Fragments: { - global: {} - } - }; - - const rtdUserObj1 = { - name: 'anonymised.io', - ext: { - segtax: 503 - }, - segment: [ - { - id: 'TCZPQOWPEJG3MJOTUQUF793A' - }, - { - id: '93SUG3H540WBJMYNT03KX8N3' - } - ] - }; - - getDataFromLocalStorageStub.withArgs('cohort_ids') - .returns(JSON.stringify(['TCZPQOWPEJG3MJOTUQUF793A', '93SUG3H540WBJMYNT03KX8N3'])); - - getRealTimeData(bidConfig, () => {}, rtdConfig, {}); - expect(bidConfig.ortb2Fragments.global.user.data).to.deep.include.members([rtdUserObj1]); - }); - - it('do not set rtd if local storage empty', function() { - const rtdConfig = { - params: { - cohortStorageKey: 'cohort_ids', - segtax: 503 - } - }; - - const bidConfig = {}; - - getDataFromLocalStorageStub.withArgs('cohort_ids') - .returns(null); - - expect(config.getConfig().ortb2).to.be.undefined; - getRealTimeData(bidConfig, () => {}, rtdConfig, {}); - expect(config.getConfig().ortb2).to.be.undefined; - }); - - it('do not set rtd if local storage has incorrect value', function() { - const rtdConfig = { - params: { - cohortStorageKey: 'cohort_ids', - segtax: 503 - } - }; - - const bidConfig = {}; - - getDataFromLocalStorageStub.withArgs('cohort_ids') - .returns('wrong cohort ids value'); - - expect(config.getConfig().ortb2).to.be.undefined; - getRealTimeData(bidConfig, () => {}, rtdConfig, {}); - expect(config.getConfig().ortb2).to.be.undefined; - }); - - it('should initialize and return with config', function () { - expect(getRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig)).to.equal(undefined) - }); - }); -}); diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 66d5a3edd00..95480e57bd1 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -3,7 +3,9 @@ import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; import {getCoreStorageManager} from '../../../src/storageManager.js'; import {stub} from 'sinon'; -import { gppDataHandler } from '../../../src/adapterManager.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; const storage = getCoreStorageManager(); @@ -65,13 +67,13 @@ describe('IdentityLinkId tests', function () { gdprApplies: true, consentString: '' }; - let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, {gdpr: consentData}); expect(submoduleCallback).to.be.undefined; }); it('should NOT call the LiveRamp envelope endpoint if gdpr applies but consent string is missing', function () { let consentData = { gdprApplies: true }; - let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, {gdpr: consentData}); expect(submoduleCallback).to.be.undefined; }); @@ -84,7 +86,7 @@ describe('IdentityLinkId tests', function () { tcfPolicyVersion: 2 } }; - let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData).callback; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, {gdpr: consentData}).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&ct=4&cv=CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA'); @@ -97,14 +99,13 @@ describe('IdentityLinkId tests', function () { }); it('should call the LiveRamp envelope endpoint with GPP consent string', function() { - gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); - gppConsentDataStub.returns({ + const gppData = { ready: true, gppString: 'DBABLA~BVVqAAAACqA.QA', applicableSections: [7] - }); + }; let callBackSpy = sinon.spy(); - let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, {gpp: gppData}).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&gpp=DBABLA~BVVqAAAACqA.QA&gpp_sid=7'); @@ -114,18 +115,16 @@ describe('IdentityLinkId tests', function () { JSON.stringify({}) ); expect(callBackSpy.calledOnce).to.be.true; - gppConsentDataStub.restore(); }); it('should call the LiveRamp envelope endpoint without GPP consent string if consent string is not provided', function () { - gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); - gppConsentDataStub.returns({ + const gppData = { ready: true, gppString: '', applicableSections: [7] - }); + }; let callBackSpy = sinon.spy(); - let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, {gpp: gppData}).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14'); @@ -135,7 +134,6 @@ describe('IdentityLinkId tests', function () { JSON.stringify({}) ); expect(callBackSpy.calledOnce).to.be.true; - gppConsentDataStub.restore(); }); it('should not throw Uncaught TypeError when envelope endpoint returns empty response', function () { @@ -238,4 +236,21 @@ describe('IdentityLinkId tests', function () { expect(envelopeValueFromStorage).to.be.a('string'); expect(envelopeValueFromStorage).to.be.eq(testEnvelopeValue); }) + + describe('eid', () => { + before(() => { + attachIdSystem(identityLinkSubmodule); + }); + it('identityLink', function() { + const userId = { + idl_env: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveramp.com', + uids: [{id: 'some-random-id-value', atype: 3}] + }); + }); + }) }); diff --git a/test/spec/modules/idxIdSystem_spec.js b/test/spec/modules/idxIdSystem_spec.js index 56e1c709c8b..bfe9d1b1e68 100644 --- a/test/spec/modules/idxIdSystem_spec.js +++ b/test/spec/modules/idxIdSystem_spec.js @@ -1,9 +1,10 @@ import { expect } from 'chai'; import {find} from 'src/polyfill.js'; import { config } from 'src/config.js'; -import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; +import { init, startAuctionHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { storage, idxIdSubmodule } from 'modules/idxIdSystem.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; +import 'src/prebid.js'; const IDX_COOKIE_NAME = '_idx'; const IDX_DUMMY_VALUE = 'idx value for testing'; @@ -100,10 +101,15 @@ describe('IDx ID System', () => { afterEach(() => { sandbox.restore(); + config.resetConfig(); + }) + + after(() => { + init(config); }) it('when a stored IDx exists it is added to bids', (done) => { - requestBidsHook(() => { + startAuctionHook(() => { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.idx'); diff --git a/test/spec/modules/illuminBidAdapter_spec.js b/test/spec/modules/illuminBidAdapter_spec.js index 9b702c027f9..83f64b2bd6b 100644 --- a/test/spec/modules/illuminBidAdapter_spec.js +++ b/test/spec/modules/illuminBidAdapter_spec.js @@ -2,6 +2,14 @@ import {expect} from 'chai'; import { spec as adapter, createDomain, + storage +} from 'modules/illuminBidAdapter.js'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; +import { hashCode, extractPID, extractCID, @@ -10,14 +18,9 @@ import { setStorageItem, tryParseJSON, getUniqueDealId, -} from 'modules/illuminBidAdapter.js'; -import * as utils from 'src/utils.js'; -import {version} from 'package.json'; -import {useFakeTimers} from 'sinon'; -import {BANNER, VIDEO} from '../../../src/mediaTypes'; -import {config} from '../../../src/config'; +} from '../../../libraries/vidazooUtils/bidderUtils.js'; -export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; +export const TEST_ID_SYSTEMS = ['criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'pubcid', 'tdid', 'pubProvidedId']; const SUB_DOMAIN = 'exchange'; @@ -91,6 +94,36 @@ const VIDEO_BID = { } } +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': '' + }, + 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'}, +}; + const BIDDER_REQUEST = { 'gdprConsent': { 'consentString': 'consent_string', @@ -104,28 +137,17 @@ const BIDDER_REQUEST = { 'ref': 'https://www.somereferrer.com' }, 'ortb2': { + 'site': { + 'content': { + 'language': 'en' + } + }, 'regs': { 'gpp': 'gpp_string', - 'gpp_sid': [7] + 'gpp_sid': [7], + 'coppa': 0 }, - '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': '' - } - } + 'device': ORTB2_DEVICE, } }; @@ -306,6 +328,7 @@ describe('IlluminBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + device: ORTB2_DEVICE, uniqueDealId: `${hashUrl}_${Date.now().toString()}`, uqs: getTopWindowQueryParams(), mediaTypes: { @@ -325,7 +348,14 @@ describe('IlluminBidAdapter', function () { startdelay: 0 } }, - gpid: '0123456789' + gpid: '0123456789', + cat: [], + contentData: [], + contentLang: 'en', + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 } }); }); @@ -370,6 +400,7 @@ describe('IlluminBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + device: ORTB2_DEVICE, url: 'https%3A%2F%2Fwww.greatsite.com', referrer: 'https://www.somereferrer.com', cb: 1000, @@ -387,6 +418,13 @@ describe('IlluminBidAdapter', function () { uqs: getTopWindowQueryParams(), 'ext.param1': 'loremipsum', 'ext.param2': 'dolorsitamet', + cat: [], + contentData: [], + contentLang: 'en', + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 } }); }); @@ -402,7 +440,7 @@ describe('IlluminBidAdapter', function () { expect(result).to.deep.equal([{ type: 'iframe', - url: 'https://sync.illumin.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + url: 'https://sync.illumin.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' }]); }); @@ -410,7 +448,7 @@ describe('IlluminBidAdapter', function () { const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ type: 'iframe', - url: 'https://sync.illumin.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + url: 'https://sync.illumin.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' }]); }); @@ -418,10 +456,21 @@ describe('IlluminBidAdapter', function () { const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ - 'url': 'https://sync.illumin.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'url': 'https://sync.illumin.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', 'type': 'image' }]); }) + + it('should have valid user sync with coppa on response', function () { + config.setConfig({ + coppa: 1 + }); + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.illumin.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1' + }]); + }); }); describe('interpret response', function () { @@ -510,8 +559,6 @@ describe('IlluminBidAdapter', function () { switch (idSystemProvider) { case 'lipb': return {lipbid: id}; - case 'parrableId': - return {eid: id}; case 'id5id': return {uid: id}; default: @@ -564,13 +611,13 @@ describe('IlluminBidAdapter', function () { const key = 'myKey'; let uniqueDealId; beforeEach(() => { - uniqueDealId = getUniqueDealId(key, 0); + uniqueDealId = getUniqueDealId(storage, key, 0); }) it('should get current unique deal id', function (done) { // waiting some time so `now` will become past setTimeout(() => { - const current = getUniqueDealId(key); + const current = getUniqueDealId(storage, key); expect(current).to.be.equal(uniqueDealId); done(); }, 200); @@ -578,7 +625,7 @@ describe('IlluminBidAdapter', function () { it('should get new unique deal id on expiration', function (done) { setTimeout(() => { - const current = getUniqueDealId(key, 100); + const current = getUniqueDealId(storage, key, 100); expect(current).to.not.be.equal(uniqueDealId); done(); }, 200) @@ -602,8 +649,8 @@ describe('IlluminBidAdapter', function () { shouldAdvanceTime: true, now }); - setStorageItem('myKey', 2020); - const {value, created} = getStorageItem('myKey'); + 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'); @@ -614,7 +661,7 @@ describe('IlluminBidAdapter', function () { it('should get external stored value', function () { const value = 'superman' window.localStorage.setItem('myExternalKey', value); - const item = getStorageItem('myExternalKey'); + const item = getStorageItem(storage, 'myExternalKey'); expect(item).to.be.equal(value); }); diff --git a/test/spec/modules/imdsBidAdapter_spec.js b/test/spec/modules/imdsBidAdapter_spec.js index b71a0bc51d9..2911ee588c0 100644 --- a/test/spec/modules/imdsBidAdapter_spec.js +++ b/test/spec/modules/imdsBidAdapter_spec.js @@ -585,7 +585,7 @@ describe('imdsBidAdapter ', function () { maxduration: 45, startdelay: 1, linearity: 1, - placement: 1, + plcmt: 1, mimes: ['video/mp4'], protocols: [1], api: 1 @@ -622,7 +622,7 @@ describe('imdsBidAdapter ', function () { maxduration: 45, startdelay: 1, linearity: 1, - placement: 1, + plcmt: 1, mimes: ['video/mp4'], protocols: [1], api: 1 @@ -651,7 +651,7 @@ describe('imdsBidAdapter ', function () { playerSize: [[640, 480]], startdelay: 1, linearity: 1, - placement: 1, + plcmt: 1, mimes: ['video/mp4'] } }, @@ -680,7 +680,7 @@ describe('imdsBidAdapter ', function () { maxduration: 45, startdelay: 1, linearity: 1, - placement: 1, + plcmt: 1, mimes: ['video/mp4'], protocols: [1], api: 1 @@ -703,7 +703,7 @@ describe('imdsBidAdapter ', function () { playerSize: [[ 640, 480 ]], startdelay: 1, linearity: 1, - placement: 1, + plcmt: 1, mimes: ['video/mp4'] } }, @@ -726,7 +726,7 @@ describe('imdsBidAdapter ', function () { w: 640, startdelay: 1, linearity: 1, - placement: 1, + plcmt: 1, mimes: ['video/mp4'] }, id: 'v2624fabbb078e8-640x480', diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index a955b716584..adbf30bb5f1 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -10,18 +10,19 @@ import 'modules/currency.js'; import 'modules/userId/index.js'; import 'modules/multibid/index.js'; import 'modules/priceFloors.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; -import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {hook} from '../../../src/hook.js'; +import {addFPDToBidderRequest} from '../../helpers/fpd.js'; +import * as prebidGlobal from 'src/prebidGlobal.js'; describe('Improve Digital Adapter Tests', function () { const METHOD = 'POST'; const AD_SERVER_BASE_URL = 'https://ad.360yield.com'; const BASIC_ADS_BASE_URL = 'https://ad.360yield-basic.com'; - const PB_ENDPOINT = 'pb'; + const PB_ENDPOINT = 'pb'; [] const AD_SERVER_URL = `${AD_SERVER_BASE_URL}/${PB_ENDPOINT}`; const BASIC_ADS_URL = `${BASIC_ADS_BASE_URL}/${PB_ENDPOINT}`; const EXTEND_URL = 'https://pbs.360yield.com/openrtb2/auction'; @@ -32,6 +33,7 @@ describe('Improve Digital Adapter Tests', function () { const simpleBidRequest = { bidder: 'improvedigital', params: { + publisherId: 1234, placementId: 1053688 }, adUnitCode: 'div-gpt-ad-1499748733608-0', @@ -59,6 +61,7 @@ describe('Improve Digital Adapter Tests', function () { const instreamBidRequest = { bidder: 'improvedigital', params: { + publisherId: 1234, placementId: 123456 }, adUnitCode: 'video1', @@ -107,17 +110,6 @@ describe('Improve Digital Adapter Tests', function () { } }; - const simpleSmartTagBidRequest = { - mediaTypes: {}, - bidder: 'improvedigital', - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - publisherId: 1032, - placementKey: 'data_team_test_hb_smoke_test' - } - }; - const bidderRequest = { ortb2: { source: { @@ -174,6 +166,10 @@ describe('Improve Digital Adapter Tests', function () { return bidRequests; } + function formatPublisherUrl(baseUrl, publisherId) { + return `${baseUrl}/${publisherId}/${PB_ENDPOINT}`; + } + before(() => { hook.ready(); }); @@ -188,12 +184,7 @@ describe('Improve Digital Adapter Tests', function () { expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should return false when both placementId and placementKey + publisherId are missing', function () { - const bid = { 'params': {} }; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should return false when only one of placementKey and publisherId is present', function () { + it('should return false when only one of placementId or publisherId is present', function () { let bid = { params: { publisherId: 1234 @@ -202,38 +193,38 @@ describe('Improve Digital Adapter Tests', function () { expect(spec.isBidRequestValid(bid)).to.equal(false); bid = { params: { - placementKey: 'xyz' + placementId: 1234 } }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should return true when placementId is passed', function () { + it('should return true when both placementId and publisherId are passed', function () { expect(spec.isBidRequestValid(simpleBidRequest)).to.equal(true); }); - - it('should return true when both placementKey and publisherId are passed', function () { - expect(spec.isBidRequestValid(simpleSmartTagBidRequest)).to.equal(true); - }); }); describe('buildRequests', function () { let getConfigStub = null; + let getGlobalStub = null; afterEach(function () { if (getConfigStub) { getConfigStub.restore(); getConfigStub = null; } + + if (getGlobalStub) { + getGlobalStub.restore(); + getGlobalStub = null; + } }); - it('should make a well-formed request objects', function () { - getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); - const request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequest))[0]; + it('should make a well-formed request objects', async function () { + const request = spec.buildRequests([simpleBidRequest], await addFPDToBidderRequest(bidderRequest))[0]; expect(request).to.be.an('object'); expect(request.method).to.equal(METHOD); - expect(request.url).to.equal(AD_SERVER_URL); + expect(request.url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1234)); const payload = JSON.parse(request.data); expect(payload).to.be.an('object'); @@ -247,7 +238,7 @@ describe('Improve Digital Adapter Tests', function () { sinon.assert.match(payload.imp, [ sinon.match({ id: '33e9500b21129f', - secure: 0, + secure: 1, ext: { bidder: { placementId: 1053688, @@ -264,19 +255,17 @@ describe('Improve Digital Adapter Tests', function () { }); it('should make a well-formed request object for multi-format ad unit', function () { - getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); const request = spec.buildRequests(updateNativeParams([multiFormatBidRequest]), multiFormatBidderRequest)[0]; expect(request).to.be.an('object'); expect(request.method).to.equal(METHOD); - expect(request.url).to.equal(AD_SERVER_URL); + expect(request.url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1234)); const payload = JSON.parse(request.data); expect(payload).to.be.an('object'); sinon.assert.match(payload.imp, [ sinon.match({ id: '33e9500b21129f', - secure: 0, + secure: 1, ext: { bidder: { placementId: 1053688, @@ -284,7 +273,6 @@ describe('Improve Digital Adapter Tests', function () { }, ...(FEATURES.VIDEO && { video: { - placement: OUTSTREAM_TYPE, w: 640, h: 480, mimes: ['video/mp4'], @@ -345,12 +333,6 @@ describe('Improve Digital Adapter Tests', function () { }); } - it('should set placementKey and publisherId for smart tags', function () { - const payload = JSON.parse(spec.buildRequests([simpleSmartTagBidRequest], bidderRequest)[0].data); - expect(payload.imp[0].ext.bidder.publisherId).to.equal(1032); - expect(payload.imp[0].ext.bidder.placementKey).to.equal('data_team_test_hb_smoke_test'); - }); - it('should add keyValues', function () { const bidRequest = Object.assign({}, simpleBidRequest); const keyValues = { @@ -374,10 +356,21 @@ describe('Improve Digital Adapter Tests', function () { } }); - it('should add bid floor', function () { - const bidRequest = Object.assign({}, simpleBidRequest); - let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + it('should add bid floor correctly', function () { + getGlobalStub = sinon.stub(prebidGlobal, 'getGlobal').returns({ + convertCurrency: (cpm, from, to) => { + const conversionKeys = { 'EUR-USD': 1.75 }; + const conversionRate = conversionKeys[`${from}-${to}`]; + if (!conversionRate) { + throw new Error(`No conversion rate found for ${from}-${to}`); + } + return cpm * conversionRate; + } + }); + const bidRequest = deepClone(simpleBidRequest); + // Floor price currency shouldn't be populated without a floor price + let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); expect(payload.imp[0].bidfloorcur).to.not.exist; // Default floor price currency @@ -386,68 +379,74 @@ describe('Improve Digital Adapter Tests', function () { expect(payload.imp[0].bidfloor).to.equal(0.05); expect(payload.imp[0].bidfloorcur).to.equal('USD'); - // Floor price currency - bidRequest.params.bidFloorCur = 'eUR'; + // Floor price sent as is when currency cannot be converted to default bid adapter currency + bidRequest.params.bidFloorCur = 'UAH'; + bidRequest.params.bidFloor = 0.05; payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); expect(payload.imp[0].bidfloor).to.equal(0.05); - expect(payload.imp[0].bidfloorcur).to.equal('EUR'); + expect(payload.imp[0].bidfloorcur).to.equal('UAH'); + + // Floor price currency converted to default bid adapter currency + bidRequest.params.bidFloorCur = 'eUR'; + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].bidfloor).to.equal(0.08750000000000001); + expect(payload.imp[0].bidfloorcur).to.equal('USD'); // getFloor defined -> use it over bidFloor let getFloorResponse = { currency: 'USD', floor: 3 }; bidRequest.getFloor = () => getFloorResponse; payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); expect(payload.imp[0].bidfloor).to.equal(3); - // expect(payload.imp[0].bidfloorcur).to.equal('USD'); + expect(payload.imp[0].bidfloorcur).to.equal('USD'); }); - it('should add GDPR consent string', function () { + it('should add GDPR consent string', async function () { const bidRequest = Object.assign({}, simpleBidRequest); - const payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdpr))[0].data); + const payload = JSON.parse(spec.buildRequests([bidRequest], await addFPDToBidderRequest(bidderRequestGdpr))[0].data); expect(payload.regs.ext.gdpr).to.exist.and.to.equal(1); expect(payload.user.ext.consent).to.equal('CONSENT'); - expect(payload.user.ext.ConsentedProvidersSettings).to.not.exist; - expect(payload.user.ext.consented_providers_settings.consented_providers).to.exist.and.to.deep.equal([1, 35, 41, 101]); + expect(payload.user.ext.ConsentedProvidersSettings.consented_providers).to.exist.and.to.deep.equal('1~1.35.41.101'); }); - it('should not add consented providers when empty', function () { + it('should not add consented providers when empty', async function () { const bidderRequestGdprEmptyAddtl = deepClone(bidderRequestGdpr); bidderRequestGdprEmptyAddtl.gdprConsent.addtlConsent = '1~'; const bidRequest = Object.assign({}, simpleBidRequest); - const payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdprEmptyAddtl))[0].data); + const payload = JSON.parse(spec.buildRequests([bidRequest], await addFPDToBidderRequest(bidderRequestGdprEmptyAddtl))[0].data); expect(payload.user.ext.consented_providers_settings).to.not.exist; }); - it('should add ConsentedProvidersSettings when extend mode enabled', function () { + it('should add ConsentedProvidersSettings when extend mode enabled', async function () { const bidRequest = deepClone(extendBidRequest); - const payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdpr))[0].data); + const payload = JSON.parse(spec.buildRequests([bidRequest], await addFPDToBidderRequest(bidderRequestGdpr))[0].data); expect(payload.regs.ext.gdpr).to.exist.and.to.equal(1); expect(payload.user.ext.consent).to.equal('CONSENT'); expect(payload.user.ext.ConsentedProvidersSettings.consented_providers).to.exist.and.to.equal('1~1.35.41.101'); expect(payload.user.ext.consented_providers_settings).to.not.exist; }); - it('should add CCPA consent string', function () { + it('should add CCPA consent string', async function () { const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], syncAddFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); + const request = spec.buildRequests([bidRequest], await addFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); const payload = JSON.parse(request[0].data); expect(payload.regs.ext.us_privacy).to.equal('1YYY'); }); - it('should add COPPA flag', function () { + it('should add COPPA flag', async function () { getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('coppa').returns(true); let bidRequest = Object.assign({}, simpleBidRequest); - let payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdpr))[0].data); + let payload = JSON.parse(spec.buildRequests([bidRequest], await addFPDToBidderRequest(bidderRequestGdpr))[0].data); expect(payload.regs.coppa).to.equal(1); getConfigStub.withArgs('coppa').returns(false); bidRequest = Object.assign({}, simpleBidRequest); - payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdpr))[0].data); + payload = JSON.parse(spec.buildRequests([bidRequest], await addFPDToBidderRequest(bidderRequestGdpr))[0].data); expect(payload.regs.coppa).to.equal(0); }); - it('should add referrer', function () { + it('should add referrer', async function () { const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestReferrer))[0]; + const request = spec.buildRequests([bidRequest], await addFPDToBidderRequest(bidderRequestReferrer))[0]; const payload = JSON.parse(request.data); expect(payload.site.page).to.equal('https://blah.com/test.html'); }); @@ -475,25 +474,6 @@ describe('Improve Digital Adapter Tests', function () { }); if (FEATURES.VIDEO) { - it('should add correct placement value for instream and outstream video', function () { - let bidRequest = deepClone(simpleBidRequest); - let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); - expect(payload.imp[0].video).to.not.exist; - - bidRequest = deepClone(simpleBidRequest); - bidRequest.mediaTypes = { - video: { - context: 'instream', - playerSize: [640, 480] - } - }; - payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); - expect(payload.imp[0].video.placement).to.exist.and.equal(1); - bidRequest.mediaTypes.video.context = 'outstream'; - payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); - expect(payload.imp[0].video.placement).to.exist.and.equal(3); - }); - it('should set video params for instream', function() { const bidRequest = deepClone(instreamBidRequest); delete bidRequest.mediaTypes.video.playerSize; @@ -508,13 +488,12 @@ describe('Improve Digital Adapter Tests', function () { minbitrate: 500, maxbitrate: 2000, w: 1024, - h: 640, - placement: INSTREAM_TYPE, + h: 640 }; bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest], bidderRequest)[0]; const payload = JSON.parse(request.data); - expect(payload.imp[0].video).to.deep.equal(videoParams); + expect(payload.imp[0].video).to.deep.include(videoParams); }); it('should set video playerSize over video params', () => { @@ -551,7 +530,6 @@ describe('Improve Digital Adapter Tests', function () { const payload = JSON.parse(request.data); expect(payload.imp[0].video).to.deep.equal({...{ mimes: ['video/mp4'], - placement: OUTSTREAM_TYPE, w: bidRequest.mediaTypes.video.playerSize[0], h: bidRequest.mediaTypes.video.playerSize[1], }, @@ -564,7 +542,6 @@ describe('Improve Digital Adapter Tests', function () { const request = spec.buildRequests([bidRequest], {})[0]; const payload = JSON.parse(request.data); const testVideoParams = Object.assign({ - placement: OUTSTREAM_TYPE, w: 640, h: 480, mimes: ['video/mp4'], @@ -583,7 +560,7 @@ describe('Improve Digital Adapter Tests', function () { }); it('should add eids', function () { - const userIdAsEids = [ + const eids = [ { source: 'id5-sync.com', uids: [{ @@ -599,9 +576,10 @@ describe('Improve Digital Adapter Tests', function () { id: '1111' }] }]}}; - const bidRequest = Object.assign({}, simpleBidRequest); - bidRequest.userIdAsEids = userIdAsEids; - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const request = spec.buildRequests([simpleBidRequest], { + ...bidderRequestReferrer, + ortb2: {user: {ext: {eids: eids}}} + })[0]; const payload = JSON.parse(request.data); expect(payload.user).to.deep.equal(expectedUserObject); }); @@ -609,7 +587,7 @@ describe('Improve Digital Adapter Tests', function () { it('should return 2 requests', function () { const requests = spec.buildRequests([ simpleBidRequest, - simpleSmartTagBidRequest + instreamBidRequest ], bidderRequest); expect(requests).to.be.an('array'); expect(requests.length).to.equal(2); @@ -621,7 +599,7 @@ describe('Improve Digital Adapter Tests', function () { const requests = spec.buildRequests([ simpleBidRequest, instreamBidRequest ], bidderRequest); expect(requests).to.be.an('array'); expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal(AD_SERVER_URL); + expect(requests[0].url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1234)); const request = JSON.parse(requests[0].data); expect(request.imp.length).to.equal(2); expect(request.imp[0].banner).to.exist; @@ -635,7 +613,7 @@ describe('Improve Digital Adapter Tests', function () { expect(requests).to.be.an('array'); expect(requests.length).to.equal(2); expect(requests[0].url).to.equal(EXTEND_URL); - expect(requests[1].url).to.equal(AD_SERVER_URL); + expect(requests[1].url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1234)); const adServerRequest = JSON.parse(requests[1].data); expect(adServerRequest.imp.length).to.equal(2); expect(adServerRequest.imp[0].banner).to.exist; @@ -643,8 +621,6 @@ describe('Improve Digital Adapter Tests', function () { }); it('should set Prebid sizes in bid request', function () { - getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); const request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; const payload = JSON.parse(request.data); sinon.assert.match(payload.imp[0].banner, { @@ -656,8 +632,6 @@ describe('Improve Digital Adapter Tests', function () { }); it('should not add single size filter when using Prebid sizes', function () { - getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); const bidRequest = Object.assign({}, simpleBidRequest); const size = { w: 800, @@ -683,61 +657,28 @@ describe('Improve Digital Adapter Tests', function () { expect(payload.app.content).does.exist.and.equal('XYZ'); }); - it('should not set site when app is defined in CONFIG', function () { - getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('app').returns({ content: 'XYZ' }); - let request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequest))[0]; - let payload = JSON.parse(request.data); - expect(payload.site).does.not.exist; - expect(payload.app).does.exist; - expect(payload.app.content).does.exist.and.equal('XYZ'); - }); - - it('should set correct site params', function () { - getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('site').returns({ - content: 'XYZ', - page: 'https://improveditigal.com/', - domain: 'improveditigal.com' - }); - let request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequestReferrer))[0]; + it('should set correct site params', async function () { + let request = spec.buildRequests([simpleBidRequest], await addFPDToBidderRequest(bidderRequestReferrer))[0]; let payload = JSON.parse(request.data); - expect(payload.site.content).does.exist.and.equal('XYZ'); - expect(payload.site.page).does.exist.and.equal('https://improveditigal.com/'); - expect(payload.site.domain).does.exist.and.equal('improveditigal.com'); - getConfigStub.reset(); - - request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequestReferrer))[0]; - payload = JSON.parse(request.data); expect(payload.site.content).does.not.exist; expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); expect(payload.site.domain).does.exist.and.equal('blah.com'); const ortb2 = {site: {content: 'ZZZ'}}; - request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest({...bidderRequestReferrer, ortb2}))[0]; + request = spec.buildRequests([simpleBidRequest], await addFPDToBidderRequest({...bidderRequestReferrer, ortb2}))[0]; payload = JSON.parse(request.data); expect(payload.site.content).does.exist.and.equal('ZZZ'); expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); expect(payload.site.domain).does.exist.and.equal('blah.com'); }); - it('should set site when app not available', function () { - getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('app').returns(undefined); - getConfigStub.withArgs('site').returns({}); - let request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequest))[0]; - let payload = JSON.parse(request.data); - expect(payload.site).does.exist; - expect(payload.app).does.not.exist; - }); - it('should call basic ads endpoint when no consent for purpose 1', function () { const consent = deepClone(gdprConsent); deepSetValue(consent, 'vendorData.purpose.consents.1', false); const bidderRequestWithConsent = deepClone(bidderRequest); bidderRequestWithConsent.gdprConsent = consent; const request = spec.buildRequests([simpleBidRequest], bidderRequestWithConsent)[0]; - expect(request.url).to.equal(BASIC_ADS_URL); + expect(request.url).to.equal(formatPublisherUrl(BASIC_ADS_BASE_URL, 1234)); }); it('should set extend params when extend mode enabled from global configuration', function () { @@ -756,6 +697,7 @@ describe('Improve Digital Adapter Tests', function () { expect(payload.imp[0].ext.bidder).to.not.exist; expect(payload.imp[0].ext.prebid.bidder.improvedigital).to.deep.equal({ placementId: 1053688, + publisherId: 1234, keyValues }); expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal('1053688'); @@ -766,6 +708,28 @@ describe('Improve Digital Adapter Tests', function () { expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal('123456'); }); + it('should add max_bids param in imp.ext objects when bidLimit is specified in the bidderRequest', function () { + const bidderRequestDeepClone = deepClone(bidderRequest); + bidderRequestDeepClone.bidLimit = 3; + const requests = spec.buildRequests([simpleBidRequest, instreamBidRequest], bidderRequestDeepClone); + // banner + let payload = JSON.parse(requests[0].data); + expect(payload.imp[0].ext.max_bids).to.equal(3); + // video + payload = JSON.parse(requests[1].data); + expect(payload.imp[0].ext.max_bids).to.equal(3); + }); + + it('should not add max_bids param in imp.ext objects when bidLimit is not specified in the bidderRequest', function () { + const requests = spec.buildRequests([simpleBidRequest, instreamBidRequest], bidderRequest); + // banner + let payload = JSON.parse(requests[0].data); + expect(payload.imp[0].ext.max_bids).to.not.exist; + // video + payload = JSON.parse(requests[1].data); + expect(payload.imp[0].ext.max_bids).to.not.exist; + }); + it('should set extend url when extend mode enabled in adunit params', function () { const bidRequest = deepClone(extendBidRequest); let request = spec.buildRequests([bidRequest], { bids: [bidRequest] })[0]; @@ -781,18 +745,15 @@ describe('Improve Digital Adapter Tests', function () { bidRequest.params.extend = false; getConfigStub.withArgs('improvedigital.extend').returns(true); request = spec.buildRequests([bidRequest], { bids: [bidRequest] })[0]; - expect(request.url).to.equal(AD_SERVER_URL); + expect(request.url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1234)); const requests = spec.buildRequests([bidRequest, instreamBidRequest], { bids: [bidRequest, instreamBidRequest] }); expect(requests.length).to.equal(2); - expect(requests[0].url).to.equal(AD_SERVER_URL); + expect(requests[0].url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1234)); expect(requests[1].url).to.equal(EXTEND_URL); }); it('should add publisherId to request URL when available in request params', function() { - function formatPublisherUrl(baseUrl, publisherId) { - return `${baseUrl}/${publisherId}/${PB_ENDPOINT}`; - } const bidRequest = deepClone(simpleBidRequest); bidRequest.params.publisherId = 1000; let request = spec.buildRequests([bidRequest], bidderRequest)[0]; @@ -841,10 +802,6 @@ describe('Improve Digital Adapter Tests', function () { bidderRequestWithConsent.gdprConsent = consent; request = spec.buildRequests([bidRequest], bidderRequestWithConsent)[0]; expect(request.url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1000)); - - delete bidRequest.params.publisherId; - request = spec.buildRequests([bidRequest], bidderRequestWithConsent)[0]; - expect(request.url).to.equal(AD_SERVER_URL); }); }); @@ -1071,7 +1028,7 @@ describe('Improve Digital Adapter Tests', function () { width: 728, height: 90, ttl: 300, - ad: '\x3Cscript>window.__razr_config = {"prebid":{"bidRequest":{"bidder":"improvedigital","params":{"placementId":1053688,"keyValues":{"testKey":["testValue"]},"bidFloor":0.05,"bidFloorCur":"eUR","size":{"w":800,"h":600}},"adUnitCode":"div-gpt-ad-1499748733608-0","transactionId":"f183e871-fbed-45f0-a427-c8a63c4c01eb","bidId":"33e9500b21129f","bidderRequestId":"2772c1e566670b","auctionId":"192721e36a0239","mediaTypes":{"banner":{"sizes":[[300,250],[160,600]]}},"sizes":[[300,250],[160,600]]},"bid":{"mediaType":"banner","ad":"\\"\\"","requestId":"33e9500b21129f","seatBidId":"35adfe19-d6e9-46b9-9f7d-20da7026b965","cpm":1.9200543539802946,"currency":"EUR","width":728,"height":90,"creative_id":"510265","creativeId":"510265","ttl":300,"meta":{},"dealId":320896,"netRevenue":false}}};\x3C/script>  ', + ad: '  ', creativeId: '510265', dealId: 320896, netRevenue: false, @@ -1081,7 +1038,7 @@ describe('Improve Digital Adapter Tests', function () { const multiFormatExpectedBid = [ Object.assign({}, expectedBid[0], { - ad: '\x3Cscript>window.__razr_config = {"prebid":{"bidRequest":{"bidder":"improvedigital","params":{"placementId":1053688},"adUnitCode":"div-gpt-ad-1499748733608-0","transactionId":"f183e871-fbed-45f0-a427-c8a63c4c01eb","bidId":"33e9500b21129f","bidderRequestId":"2772c1e566670b","auctionId":"192721e36a0239","mediaTypes":{"banner":{"sizes":[[300,250],[160,600]]},"native":{},"video":{"context":"outstream","playerSize":[640,480]}},"sizes":[[300,250],[160,600]],"nativeParams":{"body":{"required":true}}},"bid":{"mediaType":"banner","ad":"\\"\\"","requestId":"33e9500b21129f","seatBidId":"35adfe19-d6e9-46b9-9f7d-20da7026b965","cpm":1.9200543539802946,"currency":"EUR","width":728,"height":90,"creative_id":"510265","creativeId":"510265","ttl":300,"meta":{},"dealId":320896,"netRevenue":false}}};\x3C/script>  ', + ad: '  ', }) ]; @@ -1094,7 +1051,7 @@ describe('Improve Digital Adapter Tests', function () { width: 300, height: 250, ttl: 300, - ad: '\x3Cscript>window.__razr_config = {"prebid":{"bidRequest":{"bidder":"improvedigital","params":{"placementId":1053688,"keyValues":{"testKey":["testValue"]},"bidFloor":0.05,"bidFloorCur":"eUR","size":{"w":800,"h":600}},"adUnitCode":"div-gpt-ad-1499748733608-0","transactionId":"f183e871-fbed-45f0-a427-c8a63c4c01eb","bidId":"33e9500b21129f","bidderRequestId":"2772c1e566670b","auctionId":"192721e36a0239","mediaTypes":{"banner":{"sizes":[[300,250],[160,600]]}},"sizes":[[300,250],[160,600]]},"bid":{"mediaType":"banner","ad":"\\"\\"","requestId":"33e9500b21129f","seatBidId":"83c8d524-0955-4d0c-b558-4c9f3600e09b","cpm":1.9200543539802946,"currency":"EUR","width":300,"height":250,"creative_id":"479163","creativeId":"479163","ttl":300,"meta":{},"dealId":320896,"netRevenue":false}}};\x3C/script>  ', + ad: '  ', creativeId: '479163', dealId: 320896, netRevenue: false, diff --git a/test/spec/modules/imuIdSystem_spec.js b/test/spec/modules/imuIdSystem_spec.js index 3650302a2ed..1d6f79786a0 100644 --- a/test/spec/modules/imuIdSystem_spec.js +++ b/test/spec/modules/imuIdSystem_spec.js @@ -13,6 +13,9 @@ import { } from 'modules/imuIdSystem.js'; import * as utils from 'src/utils.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; describe('imuId module', function () { // let setLocalStorageStub; @@ -181,4 +184,38 @@ describe('imuId module', function () { expect(res.success('error response')).to.equal(undefined); }); }); + describe('eid', () => { + before(() => { + attachIdSystem(imuIdSubmodule); + }); + it('should return the correct EID schema with imuid', function() { + const userId = { + imuid: 'testimuid' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'intimatemerger.com', + uids: [{ + id: 'testimuid', + atype: 1 + }] + }); + }); + + it('should return the correct EID schema with imppid', function() { + const userId = { + imppid: 'imppid-value-imppid-value-imppid-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'ppid.intimatemerger.com', + uids: [{ + id: 'imppid-value-imppid-value-imppid-value', + atype: 1 + }] + }); + }); + }) }); diff --git a/test/spec/modules/incrxBidAdapter_spec.js b/test/spec/modules/incrxBidAdapter_spec.js index 3fb4ffe2cd3..24be0dbde57 100644 --- a/test/spec/modules/incrxBidAdapter_spec.js +++ b/test/spec/modules/incrxBidAdapter_spec.js @@ -1,14 +1,13 @@ import { expect } from 'chai'; import { spec } from 'modules/incrxBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; +import { INSTREAM, OUTSTREAM } from 'src/video.js'; -describe('IncrementX', function () { - const METHOD = 'POST'; - const URL = 'https://hb.incrementxserv.com/vzhbidder/bid'; - - const bidRequest = { - bidder: 'IncrementX', +describe('incrementx', function () { + const bannerBidRequest = { + bidder: 'incrementx', params: { - placementId: 'PNX-HB-F796830VCF3C4B' + placementId: 'IX-HB-12345' }, mediaTypes: { banner: { @@ -19,86 +18,212 @@ describe('IncrementX', function () { [300, 250], [300, 600] ], - bidId: 'bid-id-123456', - adUnitCode: 'ad-unit-code-1', - bidderRequestId: 'bidder-request-id-123456', - auctionId: 'auction-id-123456', - transactionId: 'transaction-id-123456' + adUnitCode: 'div-gpt-ad-1460505748561-0', + bidId: '2faedf3e89d123', + bidderRequestId: '1c78fb49cc71c6', + auctionId: 'b4f81e8e36232', + transactionId: '0d95b2c1-a834-4e50-a962-9b6aa0e1c8fb' + }; + const videoBidRequest = { + bidder: 'incrementx', + params: { + placementId: 'IX-HB-12346' + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: ['640x480'] + } + }, + adUnitCode: 'div-gpt-ad-1460505748561-1', + bidId: '2faedf3e89d124', + bidderRequestId: '1c78fb49cc71c7', + auctionId: 'b4f81e8e36233', + transactionId: '0d95b2c1-a834-4e50-a962-9b6aa0e1c8fc' + }; + const instreamVideoBidRequest = { + bidder: 'incrementx', + params: { + placementId: 'IX-HB-12347' + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6], + playbackmethod: [1, 2], + skip: 1, + skipafter: 5 + } + }, + adUnitCode: 'div-gpt-ad-1460505748561-2', + bidId: '2faedf3e89d125', + bidderRequestId: '1c78fb49cc71c8', + auctionId: 'b4f81e8e36234', + transactionId: '0d95b2c1-a834-4e50-a962-9b6aa0e1c8fd' }; describe('isBidRequestValid', function () { - it('should return true where required params found', function () { - expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + it('should return true when required params are found', function () { + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(true); + expect(spec.isBidRequestValid(videoBidRequest)).to.equal(true); + expect(spec.isBidRequestValid(instreamVideoBidRequest)).to.equal(true); }); }); describe('buildRequests', function () { - let bidderRequest = { + const bidderRequest = { refererInfo: { - page: 'https://www.test.com', - reachedTop: true, - isAmp: false, - numIframes: 0, - stack: [ - 'https://www.test.com' - ], - canonicalUrl: null + page: 'https://someurl.com' } }; + it('should build banner request', function () { + const requests = spec.buildRequests([bannerBidRequest], bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.equal('https://hb.incrementxserv.com/vzhbidder/bid'); + const data = JSON.parse(decodeURI(requests[0].data.q)); + expect(data._vzPlacementId).to.equal('IX-HB-12345'); + expect(data.sizes).to.to.a('array'); + expect(data.mChannel).to.equal(1); + }); + it('should build outstream video request', function () { + const requests = spec.buildRequests([videoBidRequest], bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.equal('https://hb.incrementxserv.com/vzhbidder/bid'); + const data = JSON.parse(decodeURI(requests[0].data.q)); + expect(data._vzPlacementId).to.equal('IX-HB-12346'); + expect(data.sizes).to.be.a('array'); + expect(data.mChannel).to.equal(2); + // For video, bidderRequestData should be included + expect(requests[0].data.bidderRequestData).to.exist; + }); + it('should build instream video request', function () { + const requests = spec.buildRequests([instreamVideoBidRequest], bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.equal('https://hb.incrementxserv.com/vzhbidder/bid'); + const data = JSON.parse(decodeURI(requests[0].data.q)); + expect(data._vzPlacementId).to.equal('IX-HB-12347'); + expect(data.sizes).to.be.a('array'); + expect(data.mChannel).to.equal(2); - it('should build correct POST request for banner bid', function () { - const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - expect(request).to.be.an('object'); - expect(request.method).to.equal(METHOD); - expect(request.url).to.equal(URL); - - const payload = JSON.parse(decodeURI(request.data.q)); - expect(payload).to.be.an('object'); - expect(payload._vzPlacementId).to.be.a('string'); - expect(payload.sizes).to.be.an('array'); - expect(payload._slotBidId).to.be.a('string'); - expect(payload._rqsrc).to.be.a('string'); + // For video, bidderRequestData should be included + expect(requests[0].data.bidderRequestData).to.exist; + const decodedBidderRequestData = decodeURI(requests[0].data.bidderRequestData); + expect(decodedBidderRequestData).to.be.a('string'); + // Verify it can be parsed as JSON + expect(() => JSON.parse(decodedBidderRequestData)).to.not.throw(); }); }); describe('interpretResponse', function () { - let serverResponse = { + const bannerServerResponse = { body: { - vzhPlacementId: 'PNX-HB-F796830VCF3C4B', - bid: 'BID-XXXX-XXXX', - adWidth: '300', - adHeight: '250', - cpm: '0.7', - ad: '

Ad from IncrementX

', - slotBidId: 'bid-id-123456', - adType: '1', - settings: '1,2', - nurl: 'htt://nurl.com', - statusText: 'Success' + slotBidId: '2faedf3e89d123', + cpm: 0.5, + adWidth: 300, + adHeight: 250, + ad: '
Banner Ad
', + mediaType: BANNER, + netRevenue: true, + currency: 'USD', + advertiserDomains: ['example.com'] + } + }; + const videoServerResponse = { + body: { + slotBidId: '2faedf3e89d124', + cpm: 1.0, + adWidth: 640, + adHeight: 480, + ad: 'Test VAST', + mediaType: VIDEO, + netRevenue: true, + currency: 'USD', + rUrl: 'https://example.com/vast.xml', + advertiserDomains: ['example.com'] + } + }; + const instreamVideoServerResponse = { + body: { + slotBidId: '2faedf3e89d125', + cpm: 1.5, + adWidth: 640, + adHeight: 480, + ad: 'Test Instream VAST', + mediaType: VIDEO, + netRevenue: true, + currency: 'USD', + ttl: 300, + advertiserDomains: ['example.com'] + } + }; + + const bidderRequest = { + refererInfo: { + page: 'https://someurl.com' + }, + data: { + bidderRequestData: JSON.stringify({ + bids: [videoBidRequest] + }) } }; - let expectedResponse = [{ - requestId: 'bid-id-123456', - cpm: '0.7', - currency: 'USD', - adType: '1', - settings: '1,2', - netRevenue: false, - width: '300', - height: '250', - creativeId: 0, - ttl: 300, - ad: '

Ad from IncrementX

', - meta: { - mediaType: 'banner', - advertiserDomains: [] + const instreamBidderRequest = { + refererInfo: { + page: 'https://someurl.com' + }, + data: { + bidderRequestData: JSON.stringify({ + bids: [instreamVideoBidRequest] + }) } - }]; + }; + + it('should handle banner response', function () { + const bidResponses = spec.interpretResponse(bannerServerResponse, bidderRequest); + expect(bidResponses).to.have.lengthOf(1); + const bid = bidResponses[0]; + expect(bid.requestId).to.equal('2faedf3e89d123'); + expect(bid.cpm).to.equal(0.5); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ad).to.equal('
Banner Ad
'); + expect(bid.mediaType).to.equal(BANNER); + expect(bid.meta.advertiserDomains).to.deep.equal(['example.com']); + }); + + it('should handle outstream video response', function () { + const bidResponses = spec.interpretResponse(videoServerResponse, bidderRequest); + expect(bidResponses).to.have.lengthOf(1); + const bid = bidResponses[0]; + expect(bid.requestId).to.equal('2faedf3e89d124'); + expect(bid.cpm).to.equal(1.0); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.vastXml).to.equal('Test VAST'); + expect(bid.renderer).to.exist; + expect(bid.mediaType).to.equal(VIDEO); + expect(bid.meta.advertiserDomains).to.deep.equal(['example.com']); + }); - it('should correctly interpret valid banner response', function () { - let result = spec.interpretResponse(serverResponse); - expect(result).to.deep.equal(expectedResponse); + it('should handle instream video response', function () { + const bidResponses = spec.interpretResponse(instreamVideoServerResponse, instreamBidderRequest); + expect(bidResponses).to.have.lengthOf(1); + const bid = bidResponses[0]; + expect(bid.requestId).to.equal('2faedf3e89d125'); + expect(bid.cpm).to.equal(1.5); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.vastUrl).to.equal('Test Instream VAST'); + expect(bid.mediaType).to.equal(VIDEO); + expect(bid.ttl).to.equal(300); + expect(bid.renderer).to.not.exist; + expect(bid.meta.advertiserDomains).to.deep.equal(['example.com']); }); }); }); diff --git a/test/spec/modules/inmobiBidAdapter_spec.js b/test/spec/modules/inmobiBidAdapter_spec.js new file mode 100644 index 00000000000..ce2f3783e09 --- /dev/null +++ b/test/spec/modules/inmobiBidAdapter_spec.js @@ -0,0 +1,1963 @@ +import { expect } from 'chai'; +import { + spec, +} from 'modules/inmobiBidAdapter.js'; +import * as utils from 'src/utils.js'; +import * as ajax from 'src/ajax.js'; +import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; +import { hook } from '../../../src/hook'; +import { config } from '../../../src/config.js'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; +import 'modules/consentManagementTcf.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/consentManagementGpp.js'; +import 'modules/priceFloors.js'; +import sinon from 'sinon'; + +// constants +const GVLID = 333; +export const ADAPTER_VERSION = 1.0; +const BIDDER_CODE = 'inmobi'; +export const EVENT_ENDPOINT = 'https://sync.inmobi.com'; + +describe('The inmobi bidding adapter', function () { + let utilsMock, sandbox, ajaxStub, fetchStub; ; + + beforeEach(function () { + // mock objects + utilsMock = sinon.mock(utils); + sandbox = sinon.sandbox.create(); + ajaxStub = sandbox.stub(ajax, 'ajax'); + fetchStub = sinon.stub(global, 'fetch').resolves(new Response('OK')); + }); + + afterEach(function () { + utilsMock.restore(); + sandbox.restore(); + ajaxStub.restore(); + fetchStub.restore(); + }); + + describe('onBidWon', function () { + // existence test + it('onBidWon function should be defined', function () { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + + it('It should invoke onBidWon, resolving the eventType and domain', function () { + const bid = { + bidder: 'inmobi', + width: 300, + height: 250, + adId: '330a22bdea4cac1', + mediaType: 'banner', + cpm: 0.28, + ad: 'inmobiAd', + requestId: '418b37f85e772c1', + adUnitCode: 'div-gpt-ad-1460505748561-01', + size: '350x250', + adserverTargeting: { + hb_bidder: 'prebid', + hb_adid: '330a22bdea4cac1', + hb_pb: '0.20', + hb_size: '350x250' + }, + meta: { + loggingPercentage: 100 + } + }; + spec.onBidWon(bid); + // expected url and payload + const expectedUrl = `${EVENT_ENDPOINT}/report/onBidWon`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + eventPayload: bid.meta + }); + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + const fetchArgs = fetchStub.getCall(0).args; + /* + index fetch parameter + 0 -> URL + 1 -> options (method, headers, body, etc.) + */ + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + credentials: 'include', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'text/plain', + }); + }); + + it('onBidWon should not be called when loggingPercentage is set to 0', function () { + const bid = { + bidder: 'inmobi', + width: 300, + height: 250, + adId: '330a22bdea4cac1', + mediaType: 'banner', + cpm: 0.28, + ad: 'inmobiAd', + requestId: '418b37f85e772c1', + adUnitCode: 'div-gpt-ad-1460505748561-01', + size: '350x250', + adserverTargeting: { + hb_bidder: 'prebid', + hb_adid: '330a22bdea4cac1', + hb_pb: '0.20', + hb_size: '350x250' + }, + meta: { + loggingPercentage: 0 + } + }; + spec.onBidWon(bid); + // expected url and payload + const expectedUrl = `${EVENT_ENDPOINT}/report/onBidWon`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + eventPayload: bid.meta + }); + // assert statements + expect(fetchStub.callCount).to.be.equal(0); + }); + + it('onBidWon should not be called if the bid data is null', function () { + // Call onBidWon with null data + spec.onBidWon(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); + + describe('onBidderError', function () { + it('onBidderError function should be defined', function () { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + + it('onBidderError should not be called if the bid data is null', function () { + // Call onBidError with null data + spec.onBidderError(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + + it('onBidderError should be called with the eventType', function () { + const bid = { + error: 'error', // Assuming this will be a mock or reference to an actual XMLHttpRequest object + bidderRequest: { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + auctionStart: 1579746300522, + bidder: 'inmobi', + bidderRequestId: '15246a574e859f' + } + }; + spec.onBidderError(bid); + // expected url and payload + const expectedUrl = `${EVENT_ENDPOINT}/report/onBidderError`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + eventPayload: bid + }); + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + const fetchArgs = fetchStub.getCall(0).args; + /* + index fetch parameter + 0 -> URL + 1 -> options (method, headers, body, etc.) + */ + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + credentials: 'include', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'text/plain', + }); + }); + }); + + describe('onAdRenderSucceeded', function () { + // existence test + it('onAdRenderSucceeded function should be defined', function () { + expect(spec.onAdRenderSucceeded).to.exist.and.to.be.a('function'); + }); + + it('should invoke onAdRenderSucceeded, resolving the eventType and domain', function () { + const bid = { + bidder: 'inmobi', + width: 300, + height: 250, + adId: '330a22bdea4cac1', + mediaType: 'banner', + cpm: 0.28, + ad: 'inmobiAd', + requestId: '418b37f85e772c1', + adUnitCode: 'div-gpt-ad-1460505748561-01', + size: '350x250', + adserverTargeting: { + hb_bidder: 'prebid', + hb_adid: '330a22bdea4cac1', + hb_pb: '0.20', + hb_size: '350x250' + }, + meta: { + loggingPercentage: 100 + } + }; + spec.onAdRenderSucceeded(bid); + // expected url and payload + const expectedUrl = `${EVENT_ENDPOINT}/report/onAdRenderSucceeded`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + eventPayload: bid.meta + }); + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + const fetchArgs = fetchStub.getCall(0).args; + /* + index fetch parameter + 0 -> URL + 1 -> options (method, headers, body, etc.) + */ + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + credentials: 'include', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'text/plain', + }); + }); + + it('onAdRenderSucceeded should not be called when loggingPercentage is 0', function () { + const bid = { + bidder: 'inmobi', + width: 300, + height: 250, + adId: '330a22bdea4cac1', + mediaType: 'banner', + cpm: 0.28, + ad: 'inmobiAd', + requestId: '418b37f85e772c1', + adUnitCode: 'div-gpt-ad-1460505748561-01', + size: '350x250', + adserverTargeting: { + hb_bidder: 'prebid', + hb_adid: '330a22bdea4cac1', + hb_pb: '0.20', + hb_size: '350x250' + }, + meta: { + loggingPercentage: 0 + } + }; + spec.onAdRenderSucceeded(bid); + // expected url and payload + const expectedUrl = `${EVENT_ENDPOINT}/report/onAdRenderSucceeded`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + eventPayload: bid.meta + }); + // assert statements + expect(fetchStub.callCount).to.be.equal(0); + }); + + it('onAdRenderSucceeded should not be called if the bid data is null', function () { + // Call onAdRenderSucceeded with null data + spec.onAdRenderSucceeded(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); + + describe('onTimeout', function () { + // existence test + it('onTimeout function should be defined', function () { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + + it('should invoke onTimeout, resolving the eventType and domain', function () { + const bid = [ + { + bidder: 'inmobi', + bidId: '51ef8751f9aead1', + adUnitCode: 'div-gpt-ad-14605057481561-0', + timeout: 3000, + auctionId: '18fd8b8b0bd7517' + } + ]; + spec.onTimeout(bid); + // expected url and payload + const expectedUrl = `${EVENT_ENDPOINT}/report/onTimeout`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + eventPayload: bid + }); + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + const fetchArgs = fetchStub.getCall(0).args; + /* + index fetch parameter + 0 -> URL + 1 -> options (method, headers, body, etc.) + */ + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + credentials: 'include', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'text/plain', + }); + }); + + it('onTimeout should not be called if the bid data is null', function () { + // Call onTimeout with null data + spec.onTimeout(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); + + describe('onSetTargeting', function () { + // existence test + it('The onSetTargeting function should be defined', function () { + expect(spec.onSetTargeting).to.exist.and.to.be.a('function'); + }); + + it('should invoke onSetTargeting, resolving the eventType and domain', function () { + const bid = { + bidder: 'inmobi', + width: 300, + height: 250, + adId: '330a22bdea4cac1', + mediaType: 'banner', + cpm: 0.28, + ad: 'inmobiAd', + requestId: '418b37f85e7721c', + adUnitCode: 'div-gpt-ad-1460505748561-01', + size: '350x250', + adserverTargeting: { + hb_bidder: 'prebid', + hb_adid: '330a22bdea4cac1', + hb_pb: '0.20', + hb_size: '350x250' + }, + meta: { + loggingPercentage: 100 + } + }; + spec.onSetTargeting(bid); + // expected url and payload + const expectedUrl = `${EVENT_ENDPOINT}/report/onSetTargeting`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + eventPayload: bid.meta + }); + // assert statements + expect(fetchStub.callCount).to.be.equal(1); + const fetchArgs = fetchStub.getCall(0).args; + /* + index fetch parameter + 0 -> URL + 1 -> options (method, headers, body, etc.) + */ + expect(fetchArgs[0]).to.equal(expectedUrl); + const actualPayload = fetchArgs[1]?.body; + expect(actualPayload).to.equal(expectedPayload); + expect(fetchArgs[1]).to.deep.include({ + method: 'POST', + credentials: 'include', + keepalive: true, + }); + expect(fetchArgs[1]?.headers).to.deep.equal({ + 'Content-Type': 'text/plain', + }); + }); + + it('onSetTargeting should not be called when loggingPercentage is 0', function () { + const bid = { + bidder: 'inmobi', + width: 300, + height: 250, + adId: '330a22bdea4cac1', + mediaType: 'banner', + cpm: 0.28, + ad: 'inmobiAd', + requestId: '418b37f85e7721c', + adUnitCode: 'div-gpt-ad-1460505748561-01', + size: '350x250', + adserverTargeting: { + hb_bidder: 'prebid', + hb_adid: '330a22bdea4cac1', + hb_pb: '0.20', + hb_size: '350x250' + }, + meta: { + loggingPercentage: 0 + } + }; + spec.onSetTargeting(bid); + // expected url and payload + const expectedUrl = `${EVENT_ENDPOINT}/report/onSetTargeting`; + const expectedPayload = JSON.stringify({ + domain: location.hostname, + eventPayload: bid.meta + }); + // assert statements + expect(fetchStub.callCount).to.be.equal(0); + }); + + it('onSetTargeting should not be called if the bid data is null', function () { + // Call onSetTargeting with null data + spec.onSetTargeting(null); + // Assert that ajax was not called since bid data is null + expect(fetchStub.callCount).to.be.equal(0); + }); + }); + + describe('isBidRequestValid', function () { + it('should return false when an invalid bid is provided', function () { + const bid = { + bidder: 'inmobi', + }; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equal(false); + }); + + it('should return true when the bid contains a PLC', function () { + const bid = { + bidder: 'inmobi', + params: { + plc: '123a', + }, + }; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equal(true); + }); + }); + + describe('buildRequests', function () { + before(() => { + hook.ready(); + }) + afterEach(function () { + config.resetConfig(); + }); + const bidderRequest = { + refererInfo: { + page: 'inmobi', + topmostLocation: 'inmobi' + }, + timeout: 3000, + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '333': 1 + }, + }, + apiVersion: 1, + }, + }; + + it('request should build with correct plc', async function () { + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'inmobi', + adUnitCode: 'bid-12', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123' + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].ext.bidder.plc).to.deep.equal('123'); + }); + + it('request should build with correct imp', async function () { + const expectedMetric = { + url: 'https://inmobi.com' + } + const bidRequests = [{ + bidId: 'bidId', + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + ortb2Imp: { + instl: 1, + metric: expectedMetric, + ext: { + gpid: 'gpid_inmobi' + }, + rwdd: 1 + }, + params: { + plc: '123ai', + bidfloor: 4.66, + bidfloorcur: 'USD' + } + }]; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].id).to.deep.equal('bidId'); + expect(ortbRequest.imp[0].tagid).to.deep.equal('impId'); + expect(ortbRequest.imp[0].instl).to.equal(1); + expect(ortbRequest.imp[0].bidfloor).to.equal(4.66); + expect(ortbRequest.imp[0].bidfloorcur).to.equal('USD'); + expect(ortbRequest.imp[0].metric).to.deep.equal(expectedMetric); + expect(ortbRequest.imp[0].secure).to.equal(0); + expect(ortbRequest.imp[0].ext.gpid).to.equal('gpid_inmobi'); + expect(ortbRequest.imp[0].rwdd).to.equal(1); + }); + + it('request should build with proper site data', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '1234a', + }, + }, + ]; + const ortb2 = { + site: { + name: 'raapchikgames.com', + domain: 'raapchikgames.com', + keywords: 'test1, test2', + cat: ['IAB2'], + pagecat: ['IAB3'], + sectioncat: ['IAB4'], + page: 'https://raapchikgames.com', + ref: 'inmobi.com', + privacypolicy: 1, + content: { + url: 'https://raapchikgames.com/games1' + } + } + }; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.site.domain).to.equal('raapchikgames.com'); + expect(ortbRequest.site.publisher.domain).to.equal('inmobi'); + expect(ortbRequest.site.page).to.equal('https://raapchikgames.com'); + expect(ortbRequest.site.name).to.equal('raapchikgames.com'); + expect(ortbRequest.site.keywords).to.equal('test1, test2'); + expect(ortbRequest.site.cat).to.deep.equal(['IAB2']); + expect(ortbRequest.site.pagecat).to.deep.equal(['IAB3']); + expect(ortbRequest.site.sectioncat).to.deep.equal(['IAB4']); + expect(ortbRequest.site.ref).to.equal('inmobi.com'); + expect(ortbRequest.site.privacypolicy).to.equal(1); + expect(ortbRequest.site.content.url).to.equal('https://raapchikgames.com/games1') + }); + + it('request should build with proper device data', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '1234a', + }, + }, + ]; + const ortb2 = { + device: { + dnt: 0, + ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + ip: '195.199.250.144', + h: 919, + w: 1920, + language: 'hu', + lmt: 1, + js: 1, + connectiontype: 0, + hwv: '5S', + model: 'iphone', + mccmnc: '310-005', + geo: { + lat: 40.0964439, + lon: -75.3009142 + } + } + }; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.device.dnt).to.equal(0); + expect(ortbRequest.device.lmt).to.equal(1); + expect(ortbRequest.device.js).to.equal(1); + expect(ortbRequest.device.connectiontype).to.equal(0); + expect(ortbRequest.device.ua).to.equal('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36'); + expect(ortbRequest.device.ip).to.equal('195.199.250.144'); + expect(ortbRequest.device.h).to.equal(919); + expect(ortbRequest.device.w).to.equal(1920); + expect(ortbRequest.device.language).to.deep.equal('hu'); + expect(ortbRequest.device.hwv).to.deep.equal('5S'); + expect(ortbRequest.device.model).to.deep.equal('iphone'); + expect(ortbRequest.device.mccmnc).to.deep.equal('310-005'); + expect(ortbRequest.device.geo.lat).to.deep.equal(40.0964439); + expect(ortbRequest.device.geo.lon).to.deep.equal(-75.3009142); + }); + + it('should properly build a request with source object', async function () { + const expectedSchain = {id: 'prebid'}; + const ortb2 = { + source: { + pchain: 'inmobi', + schain: expectedSchain + } + }; + const bidRequests = [ + { + bidder: 'inmobi', + bidId: 'bidId', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '124', + }, + }, + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.source.ext.schain).to.deep.equal(expectedSchain); + expect(ortbRequest.source.pchain).to.equal('inmobi'); + }); + + it('should properly user object', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123' + } + }, + ]; + const br = { + ...bidderRequest, + ortb2: { + user: { + yob: 2002, + keyowrds: 'test test', + gender: 'M', + customdata: 'test no', + geo: { + lat: 40.0964439, + lon: -75.3009142 + }, + ext: { + eids: [ + { + source: 'inmobi.com', + uids: [{ + id: 'iid', + atype: 1 + }] + } + ] + } + } + } + } + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(br)); + const ortbRequest = request.data; + expect(ortbRequest.user.yob).to.deep.equal(2002); + expect(ortbRequest.user.keyowrds).to.deep.equal('test test'); + expect(ortbRequest.user.gender).to.deep.equal('M'); + expect(ortbRequest.user.customdata).to.deep.equal('test no'); + expect(ortbRequest.user.geo.lat).to.deep.equal(40.0964439); + expect(ortbRequest.user.geo.lon).to.deep.equal(-75.3009142); + expect(ortbRequest.user.ext.eids).to.deep.equal([ + { + source: 'inmobi.com', + uids: [{ + id: 'iid', + atype: 1 + }] + } + ]); + }); + + it('should properly build a request regs object', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '1234a', + }, + }, + ]; + const ortb2 = { + regs: { + coppa: 1, + gpp: 'gpp_consent_string', + gpp_sid: [0, 1, 2], + us_privacy: 'yes us privacy applied' + } + }; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest({...bidderRequest, ortb2})).data; + expect(ortbRequest.regs.coppa).to.equal(1); + expect(ortbRequest.regs.ext.gpp).to.equal('gpp_consent_string'); + expect(ortbRequest.regs.ext.gpp_sid).to.deep.equal([0, 1, 2]); + expect(ortbRequest.regs.ext.us_privacy).to.deep.equal('yes us privacy applied'); + }); + + it('gdpr test', async function () { + // using privacy params from global bidder Request + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '1234a', + }, + }, + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs.ext.gdpr).to.deep.equal(1); + expect(ortbRequest.user.ext.consent).to.equal('consentDataString'); + }); + + it('should properly set tmax if available', async function () { + // using tmax from global bidder Request + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'bid-1', + transactionId: 'trans-1', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123' + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.tmax).to.equal(bidderRequest.timeout); + }); + + it('should properly build a request with bcat field', async function () { + const bcat = ['IAB1', 'IAB2']; + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123a', + }, + }, + ]; + const bidderRequest = { + ortb2: { + bcat + } + }; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.bcat).to.deep.equal(bcat); + }); + + it('should properly build a request with badv field', async function () { + const badv = ['ford.com']; + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '12ea', + }, + }, + ]; + const bidderRequest = { + ortb2: { + badv + } + }; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.badv).to.deep.equal(badv); + }); + + it('should properly build a request with bapp field', async function () { + const bapp = ['raapchik.com']; + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123a', + }, + }, + ]; + const bidderRequest = { + ortb2: { + bapp + } + }; + + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.bapp).to.deep.equal(bapp); + }); + + it('banner request test', async function () { + const bidderRequest = {}; + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'inmobi', + adUnitCode: 'bid-12', + mediaTypes: { + banner: { + sizes: [[320, 50]], + pos: 1, + topframe: 0, + } + }, + params: { + plc: '123' + }, + ortb2Imp: { + banner: { + api: [1, 2], + mimes: ['image/jpg', 'image/gif'] + } + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].banner).not.to.be.null; + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(320); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(50); + expect(ortbRequest.imp[0].banner.pos).to.equal(1); + expect(ortbRequest.imp[0].banner.topframe).to.equal(0); + expect(ortbRequest.imp[0].banner.api).to.deep.equal([1, 2]); + expect(ortbRequest.imp[0].banner.mimes).to.deep.equal(['image/jpg', 'image/gif']); + }); + + it('banner request test with sizes > 1', async function () { + const bidderRequest = {}; + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'inmobi', + adUnitCode: 'bid-12', + mediaTypes: { + banner: { + sizes: [[320, 50], [720, 50]] + } + }, + params: { + plc: '123' + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].banner).not.to.be.null; + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(320); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(50); + expect(ortbRequest.imp[0].banner.format[1].w).to.equal(720); + expect(ortbRequest.imp[0].banner.format[1].h).to.equal(50); + }); + + if (FEATURES.VIDEO) { + it('video request test', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'bid-123', + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'inbanner', + playerSize: [640, 360], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30, + api: [1, 2], + protocols: [2, 3], + plcmt: 3, + w: 640, + h: 360, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], + skip: 1, + minduration: 5, + startdelay: 5, + playbackmethod: [1, 3], + placement: 2 + } + }, + params: { + plc: '123' + }, + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.startdelay).to.equal(5); + expect(ortbRequest.imp[0].video.playbackmethod).to.deep.equal([1, 3]); + expect(ortbRequest.imp[0].video.placement).to.equal(2); + expect(ortbRequest.imp[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(ortbRequest.imp[0].video.maxduration).to.equal(30); + expect(ortbRequest.imp[0].video.api).to.deep.equal([1, 2]); + expect(ortbRequest.imp[0].video.protocols).to.deep.equal([2, 3]); + expect(ortbRequest.imp[0].video.plcmt).to.equal(3); + expect(ortbRequest.imp[0].video.w).to.equal(640); + expect(ortbRequest.imp[0].video.h).to.equal(360); + expect(ortbRequest.imp[0].video.linearity).to.equal(1); + expect(ortbRequest.imp[0].video.skipmin).to.equal(30); + expect(ortbRequest.imp[0].video.skipafter).to.equal(30); + expect(ortbRequest.imp[0].video.minbitrate).to.equal(10000); + expect(ortbRequest.imp[0].video.maxbitrate).to.equal(48000); + expect(ortbRequest.imp[0].video.delivery).to.deep.equal([1, 2, 3]); + expect(ortbRequest.imp[0].video.pos).to.equal(1); + expect(ortbRequest.imp[0].video.playbackend).to.equal(1); + }); + } + + if (FEATURES.VIDEO) { + it('video request with player size > 1 ', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'bid-123', + sizes: [[640, 360], [480, 320]], + mediaTypes: { + video: { + context: 'inbanner', + playerSize: [[640, 360], [480, 320]], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30, + api: [1, 2], + protocols: [2, 3], + plcmt: 3, + w: 640, + h: 360, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], + skip: 1, + minduration: 5, + startdelay: 5, + playbackmethod: [1, 3], + placement: 2 + } + }, + params: { + plc: '123' + }, + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video.w).to.be.equal(640); + expect(ortbRequest.imp[0].video.h).to.be.equal(360); + }); + } + + if (FEATURES.VIDEO) { + it('video request test when skip is 0', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'bid-123', + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'inbanner', + playerSize: [640, 360], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30, + api: [1, 2], + protocols: [2, 3], + plcmt: 3, + w: 640, + h: 360, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], + skip: 0, + minduration: 5, + startdelay: 5, + playbackmethod: [1, 3], + placement: 2 + } + }, + params: { + plc: '123' + }, + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video.skip).to.equal(0); + }); + } + + if (FEATURES.NATIVE) { + it('native request test without assests', async function () { + const bidRequests = [ + { + mediaTypes: { + native: {} + }, + params: { + 'plc': '123a' + } + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + expect(ortbRequest.imp[0].native).to.be.undefined; + }); + } + + if (FEATURES.NATIVE) { + it('native request with assets', async function () { + const assets = [{ + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }, + { + required: 1, + id: 2, + title: { + len: 140, + } + }, + { + required: 1, + id: 3, + data: { + type: 1, + } + }]; + const bidRequests = [ + { + mediaTypes: { + native: {} + }, + nativeOrtbRequest: { + assets: assets + }, + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + + expect(ortbRequest.imp[0].native.request).to.not.be.null; + const nativeRequest = JSON.parse(ortbRequest.imp[0].native.request); + expect(nativeRequest).to.have.property('assets'); + expect(nativeRequest.assets).to.deep.equal(assets); + }); + } + + it('should properly build a request when coppa flag is true', async function () { + const bidRequests = []; + const bidderRequest = {}; + config.setConfig({coppa: true}); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs.coppa).to.equal(1); + }); + + it('should properly build a request when coppa flag is false', async function () { + const bidRequests = []; + const bidderRequest = {}; + config.setConfig({coppa: false}); + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs.coppa).to.equal(0); + }); + + it('should properly build a request when coppa flag is not defined', async function () { + const bidRequests = []; + const bidderRequest = {}; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.regs?.coppa).to.be.undefined; + }); + + it('build a banner request with bidFloor', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50], [720, 90]] + } + }, + params: { + plc: '123a', + bidfloor: 1, + bidfloorcur: 'USD' + } + } + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].bidfloor).to.deep.equal(1); + expect(ortbRequest.imp[0].bidfloorcur).to.deep.equal('USD'); + }); + + it('build a banner request with getFloor', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123a' + }, + getFloor: inputParams => { + return {currency: 'USD', floor: 1.23, size: '*', mediaType: '*'}; + } + } + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].bidfloor).equal(1.23); + expect(ortbRequest.imp[0].bidfloorcur).equal('USD'); + }); + + if (FEATURES.VIDEO) { + it('build a video request with bidFloor', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + video: { + playerSize: [[480, 320], [720, 480]] + } + }, + params: { + plc: '123a', + bidfloor: 1, + bidfloorcur: 'USD' + } + } + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].bidfloor).to.equal(1); + expect(ortbRequest.imp[0].bidfloorcur).to.equal('USD'); + }); + } + + if (FEATURES.VIDEO) { + it('build a video request with getFloor', async function () { + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + video: { + playerSize: [[480, 320], [720, 480]] + } + }, + params: { + plc: '123a' + }, + getFloor: inputParams => { + return {currency: 'USD', floor: 1.23, size: '*', mediaType: '*'}; + } + } + ]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].bidfloor).to.equal(1.23); + expect(ortbRequest.imp[0].bidfloorcur).to.equal('USD'); + }); + } + + if (FEATURES.NATIVE && FEATURES.VIDEO) { + it('build a mutli format request with getFloor', async function () { + const assets = [{ + required: 1, + id: 1, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }, + { + required: 1, + id: 2, + title: { + len: 140, + } + }, + { + required: 1, + id: 3, + data: { + type: 1, + } + }]; + const bidRequests = [ + { + bidder: 'inmobi', + adUnitCode: 'impId', + mediaTypes: { + banner: { + sizes: [[320, 50], [720, 90]] + }, + video: { + playerSize: [640, 480], + }, + native: {} + }, + nativeOrtbRequest: { + assets: assets + }, + params: { + plc: '12456' + }, + getFloor: inputParams => { + return {currency: 'USD', floor: 1.23, size: '*', mediaType: '*'}; + } + }, + ]; + const bidderRequest = {}; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp[0].banner).not.to.be.undefined; + expect(ortbRequest.imp[0].video).not.to.be.undefined; + expect(ortbRequest.imp[0].native.request).not.to.be.undefined; + expect(ortbRequest.imp[0].bidfloor).to.deep.equal(1.23); + expect(ortbRequest.imp[0].bidfloorcur).to.deep.equal('USD'); + }); + } + + if (FEATURES.VIDEO) { + it('build a multi imp request', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + video: { + context: 'inbanner', + playerSize: [640, 360], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30, + api: [1, 2], + protocols: [2, 3], + plcmt: 3, + w: 640, + h: 360, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], + skip: 1, + minduration: 5, + startdelay: 5, + playbackmethod: [1, 3], + placement: 2 + } + }, + params: { + plc: '123' + }, + }, { + adUnitCode: 'impId', + bidId: 'bidId2', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123', + } + }]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp).to.have.lengthOf(2); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.startdelay).to.equal(5); + expect(ortbRequest.imp[0].video.playbackmethod).to.deep.equal([1, 3]); + expect(ortbRequest.imp[0].video.placement).to.equal(2); + // banner test + expect(ortbRequest.imp[1].banner).not.to.be.undefined; + }); + } + + if (FEATURES.VIDEO) { + it('build a multi format request', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + video: { + context: 'inbanner', + playerSize: [640, 360], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30, + api: [1, 2], + protocols: [2, 3], + plcmt: 3, + w: 640, + h: 360, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], + skip: 1, + minduration: 5, + startdelay: 5, + playbackmethod: [1, 3], + placement: 2 + }, + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123' + }, + }]; + const ortbRequest = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)).data; + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.startdelay).to.equal(5); + expect(ortbRequest.imp[0].video.playbackmethod).to.deep.equal([1, 3]); + expect(ortbRequest.imp[0].video.placement).to.equal(2); + // banner test + expect(ortbRequest.imp[0].banner).not.to.be.undefined; + }); + } + }); + + describe('getUserSyncs', function () { + const syncEndPoint = 'https://sync.inmobi.com/prebidjs?'; + + it('should return empty if iframe disabled and pixelEnabled is false', function () { + const res = spec.getUserSyncs({}); + expect(res).to.deep.equal([]); + }); + + it('should return user sync if iframe enabled is true', function () { + const res = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }); + expect(res).to.deep.equal([{ type: 'iframe', url: syncEndPoint }]); + }); + + it('should return user sync if iframe enabled is true and gdpr not present', function () { + const res = spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: false }); + expect(res).to.deep.equal([{ type: 'iframe', url: `${syncEndPoint}` }]); + }); + + it('should return user sync if iframe enabled is true and gdpr = 1 and gdpr consent present', function () { + const res = spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: 'GDPR_CONSENT' }); + expect(res).to.deep.equal([{ type: 'iframe', url: `${syncEndPoint}gdpr=1&gdpr_consent=GDPR_CONSENT` }]); + }); + + it('should return user sync if iframe enabled is true and gdpr = 1 , gdpr consent present and usp_consent present', function () { + const res = spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: 'GDPR_CONSENT' }, 'USP_CONSENT'); + expect(res).to.deep.equal([{ type: 'iframe', url: `${syncEndPoint}gdpr=1&gdpr_consent=GDPR_CONSENT&us_privacy=USP_CONSENT` }]); + }); + + it('should return user sync if iframe enabled is true usp_consent present and gppConsent present', function () { + const res = spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: false, consentString: undefined }, 'USP_CONSENT', { gppString: 'GPP_STRING', applicableSections: [32, 51] }); + expect(res).to.deep.equal([{ type: 'iframe', url: `${syncEndPoint}us_privacy=USP_CONSENT&gpp=GPP_STRING&gpp_sid=32%2C51` }]); + }); + + const responses = [{ + body: { + ext: { + prebidjs: { + urls: [ + { + url: 'https://sync.inmobi.com/prebidjs' + }, + { + url: 'https://eus.rubiconproject.com/usync.html?p=test' + } + ] + } + } + } + }]; + + it('should return urls from response when iframe enabled is false and pixel enabled', function () { + const res = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }, responses); + expect(res).to.deep.equal([ + { type: 'image', url: 'https://sync.inmobi.com/prebidjs' }, + { type: 'image', url: 'https://eus.rubiconproject.com/usync.html?p=test' } + ]) + }); + + it('should return urls from response when iframe enabled is false and pixel enabled and empty responses', function () { + const responses = []; + const res = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }, responses); + expect(res).to.deep.equal([ + { type: 'image', url: `${syncEndPoint}` } + ]) + }); + + it('should return urls from response when iframe enabled is false and pixel enabled and no response', function () { + const responses = undefined; + const res = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }, responses); + expect(res).to.deep.equal([ + { type: 'image', url: `${syncEndPoint}` } + ]) + }); + + it('should return urls from response when iframe enabled is false and all consent parameters present', function () { + const res = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }, responses, { gdprApplies: true, consentString: 'GDPR_CONSENT' }, 'USP_CONSENT', { gppString: 'GPP_STRING', applicableSections: [32, 51] }); + expect(res).to.deep.equal([ + { type: 'image', url: 'https://sync.inmobi.com/prebidjs?gdpr=1&gdpr_consent=GDPR_CONSENT&us_privacy=USP_CONSENT&gpp=GPP_STRING&gpp_sid=32%2C51' }, + { type: 'image', url: 'https://eus.rubiconproject.com/usync.html?p=test&gdpr=1&gdpr_consent=GDPR_CONSENT&us_privacy=USP_CONSENT&gpp=GPP_STRING&gpp_sid=32%2C51' } + ]) + }); + }); + + describe('interpretResponse', function () { + const bidderRequest = { + refererInfo: { + page: 'https://raapchikgames.com', + topmostLocation: 'https://raapchikgames.com' + }, + timeout: 3000, + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '333': 1 + }, + }, + apiVersion: 1, + }, + }; + function mockResponse(winningBidId, mediaType) { + return { + id: '95d08af8-2d50-4d75-a411-8ecd9224970e', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: '20dd72ed-930f-1000-e56f-07c37a793f30', + impid: winningBidId, + price: 1.1645, + adomain: ['advertiserDomain.sandbox.inmobi.com'], + crid: '88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301', + w: 320, + h: 50, + adm: 'test-ad', + mtype: mediaType, + nurl: 'https://et-l.w.inmobi.com/c.asm/', + api: 3, + cat: [], + ext: { + loggingPercentage: 100, + prebid: { + meta: { + advertiserDomains: ['advertiserDomain.sandbox.inmobi.com'], + networkName: 'inmobi' + } + } + } + } + ] + } + ], + ext: { + prebidjs: { + urls: [ + { url: 'https://sync.inmobi.com/prebidjs' }, + { url: 'https://eus.rubiconproject.com/usync.html?p=test' } + ] + } + } + }; + }; + + function mockResponseNative(winningBidId, mediaType) { + return { + id: '95d08af8-2d50-4d75-a411-8ecd9224970e', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: '20dd72ed-930f-1000-e56f-07c37a793f30', + impid: winningBidId, + price: 1.1645, + adomain: ['advertiserDomain.sandbox.inmobi.com'], + crid: '88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301', + w: 320, + h: 50, + adm: '{"native":{"ver":"1.2","assets":[{"img":{"w":100,"h":100,"type":3,"url":"https://supply.inmobicdn.net/sandbox-prod-assets/Native_testAd.png"},"id":1,"required":1},{"id":2,"title":{"len":140,"text":"Native-Title-InMobi-Sandbox"},"required":1},{"data":{"type":1,"value":""},"id":3,"required":1},{"data":{"type":2,"value":"InMobi native test - Subtitle"},"id":4,"required":0},{"img":{"w":20,"h":20,"type":1,"url":"https://supply.inmobicdn.net/sandbox-prod-assets/inmobi-Logo-150x150.png"},"id":5,"required":0}],"link":{"clicktrackers":["https://c-eus.w.inmobi.com/"],"url":"https://www.inmobi.com"},"eventtrackers":[{"method":1,"event":1,"url":"https://et-eus.w.inmobi.com/"}]}}', + mtype: mediaType, + nurl: 'https://et-l.w.inmobi.com/c.asm/', + api: 3, + cat: [], + ext: { + loggingPercentage: 100, + prebid: { + meta: { + advertiserDomains: ['advertiserDomain.sandbox.inmobi.com'], + networkName: 'inmobi' + } + } + } + } + ] + } + ], + ext: { + prebidjs: { + urls: [ + { url: 'https://sync.inmobi.com/prebidjs' }, + { url: 'https://eus.rubiconproject.com/usync.html?p=test' } + ] + } + } + }; + }; + + it('returns an empty array when bid response is empty', async function () { + const bidRequests = []; + const response = {}; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(0); + }); + + it('should return an empty array when there is no bid response', async function () { + const bidRequests = []; + const response = {seatbid: []}; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(0); + }); + + it('return banner response', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '124a', + } + }]; + const response = mockResponse('bidId', 1); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.length(1); + expect(bids[0].currency).to.deep.equal('USD'); + expect(bids[0].mediaType).to.deep.equal('banner'); + expect(bids[0].requestId).to.deep.equal('bidId'); + expect(bids[0].seatBidId).to.deep.equal('20dd72ed-930f-1000-e56f-07c37a793f30'); + expect(bids[0].cpm).to.equal(1.1645); + expect(bids[0].width).to.equal(320); + expect(bids[0].height).to.equal(50); + expect(bids[0].creativeId).to.deep.equal('88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301'); + expect(bids[0].meta.loggingPercentage).to.equal(100); + expect(bids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.inmobi.com'); + expect(bids[0].meta.prebid.meta.networkName).to.deep.equal('inmobi'); + }); + + it('bid response when banner wins among two ad units', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 320], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + plc: '123', + }, + }, { + adUnitCode: 'impId', + bidId: 'bidId2', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123', + } + }]; + const response = mockResponse('bidId2', 1); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids[0].currency).to.deep.equal('USD'); + expect(bids[0].mediaType).to.deep.equal('banner'); + expect(bids[0].requestId).to.deep.equal('bidId2'); + expect(bids[0].seatBidId).to.deep.equal('20dd72ed-930f-1000-e56f-07c37a793f30'); + expect(bids[0].cpm).to.equal(1.1645); + expect(bids[0].width).to.equal(320); + expect(bids[0].height).to.equal(50); + expect(bids[0].creativeId).to.deep.equal('88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301'); + expect(bids[0].meta.loggingPercentage).to.equal(100); + expect(bids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.inmobi.com'); + expect(bids[0].meta.prebid.meta.networkName).to.deep.equal('inmobi'); + }); + + if (FEATURES.VIDEO) { + it('return instream video response', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 320], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + plc: '123a1', + }, + }]; + const response = mockResponse('bidId', 2); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].currency).to.deep.equal('USD'); + expect(bids[0].requestId).to.deep.equal('bidId'); + expect(bids[0].seatBidId).to.deep.equal('20dd72ed-930f-1000-e56f-07c37a793f30'); + expect(bids[0].cpm).to.equal(1.1645); + expect(bids[0].playerWidth).to.equal(640); + expect(bids[0].playerHeight).to.equal(320); + expect(bids[0].creativeId).to.deep.equal('88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301'); + expect(bids[0].meta.loggingPercentage).to.equal(100); + expect(bids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.inmobi.com'); + expect(bids[0].meta.prebid.meta.networkName).to.deep.equal('inmobi'); + expect(bids[0].vastUrl).to.equal('https://et-l.w.inmobi.com/c.asm/'); + expect(bids[0].vastXml).to.equal('test-ad'); + }); + } + + if (FEATURES.VIDEO) { + it('return video outstream response', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mpeg'], + playerSize: [640, 320], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + plc: '123a1', + }, + }]; + const response = mockResponse('bidId', 2); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].currency).to.deep.equal('USD'); + expect(bids[0].requestId).to.deep.equal('bidId'); + expect(bids[0].seatBidId).to.deep.equal('20dd72ed-930f-1000-e56f-07c37a793f30'); + expect(bids[0].cpm).to.equal(1.1645); + expect(bids[0].playerWidth).to.equal(640); + expect(bids[0].playerHeight).to.equal(320); + expect(bids[0].creativeId).to.deep.equal('88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301'); + expect(bids[0].meta.loggingPercentage).to.equal(100); + expect(bids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.inmobi.com'); + expect(bids[0].meta.prebid.meta.networkName).to.deep.equal('inmobi'); + expect(bids[0].vastUrl).to.equal('https://et-l.w.inmobi.com/c.asm/'); + expect(bids[0].vastXml).to.equal('test-ad'); + }); + } + + if (FEATURES.VIDEO) { + it('bid response when video wins among two ad units', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 320], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + plc: '123', + }, + }, { + adUnitCode: 'impId', + bidId: 'bidId2', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123', + } + }]; + const response = mockResponse('bidId', 2); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].currency).to.deep.equal('USD'); + expect(bids[0].requestId).to.deep.equal('bidId'); + expect(bids[0].seatBidId).to.deep.equal('20dd72ed-930f-1000-e56f-07c37a793f30'); + expect(bids[0].cpm).to.equal(1.1645); + expect(bids[0].playerWidth).to.equal(640); + expect(bids[0].playerHeight).to.equal(320); + expect(bids[0].creativeId).to.deep.equal('88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301'); + expect(bids[0].meta.loggingPercentage).to.equal(100); + expect(bids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.inmobi.com'); + expect(bids[0].meta.prebid.meta.networkName).to.deep.equal('inmobi'); + expect(bids[0].vastUrl).to.equal('https://et-l.w.inmobi.com/c.asm/'); + expect(bids[0].vastXml).to.equal('test-ad'); + }); + } + + if (FEATURES.NATIVE) { + it('should correctly parse a native bid response', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + params: { + plc: '123', + }, + native: true, + bidder: 'inmobi', + mediaTypes: { + native: { + image: { + required: true, + sizes: [120, 60], + sendId: true, + sendTargetingKeys: false + } + } + } + }]; + const response = mockResponseNative('bidId', 4); + const expectedAdmNativeOrtb = JSON.parse(response.seatbid[0].bid[0].adm).native; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(NATIVE); + // testing + expect(bids[0].requestId).to.deep.equal('bidId'); + expect(bids[0].seatBidId).to.deep.equal('20dd72ed-930f-1000-e56f-07c37a793f30') + expect(bids[0].cpm).to.deep.equal(1.1645); + expect(bids[0].currency).to.deep.equal('USD'); + expect(bids[0].width).to.deep.equal(320); + expect(bids[0].height).to.deep.equal(50); + expect(bids[0].ad).to.equal(undefined); + expect(bids[0].native.ortb).not.to.be.null; + expect(bids[0].native.ortb).to.deep.equal(expectedAdmNativeOrtb); + expect(bids[0].creativeId).to.deep.equal('88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301'); + expect(bids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.inmobi.com'); + expect(bids[0].meta.prebid.meta.networkName).to.deep.equal('inmobi'); + }); + } + + if (FEATURES.NATIVE) { + it('should correctly parse a native bid response when there are two ad units', async function () { + const bidRequests = [{ + adUnitCode: 'impId', + bidId: 'bidId', + params: { + plc: '123', + }, + native: true, + bidder: 'inmobi', + mediaTypes: { + native: { + image: { + required: true, + sizes: [120, 60], + sendId: true, + sendTargetingKeys: false + } + } + } + }, + { + adUnitCode: 'impId', + bidId: 'bidId2', + mediaTypes: { + banner: { + sizes: [[320, 50]] + } + }, + params: { + plc: '123', + } + }]; + const response = mockResponseNative('bidId', 4); + const expectedAdmNativeOrtb = JSON.parse(response.seatbid[0].bid[0].adm).native; + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse({body: response}, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].mediaType).to.equal(NATIVE); + // testing + expect(bids[0].requestId).to.deep.equal('bidId'); + expect(bids[0].seatBidId).to.deep.equal('20dd72ed-930f-1000-e56f-07c37a793f30') + expect(bids[0].cpm).to.deep.equal(1.1645); + expect(bids[0].currency).to.deep.equal('USD'); + expect(bids[0].width).to.deep.equal(320); + expect(bids[0].height).to.deep.equal(50); + expect(bids[0].ad).to.equal(undefined); + expect(bids[0].native.ortb).not.to.be.null; + expect(bids[0].native.ortb).to.deep.equal(expectedAdmNativeOrtb); + expect(bids[0].creativeId).to.deep.equal('88b37efcd5f34d368e60317c706942a4ef6f6976eb394046958fd0e8a7b75301'); + expect(bids[0].meta.advertiserDomains[0]).to.deep.equal('advertiserDomain.sandbox.inmobi.com'); + expect(bids[0].meta.prebid.meta.networkName).to.deep.equal('inmobi'); + }); + } + }); +}); diff --git a/test/spec/modules/insticatorBidAdapter_spec.js b/test/spec/modules/insticatorBidAdapter_spec.js index 5e41cd6d7aa..d6daceddd6e 100644 --- a/test/spec/modules/insticatorBidAdapter_spec.js +++ b/test/spec/modules/insticatorBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec, storage } from '../../../modules/insticatorBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js' +import { getWinDimensions } from '../../../src/utils.js'; const USER_ID_KEY = 'hb_insticator_uid'; const USER_ID_DUMMY_VALUE = '74f78609-a92d-4cf1-869f-1b244bbfb5d2'; @@ -175,25 +176,6 @@ describe('InsticatorBidAdapter', function () { })).to.be.true; }) - it('should return false if video placement is not a number', () => { - expect(spec.isBidRequestValid({ - ...bidRequest, - ...{ - mediaTypes: { - video: { - mimes: [ - 'video/mp4', - 'video/mpeg', - ], - w: 250, - h: 300, - placement: 'NaN', - }, - } - } - })).to.be.false; - }); - it('should return false if video plcmt is not a number', () => { expect(spec.isBidRequestValid({ ...bidRequest, @@ -224,7 +206,7 @@ describe('InsticatorBidAdapter', function () { 'video/mpeg', ], playerSize: [250, 300], - placement: 1, + plcmt: 1, }, } } @@ -293,7 +275,7 @@ describe('InsticatorBidAdapter', function () { 'video/mpeg', ], playerSize: [250, 300], - placement: 1, + plcmt: 1, }, } }, @@ -306,7 +288,7 @@ describe('InsticatorBidAdapter', function () { 'video/x-flv', 'video/webm', ], - placement: 2, + plcmt: 2, }, } })).to.be.true; @@ -406,9 +388,9 @@ describe('InsticatorBidAdapter', function () { expect(data.site.page).not.to.be.empty; expect(data.site.ref).to.equal(bidderRequest.refererInfo.ref); expect(data.device).to.be.an('object'); - expect(data.device.w).to.equal(window.innerWidth); - expect(data.device.h).to.equal(window.innerHeight); - expect(data.device.js).to.equal(true); + expect(data.device.w).to.equal(getWinDimensions().innerWidth); + expect(data.device.h).to.equal(getWinDimensions().innerHeight); + expect(data.device.js).to.equal(1); expect(data.device.ext).to.be.an('object'); expect(data.device.ext.localStorage).to.equal(true); expect(data.device.ext.cookies).to.equal(false); @@ -458,6 +440,13 @@ describe('InsticatorBidAdapter', function () { insticator: { adUnitId: bidRequest.params.adUnitId, }, + prebid: { + bidder: { + insticator: { + adUnitId: bidRequest.params.adUnitId, + } + } + } } }]); expect(data.ext).to.be.an('object'); @@ -569,6 +558,100 @@ describe('InsticatorBidAdapter', function () { expect(data.imp[0].video.h).to.equal(480); }); + it('should have bidder bidfloor from the request', function () { + const tempBiddRequest = { + ...bidRequest, + params: { + ...bidRequest.params, + floor: 0.5, + }, + } + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.imp[0].bidfloor).to.equal(0.5); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + }); + + it('should have bidder bidfloorcur from the request', function () { + const expectedFloor = 1.5; + const currency = 'USD'; + const tempBiddRequest = { + ...bidRequest, + params: { + ...bidRequest.params, + floor: 0.5, + currency: 'USD', + }, + } + tempBiddRequest.getFloor = () => ({ floor: expectedFloor, currency }) + + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.imp[0].bidfloor).to.equal(1.5); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + }); + + it('should have 1 floor for banner 300x250 and 1.5 for 300x600', function () { + const tempBiddRequest = { + ...bidRequest, + params: { + ...bidRequest.params, + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + format: [{ w: 300, h: 250 }] + }, + }, + } + tempBiddRequest.getFloor = (params) => { + return { floor: params.size[1] === 250 ? 1 : 1.5, currency: 'USD' } + } + + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.imp[0].bidfloor).to.equal(1); + + tempBiddRequest.mediaTypes.banner.format = [ { w: 300, h: 600 }, + ]; + const request2 = spec.buildRequests([tempBiddRequest], bidderRequest); + const data2 = JSON.parse(request2[0].data); + expect(data2.imp[0].bidfloor).to.equal(1.5); + }); + + it('should have 4 floor for video 300x250 and 4.5 for 300x600', function () { + const tempBiddRequest = { + ...bidRequest, + params: { + ...bidRequest.params, + }, + mediaTypes: { + video: { + mimes: [ + 'video/mp4', + 'video/mpeg', + ], + w: 300, + h: 250, + placement: 2, + }, + }, + } + tempBiddRequest.getFloor = (params) => { + return { floor: params.size[1] === 250 ? 4 : 4.5, currency: 'USD' } + } + + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.imp[0].bidfloor).to.equal(4); + + tempBiddRequest.mediaTypes.video.w = 300; + tempBiddRequest.mediaTypes.video.h = 600; + const request2 = spec.buildRequests([tempBiddRequest], bidderRequest); + const data2 = JSON.parse(request2[0].data); + expect(data2.imp[0].bidfloor).to.equal(4.5); + }); + it('should have sites first party data if present in bidderRequest ortb2', function () { bidderRequest = { ...bidderRequest, @@ -691,6 +774,37 @@ describe('InsticatorBidAdapter', function () { expect(data.regs.ext).to.have.property('us_privacy'); expect(data.regs.ext).to.have.property('gppSid'); }); + + it('should return true if publisherId is absent', () => { + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + + it('should have publisher object with id in site object, if publisherId present in params', function () { + const tempBiddRequest = { + ...bidRequest, + } + tempBiddRequest.params = { + ...tempBiddRequest.params, + publisherId: '86dd03a1-053f-4e3e-90e7-389070a0c62c' + } + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.site.publisher).to.be.an('object'); + expect(data.site.publisher.id).to.equal(tempBiddRequest.params.publisherId) + }); + + it('should have publisher object should be empty, if publisherId is empty string', function () { + const tempBiddRequest = { + ...bidRequest, + } + tempBiddRequest.params = { + ...tempBiddRequest.params, + publisherId: '' + } + const requests = spec.buildRequests([tempBiddRequest], bidderRequest); + const data = JSON.parse(requests[0].data); + expect(data.site.publisher).to.not.an('object'); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..26a70ded14e --- /dev/null +++ b/test/spec/modules/intentIqAnalyticsAdapter_spec.js @@ -0,0 +1,370 @@ +import { expect } from 'chai'; +import iiqAnalyticsAnalyticsAdapter from 'modules/intentIqAnalyticsAdapter.js'; +import * as utils from 'src/utils.js'; +import { server } from 'test/mocks/xhr.js'; +import { config } from 'src/config.js'; +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'; +import {FIRST_PARTY_KEY, VERSION} from '../../../libraries/intentIqConstants/intentIqConstants.js'; +import * as detectBrowserUtils from '../../../libraries/intentIqUtils/detectBrowserUtils.js'; +import {getReferrer, appendVrrefAndFui} from '../../../libraries/intentIqUtils/getRefferer.js'; +import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler.js'; + +const partner = 10; +const defaultData = '{"pcid":"f961ffb1-a0e1-4696-a9d2-a21d815bd344", "group": "A"}'; +const version = VERSION; +const REPORT_ENDPOINT = 'https://reports.intentiq.com/report'; +const REPORT_ENDPOINT_GDPR = 'https://reports-gdpr.intentiq.com/report'; + +const storage = getStorageManager({ moduleType: 'analytics', moduleName: 'iiqAnalytics' }); + +const USERID_CONFIG = [ + { + 'name': 'intentIqId', + 'params': { + 'partner': partner, + 'unpack': null, + }, + 'storage': { + 'type': 'html5', + 'name': 'intentIqId', + 'expires': 60, + 'refreshInSeconds': 14400 + } + } +]; + +let wonRequest = { + 'bidderCode': 'pubmatic', + 'width': 728, + 'height': 90, + 'statusMessage': 'Bid available', + 'adId': '23caeb34c55da51', + 'requestId': '87615b45ca4973', + 'transactionId': '5e69fd76-8c86-496a-85ce-41ae55787a50', + 'auctionId': '0cbd3a43-ff45-47b8-b002-16d3946b23bf', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 5, + 'currency': 'USD', + 'ttl': 300, + 'referrer': '', + 'adapterCode': 'pubmatic', + 'originalCpm': 5, + 'originalCurrency': 'USD', + 'responseTimestamp': 1669644710345, + 'requestTimestamp': 1669644710109, + 'bidder': 'testbidder', + 'adUnitCode': 'addUnitCode', + 'timeToRespond': 236, + 'pbLg': '5.00', + 'pbMg': '5.00', + 'pbHg': '5.00', + 'pbAg': '5.00', + 'pbDg': '5.00', + 'pbCg': '', + 'size': '728x90', + 'status': 'rendered' +}; + +describe('IntentIQ tests all', function () { + let logErrorStub; + let getWindowSelfStub; + let getWindowTopStub; + let getWindowLocationStub; + let detectBrowserStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG); + sinon.stub(events, 'getEvents').returns([]); + iiqAnalyticsAnalyticsAdapter.enableAnalytics({ + provider: 'iiqAnalytics', + }); + iiqAnalyticsAnalyticsAdapter.initOptions = { + lsValueInitialized: false, + partner: null, + fpid: null, + userGroup: null, + currentGroup: null, + dataInLs: null, + eidl: null, + lsIdsInitialized: false, + manualWinReportEnabled: false, + domainName: null + }; + if (iiqAnalyticsAnalyticsAdapter.track.restore) { + iiqAnalyticsAnalyticsAdapter.track.restore(); + } + sinon.spy(iiqAnalyticsAnalyticsAdapter, 'track'); + }); + + afterEach(function () { + logErrorStub.restore(); + if (getWindowSelfStub) getWindowSelfStub.restore(); + if (getWindowTopStub) getWindowTopStub.restore(); + if (getWindowLocationStub) getWindowLocationStub.restore(); + if (detectBrowserStub) detectBrowserStub.restore(); + config.getConfig.restore(); + events.getEvents.restore(); + iiqAnalyticsAnalyticsAdapter.disableAnalytics(); + if (iiqAnalyticsAnalyticsAdapter.track.restore) { + iiqAnalyticsAnalyticsAdapter.track.restore(); + } + localStorage.clear(); + server.reset(); + }); + + it('IIQ Analytical Adapter bid win report', function () { + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({href: 'http://localhost:9876/'}); + const expectedVrref = encodeURIComponent(getWindowLocationStub().href); + + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + expect(request.url).to.contain(REPORT_ENDPOINT + '?pid=' + partner + '&mct=1'); + expect(request.url).to.contain(`&jsver=${version}`); + expect(request.url).to.contain(`&vrref=${expectedVrref}`); + expect(request.url).to.contain('&payload='); + expect(request.url).to.contain('iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344'); + }); + + it('should send report to report-gdpr address if gdpr is detected', function () { + const gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns({ gppString: '{"key1":"value1","key2":"value2"}' }); + const uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns('1NYN'); + const gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns({ consentString: 'gdprConsent' }); + + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + + expect(request.url).to.contain(REPORT_ENDPOINT_GDPR); + gppStub.restore(); + uspStub.restore(); + gdprStub.restore(); + }); + + it('should initialize with default configurations', function () { + expect(iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized).to.be.false; + }); + + it('should handle BID_WON event with group configuration from local storage', function () { + localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); + const expectedVrref = encodeURIComponent('http://localhost:9876/'); + + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + expect(request.url).to.contain('https://reports.intentiq.com/report?pid=' + partner + '&mct=1'); + expect(request.url).to.contain(`&jsver=${version}`); + expect(request.url).to.contain(`&vrref=${expectedVrref}`); + expect(request.url).to.contain('iiqid=testpcid'); + }); + + it('should handle BID_WON event with default group configuration', function () { + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + const defaultDataObj = JSON.parse(defaultData) + + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + const dataToSend = preparePayload(wonRequest); + const base64String = btoa(JSON.stringify(dataToSend)); + const payload = `[%22${base64String}%22]`; + const expectedUrl = appendVrrefAndFui(REPORT_ENDPOINT + + `?pid=${partner}&mct=1&iiqid=${defaultDataObj.pcid}&agid=${REPORTER_ID}&jsver=${version}&source=pbjs&payload=${payload}&uh=&gdpr=0`, iiqAnalyticsAnalyticsAdapter.initOptions.domainName + ); + expect(request.url).to.equal(expectedUrl); + expect(dataToSend.pcid).to.equal(defaultDataObj.pcid) + }); + + it('should send CMP data in report if available', function () { + const uspData = '1NYN'; + const gppData = { gppString: '{"key1":"value1","key2":"value2"}' }; + const gdprData = { consentString: 'gdprConsent' }; + + const gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(gppData); + const uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns(uspData); + const gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns(gdprData); + + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + + expect(request.url).to.contain(`&us_privacy=${encodeURIComponent(uspData)}`); + expect(request.url).to.contain(`&gpp=${encodeURIComponent(gppData.gppString)}`); + expect(request.url).to.contain(`&gdpr_consent=${encodeURIComponent(gdprData.consentString)}`); + expect(request.url).to.contain(`&gdpr=1`); + gppStub.restore(); + uspStub.restore(); + gdprStub.restore(); + }); + + it('should not send request if manualWinReportEnabled is true', function () { + iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = true; + events.emit(EVENTS.BID_WON, wonRequest); + 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); + expect(iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs).to.equal('testpcid'); + expect(iiqAnalyticsAnalyticsAdapter.initOptions.eidl).to.equal(10); + expect(iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup).to.equal('A'); + }); + + 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); + 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; + 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); + }); + + it('should return window.location.href when window.self === window.top', function () { + // Stub helper functions + getWindowSelfStub = sinon.stub(utils, 'getWindowSelf').returns(window); + getWindowTopStub = sinon.stub(utils, 'getWindowTop').returns(window); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + + const referrer = getReferrer(); + expect(referrer).to.equal(encodeURIComponent('http://localhost:9876/')); + }); + + it('should return window.top.location.href when window.self !== window.top and access is successful', function () { + // Stub helper functions to simulate iframe + getWindowSelfStub = sinon.stub(utils, 'getWindowSelf').returns({}); + getWindowTopStub = sinon.stub(utils, 'getWindowTop').returns({ location: { href: 'http://example.com/' } }); + + const referrer = getReferrer(); + + expect(referrer).to.equal(encodeURIComponent('http://example.com/')); + }); + + it('should return an empty string and log an error when accessing window.top.location.href throws an error', function () { + // Stub helper functions to simulate error + getWindowSelfStub = sinon.stub(utils, 'getWindowSelf').returns({}); + getWindowTopStub = sinon.stub(utils, 'getWindowTop').throws(new Error('Access denied')); + + const referrer = getReferrer(); + expect(referrer).to.equal(''); + expect(logErrorStub.calledOnce).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.contain('Error accessing location: Error: Access denied'); + }); + + it('should not send request if the browser is in blacklist (chrome)', function () { + const USERID_CONFIG_BROWSER = [...USERID_CONFIG]; + USERID_CONFIG_BROWSER[0].params.browserBlackList = 'ChrOmE'; + + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG_BROWSER); + detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('chrome'); + + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.equal(0); + }); + + it('should send request if the browser is not in blacklist (safari)', function () { + const USERID_CONFIG_BROWSER = [...USERID_CONFIG]; + USERID_CONFIG_BROWSER[0].params.browserBlackList = 'chrome,firefox'; + + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG_BROWSER); + detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('safari'); + + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + expect(request.url).to.contain(`https://reports.intentiq.com/report?pid=${partner}&mct=1`); + expect(request.url).to.contain(`&jsver=${version}`); + expect(request.url).to.contain(`&vrref=${encodeURIComponent('http://localhost:9876/')}`); + expect(request.url).to.contain('&payload='); + expect(request.url).to.contain('iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344'); + }); + + const testCasesVrref = [ + { + description: 'domainName matches window.top.location.href', + getWindowSelf: {}, + getWindowTop: { location: { href: 'http://example.com/page' } }, + getWindowLocation: { href: 'http://example.com/page' }, + domainName: 'example.com', + expectedVrref: encodeURIComponent('http://example.com/page'), + shouldContainFui: false + }, + { + description: 'domainName does not match window.top.location.href', + getWindowSelf: {}, + getWindowTop: { location: { href: 'http://anotherdomain.com/page' } }, + getWindowLocation: { href: 'http://anotherdomain.com/page' }, + domainName: 'example.com', + expectedVrref: encodeURIComponent('example.com'), + shouldContainFui: false + }, + { + description: 'domainName is missing, only fui=1 is returned', + getWindowSelf: {}, + getWindowTop: { location: { href: '' } }, + getWindowLocation: { href: '' }, + domainName: null, + expectedVrref: '', + shouldContainFui: true + }, + { + description: 'domainName is missing', + getWindowSelf: {}, + getWindowTop: { location: { href: 'http://example.com/page' } }, + getWindowLocation: { href: 'http://example.com/page' }, + domainName: null, + expectedVrref: encodeURIComponent('http://example.com/page'), + shouldContainFui: false + }, + ]; + + testCasesVrref.forEach(({ description, getWindowSelf, getWindowTop, getWindowLocation, domainName, expectedVrref, shouldContainFui }) => { + it(`should append correct vrref when ${description}`, function () { + getWindowSelfStub = sinon.stub(utils, 'getWindowSelf').returns(getWindowSelf); + getWindowTopStub = sinon.stub(utils, 'getWindowTop').returns(getWindowTop); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns(getWindowLocation); + + const url = 'https://reports.intentiq.com/report?pid=10'; + const modifiedUrl = appendVrrefAndFui(url, domainName); + const urlObj = new URL(modifiedUrl); + + const vrref = encodeURIComponent(urlObj.searchParams.get('vrref') || ''); + const fui = urlObj.searchParams.get('fui'); + + expect(vrref).to.equal(expectedVrref); + expect(urlObj.searchParams.has('fui')).to.equal(shouldContainFui); + if (shouldContainFui) { + expect(fui).to.equal('1'); + } + }); + }); +}); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index ef174af416b..d4220710c1f 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -1,31 +1,82 @@ import { expect } from 'chai'; -import { intentIqIdSubmodule, storage } from 'modules/intentIqIdSystem.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; +import { intentIqIdSubmodule, decryptData, handleClientHints, firstPartyData as moduleFPD } from '../../../modules/intentIqIdSystem'; +import {storage, readData} from '../../../libraries/intentIqUtils/storageUtils.js'; +import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler'; +import { clearAllCookies } from '../../helpers/cookies'; +import { detectBrowserFromUserAgent, detectBrowserFromUserAgentData } from '../../../libraries/intentIqUtils/detectBrowserUtils'; +import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, NOT_YET_DEFINED, WITH_IIQ} from '../../../libraries/intentIqConstants/intentIqConstants.js'; const partner = 10; const pai = '11'; const pcid = '12'; -const enableCookieStorage = true; const defaultConfigParams = { params: { partner: partner } }; const paiConfigParams = { params: { partner: partner, pai: pai } }; const pcidConfigParams = { params: { partner: partner, pcid: pcid } }; -const enableCookieConfigParams = { params: { partner: partner, enableCookieStorage: enableCookieStorage } }; -const allConfigParams = { params: { partner: partner, pai: pai, pcid: pcid, enableCookieStorage: enableCookieStorage } }; +const allConfigParams = { params: { partner: partner, pai: pai, pcid: pcid } }; const responseHeader = { 'Content-Type': 'application/json' } +export const testClientHints = { + architecture: 'x86', + bitness: '64', + brands: [ + { + brand: 'Not(A:Brand', + version: '24' + }, + { + brand: 'Chromium', + version: '122' + } + ], + fullVersionList: [ + { + brand: 'Not(A:Brand', + version: '24.0.0.0' + }, + { + brand: 'Chromium', + version: '122.0.6261.128' + } + ], + mobile: false, + model: '', + platform: 'Linux', + platformVersion: '6.5.0', + wow64: false +}; + +const mockGAM = () => { + const targetingObject = {}; + return { + cmd: [], + pubads: () => ({ + setTargeting: (key, value) => { + targetingObject[key] = value; + }, + getTargeting: (key) => { + return [targetingObject[key]]; + }, + getTargetingKeys: () => { + return Object.keys(targetingObject); + } + }) + }; +}; + describe('IntentIQ tests', function () { let logErrorStub; let testLSValue = { - 'date': 1651945280759, + 'date': Date.now(), 'cttl': 2000, 'rrtt': 123 } let testLSValueWithData = { - 'date': 1651945280759, + 'date': Date.now(), 'cttl': 9999999999999, 'rrtt': 123, - 'data': 'previousTestData' + 'data': 'U2FsdGVkX185JJuQ2Zk0JLGjpgEbqxNy0Yl2qMtj9PqA5Q3IkNQYyTyFyTOkJi9Nf7E43PZQvIUgiUY/A9QxKYmy1LHX9LmZMKlLOcY1Je13Kr1EN7HRF8nIIWXo2jRgS5n0Nmty5995x3YMjLw+aRweoEtcrMC6p4wOdJnxfrOhdg0d/R7b8C+IN85rDLfNXANL1ezX8zwh4rj9XpMmWw==' } let testResponseWithValues = { 'abPercentage': 90, @@ -39,11 +90,17 @@ describe('IntentIQ tests', function () { } beforeEach(function () { + localStorage.clear(); + const expiredDate = new Date(0).toUTCString(); + storage.setCookie(FIRST_PARTY_KEY, '', expiredDate, 'Lax'); + storage.setCookie(FIRST_PARTY_KEY + '_' + partner, '', expiredDate, 'Lax'); logErrorStub = sinon.stub(utils, 'logError'); }); afterEach(function () { logErrorStub.restore(); + clearAllCookies(); + localStorage.clear(); }); it('should log an error if no configParams were passed when getId', function () { @@ -64,7 +121,7 @@ describe('IntentIQ tests', function () { expect(submodule).to.be.undefined; }); - it('should not save data in cookie if enableCookieStorage configParam not set', function () { + it('should not save data in cookie if relevant type not set', function () { let callBackSpy = sinon.spy(); let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); @@ -79,9 +136,9 @@ describe('IntentIQ tests', function () { expect(storage.getCookie('_iiq_fdata_' + partner)).to.equal(null); }); - it('should save data in cookie if enableCookieStorage configParam set to true', function () { + it('should save data in cookie if storage type is "cookie"', function () { let callBackSpy = sinon.spy(); - let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; + let submoduleCallback = intentIqIdSubmodule.getId({ ...allConfigParams, enabledStorageTypes: ['cookie'] }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); @@ -91,9 +148,10 @@ describe('IntentIQ tests', function () { JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: true }) ); expect(callBackSpy.calledOnce).to.be.true; - const cookieValue = storage.getCookie('_iiq_fdata_' + partner) - expect(cookieValue).to.not.equal(null) - expect(JSON.parse(cookieValue).data).to.be.equal('test_personid'); + const cookieValue = storage.getCookie('_iiq_fdata_' + partner); + expect(cookieValue).to.not.equal(null); + const decryptedData = JSON.parse(decryptData(JSON.parse(cookieValue).data)); + expect(decryptedData).to.deep.equal({eids: ['test_personid']}); }); it('should call the IntentIQ endpoint with only partner', function () { @@ -111,8 +169,6 @@ describe('IntentIQ tests', function () { }); it('should ignore INVALID_ID and invalid responses in decode', function () { - // let resp = JSON.stringify({'RESULT': 'NA'}); - // expect(intentIqIdSubmodule.decode(resp)).to.equal(undefined); expect(intentIqIdSubmodule.decode('INVALID_ID')).to.equal(undefined); expect(intentIqIdSubmodule.decode('')).to.equal(undefined); expect(intentIqIdSubmodule.decode(undefined)).to.equal(undefined); @@ -160,6 +216,68 @@ describe('IntentIQ tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); + it('should set GAM targeting to U initially and update to A after server response', function () { + let callBackSpy = sinon.spy(); + let mockGamObject = mockGAM(); + let expectedGamParameterName = 'intent_iq_group'; + + const originalPubads = mockGamObject.pubads; + let setTargetingSpy = sinon.spy(); + mockGamObject.pubads = function () { + const obj = { ...originalPubads.apply(this, arguments) }; + const originalSetTargeting = obj.setTargeting; + obj.setTargeting = function (...args) { + setTargetingSpy(...args); + return originalSetTargeting.apply(this, args); + }; + return obj; + }; + + defaultConfigParams.params.gamObjectReference = mockGamObject; + + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + + submoduleCallback(callBackSpy); + let request = server.requests[0]; + + mockGamObject.cmd.forEach(cb => cb()); + mockGamObject.cmd = [] + + let groupBeforeResponse = mockGamObject.pubads().getTargeting(expectedGamParameterName); + + request.respond( + 200, + responseHeader, + JSON.stringify({ group: 'A', tc: 20 }) + ); + + mockGamObject.cmd.forEach(item => item()); + + let groupAfterResponse = mockGamObject.pubads().getTargeting(expectedGamParameterName); + + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39'); + expect(groupBeforeResponse).to.deep.equal([NOT_YET_DEFINED]); + expect(groupAfterResponse).to.deep.equal([WITH_IIQ]); + + expect(setTargetingSpy.calledTwice).to.be.true; + }); + + it('should use the provided gamParameterName from configParams', function () { + let callBackSpy = sinon.spy(); + let mockGamObject = mockGAM(); + let customParamName = 'custom_gam_param'; + + defaultConfigParams.params.gamObjectReference = mockGamObject; + defaultConfigParams.params.gamParameterName = customParamName; + + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + mockGamObject.cmd.forEach(cb => cb()); + let targetingKeys = mockGamObject.pubads().getTargetingKeys(); + + expect(targetingKeys).to.include(customParamName); + }); + it('should not throw Uncaught TypeError when IntentIQ endpoint returns empty response', function () { let callBackSpy = sinon.spy(); let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; @@ -199,7 +317,7 @@ describe('IntentIQ tests', function () { JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: true }) ); expect(callBackSpy.calledOnce).to.be.true; - expect(callBackSpy.args[0][0]).to.be.eq('test_personid'); + expect(callBackSpy.args[0][0]).to.deep.equal(['test_personid']); }); it('dont save result if ls=false', function () { @@ -214,22 +332,7 @@ describe('IntentIQ tests', function () { JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: false }) ); expect(callBackSpy.calledOnce).to.be.true; - expect(callBackSpy.args[0][0]).to.be.undefined; - }); - - it('save result as INVALID_ID on empty data and ls=true ', function () { - let callBackSpy = sinon.spy(); - let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); - request.respond( - 200, - responseHeader, - JSON.stringify({ pid: 'test_pid', data: '', ls: true }) - ); - expect(callBackSpy.calledOnce).to.be.true; - expect(callBackSpy.args[0][0]).to.be.eq('INVALID_ID'); + expect(callBackSpy.args[0][0]).to.deep.equal({eids: []}); }); it('send addition parameters if were found in localstorage', function () { @@ -248,16 +351,255 @@ describe('IntentIQ tests', function () { JSON.stringify(testResponseWithValues) ); expect(callBackSpy.calledOnce).to.be.true; - expect(callBackSpy.args[0][0]).to.be.eq(testResponseWithValues.data); + expect(callBackSpy.args[0][0]).to.deep.equal([testResponseWithValues.data]); }); it('return data stored in local storage ', function () { - localStorage.setItem('_iiq_fdata_' + partner, JSON.stringify(testLSValueWithData)) + localStorage.setItem('_iiq_fdata_' + partner, JSON.stringify(testLSValueWithData)); + let returnedValue = intentIqIdSubmodule.getId(allConfigParams); + expect(returnedValue.id).to.deep.equal(JSON.parse(decryptData(testLSValueWithData.data)).eids); + }); + + it('should handle browser blacklisting', function () { + let configParamsWithBlacklist = { + params: { partner: partner, browserBlackList: 'chrome' } + }; + sinon.stub(navigator, 'userAgent').value('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'); + let submoduleCallback = intentIqIdSubmodule.getId(configParamsWithBlacklist); + expect(logErrorStub.calledOnce).to.be.true; + expect(submoduleCallback).to.be.undefined; + }); + + it('should handle invalid JSON in readData', function () { + localStorage.setItem('_iiq_fdata_' + partner, 'invalid_json'); let callBackSpy = sinon.spy(); - let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - expect(server.requests.length).to.be.equal(0); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&iiqidtype=2&iiqpcid='); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); expect(callBackSpy.calledOnce).to.be.true; - expect(callBackSpy.args[0][0]).to.be.equal(testLSValueWithData.data); + expect(logErrorStub.called).to.be.true; + }); + + describe('detectBrowserFromUserAgent', function () { + it('should detect Chrome browser', function () { + const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'; + const result = detectBrowserFromUserAgent(userAgent); + expect(result).to.equal('chrome'); + }); + + it('should detect Safari browser', function () { + const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15'; + const result = detectBrowserFromUserAgent(userAgent); + expect(result).to.equal('safari'); + }); + + it('should detect Firefox browser', function () { + const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0'; + const result = detectBrowserFromUserAgent(userAgent); + expect(result).to.equal('firefox'); + }); + }); + + describe('detectBrowserFromUserAgentData', function () { + it('should detect Microsoft Edge browser', function () { + const userAgentData = { + brands: [ + { brand: 'Microsoft Edge', version: '91' }, + { brand: 'Chromium', version: '91' } + ] + }; + const result = detectBrowserFromUserAgentData(userAgentData); + expect(result).to.equal('edge'); + }); + + it('should detect Chrome browser', function () { + const userAgentData = { + brands: [ + { brand: 'Google Chrome', version: '91' }, + { brand: 'Chromium', version: '91' } + ] + }; + const result = detectBrowserFromUserAgentData(userAgentData); + expect(result).to.equal('chrome'); + }); + + it('should return unknown for unrecognized user agent data', function () { + const userAgentData = { + brands: [ + { brand: 'Unknown Browser', version: '1.0' } + ] + }; + const result = detectBrowserFromUserAgentData(userAgentData); + expect(result).to.equal('unknown'); + }); + }); + + describe('IntentIQ consent management within getId', function () { + let uspDataHandlerStub; + let gppDataHandlerStub; + let gdprDataHandlerStub; + let uspData; + let gppData; + let gdprData; + + function mockConsentHandlers(usp, gpp, gdpr) { + uspDataHandlerStub.returns(usp); + gppDataHandlerStub.returns(gpp); + gdprDataHandlerStub.returns(gdpr); + } + + beforeEach(function () { + localStorage.clear(); + uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + gppDataHandlerStub = sinon.stub(gppDataHandler, 'getConsentData'); + gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + // Initialize data here so it can be reused in all tests + uspData = '1NYN'; + gppData = { gppString: '{"key1":"value1","key2":"value2"}' }; + gdprData = { gdprApplies: true, consentString: 'gdprConsent' }; + }); + + afterEach(function () { + uspDataHandlerStub.restore(); + gppDataHandlerStub.restore(); + gdprDataHandlerStub.restore(); + }); + + it('should set isOptOut to true for new users if GDPR is detected and update it upon receiving a server response', function () { + localStorage.clear(); + mockConsentHandlers(uspData, gppData, gdprData); + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; + submoduleCallback(callBackSpy); + + let lsBeforeReq = JSON.parse(localStorage.getItem(FIRST_PARTY_KEY)); + + let request = server.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ isOptedOut: false }) + ); + + let updatedFirstPartyData = JSON.parse(localStorage.getItem(FIRST_PARTY_KEY)); + + expect(lsBeforeReq).to.not.be.null; + expect(lsBeforeReq.isOptedOut).to.be.true; + expect(callBackSpy.calledOnce).to.be.true; + expect(updatedFirstPartyData).to.not.be.undefined; + expect(updatedFirstPartyData.isOptedOut).to.equal(false); + }); + + it('should save cmpData parameters in LS data and used it request if uspData, gppData, gdprData exists', function () { + mockConsentHandlers(uspData, gppData, gdprData); + + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + const data = {eids: {key1: 'value1', key2: 'value2'}} + + submoduleCallback(callBackSpy); + let request = server.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ ...data, isOptedOut: false }) + ); + + expect(request.url).to.contain(`&gpp=${encodeURIComponent(gppData.gppString)}`); + expect(request.url).to.contain(`&us_privacy=${encodeURIComponent(uspData)}`); + expect(request.url).to.contain(`&gdpr_consent=${encodeURIComponent(gdprData.consentString)}`); + + const lsFirstPartyData = JSON.parse(localStorage.getItem(FIRST_PARTY_KEY)); + expect(lsFirstPartyData.uspString).to.equal(uspData); + expect(lsFirstPartyData.gppString).to.equal(gppData.gppString); + expect(lsFirstPartyData.gdprString).to.equal(gdprData.consentString); + + expect(moduleFPD.uspString).to.equal(uspData); + expect(moduleFPD.gppString).to.equal(gppData.gppString); + expect(moduleFPD.gdprString).to.equal(gdprData.consentString); + }); + + it('should clear localStorage, update runtimeEids and trigger callback with empty data if isOptedOut is true in response', function () { + // Save some data to localStorage for FPD and CLIENT_HINTS + const FIRST_PARTY_DATA_KEY = FIRST_PARTY_KEY + '_' + partner; + localStorage.setItem(FIRST_PARTY_DATA_KEY, JSON.stringify({terminationCause: 35, some_key: 'someValue'})); + localStorage.setItem(CLIENT_HINTS_KEY, JSON.stringify({ hint: 'someClientHintData' })); + + mockConsentHandlers(uspData, gppData, gdprData); + + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + + submoduleCallback(callBackSpy); + + let request = server.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({isOptedOut: true}) + ); + + // Check that the URL contains the expected consent data + expect(request.url).to.contain(`&gpp=${encodeURIComponent(gppData.gppString)}`); + expect(request.url).to.contain(`&us_privacy=${encodeURIComponent(uspData)}`); + expect(request.url).to.contain(`&gdpr_consent=${encodeURIComponent(gdprData.consentString)}`); + + const lsFirstPartyData = JSON.parse(localStorage.getItem(FIRST_PARTY_KEY)); + + // Ensure that keys are removed if isOptedOut is true + expect(localStorage.getItem(FIRST_PARTY_DATA_KEY)).to.be.null; + expect(localStorage.getItem(CLIENT_HINTS_KEY)).to.be.null; + + expect(lsFirstPartyData.isOptedOut).to.equal(true); + expect(callBackSpy.calledOnce).to.be.true; + // Get the parameter with which the callback was called + const callbackArgument = callBackSpy.args[0][0]; // The first argument from the callback call (runtimeEids) + expect(callbackArgument).to.deep.equal({ eids: [] }); // Ensure that runtimeEids was updated to { eids: [] } + }); + + it('should make request to correct address api-gdpr.intentiq.com if gdpr is detected', function() { + const ENDPOINT_GDPR = 'https://api-gdpr.intentiq.com'; + mockConsentHandlers(uspData, gppData, gdprData); + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId({...defaultConfigParams}).callback; + + submoduleCallback(callBackSpy); + let request = server.requests[0]; + + expect(request.url).to.contain(ENDPOINT_GDPR); + }); + }); + + it('should get and save client hints to storage', async () => { + localStorage.clear(); + Object.defineProperty(navigator, 'userAgentData', { + value: { getHighEntropyValues: async () => testClientHints }, + configurable: true + }); + await intentIqIdSubmodule.getId(defaultConfigParams); + + const savedClientHints = readData(CLIENT_HINTS_KEY, ['html5'], storage); + const expectedClientHints = handleClientHints(testClientHints); + expect(savedClientHints).to.equal(expectedClientHints); + }); + + it('should run callback from params', async () => { + let wasCallbackCalled = false + const callbackConfigParams = { params: { partner: partner, + pai: pai, + pcid: pcid, + browserBlackList: 'Chrome', + callback: () => { + wasCallbackCalled = true + } } }; + + await intentIqIdSubmodule.getId(callbackConfigParams); + expect(wasCallbackCalled).to.equal(true); }); }); diff --git a/test/spec/modules/invibesBidAdapter_spec.js b/test/spec/modules/invibesBidAdapter_spec.js index 056255c7738..e12376b2a37 100644 --- a/test/spec/modules/invibesBidAdapter_spec.js +++ b/test/spec/modules/invibesBidAdapter_spec.js @@ -1,6 +1,6 @@ import {expect} from 'chai'; import { config } from 'src/config.js'; -import {spec, resetInvibes, stubDomainOptions, readGdprConsent} from 'modules/invibesBidAdapter.js'; +import {spec, resetInvibes, stubDomainOptions, readGdprConsent, storage} from 'modules/invibesBidAdapter.js'; describe('invibesBidAdapter:', function () { const BIDDER_CODE = 'invibes'; @@ -187,6 +187,8 @@ describe('invibesBidAdapter:', function () { }; } + let sandbox; + beforeEach(function () { resetInvibes(); $$PREBID_GLOBAL$$.bidderSettings = { @@ -196,11 +198,13 @@ describe('invibesBidAdapter:', function () { }; document.cookie = ''; this.cStub1 = sinon.stub(console, 'info'); + sandbox = sinon.sandbox.create(); }); afterEach(function () { $$PREBID_GLOBAL$$.bidderSettings = {}; this.cStub1.restore(); + sandbox.restore(); }); describe('isBidRequestValid:', function () { @@ -528,6 +532,8 @@ describe('invibesBidAdapter:', function () { }); it('sends undefined lid when no cookie', function () { + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'getCookie').returns(null); let request = spec.buildRequests(bidRequests, bidderRequestWithPageInfo); expect(request.data.lId).to.be.undefined; }); @@ -560,6 +566,50 @@ describe('invibesBidAdapter:', function () { expect(request.data.lId).to.exist; }); + it('does not send handIid when it doesnt exist in cookie', function () { + top.window.invibes.optIn = 1; + top.window.invibes.purposes = [true, false, false, false, false, false, false, false, false, false]; + sandbox.stub(storage, 'getCookie').returns(null) + let bidderRequest = { + gdprConsent: { + vendorData: { + vendorConsents: { + 436: true + } + } + }, + refererInfo: { + page: 'https://randomWeb.com?someFakePara=fakeValue&secondParam=secondValue' + } + }; + SetBidderAccess(); + + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.handIid).to.not.exist; + }); + + it('sends handIid when comes on cookie', function () { + top.window.invibes.optIn = 1; + top.window.invibes.purposes = [true, false, false, false, false, false, false, false, false, false]; + global.document.cookie = 'handIid=abcdefghijkk'; + let bidderRequest = { + gdprConsent: { + vendorData: { + vendorConsents: { + 436: true + } + } + }, + refererInfo: { + page: 'https://randomWeb.com?someFakePara=fakeValue&secondParam=secondValue' + } + }; + SetBidderAccess(); + + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.handIid).to.equal('abcdefghijkk'); + }); + it('should send purpose 1', function () { let bidderRequest = { gdprConsent: { @@ -1461,5 +1511,28 @@ describe('invibesBidAdapter:', function () { let response = spec.getUserSyncs({iframeEnabled: false}); expect(response).to.equal(undefined); }); + + it('uses uspConsent when no gdprConsent', function () { + let bidderRequest = { + uspConsent: '1YNY', + refererInfo: { + page: 'https://randomWeb.com?someFakePara=fakeValue&secondParam=secondValue' + } + }; + + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(top.window.invibes.optIn).to.equal(2); + expect(top.window.invibes.GdprModuleInstalled).to.be.false; + expect(top.window.invibes.UspModuleInstalled).to.be.true; + var index; + for (index = 0; index < top.window.invibes.purposes.length; ++index) { + expect(top.window.invibes.purposes[index]).to.be.true; + } + for (index = 0; index < top.window.invibes.legitimateInterests.length; ++index) { + expect(top.window.invibes.legitimateInterests[index]).to.be.true; + } + expect(request.data.tc).to.not.exist; + expect(request.data.uspc).to.equal(bidderRequest.uspConsent); + }); }); }); diff --git a/test/spec/modules/iqmBidAdapter_spec.js b/test/spec/modules/iqmBidAdapter_spec.js deleted file mode 100644 index 2f8b5811b2f..00000000000 --- a/test/spec/modules/iqmBidAdapter_spec.js +++ /dev/null @@ -1,414 +0,0 @@ -import { expect } from 'chai'; -import { newBidder } from 'src/adapters/bidderFactory.js'; -import * as bidderFactory from 'src/adapters/bidderFactory.js'; -import {spec} from 'modules/iqmBidAdapter'; - -const ENDPOINT = 'https://pbd.bids.iqm.com'; - -describe('iqmAdapter', 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 () { - let bid = - { - bidder: 'iqm', - params: { - publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', - placementId: 23451, - bidfloor: 0.50 - }, - - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - }; - - it('should return false when no bid', function () { - expect(spec.isBidRequestValid()).to.equal(false); - }); - - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - it('should return false when it is video and mimes and protcol are not present', function () { - const bid = { - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: 'a0aca162-e3d0-44db-a465-5c96a64fa5fb', - bidId: '2cbdc9b506be33', - bidRequestsCount: 1, - bidder: 'iqm', - bidderRequestId: '185c3a4c7f88ec', - bidderRequestsCount: 1, - bidderWinsCount: 0, - crumbs: {pubcid: 'f56a553d-370d-4cea-b31a-7214a3d8f8e1'}, - mediaTypes: { - video: { - context: 'instream', - playerSize: [ - [ - 640, - 480 - ] - ] - } - }, - params: { - publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', - placementId: 23451, - geo: { - country: 'USA' - }, - - bidfloor: 0.50, - video: { - placement: 2, - mimes: null, - protocols: null, - skipppable: true, - playback_method: ['auto_play_sound_off'] - } - }, - src: 'client', - transactionId: 'a57d06fd-cc6d-4a90-87af-c10727998f0b' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - it('should return false when required params are not found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { - placementId: 0, - publisherId: null - - }; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - }); - - describe('buildRequests', function () { - let validBidRequests = [ - {bidder: 'iqm', - params: { - publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', - placementId: 23451, - bidfloor: 0.5}, - crumbs: { - pubcid: 'a0f51f64-6d86-41d0-abaf-7ece71404d94'}, - ortb2Imp: {ext: {data: {'pbadslot': '/19968336/header-bid-tag-0'}}}, - mediaTypes: { - banner: { - sizes: [[300, 250]]}}, - adUnitCode: '/19968336/header-bid-tag-0', - transactionId: '56fe8d92-ff6e-4c34-90ad-2f743cd0eae8', - sizes: [[300, 250]], - bidId: '266d810da21904', - bidderRequestId: '13c05d264c7ffe', - auctionId: '565ab569-ab95-40d6-8b42-b9707a92062f', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0}]; - - let bidderRequest = { - bidderCode: 'iqm', - auctionId: '565ab569-ab95-40d6-8b42-b9707a92062f', - bidderRequestId: '13c05d264c7ffe', - bids: [{ - bidder: 'iqm', - params: {publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', placementId: 23451, bidfloor: 0.5}, - crumbs: {pubcid: 'a0f51f64-6d86-41d0-abaf-7ece71404d94'}, - ortb2Imp: {ext: {data: {'pbadslot': '/19968336/header-bid-tag-0'}}}, - mediaTypes: {banner: {sizes: [[300, 250]]}}, - adUnitCode: '/19968336/header-bid-tag-0', - transactionId: '56fe8d92-ff6e-4c34-90ad-2f743cd0eae8', - sizes: [[300, 250]], - bidId: '266d810da21904', - bidderRequestId: '13c05d264c7ffe', - auctionId: '565ab569-ab95-40d6-8b42-b9707a92062f', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - }], - auctionStart: 1615205942159, - timeout: 7000, - refererInfo: { - page: 'http://test.localhost:9999/integrationExamples/gpt/hello_world.html', - domain: 'test.localhost.com:9999', - ref: null, - reachedTop: true, - isAmp: false, - numIframes: 0, - stack: ['http://test.localhost:9999/integrationExamples/gpt/hello_world.html'], - canonicalUrl: null - }, - start: 1615205942162 - }; - - it('should parse out sizes', function () { - let temp = []; - const request = spec.buildRequests(validBidRequests, bidderRequest); - const payload = request[0].data; - - expect(payload.sizes).to.exist; - expect(payload.sizes[0]).to.deep.equal([300, 250]); - }); - - it('should populate the ad_types array on all requests', function () { - // const bidRequest = Object.assign({}, bidRequests[0]); - - const request = spec.buildRequests(validBidRequests, bidderRequest); - const payload = request[0].data; - - expect(payload.imp.mediatype).to.deep.equal('banner'); - }); - it('sends bid request to ENDPOINT via POST', function () { - const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request[0].url).to.equal(ENDPOINT); - expect(request[0].method).to.equal('POST'); - }); - it('should attach valid video params to the tag', function () { - let validBidRequests_video = [{ - bidder: 'iqm', - params: { - publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', - placementId: 23451, - bidfloor: 0.5, - video: { - placement: 2, - mimes: ['video/mp4'], - protocols: [2, 5], - skipppable: true, - playback_method: ['auto_play_sound_off'] - } - }, - crumbs: {pubcid: '09b8f065-9d1b-4a36-bd0c-ea22e2dad807'}, - ortb2Imp: {ext: {data: {'pbadslot': 'video1'}}}, - mediaTypes: {video: {playerSize: [[640, 480]], context: 'instream'}}, - adUnitCode: 'video1', - transactionId: '86795c66-acf9-4dd5-998f-6d5362aaa541', - sizes: [[640, 480]], - bidId: '28bfb7e2d12897', - bidderRequestId: '16e1ce8481bc6d', - auctionId: '3140a2ec-d567-4db0-9bbb-eb6fa20ccb71', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - }]; - let bidderRequest_video = { - bidderCode: 'iqm', - auctionId: '3140a2ec-d567-4db0-9bbb-eb6fa20ccb71', - bidderRequestId: '16e1ce8481bc6d', - bids: [{ - bidder: 'iqm', - params: { - publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', - placementId: 23451, - bidfloor: 0.5, - video: { - placement: 2, - mimes: ['video/mp4'], - protocols: [2, 5], - skipppable: true, - playback_method: ['auto_play_sound_off'] - } - }, - crumbs: {pubcid: '09b8f065-9d1b-4a36-bd0c-ea22e2dad807'}, - fpd: {context: {pbAdSlot: 'video1'}}, - mediaTypes: {video: {playerSize: [[640, 480]], context: 'instream'}}, - adUnitCode: 'video1', - transactionId: '86795c66-acf9-4dd5-998f-6d5362aaa541', - sizes: [[640, 480]], - bidId: '28bfb7e2d12897', - bidderRequestId: '16e1ce8481bc6d', - auctionId: '3140a2ec-d567-4db0-9bbb-eb6fa20ccb71', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - }], - auctionStart: 1615271191985, - timeout: 3000, - refererInfo: { - page: 'http://test.localhost:9999/integrationExamples/gpt/pbjs_video_adUnit.html', - domain: 'test.localhost.com:9999', - ref: null, - reachedTop: true, - isAmp: false, - numIframes: 0, - stack: ['http://test.localhost:9999/integrationExamples/gpt/pbjs_video_adUnit.html'], - canonicalUrl: null - }, - start: 1615271191988 - }; - const request = spec.buildRequests(validBidRequests_video, bidderRequest_video); - const payload = request[0].data; - expect(payload.imp.id).to.exist; - expect(payload.imp.displaymanager).to.exist; - expect(payload.imp.displaymanagerver).to.exist; - - expect(payload.imp.video).to.deep.equal({ - context: 'instream', - w: 640, - h: 480, - mimes: ['video/mp4'], - placement: 1, - protocols: [2, 5], - startdelay: 0 - }); - }); - - it('should add referer info to payload', function () { - // TODO: this is wrong on multiple levels - // The payload contains everything in `bidderRequest`; that is sometimes not even serializable - // this should not be testing the validity of internal Prebid structures - const request = spec.buildRequests(validBidRequests, bidderRequest); - const payload = request[0].data; - - expect(payload.bidderRequest.refererInfo).to.exist; - }); - }) - - describe('interpretResponse', function () { - let tempResult = {requestId: '2d9601dd8328f8', currency: 'USD', cpm: 4.5, netRevenue: true, creativeId: 'cr-121004', adUnitCode: 'div-gpt-ad-1460505748561-0', 'auctionId': '22a4f3d8-511f-46ba-91be-53b9949e4b48', mediaType: 'banner', ttl: 3000, ad: " ", width: 844, height: 617}; - let validBidRequests_temp = [ - {bidder: 'iqm', - params: { - publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', - placementId: 23451, - bidfloor: 0.5}, - crumbs: { - pubcid: 'a0f51f64-6d86-41d0-abaf-7ece71404d94'}, - ortb2Imp: {ext: {data: {'pbadslot': '/19968336/header-bid-tag-0'}}}, - mediaTypes: { - banner: { - sizes: [[300, 250]]}}, - adUnitCode: '/19968336/header-bid-tag-0', - transactionId: '56fe8d92-ff6e-4c34-90ad-2f743cd0eae8', - sizes: [[300, 250]], - bidId: '266d810da21904', - bidderRequestId: '13c05d264c7ffe', - auctionId: '565ab569-ab95-40d6-8b42-b9707a92062f', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0}]; - let bidderRequest = { - bidderCode: 'iqm', - auctionId: '565ab569-ab95-40d6-8b42-b9707a92062f', - bidderRequestId: '13c05d264c7ffe', - bids: [{ - bidder: 'iqm', - params: {publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', placementId: 23451, bidfloor: 0.5}, - crumbs: {pubcid: 'a0f51f64-6d86-41d0-abaf-7ece71404d94'}, - ortb2Imp: {ext: {data: {'pbadslot': '/19968336/header-bid-tag-0'}}}, - mediaTypes: {banner: {sizes: [[300, 250]]}}, - adUnitCode: '/19968336/header-bid-tag-0', - transactionId: '56fe8d92-ff6e-4c34-90ad-2f743cd0eae8', - sizes: [[300, 250]], - bidId: '266d810da21904', - bidderRequestId: '13c05d264c7ffe', - auctionId: '565ab569-ab95-40d6-8b42-b9707a92062f', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - }], - auctionStart: 1615205942159, - timeout: 7000, - refererInfo: { - page: 'http://test.localhost:9999/integrationExamples/gpt/hello_world.html', - domain: 'test.localhost.com:9999', - ref: null, - reachedTop: true, - isAmp: false, - numIframes: 0, - stack: ['http://test.localhost:9999/integrationExamples/gpt/hello_world.html'], - canonicalUrl: null - }, - start: 1615205942162 - }; - let response = { - - id: '5bdbab92aae961cfbdf7465d', - seatbid: [{bid: [{id: 'bid-5bdbab92aae961cfbdf7465d-5bdbab92aae961cfbdf74653', impid: '5bdbab92aae961cfbdf74653', price: 9.9, nurl: 'https://winn.stage.iqm.com/smaato?raw=w9XViV4dovBHrxujHhBj-l-uWB08CUOMW_oR-EUxZbaWLL0ENzcMlP3CJFEURN6FgRp_HdjAjxTYHR7uG4S6h6dl_vjU_YNABiPd607-iTqxOCl-2cKLo-hhQus4sMw01VIqyqrPmzOTHTwJm4vTjUIoWMPZbARgQvUnBzjRH9xeYS-Bv3kgAW9NSBfgBZeLyT3WJJ_3VKIE_Iurt8OjpA%3D%3D&req_id=5bdbab92aae961cfbdf7465d&ap=${AUCTION_PRICE}', adm: " ", adomain: ['click.iqm.com'], iurl: 'https://d3jme5si7t6llb.cloudfront.net/image/1/404/owVo6mc_1588902031079.png', cid: '169218', crid: 'cr-301435', attr: [], h: 250, w: 250}]}], - bidid: '5bdbab92aae961cfbdf7465d' - }; - - it('should get correct bid response', function () { - let expectedResponse = [ - {requestId: '49ad5f21156efd', currency: 'USD', cpm: 9.9, netRevenue: true, creativeId: 'cr-301435', adUnitCode: '/19968336/header-bid-tag-0', auctionId: '853cddf1-8d13-4482-bd88-f5ef927d5ab3', mediaType: 'banner', ttl: 3000, ad: " ", width: 250, height: 250} - ]; - let temprequest = spec.buildRequests(validBidRequests_temp, bidderRequest); - - let result = spec.interpretResponse({ body: response }, temprequest[0]); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); - }); - - let validBidRequests_temp_video = - [{bidder: 'iqm', params: {publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', placementId: 23451, bidfloor: 0.5, video: {placement: 2, mimes: ['video/mp4'], protocols: [2, 5], skipppable: true, playback_method: ['auto_play_sound_off']}}, crumbs: {pubcid: 'cd86c3ff-d630-40e6-83ab-420e9e800594'}, fpd: {context: {pbAdSlot: 'video1'}}, mediaTypes: {video: {playerSize: [[640, 480]], context: 'instream'}}, adUnitCode: 'video1', transactionId: '8335b266-7a41-45f9-86a2-92fdc7cf0cd9', sizes: [[640, 480]], bidId: '26274beff25455', bidderRequestId: '17c5d8c3168761', auctionId: '2c592dcf-7dfc-4823-8203-dd1ebab77fe0', src: 'client', bidRequestsCount: 1, bidderRequestsCount: 1, bidderWinsCount: 0}]; - let bidderRequest_video = { - bidderCode: 'iqm', - auctionId: '3140a2ec-d567-4db0-9bbb-eb6fa20ccb71', - bidderRequestId: '16e1ce8481bc6d', - bids: [{ - bidder: 'iqm', - params: { - publisherId: 'df5fd732-c5f3-11e7-abc4-cec278b6b50a', - placementId: 23451, - bidfloor: 0.5, - video: { - placement: 2, - mimes: ['video/mp4'], - protocols: [2, 5], - skipppable: true, - playback_method: ['auto_play_sound_off'] - } - }, - crumbs: {pubcid: '09b8f065-9d1b-4a36-bd0c-ea22e2dad807'}, - ortb2Imp: {ext: {data: {'pbadslot': 'video1'}}}, - mediaTypes: {video: {playerSize: [[640, 480]], context: 'instream'}}, - adUnitCode: 'video1', - transactionId: '86795c66-acf9-4dd5-998f-6d5362aaa541', - sizes: [[640, 480]], - bidId: '28bfb7e2d12897', - bidderRequestId: '16e1ce8481bc6d', - auctionId: '3140a2ec-d567-4db0-9bbb-eb6fa20ccb71', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - }], - auctionStart: 1615271191985, - timeout: 3000, - refererInfo: { - page: 'http://test.localhost:9999/integrationExamples/gpt/pbjs_video_adUnit.html', - domain: 'test.localhost.com:9999', - ref: '', - reachedTop: true, - isAmp: false, - numIframes: 0, - stack: ['http://test.localhost:9999/integrationExamples/gpt/pbjs_video_adUnit.html'], - canonicalUrl: null - }, - start: 1615271191988 - }; - - it('handles non-banner media responses', function () { - let response = {id: '2341234', seatbid: [{bid: [{id: 'bid-2341234-1', impid: '1', price: 9, nurl: 'https://frontend.stage.iqm.com/static/vast-01.xml', adm: 'http://cdn.iqm.com/pbd?raw=312730_203cf73dc83fb_2824348636878_pbd', adomain: ['app1.stage.iqm.com'], cid: '168900', crid: 'cr-304503', attr: []}]}], bidid: '2341234'}; - - let temprequest_video = spec.buildRequests(validBidRequests_temp_video, bidderRequest_video); - - let result = spec.interpretResponse({ body: response }, temprequest_video[0]); - expect(result[0]).to.have.property('vastUrl'); - }); - }); -}); diff --git a/test/spec/modules/iqxBidAdapter_spec.js b/test/spec/modules/iqxBidAdapter_spec.js index f5e680c8e0b..553bfa4a87d 100644 --- a/test/spec/modules/iqxBidAdapter_spec.js +++ b/test/spec/modules/iqxBidAdapter_spec.js @@ -1,11 +1,13 @@ import {expect} from 'chai'; import {config} from 'src/config.js'; -import {spec, getBidFloor} from 'modules/iqxBidAdapter.js'; +import {spec} from 'modules/iqxBidAdapter.js'; import {deepClone} from 'src/utils'; +import {getBidFloor} from '../../../libraries/xeUtils/bidderUtils.js'; const ENDPOINT = 'https://pbjs.iqzonertb.live'; const defaultRequest = { + tmax: 0, adUnitCode: 'test', bidId: '1', requestId: 'qwerty', @@ -90,6 +92,7 @@ describe('iqxBidAdapter', () => { it('should build basic request structure', function () { const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('tmax').and.to.equal(defaultRequest.tmax); expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); @@ -97,11 +100,9 @@ describe('iqxBidAdapter', () => { expect(request).to.have.property('bc').and.to.equal(1); expect(request).to.have.property('floor').and.to.equal(null); expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); - expect(request).to.have.property('gdprApplies').and.to.equal(0); - expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('gdprConsent').and.to.deep.equal({}); expect(request).to.have.property('userEids').and.to.deep.equal([]); expect(request).to.have.property('usPrivacy').and.to.equal(''); - expect(request).to.have.property('coppa').and.to.equal(0); expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); expect(request).to.have.property('ext').and.to.deep.equal({}); expect(request).to.have.property('env').and.to.deep.equal({ @@ -194,18 +195,6 @@ describe('iqxBidAdapter', () => { expect(request).to.have.property('floor').and.to.equal(5); }); - it('should build request with gdpr consent data if applies', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'qwerty' - } - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('gdprApplies').and.equals(1); - expect(request).to.have.property('consentString').and.equals('qwerty'); - }); - it('should build request with usp consent data if applies', function () { const bidderRequest = { uspConsent: '1YA-' @@ -214,14 +203,6 @@ describe('iqxBidAdapter', () => { expect(request).to.have.property('usPrivacy').and.equals('1YA-'); }); - it('should build request with coppa 1', function () { - config.setConfig({ - coppa: true - }); - const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; - expect(request).to.have.property('coppa').and.equals(1); - }); - it('should build request with extended ids', function () { const idRequest = deepClone(defaultRequest); idRequest.userIdAsEids = [ @@ -452,4 +433,4 @@ describe('iqxBidAdapter', () => { expect(result).to.equal(5); }); }); -}) +}); diff --git a/test/spec/modules/iqzoneBidAdapter_spec.js b/test/spec/modules/iqzoneBidAdapter_spec.js index 2e920d3b769..210d3a2d60b 100644 --- a/test/spec/modules/iqzoneBidAdapter_spec.js +++ b/test/spec/modules/iqzoneBidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/iqzoneBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'iqzone' +const bidder = 'iqzone'; describe('IQZoneBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('IQZoneBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('IQZoneBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('IQZoneBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -73,9 +86,20 @@ describe('IQZoneBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -112,6 +136,7 @@ describe('IQZoneBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -120,7 +145,11 @@ describe('IQZoneBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -129,7 +158,7 @@ describe('IQZoneBidAdapter', function () { 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('string'); + 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); @@ -145,6 +174,56 @@ describe('IQZoneBidAdapter', function () { 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 + } + ]; + + let 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'); @@ -170,8 +249,10 @@ describe('IQZoneBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -186,12 +267,38 @@ describe('IQZoneBidAdapter', function () { 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -381,7 +488,7 @@ describe('IQZoneBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.smartssp.iqzone.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + expect(syncData[0].url).to.equal('https://cs.iqzone.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') }); it('Should return array of objects with proper sync config , include CCPA', function() { const syncData = spec.getUserSyncs({}, {}, {}, { @@ -392,7 +499,19 @@ describe('IQZoneBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.smartssp.iqzone.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + expect(syncData[0].url).to.equal('https://cs.iqzone.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.iqzone.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') }); }); }); diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 7655868ffc3..e0fc7d5affd 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -2,8 +2,9 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import { spec, storage, ERROR_CODES, FEATURE_TOGGLES, LOCAL_STORAGE_FEATURE_TOGGLES_KEY, REQUESTED_FEATURE_TOGGLES, combineImps, bidToVideoImp, bidToNativeImp, deduplicateImpExtFields, removeSiteIDs, addDeviceInfo } from '../../../modules/ixBidAdapter.js'; +import { spec, storage, FEATURE_TOGGLES, LOCAL_STORAGE_FEATURE_TOGGLES_KEY, REQUESTED_FEATURE_TOGGLES, combineImps, bidToVideoImp, bidToNativeImp, deduplicateImpExtFields, removeSiteIDs, addDeviceInfo } from '../../../modules/ixBidAdapter.js'; import { deepAccess, deepClone } from '../../../src/utils.js'; +import * as ajaxLib from 'src/ajax.js'; describe('IndexexchangeAdapter', function () { const IX_SECURE_ENDPOINT = 'https://htlb.casalemedia.com/openrtb/pbjs'; @@ -615,7 +616,8 @@ describe('IndexexchangeAdapter', function () { dspid: 50, pricelevel: '_100', advbrandid: 303325, - advbrand: 'OECTA' + advbrand: 'OECTA', + ibv: true }, adm: '' } @@ -821,8 +823,9 @@ describe('IndexexchangeAdapter', function () { tid: 'mock-tid' } }, - fledgeEnabled: true, - defaultForSlots: 1 + paapi: { + enabled: true + }, }; const DEFAULT_OPTION_FLEDGE_ENABLED = { @@ -843,7 +846,9 @@ describe('IndexexchangeAdapter', function () { tid: 'mock-tid' } }, - fledgeEnabled: true + paapi: { + enabled: true + } }; const DEFAULT_IDENTITY_RESPONSE = { @@ -853,7 +858,8 @@ describe('IndexexchangeAdapter', function () { source: 'identityinc.com', uids: [ { - id: 'identityid' + id: 'identityid', + atype: 1 } ] } @@ -1348,34 +1354,6 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('Roundel alias adapter', function () { - const vaildBids = [DEFAULT_BANNER_VALID_BID, DEFAULT_VIDEO_VALID_BID, DEFAULT_MULTIFORMAT_BANNER_VALID_BID, DEFAULT_MULTIFORMAT_VIDEO_VALID_BID]; - const ALIAS_OPTIONS = Object.assign({ - bidderCode: 'roundel' - }, DEFAULT_OPTION); - - it('should not build requests for mediaTypes if liveramp data is unavaliable', function () { - vaildBids.forEach((validBid) => { - const request = spec.buildRequests(validBid, ALIAS_OPTIONS); - expect(request).to.be.an('array'); - expect(request).to.have.lengthOf(0); - }); - }); - - it('should build requests for mediaTypes if liveramp data is avaliable', function () { - vaildBids.forEach((validBid) => { - const cloneValidBid = utils.deepClone(validBid); - cloneValidBid[0].userIdAsEids = utils.deepClone(DEFAULT_USERIDASEIDS_DATA); - const request = spec.buildRequests(cloneValidBid, ALIAS_OPTIONS); - const payload = extractPayload(request[0]); - expect(request).to.be.an('array'); - expect(request).to.have.lengthOf.above(0); // should be 1 or more - expect(payload.user.eids).to.have.lengthOf(11); - expect(payload.user.eids).to.deep.include(DEFAULT_USERID_PAYLOAD[0]); - }); - }); - }); - describe('buildRequestsIdentity', function () { let request; let payload; @@ -1408,8 +1386,14 @@ describe('IndexexchangeAdapter', function () { it('identity data in impression should have correct format and value (single identity partner)', function () { const impression = payload.user.eids; + expect(impression).to.be.an('array'); + expect(impression).to.have.lengthOf(1); expect(impression[0].source).to.equal(testCopy.IdentityIp.data.source); + expect(impression[0].uids).to.be.an('array'); + expect(impression[0].uids).to.have.lengthOf(1); expect(impression[0].uids[0].id).to.equal(testCopy.IdentityIp.data.uids[0].id); + expect(impression[0].uids[0].atype).to.exist; + expect(impression[0].uids[0].atype).to.equal(testCopy.IdentityIp.data.uids[0].atype); }); }); @@ -1545,9 +1529,7 @@ describe('IndexexchangeAdapter', function () { body: { ext: { pbjs_allow_all_eids: { - test: { - activated: false - } + activated: true } } } @@ -1562,11 +1544,6 @@ describe('IndexexchangeAdapter', function () { afterEach(function () { delete window.headertag; - serverResponse.body.ext.features = { - pbjs_allow_all_eids: { - activated: false - } - }; validIdentityResponse = {} }); @@ -1580,12 +1557,6 @@ describe('IndexexchangeAdapter', function () { }); it('IX adapter filters eids from prebid past the maximum eid limit', function () { - serverResponse.body.ext.features = { - pbjs_allow_all_eids: { - activated: true - } - }; - FEATURE_TOGGLES.setFeatureToggles(serverResponse); const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); let eid_sent_from_prebid = generateEid(55); cloneValidBid[0].userIdAsEids = utils.deepClone(eid_sent_from_prebid); @@ -1627,12 +1598,6 @@ describe('IndexexchangeAdapter', function () { } } }; - serverResponse.body.ext.features = { - pbjs_allow_all_eids: { - activated: true - } - }; - FEATURE_TOGGLES.setFeatureToggles(serverResponse); const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); let eid_sent_from_prebid = generateEid(49); cloneValidBid[0].userIdAsEids = utils.deepClone(eid_sent_from_prebid); @@ -1653,15 +1618,21 @@ describe('IndexexchangeAdapter', function () { expect(payload.ext.ixdiag.eidLength).to.equal(49); }); - it('All incoming eids are from unsupported source with feature toggle off', function () { - FEATURE_TOGGLES.setFeatureToggles(serverResponse); + it('Has incoming eids with no uid', function () { const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); - let eid_sent_from_prebid = generateEid(20); + let eid_sent_from_prebid = [ + { + source: 'catijah.org' + }, + { + source: 'bagel.com' + } + ]; cloneValidBid[0].userIdAsEids = utils.deepClone(eid_sent_from_prebid); const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; const payload = extractPayload(request); expect(payload.user.eids).to.be.undefined - expect(payload.ext.ixdiag.eidLength).to.equal(20); + expect(payload.ext.ixdiag.eidLength).to.equal(2); }); it('We continue to send in IXL identity info and Prebid takes precedence over IXL', function () { @@ -2119,7 +2090,11 @@ describe('IndexexchangeAdapter', function () { it('request should be made to IX endpoint with POST method and siteId in query param', function () { expect(requestMethod).to.equal('POST'); expect(requestUrl).to.equal(IX_SECURE_ENDPOINT + '?s=' + DEFAULT_BANNER_VALID_BID[0].params.siteId); - expect(request.option.contentType).to.equal('text/plain') + }); + + it('request made to IX endpoint with POST method should have correct options fields set', function () { + expect(request.options.contentType).to.equal('text/plain') + expect(request.options.withCredentials).to.equal(true) }); it('auction type should be set correctly', function () { @@ -3464,16 +3439,7 @@ describe('IndexexchangeAdapter', function () { expect(impression.ext.ae).to.equal(1); }); - it('impression should have ae=1 in ext when fledge module is enabled globally and default is set through setConfig', function () { - const bidderRequest = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED_GLOBALLY); - const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); - const requestBidFloor = spec.buildRequests([bid], bidderRequest)[0]; - const impression = extractPayload(requestBidFloor).imp[0]; - - expect(impression.ext.ae).to.equal(1); - }); - - it('impression should have ae=1 in ext when fledge module is enabled globally but no default set through setConfig but set at ad unit level', function () { + it('impression should have ae=1 in ext when request has paapi.enabled = true and ext.ae = 1', function () { const bidderRequest = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED); const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED[0]); const requestBidFloor = spec.buildRequests([bid], bidderRequest)[0]; @@ -3517,6 +3483,22 @@ describe('IndexexchangeAdapter', function () { expect(logWarnSpy.calledWith('error setting auction environment flag - must be an integer')).to.be.true; logWarnSpy.restore(); }); + + it('impression should have paapi extension when passed', function () { + const bidderRequest = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED); + let bid = utils.deepClone(DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED[0]); + bid.ortb2Imp.ext.ae = 1 + bid.ortb2Imp.ext.paapi = { + requestedSize: { + width: 300, + height: 250 + } + } + const requestBidFloor = spec.buildRequests([bid], bidderRequest)[0]; + const impression = extractPayload(requestBidFloor).imp[0]; + expect(impression.ext.paapi.requestedSize.width).to.equal(300); + expect(impression.ext.paapi.requestedSize.height).to.equal(250); + }); }); describe('integration through exchangeId and externalId', function () { @@ -3627,6 +3609,9 @@ describe('IndexexchangeAdapter', function () { brandId: 303325, brandName: 'OECTA', advertiserDomains: ['www.abc.com'] + }, + ext: { + ibv: true } } ]; @@ -3738,6 +3723,9 @@ describe('IndexexchangeAdapter', function () { brandId: 303325, brandName: 'OECTA', advertiserDomains: ['www.abc.com'] + }, + ext: { + ibv: true } } ]; @@ -3794,6 +3782,9 @@ describe('IndexexchangeAdapter', function () { brandId: 303325, brandName: 'OECTA', advertiserDomains: ['www.abc.com'] + }, + ext: { + ibv: true } } ]; @@ -4164,7 +4155,7 @@ describe('IndexexchangeAdapter', function () { beforeEach(() => { bidderRequestWithFledgeEnabled = spec.buildRequests(DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED, {})[0]; - bidderRequestWithFledgeEnabled.fledgeEnabled = true; + bidderRequestWithFledgeEnabled.paapi = {enabled: true}; serverResponseWithoutFledgeConfigs = { body: { @@ -4228,17 +4219,17 @@ describe('IndexexchangeAdapter', function () { } } ]; - expect(result.fledgeAuctionConfigs).to.deep.equal(expectedOutput); + expect(result.paapi).to.deep.equal(expectedOutput); }); it('should correctly interpret response without auction configs', () => { const result = spec.interpretResponse(serverResponseWithoutFledgeConfigs, bidderRequestWithFledgeEnabled); - expect(result.fledgeAuctionConfigs).to.be.undefined; + expect(result.paapi).to.be.undefined; }); it('should handle malformed auction configs gracefully', () => { const result = spec.interpretResponse(serverResponseWithMalformedAuctionConfig, bidderRequestWithFledgeEnabled); - expect(result.fledgeAuctionConfigs).to.be.empty; + expect(result.paapi).to.be.empty; }); it('should log warning for malformed auction configs', () => { @@ -4250,7 +4241,7 @@ describe('IndexexchangeAdapter', function () { it('should return bids when protected audience auction conigs is malformed', () => { const result = spec.interpretResponse(serverResponseWithMalformedAuctionConfigs, bidderRequestWithFledgeEnabled); - expect(result.fledgeAuctionConfigs).to.be.undefined; + expect(result.paapi).to.be.undefined; expect(result.length).to.be.greaterThan(0); }); }); @@ -4269,7 +4260,7 @@ describe('IndexexchangeAdapter', function () { }; bidderRequestWithFledgeEnabled = spec.buildRequests(DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED, {})[0]; - bidderRequestWithFledgeEnabled.fledgeEnabled = true; + bidderRequestWithFledgeEnabled.paapi = {enabled: true}; bidderRequestWithoutFledgeEnabled = spec.buildRequests(DEFAULT_BANNER_VALID_BID, {})[0]; }); @@ -4867,178 +4858,6 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('LocalStorage error codes', () => { - let TODAY = new Date().toISOString().slice(0, 10); - const key = 'ixdiag'; - - let sandbox; - let setDataInLocalStorageStub; - let getDataFromLocalStorageStub; - let removeDataFromLocalStorageStub; - let localStorageValues = {}; - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - setDataInLocalStorageStub = sandbox.stub(storage, 'setDataInLocalStorage').callsFake((key, value) => localStorageValues[key] = value) - getDataFromLocalStorageStub = sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => localStorageValues[key]) - removeDataFromLocalStorageStub = sandbox.stub(storage, 'removeDataFromLocalStorage').callsFake((key) => delete localStorageValues[key]) - sandbox.stub(storage, 'localStorageIsEnabled').returns(true); - }); - - afterEach(() => { - setDataInLocalStorageStub.restore(); - getDataFromLocalStorageStub.restore(); - removeDataFromLocalStorageStub.restore(); - localStorageValues = {}; - sandbox.restore(); - - config.setConfig({ - ortb2: {}, - ix: {}, - }) - }); - - it('should not log error in LocalStorage when there is no logError called.', () => { - const bid = DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]; - expect(spec.isBidRequestValid(bid)).to.be.true; - expect(localStorageValues[key]).to.be.undefined; - }); - - it('should log ERROR_CODES.BID_SIZE_INVALID_FORMAT in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.size = ['400', 100]; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.BID_SIZE_INVALID_FORMAT]: 1 } }); - }); - - it('should log ERROR_CODES.BID_SIZE_NOT_INCLUDED in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.size = [407, 100]; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.BID_SIZE_NOT_INCLUDED]: 1 } }); - }); - - it('should log ERROR_CODES.PROPERTY_NOT_INCLUDED in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.video = {}; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.PROPERTY_NOT_INCLUDED]: 4 } }); - }); - - it('should log ERROR_CODES.SITE_ID_INVALID_VALUE in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.siteId = false; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.SITE_ID_INVALID_VALUE]: 1 } }); - }); - - it('should log ERROR_CODES.BID_FLOOR_INVALID_FORMAT in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.bidFloor = true; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.BID_FLOOR_INVALID_FORMAT]: 1 } }); - }); - - it('should log ERROR_CODES.VIDEO_DURATION_INVALID in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.video.minduration = 1; - bid.params.video.maxduration = 0; - - expect(spec.isBidRequestValid(bid)).to.be.true; - spec.buildRequests([bid]); - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.VIDEO_DURATION_INVALID]: 2 } }); - }); - - it('should increment errors for errorCode', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.video = {}; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.PROPERTY_NOT_INCLUDED]: 4 } }); - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.PROPERTY_NOT_INCLUDED]: 8 } }); - }); - - it('should add new errorCode to ixdiag.', () => { - let bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.size = ['400', 100]; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.BID_SIZE_INVALID_FORMAT]: 1 } }); - - bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.siteId = false; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ - [TODAY]: { - [ERROR_CODES.BID_SIZE_INVALID_FORMAT]: 1, - [ERROR_CODES.SITE_ID_INVALID_VALUE]: 1 - } - }); - }); - - it('should clear errors with successful response', () => { - const ixdiag = { [TODAY]: { '1': 1, '3': 8, '4': 1 } }; - setDataInLocalStorageStub(key, JSON.stringify(ixdiag)); - - expect(JSON.parse(localStorageValues[key])).to.deep.equal(ixdiag); - - const request = DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]; - expect(spec.isBidRequestValid(request)).to.be.true; - - const data = { - id: '345', - imp: [ - { - id: '1a2b3c4e', - } - ], - ext: { - ixdiag: { - err: { - '4': 8 - } - } - } - }; - - const validBidRequest = DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0]; - - spec.interpretResponse({ body: DEFAULT_BANNER_BID_RESPONSE }, { data, validBidRequest }); - - expect(localStorageValues[key]).to.be.undefined; - }); - - it('should clear errors after 7 day expiry errorCode', () => { - const EXPIRED_DATE = '2019-12-12'; - - const ixdiag = { [EXPIRED_DATE]: { '1': 1, '3': 8, '4': 1 }, [TODAY]: { '3': 8, '4': 1 } }; - setDataInLocalStorageStub(key, JSON.stringify(ixdiag)); - - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.size = ['400', 100]; - - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(JSON.parse(localStorageValues[key])[EXPIRED_DATE]).to.be.undefined; - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { '1': 1, '3': 8, '4': 1 } }) - }); - - it('should not save error data into localstorage if consent is not given', () => { - config.setConfig({ deviceAccess: false }); - storage.localStorageIsEnabled.restore(); // let core manage device access - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.params.size = ['400', 100]; - expect(spec.isBidRequestValid(bid)).to.be.false; - expect(localStorageValues[key]).to.be.undefined; - }); - }); describe('combine imps test', function () { it('base test', function () { const imps = [ @@ -5199,6 +5018,7 @@ describe('IndexexchangeAdapter', function () { expect(result['b8c6b5d5-76a1-4a90-b635-0c7eae1bfaa7'].ixImps.length).to.equal(1); }); }); + describe('apply floors test', function () { it('video test', function() { const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); @@ -5563,6 +5383,7 @@ describe('IndexexchangeAdapter', function () { expect(removeSiteIDs(request)).to.deep.equal(expected); }); }); + describe('addDeviceInfo', () => { it('should add device to request when device already exists', () => { let r = { @@ -5581,4 +5402,53 @@ describe('IndexexchangeAdapter', function () { expect(r.device.h).to.exist; }); }); + + describe('fetch requests', function () { + let ajaxStub; + + beforeEach(function () { + ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => { + return sinon.spy(function (url, callback, data, options) { + callback.success('OK'); + }); + }); + }); + + afterEach(function () { + ajaxStub.restore(); + }); + + it('should send the correct headers in the actual fetch call', function (done) { + const requests = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION); + const request = requests[0]; + const ajax = ajaxLib.ajaxBuilder(); + + ajax( + request.url, + { + success: () => { + try { + sinon.assert.calledOnce(ajaxStub); + const ajaxCall = ajaxStub.returnValues[0]; + sinon.assert.calledOnce(ajaxCall); + const [calledUrl, callback, calledData, calledOptions] = ajaxCall.getCall(0).args; + + expect(calledUrl).to.equal(request.url); + expect(calledData).to.equal(request.data); + + expect(calledOptions.contentType).to.equal('text/plain'); + expect(calledOptions.withCredentials).to.be.true; + + done(); + } catch (err) { + done(err); + } + }, + error: (err) => done(err || new Error('Ajax request failed')), + }, + request.data, + request.options + ); + }); + }); }); diff --git a/test/spec/modules/justIdSystem_spec.js b/test/spec/modules/justIdSystem_spec.js index b6a8cd2d310..abdf2d39644 100644 --- a/test/spec/modules/justIdSystem_spec.js +++ b/test/spec/modules/justIdSystem_spec.js @@ -143,7 +143,7 @@ describe('JustIdSystem', function () { var scriptTagCallback; beforeEach(() => { - loadExternalScriptStub.callsFake((url, moduleCode, callback) => { + loadExternalScriptStub.callsFake((url, moduleCode, moduleType, callback) => { scriptTagCallback = callback; return scriptTag; }); @@ -190,7 +190,7 @@ describe('JustIdSystem', function () { const b = { y: 'y' } const c = { z: 'z' } - justIdSubmodule.getId(a, b, c).callback(callbackSpy); + justIdSubmodule.getId(a, {gdpr: b}, c).callback(callbackSpy); scriptTagCallback(); diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index c57c8a685e7..58cfc751a4f 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -12,6 +12,7 @@ import { getVatFromCache, getVatFromPlayer, setOverrides, + getPlayer, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; import {server} from 'test/mocks/xhr.js'; @@ -629,7 +630,7 @@ describe('jwplayerRtdProvider', function() { expect(ortb2Fragments.global).to.have.property('site'); expect(ortb2Fragments.global.site).to.have.property('content'); - expect(ortb2Fragments.global.site.content).to.have.property('id', 'jw_' + testIdForSuccess); + expect(ortb2Fragments.global.site.content).to.have.property('id', 'randomContentId'); expect(ortb2Fragments.global.site.content).to.have.property('data'); const data = ortb2Fragments.global.site.content.data; expect(data).to.have.length(3); @@ -801,7 +802,7 @@ describe('jwplayerRtdProvider', function() { describe(' Add Ortb Site Content', function () { beforeEach(() => { setOverrides({ - overrideContentId: 'always', + overrideContentId: 'whenEmpty', overrideContentUrl: 'whenEmpty', overrideContentTitle: 'whenEmpty', overrideContentDescription: 'whenEmpty' @@ -865,16 +866,16 @@ describe('jwplayerRtdProvider', function() { } }; - const expectedId = 'expectedId'; + const newId = 'newId'; const expectedUrl = 'expectedUrl'; const expectedTitle = 'expectedTitle'; const expectedDescription = 'expectedDescription'; const expectedData = { datum: 'datum' }; - addOrtbSiteContent(ortb2, expectedId, expectedData, expectedTitle, expectedDescription, expectedUrl); + addOrtbSiteContent(ortb2, newId, expectedData, expectedTitle, expectedDescription, expectedUrl); expect(ortb2).to.have.nested.property('site.random.random_sub', 'randomSub'); expect(ortb2).to.have.nested.property('app.content.id', 'appId'); expect(ortb2).to.have.nested.property('site.content.ext.random_field', 'randomField'); - expect(ortb2).to.have.nested.property('site.content.id', expectedId); + expect(ortb2).to.have.nested.property('site.content.id', 'oldId'); expect(ortb2).to.have.nested.property('site.content.url', expectedUrl); expect(ortb2).to.have.nested.property('site.content.title', expectedTitle); expect(ortb2).to.have.nested.property('site.content.ext.description', expectedDescription); @@ -889,7 +890,7 @@ describe('jwplayerRtdProvider', function() { expect(ortb2).to.have.nested.property('site.content.id', expectedId); }); - it('should override content id by default', function () { + it('should keep old content id by default', function () { const ortb2 = { site: { content: { @@ -898,9 +899,8 @@ describe('jwplayerRtdProvider', function() { } }; - const expectedId = 'expectedId'; - addOrtbSiteContent(ortb2, expectedId); - expect(ortb2).to.have.nested.property('site.content.id', expectedId); + addOrtbSiteContent(ortb2, 'newId'); + expect(ortb2).to.have.nested.property('site.content.id', 'oldId'); }); it('should keep previous content id when new value is not available', function () { @@ -1604,6 +1604,66 @@ describe('jwplayerRtdProvider', function() { }); }); + describe('Player detection', function () { + const playerInstanceMock = { + getPlaylist: () => [], + getPlaylistItem: () => ({}) + }; + + beforeEach(function () { + window.jwplayer = sinon.stub(); + }); + + afterEach(function () { + delete window.jwplayer; + }); + + it('should fail if jwplayer global does not exist', function () { + delete window.jwplayer; + expect(getPlayer('divId')).to.be.undefined; + }); + + it('should return the player instance for the specified div id', function () { + window.jwplayer.returns(playerInstanceMock); + const player = getPlayer('divId'); + expect(player).to.deep.equal(playerInstanceMock); + }); + + it('should request a player when the div id does not match a player on the page and only 1 player is in the DOM', function () { + const playerDomElement = document.createElement('div'); + playerDomElement.className = 'jwplayer'; + document.body.appendChild(playerDomElement); + + window.jwplayer.withArgs('invalidDivId').returns(undefined); + window.jwplayer.returns(playerInstanceMock); + + const playerInstance = getPlayer('invalidDivId'); + + expect(playerInstance).to.deep.equal(playerInstanceMock); + + document.body.removeChild(playerDomElement); + }); + + it('should fail when the div id does not match a player on the page, and multiple players are instantiated', function () { + const firstPlayerDomElement = document.createElement('div'); + const secondPlayerDomElement = document.createElement('div'); + firstPlayerDomElement.className = 'jwplayer'; + secondPlayerDomElement.className = 'jwplayer'; + document.body.appendChild(firstPlayerDomElement); + document.body.appendChild(secondPlayerDomElement); + + window.jwplayer.withArgs('invalidDivId').returns(undefined); + window.jwplayer.returns(playerInstanceMock); + + const playerInstance = getPlayer('invalidDivId'); + + expect(playerInstance).to.be.undefined; + + document.body.removeChild(firstPlayerDomElement); + document.body.removeChild(secondPlayerDomElement); + }); + }); + describe('jwplayerSubmodule', function () { it('successfully instantiates', function () { expect(jwplayerSubmodule.init()).to.equal(true); diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index 7aa853ad902..a9121a7a59f 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -300,7 +300,7 @@ describe('kargo adapter tests', function() { domain, isAmp: false, location: topUrl, - numIframs: 0, + numIframes: 0, page: topUrl, reachedTop: true, ref: referer, @@ -428,12 +428,12 @@ describe('kargo adapter tests', function() { } } }]); - expect(payload.ext).to.deep.equal({ ortb2: { + expect(payload.ext.ortb2).to.deep.equal({ user: { key: 'value' } - }}); + }); payload = getPayloadFromTestBids(testBids); - expect(payload.ext).to.be.undefined; + expect(payload.ext.ortb2).to.be.undefined; payload = getPayloadFromTestBids([{ ...minimumBidParams, @@ -450,9 +450,33 @@ describe('kargo adapter tests', function() { } } }]); - expect(payload.ext).to.deep.equal({ortb2: { + expect(payload.ext.ortb2).to.deep.equal({ user: { key: 'value' } - }}); + } + ); + }); + + it('copies the refererInfo object from bidderRequest if present', function() { + let payload; + payload = getPayloadFromTestBids(testBids); + expect(payload.ext.refererInfo).to.deep.equal({ + canonicalUrl: 'https://random.com/this/is/a/url', + domain: 'random.com', + isAmp: false, + location: 'https://random.com/this/is/a/url', + numIframes: 0, + page: 'https://random.com/this/is/a/url', + reachedTop: true, + ref: 'https://random.com/', + stack: [ + 'https://random.com/this/is/a/url' + ], + topmostLocation: 'https://random.com/this/is/a/url' + }); + + delete bidderRequest.refererInfo + payload = getPayloadFromTestBids(testBids); + expect(payload.ext).to.be.undefined; }); it('pulls the site category from the first bids ortb2 object', function() { @@ -1807,21 +1831,69 @@ describe('kargo adapter tests', function() { advertiserDomains: [ 'https://foo.com', 'https://bar.com' ] }); }); + + it('should return paapi if provided in bid response', function () { + const auctionConfig = { + seller: 'https://kargo.com', + decisionLogicUrl: 'https://kargo.com/decision_logic.js', + interestGroupBuyers: ['https://some_buyer.com'], + perBuyerSignals: { + 'https://some_buyer.com': { a: 1 } + } + } + + const body = response.body; + for (const key in body) { + if (body.hasOwnProperty(key)) { + if (key % 2 !== 0) { // Add auctionConfig to every other object + body[key].auctionConfig = auctionConfig; + } + } + } + + let result = spec.interpretResponse(response, bidderRequest); + + // Test properties of bidResponses + result.bids.forEach(bid => { + expect(bid).to.have.property('requestId'); + expect(bid).to.have.property('cpm'); + expect(bid).to.have.property('width'); + expect(bid).to.have.property('height'); + expect(bid).to.have.property('ttl'); + expect(bid).to.have.property('creativeId'); + expect(bid.netRevenue).to.be.true; + expect(bid).to.have.property('meta').that.is.an('object'); + }); + + // Test properties of paapi + expect(result.paapi).to.have.lengthOf(3); + + const expectedBidIds = ['1', '3', '5']; // Expected bidIDs + result.paapi.forEach(config => { + expect(config).to.have.property('bidId'); + expect(expectedBidIds).to.include(config.bidId); + + expect(config).to.have.property('config').that.is.an('object'); + expect(config.config).to.have.property('seller', 'https://kargo.com'); + expect(config.config).to.have.property('decisionLogicUrl', 'https://kargo.com/decision_logic.js'); + expect(config.config.interestGroupBuyers).to.be.an('array').that.includes('https://some_buyer.com'); + expect(config.config.perBuyerSignals).to.have.property('https://some_buyer.com').that.deep.equals({ a: 1 }); + }); + }); }); describe('getUserSyncs', function() { let crb = {}; const clientId = 'random-client-id-string'; - const baseUrl = 'https://crb.kargo.com/api/v1/initsyncrnd/random-client-id-string?seed=3205e885-8d37-4139-b47e-f82cff268000&idx=0&gdpr=0&gdpr_consent=&us_privacy=&gpp=&gpp_sid='; + const baseUrl = 'https://crb.kargo.com/api/v1/initsyncrnd/random-client-id-string?seed=3205e885-8d37-4139-b47e-f82cff268000&gdpr=0&gdpr_consent=&us_privacy=&gpp=&gpp_sid='; - function buildSyncUrls(baseUrl = 'https://crb.kargo.com/api/v1/initsyncrnd/random-client-id-string?seed=3205e885-8d37-4139-b47e-f82cff268000&idx=0&gdpr=0&gdpr_consent=&us_privacy=&gpp=&gpp_sid=') { + function buildSyncUrls(baseUrl = 'https://crb.kargo.com/api/v1/initsyncrnd/random-client-id-string?seed=3205e885-8d37-4139-b47e-f82cff268000&gdpr=0&gdpr_consent=&us_privacy=&gpp=&gpp_sid=') { let syncs = []; - for (let i = 0; i < 5; i++) { - syncs.push({ - type: 'iframe', - url: baseUrl.replace(/idx=\d+&/, `idx=${i}&`), - }); - } + + syncs.push({ + type: 'iframe', + url: baseUrl + }); return syncs; } @@ -1958,31 +2030,49 @@ describe('kargo adapter tests', function() { }); }); - describe('onTimeout', function() { + describe('onTimeout', function () { + let fetchStub; + beforeEach(function () { - sinon.stub(utils, 'triggerPixel'); + fetchStub = sinon.stub(global, 'fetch').resolves(); // Stub fetch globally }); afterEach(function () { - utils.triggerPixel.restore(); + fetchStub.restore(); // Restore the original fetch function }); - it('does not call triggerPixel if timeout data is not provided', function() { + it('does not call fetch if timeout data is not provided', function () { spec.onTimeout(null); - expect(utils.triggerPixel.callCount).to.equal(0); + expect(fetchStub.callCount).to.equal(0); }); - it('calls triggerPixel if any timeout data is provided', function() { + it('calls fetch with the correct URLs if timeout data is provided', function () { spec.onTimeout([ - {auctionId: 'test-auction-id', timeout: 400}, - {auctionId: 'test-auction-id-2', timeout: 100}, - {auctionId: 'test-auction-id-3', timeout: 450}, - {auctionId: 'test-auction-id-4', timeout: 500}, + { auctionId: 'test-auction-id', timeout: 400 }, + { auctionId: 'test-auction-id-2', timeout: 100 }, + { auctionId: 'test-auction-id-3', timeout: 450 }, + { auctionId: 'test-auction-id-4', timeout: 500 }, ]); - expect(utils.triggerPixel.calledWith('https://krk2.kargo.com/api/v1/event/timeout?aid=test-auction-id&ato=400')).to.be.true; - expect(utils.triggerPixel.calledWith('https://krk2.kargo.com/api/v1/event/timeout?aid=test-auction-id-2&ato=100')).to.be.true; - expect(utils.triggerPixel.calledWith('https://krk2.kargo.com/api/v1/event/timeout?aid=test-auction-id-3&ato=450')).to.be.true; - expect(utils.triggerPixel.calledWith('https://krk2.kargo.com/api/v1/event/timeout?aid=test-auction-id-4&ato=500')).to.be.true; + + expect(fetchStub.calledWith( + 'https://krk2.kargo.com/api/v1/event/timeout?aid=test-auction-id&ato=400', + { method: 'GET', keepalive: true } + )).to.be.true; + + expect(fetchStub.calledWith( + 'https://krk2.kargo.com/api/v1/event/timeout?aid=test-auction-id-2&ato=100', + { method: 'GET', keepalive: true } + )).to.be.true; + + expect(fetchStub.calledWith( + 'https://krk2.kargo.com/api/v1/event/timeout?aid=test-auction-id-3&ato=450', + { method: 'GET', keepalive: true } + )).to.be.true; + + expect(fetchStub.calledWith( + 'https://krk2.kargo.com/api/v1/event/timeout?aid=test-auction-id-4&ato=500', + { method: 'GET', keepalive: true } + )).to.be.true; }); }); }); diff --git a/test/spec/modules/kimberliteBidAdapter_spec.js b/test/spec/modules/kimberliteBidAdapter_spec.js index 1480f1cc768..739a970603c 100644 --- a/test/spec/modules/kimberliteBidAdapter_spec.js +++ b/test/spec/modules/kimberliteBidAdapter_spec.js @@ -1,6 +1,6 @@ -import { spec } from 'modules/kimberliteBidAdapter.js'; +import { spec, ENDPOINT_URL, expandAuctionMacros } from 'modules/kimberliteBidAdapter.js'; import { assert } from 'chai'; -import { BANNER } from '../../../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; const BIDDER_CODE = 'kimberlite'; @@ -8,74 +8,104 @@ describe('kimberliteBidAdapter', function () { const sizes = [[640, 480]]; describe('isBidRequestValid', function () { - let bidRequest; + let bidRequests; beforeEach(function () { - bidRequest = { - mediaTypes: { - [BANNER]: { - sizes: [[320, 240]] + bidRequests = [ + { + mediaTypes: { + [BANNER]: { + sizes: [[320, 240]] + } + }, + params: { + placementId: 'test-placement' } }, - params: { - placementId: 'test-placement' + { + mediaTypes: { + [VIDEO]: { + mimes: ['video/mp4'] + } + }, + params: { + placementId: 'test-placement' + } } - }; + ]; }); - it('pass on valid bidRequest', function () { - assert.isTrue(spec.isBidRequestValid(bidRequest)); + it('pass on valid banner bidRequest', function () { + assert.isTrue(spec.isBidRequestValid(bidRequests[0])); }); it('fails on missed placementId', function () { - delete bidRequest.params.placementId; - assert.isFalse(spec.isBidRequestValid(bidRequest)); + delete bidRequests[0].params.placementId; + assert.isFalse(spec.isBidRequestValid(bidRequests[0])); }); it('fails on empty banner', function () { - delete bidRequest.mediaTypes.banner; - assert.isFalse(spec.isBidRequestValid(bidRequest)); + delete bidRequests[0].mediaTypes.banner; + assert.isFalse(spec.isBidRequestValid(bidRequests[0])); }); it('fails on empty banner.sizes', function () { - delete bidRequest.mediaTypes.banner.sizes; - assert.isFalse(spec.isBidRequestValid(bidRequest)); + delete bidRequests[0].mediaTypes.banner.sizes; + assert.isFalse(spec.isBidRequestValid(bidRequests[0])); }); it('fails on empty request', function () { assert.isFalse(spec.isBidRequestValid()); }); + + it('pass on valid video bidRequest', function () { + assert.isTrue(spec.isBidRequestValid(bidRequests[1])); + }); + + it('fails on missed video.mimes', function () { + delete bidRequests[1].mediaTypes.video.mimes; + assert.isFalse(spec.isBidRequestValid(bidRequests[1])); + }); }); describe('buildRequests', function () { let bidRequests, bidderRequest; beforeEach(function () { - bidRequests = [{ - mediaTypes: { - [BANNER]: {sizes: sizes} + bidRequests = [ + { + mediaTypes: { + [BANNER]: {sizes: sizes} + }, + params: { + placementId: 'test-placement' + } }, - params: { - placementId: 'test-placement' + { + mediaTypes: { + [VIDEO]: { + mimes: ['video/mp4'], + } + }, + params: { + placementId: 'test-placement' + } } - }]; + ]; bidderRequest = { refererInfo: { domain: 'example.com', page: 'https://www.example.com/test.html', - }, - bids: [{ - mediaTypes: { - [BANNER]: {sizes: sizes} - } - }] + } }; }); it('valid bid request', function () { const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + assert.equal(bidRequest.method, 'POST'); + assert.equal(bidRequest.url, ENDPOINT_URL); assert.ok(bidRequest.data); const requestData = bidRequest.data; @@ -87,10 +117,10 @@ describe('kimberliteBidAdapter', function () { expect(requestData.ext).to.be.an('Object').and.have.all.keys('prebid'); expect(requestData.ext.prebid).to.be.an('Object').and.have.all.keys('ver', 'adapterVer'); - const impData = requestData.imp[0]; - expect(impData.banner).is.to.be.an('Object').and.have.all.keys(['format', 'topframe']); + const impBannerData = requestData.imp[0]; + expect(impBannerData.banner).is.to.be.an('Object').and.have.all.keys(['format', 'topframe']); - const bannerData = impData.banner; + const bannerData = impBannerData.banner; expect(bannerData.format).to.be.an('array').and.is.not.empty; const formatData = bannerData.format[0]; @@ -98,74 +128,133 @@ describe('kimberliteBidAdapter', function () { assert.equal(formatData.w, sizes[0][0]); assert.equal(formatData.h, sizes[0][1]); + + if (FEATURES.VIDEO) { + const impVideoData = requestData.imp[1]; + expect(impVideoData.video).is.to.be.an('Object').and.have.all.keys(['mimes']); + + const videoData = impVideoData.video; + expect(videoData.mimes).to.be.an('array').and.is.not.empty; + expect(videoData.mimes[0]).to.be.a('string').that.equals('video/mp4'); + } }); }); describe('interpretResponse', function () { - let bidderResponse, bidderRequest, bidRequest, expectedBid; + let bidderResponse, bidderRequest, bidRequest, expectedBids; const requestId = '07fba8b0-8812-4dc6-b91e-4a525d81729c'; - const bidId = '222209853178'; - const impId = 'imp-id'; - const crId = 'creative-id'; - const adm = 'landing'; + const bannerAdm = 'landing'; + const videoAdm = 'http://video-test.landing.com?p=${AUCTION_PRICE}&c=${AUCTION_CURRENCY}test vast'; + const nurl = 'http://nurl.landing.com?p=${AUCTION_PRICE}&c=${AUCTION_CURRENCY}'; + const nurlPixel = `
`; - beforeEach(function () { - bidderResponse = { - body: { - id: requestId, - seatbid: [{ - bid: [{ - crid: crId, - id: bidId, - impid: impId, - price: 1, - adm: adm + const currencies = [ + undefined, + 'USD' + ]; + + currencies.forEach(function(currency) { + beforeEach(function () { + bidderResponse = { + body: { + id: requestId, + seatbid: [{ + bid: [ + { + crid: 1, + impid: 1, + price: 1, + adm: bannerAdm, + nurl: nurl + }, + { + crid: 2, + impid: 2, + price: 1, + adm: videoAdm + } + ] }] - }] - } - }; + } + }; - bidderRequest = { - refererInfo: { - domain: 'example.com', - page: 'https://www.example.com/test.html', - }, - bids: [{ - bidId: impId, - mediaTypes: { - [BANNER]: {sizes: sizes} + bidderRequest = { + refererInfo: { + domain: 'example.com', + page: 'https://www.example.com/test.html', }, - params: { - placementId: 'test-placement' - } - }] - }; + bids: [ + { + bidId: 1, + mediaTypes: { + banner: {sizes: sizes} + }, + params: { + placementId: 'test-placement' + } + }, + { + bidId: 2, + mediaTypes: { + video: { + mimes: ['video/mp4'] + } + }, + params: { + placementId: 'test-placement' + } + } + ] + }; - expectedBid = { - mediaType: 'banner', - requestId: 'imp-id', - seatBidId: '222209853178', - cpm: 1, - creative_id: 'creative-id', - creativeId: 'creative-id', - ttl: 300, - netRevenue: true, - ad: adm, - meta: {} - }; + expectedBids = [ + { + mediaType: 'banner', + requestId: 1, + cpm: 1, + creative_id: 1, + creativeId: 1, + ttl: 300, + netRevenue: true, + ad: bannerAdm + nurlPixel, + meta: {} + }, + { + mediaType: 'video', + requestId: 2, + cpm: 1, + creative_id: 2, + creativeId: 2, + ttl: 300, + netRevenue: true, + vastXml: videoAdm, + meta: {} + }, + ]; - bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); - }); + if (currency) { + expectedBids[0].currency = expectedBids[1].currency = bidderResponse.body.cur = currency; + } - it('pass on valid request', function () { - const bids = spec.interpretResponse(bidderResponse, bidRequest); - assert.deepEqual(bids[0], expectedBid); - }); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + }); + + it('pass on valid request', function () { + const bids = spec.interpretResponse(bidderResponse, bidRequest); + expectedBids[0].ad = expandAuctionMacros(expectedBids[0].ad, expectedBids[0].cpm, bidderResponse.body.cur); + assert.deepEqual(bids[0], expectedBids[0]); + if (FEATURES.VIDEO) { + expectedBids[1].vastXml = + expandAuctionMacros(expectedBids[1].vastXml, expectedBids[1].cpm, bidderResponse.body.cur); + assert.deepEqual(bids[1], expectedBids[1]); + } + }); - it('fails on empty response', function () { - const bids = spec.interpretResponse({body: ''}, bidRequest); - assert.empty(bids); + it('fails on empty response', function () { + const bids = spec.interpretResponse({body: ''}, bidRequest); + assert.empty(bids); + }); }); }); }); diff --git a/test/spec/modules/kinessoIdSystem_spec.js b/test/spec/modules/kinessoIdSystem_spec.js new file mode 100644 index 00000000000..e5d9721737d --- /dev/null +++ b/test/spec/modules/kinessoIdSystem_spec.js @@ -0,0 +1,26 @@ +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {kinessoIdSubmodule} from '../../../modules/kinessoIdSystem.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; + +describe('kinesso ID', () => { + describe('eid', () => { + before(() => { + attachIdSystem(kinessoIdSubmodule); + }); + it('kpuid', function() { + const userId = { + kpuid: 'Sample_Token' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'kpuid.com', + uids: [{ + id: 'Sample_Token', + atype: 3 + }] + }); + }); + }); +}); diff --git a/test/spec/modules/kiviadsBidAdapter_spec.js b/test/spec/modules/kiviadsBidAdapter_spec.js index 03d58cbc265..bd59a50e3ae 100644 --- a/test/spec/modules/kiviadsBidAdapter_spec.js +++ b/test/spec/modules/kiviadsBidAdapter_spec.js @@ -3,11 +3,21 @@ import { spec } from '../../../modules/kiviadsBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'kiviads' +const bidder = 'kiviads'; const adUrl = 'https://lb.kiviads.com/pbjs'; const syncUrl = 'https://sync.kiviads.com'; describe('KiviAdsBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -18,8 +28,9 @@ describe('KiviAdsBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -32,8 +43,9 @@ describe('KiviAdsBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -55,8 +67,9 @@ describe('KiviAdsBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -75,11 +88,22 @@ describe('KiviAdsBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' }, - bidderTimeout: 300 + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } + }, + timeout: 500 }; describe('isBidRequestValid', function () { @@ -114,6 +138,7 @@ describe('KiviAdsBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -122,19 +147,20 @@ describe('KiviAdsBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'gpp', - 'tmax' + '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.host).to.contain('localhost'); expect(data.page).to.be.a('string'); - expect(data.page).to.equal('/'); expect(data.coppa).to.be.a('number'); - expect(data.gdpr).to.be.a('string'); + 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); @@ -150,6 +176,56 @@ describe('KiviAdsBidAdapter', function () { 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 + } + ]; + + let 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'); @@ -175,8 +251,10 @@ describe('KiviAdsBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -191,12 +269,38 @@ describe('KiviAdsBidAdapter', function () { 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -400,5 +504,17 @@ describe('KiviAdsBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal(`${syncUrl}/image?pbjs=1&ccpa_consent=1---&coppa=0`) }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal(`${syncUrl}/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0`) + }); }); }); diff --git a/test/spec/modules/koblerBidAdapter_spec.js b/test/spec/modules/koblerBidAdapter_spec.js index 2b5830f68d2..a06d0f2e969 100644 --- a/test/spec/modules/koblerBidAdapter_spec.js +++ b/test/spec/modules/koblerBidAdapter_spec.js @@ -1,18 +1,43 @@ import {expect} from 'chai'; -import {spec} from 'modules/koblerBidAdapter.js'; +import {pageViewId, 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'; import {getRefererInfo} from 'src/refererDetection.js'; - -function createBidderRequest(auctionId, timeout, pageUrl) { +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; + +function createBidderRequest(auctionId, timeout, pageUrl, addGdprConsent) { + const gdprConsent = addGdprConsent ? { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false, + 2: true, + 3: false + } + }, + publisher: { + restrictions: { + '2': { + // require consent + '11': 1 + } + } + } + }, + gdprApplies: true + } : {}; return { bidderRequestId: 'mock-uuid', auctionId: auctionId || 'c1243d83-0bed-4fdb-8c76-42b456be17d0', timeout: timeout || 2000, refererInfo: { page: pageUrl || 'example.com' - } + }, + gdprConsent: gdprConsent }; } @@ -222,6 +247,21 @@ describe('KoblerAdapter', function () { expect(openRtbRequest.site.page).to.be.equal(testUrl); }); + it('should reuse the same page view ID on subsequent calls', function () { + const testUrl = 'kobler.no'; + const auctionId1 = '8319af54-9795-4642-ba3a-6f57d6ff9100'; + const auctionId2 = 'e19f2d0c-602d-4969-96a1-69a22d483f47'; + const timeout = 5000; + const validBidRequests = [createValidBidRequest()]; + const bidderRequest1 = createBidderRequest(auctionId1, timeout, testUrl); + const bidderRequest2 = createBidderRequest(auctionId2, timeout, testUrl); + + const openRtbRequest1 = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest1).data); + expect(openRtbRequest1.ext.kobler.page_view_id).to.be.equal(pageViewId); + const openRtbRequest2 = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest2).data); + expect(openRtbRequest2.ext.kobler.page_view_id).to.be.equal(pageViewId); + }); + it('should read data from valid bid requests', function () { const firstSize = [400, 800]; const secondSize = [450, 950]; @@ -289,27 +329,6 @@ describe('KoblerAdapter', function () { expect(openRtbRequest.test).to.be.equal(1); }); - it('should read pageUrl from config when testing', function () { - config.setConfig({ - pageUrl: 'https://testing-url.com' - }); - const validBidRequests = [ - createValidBidRequest( - { - test: true - } - ) - ]; - const bidderRequest = createBidderRequest(); - - const result = spec.buildRequests(validBidRequests, bidderRequest); - expect(result.url).to.be.equal('https://bid-service.dev.essrtb.com/bid/prebid_rtb_call'); - - const openRtbRequest = JSON.parse(result.data); - expect(openRtbRequest.site.page).to.be.equal('https://testing-url.com'); - expect(openRtbRequest.test).to.be.equal(1); - }); - it('should not read pageUrl from config when not testing', function () { config.setConfig({ pageUrl: 'https://testing-url.com' @@ -439,7 +458,8 @@ describe('KoblerAdapter', function () { const bidderRequest = createBidderRequest( '9ff580cf-e10e-4b66-add7-40ac0c804e21', 4500, - 'bid.kobler.no' + 'bid.kobler.no', + true ); const result = spec.buildRequests(validBidRequests, bidderRequest); @@ -524,12 +544,20 @@ describe('KoblerAdapter', function () { } ], device: { - devicetype: 2 + devicetype: 2, + ua: navigator.userAgent }, site: { page: 'bid.kobler.no' }, - test: 0 + test: 0, + ext: { + kobler: { + tcf_purpose_2_given: true, + tcf_purpose_3_given: false, + page_view_id: pageViewId + } + } }; expect(openRtbRequest).to.deep.equal(expectedOpenRtbRequest); @@ -562,6 +590,7 @@ describe('KoblerAdapter', function () { price: 7.981, nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=${AUCTION_PRICE}&sp_cur=${AUCTION_PRICE_CURRENCY}&asp=${AD_SERVER_PRICE}&asp_cur=${AD_SERVER_PRICE_CURRENCY}', crid: 'edea9b03-3a57-41aa-9c00-abd673e22006', + cid: '572', dealid: '', w: 320, h: 250, @@ -576,6 +605,7 @@ describe('KoblerAdapter', function () { nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=nbashgufvishdafjk23432&sp=${AUCTION_PRICE}&sp_cur=${AUCTION_PRICE_CURRENCY}&asp=${AD_SERVER_PRICE}&asp_cur=${AD_SERVER_PRICE_CURRENCY}', crid: 'fa2d5af7-2678-4204-9023-44c526160742', dealid: '2783483223432342', + cid: '800', w: 580, h: 400, adm: '', @@ -589,7 +619,7 @@ describe('KoblerAdapter', function () { cur: 'USD' } }; - const bids = spec.interpretResponse(responseWithTwoBids) + const bids = spec.interpretResponse(responseWithTwoBids, {}) const expectedBids = [ { @@ -604,6 +634,7 @@ describe('KoblerAdapter', function () { ttl: 600, ad: '', nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=${AUCTION_PRICE}&sp_cur=${AUCTION_PRICE_CURRENCY}&asp=${AD_SERVER_PRICE}&asp_cur=${AD_SERVER_PRICE_CURRENCY}', + cid: '572', meta: { advertiserDomains: [ 'https://kobler.no' @@ -622,6 +653,7 @@ describe('KoblerAdapter', function () { ttl: 600, ad: '', nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=nbashgufvishdafjk23432&sp=${AUCTION_PRICE}&sp_cur=${AUCTION_PRICE_CURRENCY}&asp=${AD_SERVER_PRICE}&asp_cur=${AD_SERVER_PRICE_CURRENCY}', + cid: '800', meta: { advertiserDomains: [ 'https://bid.kobler.no' @@ -656,25 +688,30 @@ describe('KoblerAdapter', function () { }); it('Should trigger pixel with replaced nurl if nurl is not empty', function () { - config.setConfig({ - 'currency': { - 'adServerCurrency': 'NOK' - } - }); - spec.onBidWon({ - originalCpm: 1.532, - cpm: 8.341, - currency: 'NOK', - nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=${AUCTION_PRICE}&sp_cur=${AUCTION_PRICE_CURRENCY}&asp=${AD_SERVER_PRICE}&asp_cur=${AD_SERVER_PRICE_CURRENCY}', - adserverTargeting: { + setCurrencyConfig({ adServerCurrency: 'NOK' }); + let validBidRequests = [{ params: {} }]; + let refererInfo = { page: 'page' }; + const bidderRequest = { refererInfo }; + return addFPDToBidderRequest(bidderRequest).then(res => { + JSON.parse(spec.buildRequests(validBidRequests, res).data); + const bids = spec.interpretResponse({ body: { seatbid: [{ bid: [{ + originalCpm: 1.532, + price: 8.341, + currency: 'NOK', + nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=${AUCTION_PRICE}&sp_cur=${AUCTION_PRICE_CURRENCY}&asp=${AD_SERVER_PRICE}&asp_cur=${AD_SERVER_PRICE_CURRENCY}', + }]}]}}, { bidderRequest: res }); + const bidToWon = bids[0]; + bidToWon.adserverTargeting = { hb_pb: 8 } - }); + spec.onBidWon(bidToWon); - expect(utils.triggerPixel.callCount).to.be.equal(1); - expect(utils.triggerPixel.firstCall.args[0]).to.be.equal( - 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=8.341&sp_cur=NOK&asp=8&asp_cur=NOK' - ); + expect(utils.triggerPixel.callCount).to.be.equal(1); + expect(utils.triggerPixel.firstCall.args[0]).to.be.equal( + 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=8.341&sp_cur=NOK&asp=8&asp_cur=NOK' + ); + setCurrencyConfig({}); + }); }); }); @@ -702,14 +739,12 @@ describe('KoblerAdapter', function () { spec.onTimeout([ { adUnitCode: 'adunit-code', - auctionId: 'a1fba829-dd41-409f-acfb-b7b0ac5f30c6', bidId: 'ef236c6c-e934-406b-a877-d7be8e8a839a', timeout: 100, params: [], }, { adUnitCode: 'adunit-code-2', - auctionId: 'a1fba829-dd41-409f-acfb-b7b0ac5f30c6', bidId: 'ca4121c8-9a4a-46ba-a624-e9b64af206f2', timeout: 100, params: [], @@ -719,13 +754,11 @@ describe('KoblerAdapter', function () { expect(utils.triggerPixel.callCount).to.be.equal(2); expect(utils.triggerPixel.getCall(0).args[0]).to.be.equal( 'https://bid.essrtb.com/notify/prebid_timeout?ad_unit_code=adunit-code&' + - 'auction_id=a1fba829-dd41-409f-acfb-b7b0ac5f30c6&bid_id=ef236c6c-e934-406b-a877-d7be8e8a839a&timeout=100&' + - 'page_url=' + encodeURIComponent(getRefererInfo().page) + 'bid_id=ef236c6c-e934-406b-a877-d7be8e8a839a&timeout=100&page_url=' + encodeURIComponent(getRefererInfo().page) ); expect(utils.triggerPixel.getCall(1).args[0]).to.be.equal( 'https://bid.essrtb.com/notify/prebid_timeout?ad_unit_code=adunit-code-2&' + - 'auction_id=a1fba829-dd41-409f-acfb-b7b0ac5f30c6&bid_id=ca4121c8-9a4a-46ba-a624-e9b64af206f2&timeout=100&' + - 'page_url=' + encodeURIComponent(getRefererInfo().page) + 'bid_id=ca4121c8-9a4a-46ba-a624-e9b64af206f2&timeout=100&page_url=' + encodeURIComponent(getRefererInfo().page) ); }); }); diff --git a/test/spec/modules/krushmediaBidAdapter_spec.js b/test/spec/modules/krushmediaBidAdapter_spec.js index fcdcc942290..f6fe1b5661b 100644 --- a/test/spec/modules/krushmediaBidAdapter_spec.js +++ b/test/spec/modules/krushmediaBidAdapter_spec.js @@ -1,147 +1,308 @@ -import {expect} from 'chai'; -import {spec} from '../../../modules/krushmediaBidAdapter.js'; +import { expect } from 'chai'; +import { spec } from '../../../modules/krushmediaBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'krushmedia'; describe('KrushmediabBidAdapter', function () { - const bid = { - bidId: '23fhj33i987f', - bidder: 'krushmedia', + 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: { + key: 783 + }, + userIdAsEids + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + key: 783 + }, + userIdAsEids + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + key: 783 + }, + userIdAsEids + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, params: { - key: 783, - traffic: BANNER + } - }; + } const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'test.com' - } + 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(bid)).to.be.true; + expect(spec.isBidRequestValid(bids[0])).to.be.true; }); it('Should return false if at least one of parameters is not present', function () { - delete bid.params.key; - expect(spec.isBidRequestValid(bid)).to.be.false; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; }); }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid], bidderRequest); + 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 valid URL', function () { expect(serverRequest.url).to.equal('https://ads4.krushmedia.com/?c=rtb&m=hb'); }); - it('Returns valid data if array of bids is valid', function () { + + it('Returns general data valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + 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.gdpr).to.not.exist; - expect(data.ccpa).to.not.exist; - let placement = data['placements'][0]; - expect(placement).to.have.keys('key', 'bidId', 'traffic', 'sizes', 'schain', 'bidFloor'); - expect(placement.key).to.equal(783); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.traffic).to.equal(BANNER); - expect(placement.schain).to.be.an('object'); - expect(placement.sizes).to.be.an('array'); + 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 data for mediatype video', function () { - const playerSize = [300, 300]; - bid.mediaTypes = {}; - bid.params.traffic = VIDEO; - bid.mediaTypes[VIDEO] = { - playerSize - }; - serverRequest = spec.buildRequests([bid], bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - let placement = data['placements'][0]; - expect(placement).to.be.an('object'); - expect(placement).to.have.keys('key', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain', 'bidFloor', - 'minduration', 'maxduration', 'mimes', 'protocols', 'startdelay', 'placement', 'skip', - 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'); - expect(placement.traffic).to.equal(VIDEO); - expect(placement.wPlayer).to.equal(playerSize[0]); - expect(placement.hPlayer).to.equal(playerSize[1]); + 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.key).to.be.equal(783); + expect(placement.traffic).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.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + if (placement.traffic === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.traffic) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.wPlayer).to.be.an('number'); + expect(placement.hPlayer).to.be.an('number'); + 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 data for mediatype native', function () { - const native = { - title: { - required: true - }, - body: { - required: true - }, - icon: { - required: true, - size: [64, 64] + it('Returns valid endpoints', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + endpointId: 'testBanner', + }, + userIdAsEids } - }; + ]; - bid.mediaTypes = {}; - bid.params.traffic = NATIVE; - bid.mediaTypes[NATIVE] = native; - serverRequest = spec.buildRequests([bid], bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - let placement = data['placements'][0]; - expect(placement).to.be.an('object'); - expect(placement).to.have.keys('key', 'bidId', 'traffic', 'native', 'schain', 'bidFloor'); - expect(placement.traffic).to.equal(NATIVE); - expect(placement.native).to.equal(native); + let 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.traffic).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.traffic === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.traffic) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.wPlayer).to.be.an('number'); + expect(placement.hPlayer).to.be.an('number'); + 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 () { - bidderRequest.gdprConsent = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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 = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { @@ -156,7 +317,11 @@ describe('KrushmediabBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - dealId: '1' + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let bannerResponses = spec.interpretResponse(banner); @@ -164,15 +329,15 @@ describe('KrushmediabBidAdapter', function () { let 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('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.width).to.equal(300); - expect(dataItem.height).to.equal(250); - expect(dataItem.ad).to.equal('Test'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); + 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('USD'); + 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 () { @@ -186,7 +351,11 @@ describe('KrushmediabBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - dealId: '1' + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let videoResponses = spec.interpretResponse(video); @@ -220,6 +389,10 @@ describe('KrushmediabBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let nativeResponses = spec.interpretResponse(native); @@ -306,6 +479,7 @@ describe('KrushmediabBidAdapter', function () { expect(serverResponses).to.be.an('array').that.is.empty; }); }); + describe('getUserSyncs', function() { it('Should return array of objects with proper sync config , include GDPR', function() { const syncData = spec.getUserSyncs({}, {}, { @@ -315,20 +489,32 @@ describe('KrushmediabBidAdapter', function () { expect(syncData).to.be.an('array').which.is.not.empty; expect(syncData[0]).to.be.an('object') expect(syncData[0].type).to.be.a('string') - expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.krushmedia.com/html?src=pbjs&gdpr=1&gdpr_consent=ALL') + expect(syncData[0].url).to.equal('https://cs.krushmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') }); it('Should return array of objects with proper sync config , include CCPA', function() { const syncData = spec.getUserSyncs({}, {}, {}, { - consentString: '1NNN' + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.krushmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] }); expect(syncData).to.be.an('array').which.is.not.empty; expect(syncData[0]).to.be.an('object') expect(syncData[0].type).to.be.a('string') - expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.krushmedia.com/html?src=pbjs&ccpa_consent=1NNN') + expect(syncData[0].url).to.equal('https://cs.krushmedia.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') }); }); }); diff --git a/test/spec/modules/kueezRtbBidAdapter_spec.js b/test/spec/modules/kueezRtbBidAdapter_spec.js index ebd11885af4..9885aecf395 100644 --- a/test/spec/modules/kueezRtbBidAdapter_spec.js +++ b/test/spec/modules/kueezRtbBidAdapter_spec.js @@ -2,6 +2,16 @@ import {expect} from 'chai'; import { spec as adapter, createDomain, + storage, + getAndSetFirstPartyData, + createFirstPartyData, +} from 'modules/kueezRtbBidAdapter.js'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; +import { hashCode, extractPID, extractCID, @@ -10,12 +20,7 @@ import { setStorageItem, tryParseJSON, getUniqueDealId, -} from 'modules/kueezRtbBidAdapter.js'; -import * as utils from 'src/utils.js'; -import {version} from 'package.json'; -import {useFakeTimers} from 'sinon'; -import {BANNER, VIDEO} from '../../../src/mediaTypes'; -import {config} from '../../../src/config'; +} from '../../../libraries/vidazooUtils/bidderUtils.js'; export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; @@ -88,6 +93,36 @@ const VIDEO_BID = { } } +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': '' + }, + 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'}, +}; + const BIDDER_REQUEST = { 'gdprConsent': { 'consentString': 'consent_string', @@ -101,28 +136,17 @@ const BIDDER_REQUEST = { 'ref': 'https://www.somereferrer.com' }, 'ortb2': { + 'site': { + 'content': { + 'language': 'en' + } + }, 'regs': { 'gpp': 'gpp_string', - 'gpp_sid': [7] + 'gpp_sid': [7], + 'coppa': 0 }, - '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': '' - } - } + 'device': ORTB2_DEVICE, } }; @@ -242,6 +266,7 @@ describe('KueezRtbBidAdapter', function () { describe('build requests', function () { let sandbox; + let createFirstPartyDataStub; before(function () { $$PREBID_GLOBAL$$.bidderSettings = { kueezrtb: { @@ -250,6 +275,10 @@ describe('KueezRtbBidAdapter', function () { }; sandbox = sinon.sandbox.create(); sandbox.stub(Date, 'now').returns(1000); + createFirstPartyDataStub = sandbox.stub(adapter, 'createFirstPartyData').returns({ + pcid: 'pcid', + pcidDate: 1000 + }); }); it('should build video request', function () { @@ -286,6 +315,8 @@ describe('KueezRtbBidAdapter', function () { referrer: 'https://www.somereferrer.com', res: `${window.top.screen.width}x${window.top.screen.height}`, schain: VIDEO_BID.schain, + iiqpcid: 'pcid', + iiqpcidDate: 1000, sizes: ['545x307'], sua: { 'source': 2, @@ -303,6 +334,7 @@ describe('KueezRtbBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + device: ORTB2_DEVICE, uniqueDealId: `${hashUrl}_${Date.now().toString()}`, uqs: getTopWindowQueryParams(), mediaTypes: { @@ -322,7 +354,14 @@ describe('KueezRtbBidAdapter', function () { startdelay: 0 } }, - gpid: '' + gpid: '', + cat: [], + contentLang: 'en', + contentData: [], + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 } }); }); @@ -347,6 +386,8 @@ describe('KueezRtbBidAdapter', function () { auctionId: 'auction_id', bidRequestsCount: 4, bidderRequestsCount: 3, + iiqpcid: 'pcid', + iiqpcidDate: 1000, bidderWinsCount: 1, bidderTimeout: 3000, bidderRequestId: '1fdb5ff1b6eaa7', @@ -367,6 +408,7 @@ describe('KueezRtbBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + device: ORTB2_DEVICE, url: 'https%3A%2F%2Fwww.greatsite.com', referrer: 'https://www.somereferrer.com', cb: 1000, @@ -384,6 +426,13 @@ describe('KueezRtbBidAdapter', function () { uqs: getTopWindowQueryParams(), 'ext.param1': 'loremipsum', 'ext.param2': 'dolorsitamet', + cat: [], + contentLang: 'en', + contentData: [], + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 } }); }); @@ -399,7 +448,7 @@ describe('KueezRtbBidAdapter', function () { expect(result).to.deep.equal([{ type: 'iframe', - url: 'https://sync.kueezrtb.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + url: 'https://sync.kueezrtb.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' }]); }); @@ -407,7 +456,7 @@ describe('KueezRtbBidAdapter', function () { const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ type: 'iframe', - url: 'https://sync.kueezrtb.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + url: 'https://sync.kueezrtb.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' }]); }); @@ -415,11 +464,22 @@ describe('KueezRtbBidAdapter', function () { const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ - 'url': 'https://sync.kueezrtb.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'url': 'https://sync.kueezrtb.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', 'type': 'image' }]); }); + it('should have valid user sync with coppa on response', function () { + config.setConfig({ + coppa: 1 + }); + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.kueezrtb.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1' + }]); + }); + it('should generate url with consent data', function () { const gdprConsent = { gdprApplies: true, @@ -434,7 +494,7 @@ describe('KueezRtbBidAdapter', function () { const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); expect(result).to.deep.equal([{ - 'url': 'https://sync.kueezrtb.com/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&gpp=gpp_string&gpp_sid=7', + 'url': 'https://sync.kueezrtb.com/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' }]); }); @@ -526,8 +586,6 @@ describe('KueezRtbBidAdapter', function () { switch (idSystemProvider) { case 'lipb': return {lipbid: id}; - case 'parrableId': - return {eid: id}; case 'id5id': return {uid: id}; default: @@ -580,13 +638,13 @@ describe('KueezRtbBidAdapter', function () { const key = 'myKey'; let uniqueDealId; beforeEach(() => { - uniqueDealId = getUniqueDealId(key, 0); + uniqueDealId = getUniqueDealId(storage, key, 0); }) it('should get current unique deal id', function (done) { // waiting some time so `now` will become past setTimeout(() => { - const current = getUniqueDealId(key); + const current = getUniqueDealId(storage, key); expect(current).to.be.equal(uniqueDealId); done(); }, 200); @@ -594,7 +652,7 @@ describe('KueezRtbBidAdapter', function () { it('should get new unique deal id on expiration', function (done) { setTimeout(() => { - const current = getUniqueDealId(key, 100); + const current = getUniqueDealId(storage, key, 100); expect(current).to.not.be.equal(uniqueDealId); done(); }, 200) @@ -618,8 +676,8 @@ describe('KueezRtbBidAdapter', function () { shouldAdvanceTime: true, now }); - setStorageItem('myKey', 2020); - const {value, created} = getStorageItem('myKey'); + 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'); @@ -630,7 +688,7 @@ describe('KueezRtbBidAdapter', function () { it('should get external stored value', function () { const value = 'superman' window.localStorage.setItem('myExternalKey', value); - const item = getStorageItem('myExternalKey'); + const item = getStorageItem(storage, 'myExternalKey'); expect(item).to.be.equal(value); }); @@ -647,4 +705,36 @@ describe('KueezRtbBidAdapter', function () { expect(parsed).to.be.equal(value); }); }); + + describe('First party data', () => { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + kueezrtb: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + storage.removeDataFromLocalStorage('_iiq_fdata'); + }) + + it('should create first party data', function () { + const data = createFirstPartyData(); + expect(data).to.have.property('pcid'); + expect(data).to.have.property('pcidDate'); + }); + + it('should get and set first party data', function () { + storage.removeDataFromLocalStorage('_iiq_fdata'); + + const data = getAndSetFirstPartyData(); + expect(data).to.have.property('pcid'); + expect(data).to.have.property('pcidDate'); + + const stored = storage.getDataFromLocalStorage('_iiq_fdata'); + const parsed = tryParseJSON(stored); + expect(parsed).to.deep.equal(data); + }); + }); }); diff --git a/test/spec/modules/lane4BidAdapter_spec.js b/test/spec/modules/lane4BidAdapter_spec.js new file mode 100644 index 00000000000..49dc3aad6a4 --- /dev/null +++ b/test/spec/modules/lane4BidAdapter_spec.js @@ -0,0 +1,322 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/lane4BidAdapter.js'; +import * as utils from '../../../src/utils.js'; + +describe('lane4 adapter', function () { + let bannerRequest, nativeRequest; + let bannerResponse, nativeResponse, invalidBannerResponse, invalidNativeResponse; + + beforeEach(function () { + bannerRequest = [ + { + bidder: 'lane4', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placement_id: 110044, + bid_floor: 0.5 + } + } + ]; + nativeRequest = [ + { + bidder: 'lane4', + mediaTypes: { + native: { + title: { required: true, len: 100 }, + image: { required: true, sizes: [300, 250] }, + sponsored: { required: false }, + clickUrl: { required: true }, + desc: { required: true }, + icon: { required: false, sizes: [50, 50] }, + cta: { required: false } + } + }, + params: { + placement_id: 5551, + bid_floor: 1 + } + } + ]; + bannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': "", + 'adomain': ['google.com'], + 'iurl': 'https://cdn.lane4.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'lane4', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + nativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': '{\"native\":{\"ver\":\"1.1\",\"assets\": [{\"id\":1,\"required\":1,\"title\":{\"text\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":2,\"required\":1,\"img\":{\"url\":\"https://cdn.lane4.com/1_368852_0.png\",\"w\":500,\"h\":300,\"type\":\"3\"}},{\"id\":3,\"required\":0,\"data\":{\"value\":\"Diabetes In Control. A free weekly diabetes newsletter for Medical Professionals.\"}},{\"id\":4,\"required\":1,\"data\":{\"value\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":6,\"required\":1,\"data\":{\"value\":\"URL\"}}],\"link\":{\"url\":\"https://r.lane4.com/adx-rtb-d/servlet/WebF_AdManager.AdLinkManager?qs=H4sIAAAAAAAAAx2U2QHDIAxDVwKDr3F84P1HqNLPtMSRpSeG9RiPXH+5a474KzO/47YX7UoP50m61fLujlNjb76/8ZiblkimHq5nL/ZRedp3031x1tnk55LjSNN6h9/Zq+qmaLLuWTl74m1ZJKnb+m2OtQm/3L4sb933pM92qMOgjJ41MYmPXKnndRVKs+9bfSEumoZIFpTXuXbCP+WXuzl725E3O+9odi5OJrnBzhwjx9+UnFN3nTNt1/HY5aeljKtvZYpoJHNXr8BWa8ysKQY7ZmNA3DHK2qRwY7+zLu+xm9z5eheJ4Pv2usSptvO3p7JHrnXn0T5yVWdccp9Yz7hhoz2iu2zqsXsGFZ9hh14J6yU4TkJ0BgnOY8tY3tS+n2qsw7xZfKuanSNbAo+9nkJ83i20+FwhfbJeDVOllXsdxmDWauYcSRgS9+yG5qHwUDjAxxA0iZnOjlsnI+y09+ATeTEwbAVGgp0Qu/ceP0kjUvpu1Ty7O9MoegfrmLPxdjUh3mJL+XhARby+Ax8iBckf6BQdn9W+DMlvmlzYLuLlIy7YociFOIvXvEiYYCMboVk8BLHbnw3Zmr5us3xbjtXL67L96F15acJXkM5BOmTaUbBkYGdCI+Et8XmlpbuE3xVQwmxryc2y4wP3ByuuP8GogPZz8OpPaBv8diWWUTrC2nnLhdNUrJRTKc9FepDvwHTDwfbbMCTSb4LhUIFkyFrw/i7GtkPi6NCCai6N47TgNsTnzZWRoVtOSLq7FsLiF29y0Gj0GHVPVYG3QOPS7Swc3UuiFAQZJx3YvpHA2geUgVBASMEL4vcDi2Dw3NPtBSC4EQEvH/uMILu6WyUwraywTeVpoqoHTqOoD84FzReKoWemJy6jyuiBieGlQIe6wY2elTaMOwEUFF5NagzPj6nauc0+aXzQN3Q72hxFAgtfORK60RRAHYZLYymIzSJcXLgRFsqrb1UoD+5Atq7TWojaLTfOyUvH9EeJvZEOilQAXrf/ALoI8ZhABQAA\"},\"imptrackers\":[\"https://i.lane4.com/adx-rtb-d/servlet/WebF_AdManager.ImpCounter?price=${AUCTION_PRICE}&ids=5551,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=https%3A%2F%2Fqa-jboss.audiencelogy.com%2Ftn_native_prod.html\",\"https://i.lane4.com/adx-rtb-d/servlet/WebF_AdManager.ImpTracker?price=${AUCTION_PRICE}&ids=5551,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=\",\"https://rtb-east.lane4.com:9001/beacon?uid=44636f6605b06ec6d4389d6efb7e5054&cc=468132&fccap=5&nid=1\"]}}', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.lane4.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'lane4', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidBannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': 'invalid response', + 'adomain': ['google.com'], + 'iurl': 'https://cdn.lane4.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'lane4', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidNativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': 'invalid response', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.lane4.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'lane4', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + }); + + describe('validations', function () { + it('isBidValid : placement_id is passed', function () { + let bid = { + bidder: 'lane4', + params: { + placement_id: 110044 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(true); + }); + it('isBidValid : placement_id is not passed', function () { + let bid = { + bidder: 'lane4', + params: { + width: 300, + height: 250, + domain: '', + bid_floor: 0.5 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + }); + describe('Validate Banner Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(bannerRequest), + bidRequest = spec.buildRequests(bannerRequest); + expect(bannerRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(bannerRequest); + expect(_Request.url).to.equal('https://rtb.lane4.io/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(bannerRequest[0].bidId); + expect(data[0].placementId).to.equal(110044); + }); + it('Validate bid request : ad size', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].imp[0].banner).to.be.a('object'); + expect(data[0].imp[0].banner.w).to.equal(300); + expect(data[0].imp[0].banner.h).to.equal(250); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(bannerRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate banner response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(bannerRequest); + let bResponse = spec.interpretResponse(bannerResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(bannerResponse.body.seatbid[0].bid[0].impid); + expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('banner'); + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['google.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId); + }); + it('Invalid bid response check ', function () { + let bRequest = spec.buildRequests(bannerRequest); + let response = spec.interpretResponse(invalidBannerResponse, bRequest); + expect(response[0].ad).to.equal('invalid response'); + }); + }); + describe('Validate Native Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(nativeRequest), + bidRequest = spec.buildRequests(nativeRequest); + expect(nativeRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(nativeRequest); + expect(_Request.url).to.equal('https://rtb.lane4.io/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(nativeRequest[0].bidId); + expect(data[0].placementId).to.equal(5551); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(nativeRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate native response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(nativeRequest); + let bResponse = spec.interpretResponse(nativeResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(nativeResponse.body.seatbid[0].bid[0].impid); + // expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + // expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('native'); + expect(bResponse[0].native.clickUrl).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.impressionTrackers).to.be.an('array').with.length.above(0); + expect(bResponse[0].native.title).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.image.url).to.be.a('string').and.not.be.empty; + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['www.diabetesincontrol.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(nativeResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(nativeResponse.body.seatbid[0].bid[0].dealId); + }); + }); + describe('GPP and coppa', function () { + it('Request params check with GPP Consent', function () { + let bidderReq = { gppConsent: { gppString: 'gpp-string-test', applicableSections: [5] } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-string-test'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it('Request params check with GPP Consent read from ortb2', function () { + let bidderReq = { + ortb2: { + regs: { + gpp: 'gpp-test-string', + gpp_sid: [5] + } + } + }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-test-string'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it(' Bid request should have coppa flag if its true', () => { + let bidderReq = { ortb2: { regs: { coppa: 1 } } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.coppa).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/lassoBidAdapter_spec.js b/test/spec/modules/lassoBidAdapter_spec.js index ad4040c0452..94ec86aba69 100644 --- a/test/spec/modules/lassoBidAdapter_spec.js +++ b/test/spec/modules/lassoBidAdapter_spec.js @@ -3,6 +3,7 @@ import { spec } from 'modules/lassoBidAdapter.js'; import { server } from '../../mocks/xhr'; const ENDPOINT_URL = 'https://trc.lhmos.com/prebid'; +const GET_IUD_URL = 'https://secure.adnxs.com/getuid?'; const bid = { bidder: 'lasso', @@ -62,14 +63,14 @@ describe('lassoBidAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); it('should return true when there are extra params', function () { - const bid = Object.assign({}, bid, { + const invalidBid = Object.assign({}, bid, { params: { adUnitId: 123456, zone: 1, publisher: 'test' } }) - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(spec.isBidRequestValid(invalidBid)).to.equal(true); }); it('should return false when there are no params', function () { const invalidBid = { ...bid }; @@ -78,7 +79,7 @@ describe('lassoBidAdapter', function () { }); }); - describe('buildRequests', function () { + describe('buildRequests with standard flow', function () { let validBidRequests, bidRequest; before(() => { validBidRequests = spec.buildRequests([bid], bidderRequest); @@ -97,6 +98,177 @@ describe('lassoBidAdapter', function () { expect(bidRequest.method).to.exist; expect(bidRequest.method).to.equal('GET'); }); + + it('should send request to get uid and trc via get request', () => { + expect(bidRequest.data.test).to.equal(false) + expect(bidRequest.method).to.equal('GET'); + expect(bidRequest.url).to.equal(GET_IUD_URL + ENDPOINT_URL + '/request'); + }); + }); + + describe('buildRequests with dgid', function () { + let validBidRequests, bidRequest; + before(() => { + const updateBidParams = Object.assign({}, bid, { + params: { + adUnitId: 123456, + dgid: '123' + } + }); + validBidRequests = spec.buildRequests([updateBidParams], bidderRequest); + expect(validBidRequests).to.be.an('array').that.is.not.empty; + bidRequest = validBidRequests[0]; + }) + + it('Returns valid bidRequest', function () { + expect(bidRequest).to.exist; + expect(bidRequest.method).to.exist; + expect(bidRequest.url).to.exist; + expect(bidRequest.data).to.exist; + }); + + it('Returns GET method', function() { + expect(bidRequest.method).to.exist; + expect(bidRequest.method).to.equal('GET'); + }); + + it('should send request to trc via get request with dgid', () => { + expect(bidRequest.data.test).to.equal(false) + expect(bidRequest.method).to.equal('GET'); + expect(bidRequest.url).to.equal(ENDPOINT_URL + '/request'); + }); + }); + + describe('buildRequests with npi', function () { + let validBidRequests, bidRequest; + before(() => { + const updateBidParams = Object.assign({}, bid, { + params: { + adUnitId: 123456, + npi: '123' + } + }); + validBidRequests = spec.buildRequests([updateBidParams], bidderRequest); + expect(validBidRequests).to.be.an('array').that.is.not.empty; + bidRequest = validBidRequests[0]; + }) + + it('Returns valid bidRequest', function () { + expect(bidRequest).to.exist; + expect(bidRequest.method).to.exist; + expect(bidRequest.url).to.exist; + expect(bidRequest.data).to.exist; + }); + + it('Returns GET method', function() { + expect(bidRequest.method).to.exist; + expect(bidRequest.method).to.equal('GET'); + }); + + it('should send request to trc via get request with npi', () => { + expect(bidRequest.data.test).to.equal(false) + expect(bidRequest.method).to.equal('GET'); + expect(bidRequest.url).to.equal(ENDPOINT_URL + '/request'); + }); + }); + + describe('buildRequests with test npi', function () { + let validBidRequests, bidRequest; + before(() => { + const updateBidParams = Object.assign({}, bid, { + params: { + adUnitId: 123456, + testNPI: '123' + } + }); + validBidRequests = spec.buildRequests([updateBidParams], bidderRequest); + expect(validBidRequests).to.be.an('array').that.is.not.empty; + bidRequest = validBidRequests[0]; + }) + + it('Returns valid bidRequest', function () { + expect(bidRequest).to.exist; + expect(bidRequest.method).to.exist; + expect(bidRequest.url).to.exist; + expect(bidRequest.data).to.exist; + }); + + it('Returns GET method', function() { + expect(bidRequest.method).to.exist; + expect(bidRequest.method).to.equal('GET'); + }); + + it('should send request to trc via get request with npi and test param', () => { + expect(bidRequest.data.test).to.equal(true) + expect(bidRequest.method).to.equal('GET'); + expect(bidRequest.url).to.equal(ENDPOINT_URL + '/request'); + }); + }); + + describe('buildRequests with test dgid', function () { + let validBidRequests, bidRequest; + before(() => { + const updateBidParams = Object.assign({}, bid, { + params: { + adUnitId: 123456, + testDGID: '123' + } + }); + validBidRequests = spec.buildRequests([updateBidParams], bidderRequest); + expect(validBidRequests).to.be.an('array').that.is.not.empty; + bidRequest = validBidRequests[0]; + }) + + it('Returns valid bidRequest', function () { + expect(bidRequest).to.exist; + expect(bidRequest.method).to.exist; + expect(bidRequest.url).to.exist; + expect(bidRequest.data).to.exist; + }); + + it('Returns GET method', function() { + expect(bidRequest.method).to.exist; + expect(bidRequest.method).to.equal('GET'); + }); + + it('should send request to trc via get request with dgid and test param', () => { + expect(bidRequest.data.test).to.equal(true) + expect(bidRequest.method).to.equal('GET'); + expect(bidRequest.url).to.equal(ENDPOINT_URL + '/request'); + }); + }); + + describe('buildRequests with npi hash', function () { + let validBidRequests, bidRequest; + before(() => { + const updateBidParams = Object.assign({}, bid, { + params: { + adUnitId: 123456, + npiHash: '123' + } + }); + validBidRequests = spec.buildRequests([updateBidParams], bidderRequest); + expect(validBidRequests).to.be.an('array').that.is.not.empty; + bidRequest = validBidRequests[0]; + }) + + it('Returns valid bidRequest', function () { + expect(bidRequest).to.exist; + expect(bidRequest.method).to.exist; + expect(bidRequest.url).to.exist; + expect(bidRequest.data).to.exist; + }); + + it('Returns GET method', function() { + expect(bidRequest.method).to.exist; + expect(bidRequest.method).to.equal('GET'); + }); + + it('should send request to trc via get request with npi', () => { + expect(bidRequest.data.test).to.equal(false) + expect(bidRequest.method).to.equal('GET'); + expect(bidRequest.url).to.equal(ENDPOINT_URL + '/request'); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/limelightDigitalBidAdapter_spec.js b/test/spec/modules/limelightDigitalBidAdapter_spec.js index 9e8a00959d4..b13beb26d28 100644 --- a/test/spec/modules/limelightDigitalBidAdapter_spec.js +++ b/test/spec/modules/limelightDigitalBidAdapter_spec.js @@ -17,9 +17,6 @@ describe('limelightDigitalAdapter', function () { custom4: 'custom4', custom5: 'custom5' }, - refererInfo: { - page: 'https://publisher.com/page1' - }, placementCode: 'placement_0', auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', mediaTypes: { @@ -29,7 +26,11 @@ describe('limelightDigitalAdapter', function () { }, ortb2Imp: { ext: { - tid: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', + gpid: '/1111/homepage#300x250', + tid: '738d5915-6651-43b9-9b6b-d50517350917', + data: { + 'pbadslot': '/1111/homepage#300x250' + } } }, userIdAsEids: [ @@ -68,15 +69,16 @@ describe('limelightDigitalAdapter', function () { custom4: 'custom4', custom5: 'custom5' }, - refererInfo: { - page: 'https://publisher.com/page2' - }, placementCode: 'placement_1', auctionId: '482f88de-29ab-45c8-981a-d25e39454a34', sizes: [[350, 200]], ortb2Imp: { ext: { - tid: '068867d1-46ec-40bb-9fa0-e24611786fb4', + gpid: '/1111/homepage#300x250', + tid: '738d5915-6651-43b9-9b6b-d50517350917', + data: { + 'pbadslot': '/1111/homepage#300x250' + } } }, userIdAsEids: [ @@ -121,15 +123,16 @@ describe('limelightDigitalAdapter', function () { custom4: 'custom4', custom5: 'custom5' }, - refererInfo: { - page: 'https://publisher.com/page3' - }, placementCode: 'placement_2', auctionId: 'e4771143-6aa7-41ec-8824-ced4342c96c8', sizes: [[800, 600]], ortb2Imp: { ext: { + gpid: '/1111/homepage#300x250', tid: '738d5915-6651-43b9-9b6b-d50517350917', + data: { + 'pbadslot': '/1111/homepage#300x250' + } } }, userIdAsEids: [ @@ -171,9 +174,6 @@ describe('limelightDigitalAdapter', function () { custom4: 'custom4', custom5: 'custom5' }, - refererInfo: { - page: 'https://publisher.com/page4' - }, placementCode: 'placement_2', auctionId: 'e4771143-6aa7-41ec-8824-ced4342c96c8', video: { @@ -181,7 +181,11 @@ describe('limelightDigitalAdapter', function () { }, ortb2Imp: { ext: { + gpid: '/1111/homepage#300x250', tid: '738d5915-6651-43b9-9b6b-d50517350917', + data: { + 'pbadslot': '/1111/homepage#300x250' + } } }, userIdAsEids: [ @@ -218,6 +222,9 @@ describe('limelightDigitalAdapter', function () { architecture: 'arm' } } + }, + refererInfo: { + page: 'testPage' } } const serverRequests = spec.buildRequests([bid1, bid2, bid3, bid4], bidderRequest) @@ -243,7 +250,10 @@ describe('limelightDigitalAdapter', function () { 'deviceHeight', 'secure', 'adUnits', - 'sua' + 'sua', + 'page', + 'ortb2', + 'refererInfo' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -263,7 +273,7 @@ describe('limelightDigitalAdapter', function () { 'custom3', 'custom4', 'custom5', - 'page' + 'ortb2Imp' ); expect(adUnit.id).to.be.a('number'); expect(adUnit.bidId).to.be.a('string'); @@ -277,12 +287,15 @@ describe('limelightDigitalAdapter', function () { expect(adUnit.custom3).to.be.a('string'); expect(adUnit.custom4).to.be.a('string'); expect(adUnit.custom5).to.be.a('string'); - expect(adUnit.page).to.be.a('string'); + expect(adUnit.ortb2Imp).to.be.an('object'); }) expect(data.sua.browsers).to.be.a('array'); expect(data.sua.platform).to.be.a('array'); expect(data.sua.mobile).to.be.a('number'); expect(data.sua.architecture).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.page).to.be.equal('testPage'); + expect(data.ortb2).to.be.an('object'); }) }) it('Returns valid URL', function () { @@ -298,6 +311,17 @@ describe('limelightDigitalAdapter', function () { const serverRequests = spec.buildRequests([]) expect(serverRequests).to.be.an('array').that.is.empty }) + it('Returns request with page field value from ortb2 object if ortb2 has page field', function () { + bidderRequest.ortb2.site = { + page: 'testSitePage' + } + const serverRequests = spec.buildRequests([bid1], bidderRequest) + expect(serverRequests).to.have.lengthOf(1) + serverRequests.forEach(serverRequest => { + expect(serverRequest.data.page).to.be.a('string'); + expect(serverRequest.data.page).to.be.equal('testSitePage'); + }) + }) }) describe('interpretBannerResponse', function () { let resObject = { @@ -716,5 +740,5 @@ function validateAdUnit(adUnit, bid) { expect(adUnit.publisherId).to.equal(bid.params.publisherId); expect(adUnit.userIdAsEids).to.deep.equal(bid.userIdAsEids); expect(adUnit.supplyChain).to.deep.equal(bid.schain); - expect(adUnit.page).to.equal(bid.refererInfo.page); + expect(adUnit.ortb2Imp).to.deep.equal(bid.ortb2Imp); } diff --git a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js index e833440bf03..9f30c6e60f0 100644 --- a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js +++ b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js @@ -2,8 +2,9 @@ import liAnalytics from 'modules/liveIntentAnalyticsAdapter'; import { expect } from 'chai'; import { server } from 'test/mocks/xhr.js'; import { auctionManager } from 'src/auctionManager.js'; -import {expectEvents} from '../../helpers/analytics.js'; import { EVENTS } from 'src/constants.js'; +import { config } from 'src/config.js'; +import { BID_WON_EVENT, AUCTION_INIT_EVENT, BID_WON_EVENT_UNDEFINED, AUCTION_INIT_EVENT_NOT_LI } from '../../fixtures/liveIntentAuctionEvents'; let utils = require('src/utils'); let refererDetection = require('src/refererDetection'); @@ -14,13 +15,24 @@ let clock; let now = new Date(); let events = require('src/events'); -let auctionId = '99abbc81-c1f1-41cd-8f25-f7149244c897' + +const USERID_CONFIG = [ + { + 'name': 'liveIntentId', + 'params': { + 'liCollectConfig': { + 'appId': 'a123' + } + } + } +]; const configWithSamplingAll = { provider: 'liveintent', options: { bidWonTimeout: 2000, - sampling: 1 + sampling: 1, + sendAuctionInitEvents: true } } @@ -28,291 +40,123 @@ const configWithSamplingNone = { provider: 'liveintent', options: { bidWonTimeout: 2000, - sampling: 0 + sampling: 0, + sendAuctionInitEvents: true } } -let args = { - auctionId: auctionId, - timestamp: 1660915379703, - auctionEnd: 1660915381635, - adUnits: [ - { - code: 'ID_Bot100AdJ1', - mediaTypes: { - banner: { - sizes: [ - [ - 300, - 250 - ], - [ - 320, - 50 - ] - ] - } - }, - ortb2Imp: { - gpid: '/777/test/home/ID_Bot100AdJ1', - ext: { - data: { - aupName: '/777/test/home/ID_Bot100AdJ1', - adserver: { - name: 'gam', - adslot: '/777/test/home/ID_Bot100AdJ1' - }, - pbadslot: '/777/test/home/ID_Bot100AdJ1' - }, - gpid: '/777/test/home/ID_Bot100AdJ1' - } - }, - bids: [ - { - bidder: 'testBidder', - params: { - siteId: 321218, - zoneId: 1732558, - position: 'bug', - accountId: 10777 - }, - userIdAsEids: [ - { - source: 'source1.com', - uids: [ - { - id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', - atype: 1, - ext: { - linkType: 2 - } - } - ] - }, - { - source: 'source2.com', - uids: [ - { - id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', - atype: 1, - ext: { - linkType: 2 - } - } - ] - } - ] - }, - { - bidder: 'testBidder2', - params: { - adSlot: '2926251', - publisherId: '159423' - }, - userIdAsEids: [ - { - source: 'source1.com', - uids: [ - { - id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', - atype: 1, - ext: { - linkType: 2 - } - } - ] - } - ] - } - ] - } - ], - bidderRequests: [ - { - bidderCode: 'tripl_ss1', - auctionId: '8e5a5eda-a7dc-49a3-bc7f-654fc', - bidderRequestId: '953fe1ee8a1645', - uniquePbsTid: '0da1f980-8351-415d-860d-ebbdb4274179', - auctionStart: 1660915379703 - }, - { - bidderCode: 'tripl_ss2', - auctionId: '8e5a5eda-a7dc-49a3-bc7f-6ca682ae893c', - bidderRequestId: '953fe1ee8a164e', - uniquePbsTid: '0da1f980-8351-415d-860d-ebbdb4274180', - auctionStart: 1660915379703 - } - ], - bidsReceived: [ - { - adUnitCode: 'ID_Bot100AdJ1', - timeToRespond: 824, - cpm: 0.447, - currency: 'USD', - ttl: 300, - bidder: 'testBidder' - } - ], - winningBids: [] -} - -let winningBids = [ - { - adUnitCode: 'ID_Bot100AdJ1', - timeToRespond: 824, - cpm: 0.447, - currency: 'USD', - ttl: 300, - bidder: 'testBidder' +const configWithNoAuctionInit = { + provider: 'liveintent', + options: { + bidWonTimeout: 2000, + sampling: 1, + sendAuctionInitEvents: false } -]; - -let expectedEvent = { - instanceId: instanceId, - url: url, - bidsReceived: [ - { - adUnitCode: 'ID_Bot100AdJ1', - timeToRespond: 824, - cpm: 0.447, - currency: 'USD', - ttl: 300, - bidder: 'testBidder' - } - ], - auctionStart: 1660915379703, - auctionEnd: 1660915381635, - adUnits: [ - { - code: 'ID_Bot100AdJ1', - mediaType: 'banner', - sizes: [ - { - w: 300, - h: 250 - }, - { - w: 320, - h: 50 - } - ], - ortb2Imp: { - gpid: '/777/test/home/ID_Bot100AdJ1', - ext: { - data: { - aupName: '/777/test/home/ID_Bot100AdJ1', - adserver: { - name: 'gam', - adslot: '/777/test/home/ID_Bot100AdJ1' - }, - pbadslot: '/777/test/home/ID_Bot100AdJ1' - }, - gpid: '/777/test/home/ID_Bot100AdJ1' - } - } - } - ], - winningBids: [ - { - adUnitCode: 'ID_Bot100AdJ1', - timeToRespond: 824, - cpm: 0.447, - currency: 'USD', - ttl: 300, - bidder: 'testBidder' - } - ], - auctionId: auctionId, - userIds: [ - { - source: 'source1.com', - uids: [ - { - id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', - atype: 1, - ext: { - linkType: 2 - } - } - ] - }, - { - source: 'source2.com', - uids: [ - { - id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', - atype: 1, - ext: { - linkType: 2 - } - } - ] - } - ], - bidders: [ - { - bidder: 'testBidder', - params: { - siteId: 321218, - zoneId: 1732558, - position: 'bug', - accountId: 10777 - } - }, - { - bidder: 'testBidder2', - params: { - adSlot: '2926251', - publisherId: '159423' - } - } - ] -}; +} describe('LiveIntent Analytics Adapter ', () => { beforeEach(function () { sandbox = sinon.sandbox.create(); sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG); + sandbox.stub(utils, 'generateUUID').returns(instanceId); + sandbox.stub(refererDetection, 'getRefererInfo').returns({page: url}); + sandbox.stub(auctionManager.index, 'getAuction').withArgs({auctionId: AUCTION_INIT_EVENT.auctionId}).returns({ + getBidRequests: () => AUCTION_INIT_EVENT.bidderRequests, + getAuctionStart: () => AUCTION_INIT_EVENT.timestamp + }); clock = sandbox.useFakeTimers(now.getTime()); }); afterEach(function () { liAnalytics.disableAnalytics(); sandbox.restore(); clock.restore(); + window.liTreatmentRate = undefined + window.liModuleEnabled = undefined }); it('request is computed and sent correctly when sampling is 1', () => { liAnalytics.enableAnalytics(configWithSamplingAll); - sandbox.stub(utils, 'generateUUID').returns(instanceId); - sandbox.stub(refererDetection, 'getRefererInfo').returns({page: url}); - sandbox.stub(auctionManager.index, 'getAuction').withArgs(auctionId).returns({ getWinningBids: () => winningBids }); - events.emit(EVENTS.AUCTION_END, args); - clock.tick(2000); + + events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=y&aun=2') - let requestBody = JSON.parse(server.requests[0].requestBody); - expect(requestBody).to.deep.equal(expectedEvent); + events.emit(EVENTS.BID_WON, BID_WON_EVENT); + expect(server.requests.length).to.equal(2); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&liip=y'); }); - it('track is called', () => { - sandbox.stub(liAnalytics, 'track'); + it('request is computed and sent correctly when sampling is 1 and liModule is enabled', () => { + window.liModuleEnabled = true liAnalytics.enableAnalytics(configWithSamplingAll); - expectEvents().to.beTrackedBy(liAnalytics.track); - }) - it('no request is computed when sampling is 0', () => { - liAnalytics.enableAnalytics(configWithSamplingNone); - sandbox.stub(utils, 'generateUUID').returns(instanceId); - sandbox.stub(refererDetection, 'getRefererInfo').returns({page: url}); - sandbox.stub(auctionManager.index, 'getAuction').withArgs(auctionId).returns({ getWinningBids: () => winningBids }); - events.emit(EVENTS.AUCTION_END, args); - clock.tick(2000); + events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&me=y&liip=y&aun=2') + + events.emit(EVENTS.BID_WON, BID_WON_EVENT); + expect(server.requests.length).to.equal(2); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&me=y&liip=y'); + }); + + it('request is computed and sent correctly when sampling is 1 and liModule is disabled', () => { + window.liModuleEnabled = false + liAnalytics.enableAnalytics(configWithSamplingAll); + + events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&me=n&liip=y&aun=2') + + events.emit(EVENTS.BID_WON, BID_WON_EVENT); + expect(server.requests.length).to.equal(2); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&me=n&liip=y'); + }); + + it('request is computed and sent correctly when sampling is 1 and should forward the correct liTreatmentRate', () => { + window.liTreatmentRate = 0.95 + liAnalytics.enableAnalytics(configWithSamplingAll); + + events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&tr=0.95&liip=y&aun=2') + + events.emit(EVENTS.BID_WON, BID_WON_EVENT); + expect(server.requests.length).to.equal(2); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&tr=0.95&liip=y'); + }); + + it('not send any events on auction init if disabled in settings', () => { + liAnalytics.enableAnalytics(configWithNoAuctionInit); + + events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); expect(server.requests.length).to.equal(0); }); - it('track is not called', () => { - sandbox.stub(liAnalytics, 'track'); + it('not send fields that are undefined', () => { + liAnalytics.enableAnalytics(configWithSamplingAll); + + events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); + events.emit(EVENTS.BID_WON, BID_WON_EVENT_UNDEFINED); + + expect(server.requests.length).to.equal(2); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=y'); + }); + + it('liip should be n if there is no source or provider in userIdAsEids have the value liveintent.com', () => { + liAnalytics.enableAnalytics(configWithSamplingAll); + + events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT_NOT_LI); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=n&aun=2'); + }); + + it('no request is computed when sampling is 0', () => { liAnalytics.enableAnalytics(configWithSamplingNone); - sinon.assert.callCount(liAnalytics.track, 0); - }) + + events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); + events.emit(EVENTS.BID_WON, BID_WON_EVENT); + + expect(server.requests.length).to.equal(0); + }); }); diff --git a/test/spec/modules/liveIntentExternalIdSystem_spec.js b/test/spec/modules/liveIntentExternalIdSystem_spec.js new file mode 100644 index 00000000000..86e39e76f4e --- /dev/null +++ b/test/spec/modules/liveIntentExternalIdSystem_spec.js @@ -0,0 +1,662 @@ +import { liveIntentExternalIdSubmodule, resetSubmodule } from 'libraries/liveIntentId/externalIdSystem.js'; +import { DEFAULT_TREATMENT_RATE } from 'libraries/liveIntentId/shared.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler, coppaDataHandler } from '../../../src/adapterManager.js'; +import * as refererDetection from '../../../src/refererDetection.js'; +const DEFAULT_AJAX_TIMEOUT = 5000 +const PUBLISHER_ID = '89899'; +const defaultConfigParams = { params: {publisherId: PUBLISHER_ID, fireEventDelay: 1} }; + +describe('LiveIntentExternalId', function() { + let uspConsentDataStub; + let gdprConsentDataStub; + let gppConsentDataStub; + let coppaConsentDataStub; + let refererInfoStub; + let randomStub; + + beforeEach(function() { + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + coppaConsentDataStub = sinon.stub(coppaDataHandler, 'getCoppa'); + refererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + randomStub = sinon.stub(Math, 'random').returns(0.6); + }); + + afterEach(function() { + uspConsentDataStub.restore(); + gdprConsentDataStub.restore(); + gppConsentDataStub.restore(); + coppaConsentDataStub.restore(); + refererInfoStub.restore(); + randomStub.restore(); + window.liQHub = []; // reset + window.liModuleEnabled = undefined; // reset + window.liTreatmentRate = undefined; // reset + resetSubmodule(); + }); + + it('should use appId in integration when both appId and distributorId are provided', function() { + const configParams = { + params: { + ...defaultConfigParams.params, + distributorId: 'did-1111', + liCollectConfig: { + appId: 'a-0001' + }, + emailHash: '123' + } + } + liveIntentExternalIdSubmodule.decode({}, configParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { appId: 'a-0001', publisherId: '89899', type: 'application' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }, + { + clientRef: {}, + sourceEvent: { emailHash: '123' }, + type: 'collect' + }]) + }); + + it('should fire an event and resolve when getId and include the privacy settings into the resolution request', function () { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1, 2] + }) + liveIntentExternalIdSubmodule.getId(defaultConfigParams).callback(() => {}); + + const expectedConsent = { gdpr: { consentString: 'consentDataString', gdprApplies: true }, gpp: { applicableSections: [1, 2], consentString: 'gppConsentDataString' }, usPrivacy: { consentString: '1YNY' } } + + expect(window.liQHub).to.have.length(2) + + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: expectedConsent, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + const resolveCommand = window.liQHub[1] + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'nonId' ], + type: 'resolve' + }) + }); + + it('should fire an event when getId and a hash is provided', function() { + liveIntentExternalIdSubmodule.getId({ params: { + ...defaultConfigParams.params, + emailHash: '58131bc547fb87af94cebdaf3102321f' + }}).callback(() => {}); + + expect(window.liQHub).to.have.length(3) + + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + expect(window.liQHub[1]).to.eql({ + clientRef: {}, + sourceEvent: { emailHash: '58131bc547fb87af94cebdaf3102321f' }, + type: 'collect' + }) + + const resolveCommand = window.liQHub[2] + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'nonId' ], + type: 'resolve' + }) + }); + + it('should have the same data after call decode when appId, disrtributorId and sourceEvent is absent', function() { + liveIntentExternalIdSubmodule.decode({}, { + params: { + ...defaultConfigParams.params, + distributorId: undefined + } + }); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: undefined, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }]) + }); + + it('should have the same data after call decode when appId and sourceEvent is present', function() { + const configParams = { + params: { + ...defaultConfigParams.params, + liCollectConfig: { + appId: 'a-0001' + }, + emailHash: '123' + } + } + liveIntentExternalIdSubmodule.decode({}, configParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { appId: 'a-0001', publisherId: '89899', type: 'application' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }, + { + clientRef: {}, + sourceEvent: { emailHash: '123' }, + type: 'collect' + }]) + }); + + it('should have the same data after call decode when distributorId and sourceEvent is present', function() { + const configParams = { + params: { + ...defaultConfigParams.params, + distributorId: 'did-1111', + emailHash: '123' + } + } + liveIntentExternalIdSubmodule.decode({}, configParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: 'did-1111', publisherId: defaultConfigParams.params.publisherId, type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'did-1111', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }, + { + clientRef: {}, + sourceEvent: { emailHash: '123' }, + type: 'collect' + }]) + }); + + it('should include the identifier data if it is present in config', function() { + const configParams = { + params: { + ...defaultConfigParams.params, + distributorId: 'did-1111', + emailHash: '123', + ipv4: 'foov4', + ipv6: 'foov6', + userAgent: 'bar' + } + } + liveIntentExternalIdSubmodule.decode({}, configParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: 'did-1111', publisherId: defaultConfigParams.params.publisherId, type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'did-1111', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }, + { + clientRef: {}, + sourceEvent: { emailHash: '123', ipv4: 'foov4', ipv6: 'foov6', userAgent: 'bar' }, + type: 'collect' + }]) + }); + + it('should have the same data when decode with privacy settings', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: false, + consentString: 'consentDataString' + }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) + liveIntentExternalIdSubmodule.decode({}, defaultConfigParams); + expect(window.liQHub).to.eql([{ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: { gdpr: { consentString: 'consentDataString', gdprApplies: false }, gpp: { applicableSections: [1], consentString: 'gppConsentDataString' }, usPrivacy: { consentString: '1YNY' } }, + integration: { distributorId: defaultConfigParams.params.distributorId, publisherId: defaultConfigParams.params.publisherId, type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }]) + }); + + it('should not fire event again when it is already fired', function() { + liveIntentExternalIdSubmodule.decode({}, defaultConfigParams); + liveIntentExternalIdSubmodule.decode({}, defaultConfigParams); + + expect(window.liQHub).to.have.length(1) // instead of 2 + }); + + it('should decode a unifiedId to lipbId and remove it', function() { + const result = liveIntentExternalIdSubmodule.decode({ unifiedId: 'data' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'data'}}); + }); + + it('should decode a nonId to lipbId', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'data' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'data', 'nonId': 'data'}}); + }); + + it('should resolve extra attributes', function() { + liveIntentExternalIdSubmodule.getId({ params: { + ...defaultConfigParams.params, + ...{ requestedAttributesOverrides: { 'foo': true, 'bar': false } } + } }).callback(() => {}); + + expect(window.liQHub).to.have.length(2) + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + const resolveCommand = window.liQHub[1] + + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'nonId', 'foo' ], + type: 'resolve' + }) + }); + + it('should decode values with the segments but no nonId', function() { + const result = liveIntentExternalIdSubmodule.decode({segments: ['tak']}, defaultConfigParams); + expect(result).to.eql({'lipb': {'segments': ['tak']}}); + }); + + it('should decode a uid2 to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode values with uid2 but no nonId', function() { + const result = liveIntentExternalIdSubmodule.decode({ uid2: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a bidswitch id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a medianet id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a sovrn id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a magnite id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an index id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', index: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an openx id to a separate object when present', function () { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a thetradedesk id to a separate object when present', function() { + const provider = 'liveintent.com' + refererInfoStub.returns({domain: provider}) + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', thetradedesk: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'tdid': 'bar'}, 'tdid': {'id': 'bar', 'ext': {'rtiPartner': 'TDID', 'provider': provider}}}); + }); + + it('should allow disabling nonId resolution', function() { + liveIntentExternalIdSubmodule.getId({ params: { + ...defaultConfigParams.params, + ...{ requestedAttributesOverrides: { 'nonId': false, 'uid2': true } } + } }).callback(() => {}); + + expect(window.liQHub).to.have.length(2) + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: '89899', type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + type: 'register_client' + }) + + const resolveCommand = window.liQHub[1] + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'uid2' ], + type: 'resolve' + }) + }); + + it('should decode a idCookie as fpid if it exists and coppa is false', function() { + coppaConsentDataStub.returns(false) + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', idCookie: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'fpid': 'bar'}, 'fpid': {'id': 'bar'}}); + }); + + it('should not decode a idCookie as fpid if it exists and coppa is true', function() { + coppaConsentDataStub.returns(true) + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', idCookie: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo'}}) + }); + + it('should resolve fpid from cookie', function() { + const cookieName = 'testcookie' + liveIntentExternalIdSubmodule.getId({ params: { + ...defaultConfigParams.params, + fpid: { 'strategy': 'cookie', 'name': cookieName }, + requestedAttributesOverrides: { 'fpid': true } } + }).callback(() => {}); + + expect(window.liQHub).to.have.length(2) + expect(window.liQHub[0]).to.eql({ + clientDetails: { name: 'prebid', version: '$prebid.version$' }, + clientRef: {}, + collectSettings: { timeout: DEFAULT_AJAX_TIMEOUT }, + consent: {}, + integration: { distributorId: defaultConfigParams.distributorId, publisherId: PUBLISHER_ID, type: 'custom' }, + partnerCookies: new Set(), + resolveSettings: { identityPartner: 'prebid', timeout: DEFAULT_AJAX_TIMEOUT }, + idCookieSettings: { type: 'provided', key: 'testcookie', source: 'cookie' }, + type: 'register_client' + }) + + const resolveCommand = window.liQHub[1] + + // functions cannot be reasonably compared, remove them + delete resolveCommand.onSuccess[0].callback + delete resolveCommand.onFailure + + expect(resolveCommand).to.eql({ + clientRef: {}, + onSuccess: [{ type: 'callback' }], + requestedAttributes: [ 'nonId', 'idCookie' ], + type: 'resolve' + }) + }); + + it('should decode a sharethrough id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', sharethrough: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sharethrough': 'bar'}, 'sharethrough': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a sonobi id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', sonobi: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sonobi': 'bar'}, 'sonobi': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a triplelift id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', triplelift: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'triplelift': 'bar'}, 'triplelift': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a zetassp id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', zetassp: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'zetassp': 'bar'}, 'zetassp': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a vidazoo id to a separate object when present', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar'}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode the segments as part of lipb', function() { + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', 'segments': ['bar'] }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'segments': ['bar']}}); + }); + + it('getId does not set the global variables when liModuleEnabled, liTreatmentRate and activatePartialTreatment are undefined', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: undefined } }; + + liveIntentExternalIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.be.undefined + }); + + it('getId does not set the global variables when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is undefined', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: undefined } }; + + liveIntentExternalIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId does not set the global variables when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is false', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: false } }; + + liveIntentExternalIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId does not change the global variables when liModuleEnabled is true, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + randomStub.returns(1.0) // 1.0 < 0.7 = false, but should be ignored by the module + window.liModuleEnabled = true; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + liveIntentExternalIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.eq(true) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId does not change the global variables when liModuleEnabled is false, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + randomStub.returns(0.5) // 0.5 < 0.7 = true, but should be ignored by the module + window.liModuleEnabled = false; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + liveIntentExternalIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId sets the global variables correctly when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is true, and experiment returns false', function() { + randomStub.returns(1) // 1 < 0.7 = false + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + liveIntentExternalIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId sets the global variables correctly when liModuleEnabled is undefined, liTreatmentRate is undefined and activatePartialTreatment is true, and experiment returns true', function() { + randomStub.returns(0.0) // 0.0 < DEFAULT_TREATMENT_RATE (0.97) = true + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + liveIntentExternalIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.eq(true) + expect(window.liTreatmentRate).to.eq(DEFAULT_TREATMENT_RATE) + }); + + it('should decode IDs when liModuleEnabled, liTreatmentRate and activatePartialTreatment are undefined', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: undefined } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.be.undefined + }); + + it('should decode IDs when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is undefined', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: undefined } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should decode IDs when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is false', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: false } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should decode IDs when liModuleEnabled is true, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + randomStub.returns(1.0) // 1 < 0.7 = false, but should be ignored by the module as liModuleEnabled is already defined + window.liModuleEnabled = true; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.eq(true) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should not decode IDs when liModuleEnabled is false, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + randomStub.returns(0.5) // 0.5 < 0.7 = true, but should be ignored by the module as liModuleEnabled is already defined + window.liModuleEnabled = false; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should not decode IDs when liModuleEnabled is false, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + window.liModuleEnabled = false; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should not decode IDs when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is true, and experiment returns false', function() { + randomStub.returns(1.0) // 1.0 < 0.7 = false + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should not decode IDs when liModuleEnabled is undefined, liTreatmentRate is undefined and activatePartialTreatment is true, and experiment returns false', function() { + randomStub.returns(1.0) // 1.0 < DEFAULT_TREATMENT_RATE (0.97) = false + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(DEFAULT_TREATMENT_RATE) + }); + + it('should decode IDs when liModuleEnabled is undefined, liTreatmentRate is undefined and activatePartialTreatment is true, and experiment returns true', function() { + randomStub.returns(0.0) // 0.0 < DEFAULT_TREATMENT_RATE (0.97) = true + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.eq(true) + expect(window.liTreatmentRate).to.eq(DEFAULT_TREATMENT_RATE) + }); +}); diff --git a/test/spec/modules/liveIntentIdMinimalSystem_spec.js b/test/spec/modules/liveIntentIdMinimalSystem_spec.js index e280d9108a0..553d32321f5 100644 --- a/test/spec/modules/liveIntentIdMinimalSystem_spec.js +++ b/test/spec/modules/liveIntentIdMinimalSystem_spec.js @@ -1,7 +1,7 @@ import * as utils from 'src/utils.js'; import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; -import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; +import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from '../../../libraries/liveIntentId/idSystem.js'; import * as refererDetection from '../../../src/refererDetection.js'; const PUBLISHER_ID = '89899'; @@ -49,11 +49,6 @@ describe('LiveIntentMinimalId', function() { expect(server.requests[0]).to.eql(undefined) }); - it('should not return a decoded identifier when the unifiedId is not present in the value', function() { - const result = liveIntentIdSubmodule.decode({ additionalData: 'data' }); - expect(result).to.be.eql({}); - }); - it('should initialize LiveConnect and send no data', function() { liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); @@ -245,6 +240,11 @@ describe('LiveIntentMinimalId', function() { expect(callBackSpy.calledOnce).to.be.true; }); + it('should decode values with the segments but no nonId', function() { + const result = liveIntentIdSubmodule.decode({segments: ['tak']}, { params: defaultConfigParams }); + expect(result).to.eql({'lipb': {'segments': ['tak']}}); + }); + it('should decode a uid2 to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); @@ -252,7 +252,7 @@ describe('LiveIntentMinimalId', function() { it('should decode values with uid2 but no nonId', function() { const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); - expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(result).to.eql({'lipb': {'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a bidswitch id to a separate object when present', function() { @@ -313,4 +313,34 @@ describe('LiveIntentMinimalId', function() { ); expect(callBackSpy.calledOnce).to.be.true; }); + + it('should decode a sharethrough id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sharethrough: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sharethrough': 'bar'}, 'sharethrough': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a sonobi id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sonobi: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sonobi': 'bar'}, 'sonobi': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a triplelift id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', triplelift: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'triplelift': 'bar'}, 'triplelift': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a zetassp id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', zetassp: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'zetassp': 'bar'}, 'zetassp': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a vidazoo id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar'}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode the segments as part of lipb', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', 'segments': ['bar'] }, { params: defaultConfigParams }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'segments': ['bar']}}); + }); }); diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 084b4de212a..07b5121b3ae 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,15 +1,30 @@ -import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; +import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'libraries/liveIntentId/idSystem.js'; import * as utils from 'src/utils.js'; -import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; +import { DEFAULT_TREATMENT_RATE } from 'libraries/liveIntentId/shared.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler, coppaDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; import * as refererDetection from '../../../src/refererDetection.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; resetLiveIntentIdSubmodule(); liveIntentIdSubmodule.setModuleMode('standard') const PUBLISHER_ID = '89899'; -const defaultConfigParams = { params: {publisherId: PUBLISHER_ID, fireEventDelay: 1} }; +const defaultConfigParams = { params: { publisherId: PUBLISHER_ID, fireEventDelay: 1 } }; const responseHeader = {'Content-Type': 'application/json'} +function requests(...urlRegExps) { + return server.requests.filter((request) => urlRegExps.some((regExp) => request.url.match(regExp))) +} + +function rpRequests() { + return requests(/https:\/\/rp.liadm.com.*/) +} + +function idxRequests() { + return requests(/https:\/\/idx.liadm.com.*/) +} + describe('LiveIntentId', function() { let logErrorStub; let uspConsentDataStub; @@ -18,7 +33,9 @@ describe('LiveIntentId', function() { let getCookieStub; let getDataFromLocalStorageStub; let imgStub; + let coppaConsentDataStub; let refererInfoStub; + let randomStub; beforeEach(function() { liveIntentIdSubmodule.setModuleMode('standard'); @@ -29,7 +46,9 @@ describe('LiveIntentId', function() { uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + coppaConsentDataStub = sinon.stub(coppaDataHandler, 'getCoppa'); refererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + randomStub = sinon.stub(Math, 'random').returns(0.6); }); afterEach(function() { @@ -40,11 +59,15 @@ describe('LiveIntentId', function() { uspConsentDataStub.restore(); gdprConsentDataStub.restore(); gppConsentDataStub.restore(); + coppaConsentDataStub.restore(); refererInfoStub.restore(); + randomStub.restore(); + window.liModuleEnabled = undefined; // reset + window.liTreatmentRate = undefined; // reset resetLiveIntentIdSubmodule(); }); - it('should initialize LiveConnect with a privacy string when getId, and include it in the resolution request', function () { + it('should initialize LiveConnect with a privacy string when getId but not send request', function (done) { uspConsentDataStub.returns('1YNY'); gdprConsentDataStub.returns({ gdprApplies: true, @@ -57,24 +80,18 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1%2C2.*/); - const response = { - unifiedId: 'a_unified_id', - segments: [123, 234] - } - request.respond( - 200, - responseHeader, - JSON.stringify(response) - ); - expect(callBackSpy.calledOnceWith(response)).to.be.true; + setTimeout(() => { + let requests = idxRequests().concat(rpRequests()); + expect(requests).to.be.empty; + expect(callBackSpy.notCalled).to.be.true; + done(); + }, 300) }); - it('should fire an event when getId', function(done) { + it('should fire an event without privacy setting when getId', function(done) { uspConsentDataStub.returns('1YNY'); gdprConsentDataStub.returns({ - gdprApplies: true, + gdprApplies: false, consentString: 'consentDataString' }) gppConsentDataStub.returns({ @@ -83,7 +100,8 @@ describe('LiveIntentId', function() { }) liveIntentIdSubmodule.getId(defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString&gpp_s=gppConsentDataString&gpp_as=1.*/); + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=0.*&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString.*&gpp_as=1.*/); done(); }, 300); }); @@ -94,7 +112,8 @@ describe('LiveIntentId', function() { emailHash: '58131bc547fb87af94cebdaf3102321f' }}); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) done(); }, 300); }); @@ -102,7 +121,8 @@ describe('LiveIntentId', function() { it('should initialize LiveConnect and forward the prebid version when decode and emit an event', function(done) { liveIntentIdSubmodule.decode({}, defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.contain('tv=$prebid.version$') + let request = rpRequests()[0]; + expect(request.url).to.contain('tv=$prebid.version$') done(); }, 300); }); @@ -119,7 +139,8 @@ describe('LiveIntentId', function() { } }}); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); + let request = requests(/https:\/\/collector.liveintent.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); + expect(request.length).to.be.greaterThan(0); done(); }, 300); }); @@ -127,7 +148,8 @@ describe('LiveIntentId', function() { it('should fire an event with the provided distributorId', function (done) { liveIntentIdSubmodule.decode({}, { params: { fireEventDelay: 1, distributorId: 'did-1111' } }); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*did=did-1111.*&wpn=prebid.*/); + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*did=did-1111.*&wpn=prebid.*/); done(); }, 300); }); @@ -135,8 +157,9 @@ describe('LiveIntentId', function() { it('should fire an event without the provided distributorId when appId is provided', function (done) { liveIntentIdSubmodule.decode({}, { params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); - expect(server.requests[0].url).to.not.match(/.*did=*/); + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); + expect(request.url).to.not.match(/.*did=*/); done(); }, 300); }); @@ -153,7 +176,8 @@ describe('LiveIntentId', function() { }) liveIntentIdSubmodule.decode({}, defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1.*/); + let request = rpRequests()[0]; + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 300); }); @@ -164,20 +188,16 @@ describe('LiveIntentId', function() { emailHash: '58131bc547fb87af94cebdaf3102321f' }}); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); + let request = rpRequests()[0]; + expect(request.url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); done(); }, 300); }); - it('should not return a decoded identifier when the unifiedId is not present in the value', function() { - const result = liveIntentIdSubmodule.decode({ fireEventDelay: 1, additionalData: 'data' }); - expect(result).to.be.eql({}); - }); - it('should fire an event when decode', function(done) { liveIntentIdSubmodule.decode({}, defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.be.not.null + expect(rpRequests().length).to.be.eq(1); done(); }, 300); }); @@ -188,7 +208,7 @@ describe('LiveIntentId', function() { liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); setTimeout(() => { - expect(server.requests.length).to.be.eq(1); + expect(rpRequests().length).to.be.eq(1); done(); }, 300); }); @@ -198,8 +218,8 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); + let request = requests(/https:\/\/dummy.liveintent.com\/idex\/.*/)[0]; + expect(request.url).to.match(/https:\/\/dummy.liveintent.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 204, responseHeader @@ -212,8 +232,8 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111' } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/did-1111/any?did=did-1111&cd=.localhost&resolve=nonId'); + let request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/did-1111\/any\?.*did=did-1111.*&cd=.localhost.*&resolve=nonId.*/); request.respond( 204, responseHeader @@ -226,8 +246,8 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/any?cd=.localhost&resolve=nonId'); + let request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/any\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 204, responseHeader @@ -246,8 +266,8 @@ describe('LiveIntentId', function() { } } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?cd=.localhost&resolve=nonId'); + let request = requests(/https:\/\/dummy.liveintent.com\/idex\/.*/)[0]; + expect(request.url).to.match(/https:\/\/dummy.liveintent.com\/idex\/rubicon\/89899\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 200, responseHeader, @@ -261,8 +281,8 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); + let request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 200, responseHeader, @@ -276,8 +296,8 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); + let request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=nonId.*/); request.respond( 503, responseHeader, @@ -293,8 +313,9 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&resolve=nonId`); + let request = idxRequests()[0]; + const expected = new RegExp('https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*duid=' + oldCookie + '.*&cd=.localhost.*&resolve=nonId.*'); + expect(request.url).to.match(expected); request.respond( 200, responseHeader, @@ -316,8 +337,9 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&_thirdPC=third-pc&resolve=nonId`); + let request = idxRequests()[0]; + const expected = new RegExp('https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*duid=' + oldCookie + '.*&cd=.localhost.*&_thirdPC=third-pc.*&resolve=nonId.*'); + expect(request.url).to.match(expected); request.respond( 200, responseHeader, @@ -338,8 +360,8 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&_thirdPC=%7B%22key%22%3A%22value%22%7D&resolve=nonId'); + let request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&_thirdPC=%7B%22key%22%3A%22value%22%7D.*&resolve=nonId.*/); request.respond( 200, responseHeader, @@ -348,6 +370,21 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnce).to.be.true; }); + it('should include ip4,ip6,userAgent if it\'s present', function(done) { + liveIntentIdSubmodule.getId({ params: { + ...defaultConfigParams.params, + ipv4: 'foov4', + ipv6: 'foov6', + userAgent: 'boo' + }}); + setTimeout(() => { + let request = rpRequests()[0]; + expect(request.url).to.match(/^https:\/\/rp\.liadm\.com\/j?.*pip=.*&pip6=.*$/) + expect(request.requestHeaders['X-LI-Provided-User-Agent']).to.be.eq('boo') + done(); + }, 300); + }); + it('should send an error when the cookie jar throws an unexpected error', function() { getCookieStub.throws('CookieError', 'A message'); liveIntentIdSubmodule.getId(defaultConfigParams); @@ -355,12 +392,12 @@ describe('LiveIntentId', function() { }); it('should decode a unifiedId to lipbId and remove it', function() { - const result = liveIntentIdSubmodule.decode({ unifiedId: 'data' }); + const result = liveIntentIdSubmodule.decode({ unifiedId: 'data' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'data'}}); }); it('should decode a nonId to lipbId', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'data' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'data' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'data', 'nonId': 'data'}}); }); @@ -371,8 +408,8 @@ describe('LiveIntentId', function() { ...{ requestedAttributesOverrides: { 'foo': true, 'bar': false } } } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId&resolve=foo`); + let request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=nonId.*&resolve=foo.*/); request.respond( 200, responseHeader, @@ -382,57 +419,67 @@ describe('LiveIntentId', function() { }); it('should decode a uid2 to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); + it('should decode values with the segments but no nonId', function() { + const result = liveIntentIdSubmodule.decode({segments: ['tak']}, defaultConfigParams); + expect(result).to.eql({'lipb': {'segments': ['tak']}}); + }); + it('should decode values with uid2 but no nonId', function() { - const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); - expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a bidswitch id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a medianet id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a sovrn id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a magnite id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode an index id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode an openx id to a separate object when present', function () { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode an pubmatic id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode a thetradedesk id to a separate object when present', function() { const provider = 'liveintent.com' refererInfoStub.returns({domain: provider}) - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', thetradedesk: 'bar' }); + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', thetradedesk: 'bar' }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'tdid': 'bar'}, 'tdid': {'id': 'bar', 'ext': {'rtiPartner': 'TDID', 'provider': provider}}}); }); + it('should decode the segments as part of lipb', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', 'segments': ['bar'] }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'segments': ['bar']}}); + }); + it('should allow disabling nonId resolution', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { @@ -440,8 +487,8 @@ describe('LiveIntentId', function() { ...{ requestedAttributesOverrides: { 'nonId': false, 'uid2': true } } } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=uid2`); + let request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&resolve=uid2.*/); request.respond( 200, responseHeader, @@ -449,4 +496,688 @@ describe('LiveIntentId', function() { ); expect(callBackSpy.calledOnce).to.be.true; }); -}); + + it('should decode a idCookie as fpid if it exists and coppa is false', function() { + coppaConsentDataStub.returns(false) + const result = liveIntentIdSubmodule.decode({nonId: 'foo', idCookie: 'bar'}, defaultConfigParams) + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'fpid': 'bar'}, 'fpid': {'id': 'bar'}}) + }); + + it('should not decode a idCookie as fpid if it exists and coppa is true', function() { + coppaConsentDataStub.returns(true) + const result = liveIntentIdSubmodule.decode({nonId: 'foo', idCookie: 'bar'}, defaultConfigParams) + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo'}}) + }); + + it('should resolve fpid from cookie', async function() { + const expectedValue = 'someValue' + const cookieName = 'testcookie' + getCookieStub.withArgs(cookieName).returns(expectedValue) + const config = { params: { + ...defaultConfigParams.params, + fpid: { 'strategy': 'cookie', 'name': cookieName }, + requestedAttributesOverrides: { 'fpid': true } } + } + const submoduleCallback = liveIntentIdSubmodule.getId(config).callback; + const decodedResult = new Promise(resolve => { + submoduleCallback((x) => resolve(liveIntentIdSubmodule.decode(x, config))); + }); + const request = idxRequests()[0]; + expect(request.url).to.match(/https:\/\/idx.liadm.com\/idex\/prebid\/89899\?.*cd=.localhost.*&ic=someValue.*&resolve=nonId.*/); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + + const result = await decodedResult + expect(result).to.be.eql({ + lipb: { 'fpid': expectedValue }, + fpid: { id: expectedValue } + }); + }); + + it('should decode a sharethrough id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sharethrough: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sharethrough': 'bar'}, 'sharethrough': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a sonobi id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sonobi: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sonobi': 'bar'}, 'sonobi': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a triplelift id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', triplelift: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'triplelift': 'bar'}, 'triplelift': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a zetassp id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', zetassp: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'zetassp': 'bar'}, 'zetassp': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a vidazoo id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar' }, defaultConfigParams); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar'}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('getId does not set the global variables when liModuleEnabled, liTreatmentRate and activatePartialTreatment are undefined', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: undefined } }; + + liveIntentIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.be.undefined + }); + + it('getId does not set the global variables when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is undefined', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: undefined } }; + + liveIntentIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId does not set the global variables when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is false', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: false } }; + + liveIntentIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId does not change the global variables when liModuleEnabled is true, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + randomStub.returns(1.0) // 1.0 < 0.7 = false, but should be ignored by the module + window.liModuleEnabled = true; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + liveIntentIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.eq(true) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId does not change the global variables when liModuleEnabled is false, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + randomStub.returns(0.5) // 0.5 < 0.7 = true, but should be ignored by the module + window.liModuleEnabled = false; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + liveIntentIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId sets the global variables correctly when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is true, and experiment returns false', function() { + randomStub.returns(1) // 1 < 0.7 = false + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + liveIntentIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('getId sets the global variables correctly when liModuleEnabled is undefined, liTreatmentRate is undefined and activatePartialTreatment is true, and experiment returns true', function() { + randomStub.returns(0.0) // 0.0 < DEFAULT_TREATMENT_RATE (0.97) = true + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + liveIntentIdSubmodule.getId(configWithPartialTreatment).callback(() => {}); + expect(window.liModuleEnabled).to.eq(true) + expect(window.liTreatmentRate).to.eq(DEFAULT_TREATMENT_RATE) + }); + + it('should decode IDs when liModuleEnabled, liTreatmentRate and activatePartialTreatment are undefined', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: undefined } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.be.undefined + }); + + it('should decode IDs when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is undefined', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: undefined } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should decode IDs when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is false', function() { + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: false } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.be.undefined + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should decode IDs when liModuleEnabled is true, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + randomStub.returns(1.0) // 1 < 0.7 = false, but should be ignored by the module as liModuleEnabled is already defined + window.liModuleEnabled = true; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.eq(true) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should not decode IDs when liModuleEnabled is false, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + randomStub.returns(0.5) // 0.5 < 0.7 = true, but should be ignored by the module as liModuleEnabled is already defined + window.liModuleEnabled = false; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should not decode IDs when liModuleEnabled is false, liTreatmentRate is 0.7 and activatePartialTreatment is true', function() { + window.liModuleEnabled = false; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should not decode IDs when liModuleEnabled is undefined, liTreatmentRate is 0.7 and activatePartialTreatment is true, and experiment returns false', function() { + randomStub.returns(1.0) // 1.0 < 0.7 = false + window.liModuleEnabled = undefined; + window.liTreatmentRate = 0.7; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(0.7) + }); + + it('should not decode IDs when liModuleEnabled is undefined, liTreatmentRate is undefined and activatePartialTreatment is true, and experiment returns false', function() { + randomStub.returns(1.0) // 1.0 < DEFAULT_TREATMENT_RATE (0.97) = false + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({}); + expect(window.liModuleEnabled).to.eq(false) + expect(window.liTreatmentRate).to.eq(DEFAULT_TREATMENT_RATE) + }); + + it('should decode IDs when liModuleEnabled is undefined, liTreatmentRate is undefined and activatePartialTreatment is true, and experiment returns true', function() { + randomStub.returns(0.0) // 0.0 < DEFAULT_TREATMENT_RATE (0.97) = true + window.liModuleEnabled = undefined; + window.liTreatmentRate = undefined; + const configWithPartialTreatment = { params: { ...defaultConfigParams.params, activatePartialTreatment: true } }; + + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', vidazoo: 'bar', segments: ['tak'] }, configWithPartialTreatment); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar', 'segments': ['tak']}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + expect(window.liModuleEnabled).to.eq(true) + expect(window.liTreatmentRate).to.eq(DEFAULT_TREATMENT_RATE) + }); + + describe('eid', () => { + before(() => { + attachIdSystem(liveIntentIdSubmodule); + }); + it('liveIntentId; getValue call and ext', function() { + const userId = { + lipb: { + lipbid: 'some-random-id-value', + segments: ['s1', 's2'] + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.com', + uids: [{id: 'some-random-id-value', atype: 3}], + ext: {segments: ['s1', 's2']} + }); + }); + it('fpid; getValue call', function() { + const userId = { + fpid: { + id: 'some-random-id-value' + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'fpid.liveintent.com', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + it('bidswitch', function() { + const userId = { + bidswitch: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'bidswitch.net', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('bidswitch with ext', function() { + const userId = { + bidswitch: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'bidswitch.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('medianet', function() { + const userId = { + medianet: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'media.net', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('medianet with ext', function() { + const userId = { + medianet: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'media.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('sovrn', function() { + const userId = { + sovrn: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sovrn.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('sovrn with ext', function() { + const userId = { + sovrn: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sovrn.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('magnite', function() { + const userId = { + magnite: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'rubiconproject.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('magnite with ext', function() { + const userId = { + magnite: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'rubiconproject.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('index', function() { + const userId = { + index: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('index with ext', function() { + const userId = { + index: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('openx', function () { + const userId = { + openx: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.net', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('openx with ext', function () { + const userId = { + openx: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('pubmatic', function() { + const userId = { + pubmatic: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('pubmatic with ext', function() { + const userId = { + pubmatic: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('liveIntentId; getValue call and NO ext', function() { + const userId = { + lipb: { + lipbid: 'some-random-id-value' + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.com', + uids: [{id: 'some-random-id-value', atype: 3}] + }); + }); + + it('sharethrough', function () { + const userId = { + sharethrough: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'sharethrough.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('sharethrough with ext', function () { + const userId = { + sharethrough: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'sharethrough.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('sonobi', function () { + const userId = { + sonobi: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sonobi.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('sonobi with ext', function () { + const userId = { + sonobi: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sonobi.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('triplelift', function () { + const userId = { + triplelift: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.triplelift.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('triplelift with ext', function () { + const userId = { + triplelift: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.triplelift.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('zetassp', function () { + const userId = { + zetassp: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'zeta-ssp.liveintent.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('zetassp with ext', function () { + const userId = { + zetassp: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'zeta-ssp.liveintent.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('vidazoo', function () { + const userId = { + vidazoo: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.vidazoo.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('vidazoo with ext', function () { + const userId = { + vidazoo: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.vidazoo.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + }) +}) diff --git a/test/spec/modules/liveIntentRtdProvider_spec.js b/test/spec/modules/liveIntentRtdProvider_spec.js new file mode 100644 index 00000000000..d3c34830dd0 --- /dev/null +++ b/test/spec/modules/liveIntentRtdProvider_spec.js @@ -0,0 +1,116 @@ +import {liveIntentRtdSubmodule} from 'modules/liveIntentRtdProvider.js'; +import * as utils from 'src/utils.js'; +import { expect } from 'chai'; + +describe('LiveIntent Rtd Provider', function () { + const SUBMODULE_NAME = 'liveintent'; + + describe('submodule `init`', function () { + const config = { + name: SUBMODULE_NAME, + }; + it('init returns true when the subModuleName is defined', function () { + const value = liveIntentRtdSubmodule.init(config); + expect(value).to.equal(true); + }); + }) + + describe('submodule `onBidRequestEvent`', function () { + const bidRequestExample = { + bidderCode: 'appnexus', + auctionId: '8dbd7cb1-7f6d-4f84-946c-d0df4837234a', + bidderRequestId: '2a038c6820142b', + bids: [ + { + bidder: 'appnexus', + userId: { + lipb: { + segments: [ + 'asa_1231', + 'lalo_4311', + 'liurl_99123' + ] + } + } + } + ] + } + + it('exists', function () { + expect(liveIntentRtdSubmodule.onBidRequestEvent).to.be.a('function'); + }); + + it('undefined segments field does not change the ortb2', function() { + const bidRequest = { + bidderCode: 'appnexus', + auctionId: '8dbd7cb1-7f6d-4f84-946c-d0df4837234a', + bidderRequestId: '2a038c6820142b', + bids: [ + { + bidder: 'appnexus', + ortb2: {} + } + ] + } + + liveIntentRtdSubmodule.onBidRequestEvent(bidRequest); + const ortb2 = bidRequest.bids[0].ortb2; + expect(ortb2).to.deep.equal({}); + }); + + it('extracts segments and move them to the bidRequest.ortb2.user.data when the ortb2 is undefined', function() { + const bidRequest = utils.deepClone(bidRequestExample); + + liveIntentRtdSubmodule.onBidRequestEvent(bidRequest); + const ortb2 = bidRequest.bids[0].ortb2; + const expectedOrtb2 = {user: {data: [{name: 'liveintent.com', segment: [{id: 'asa_1231'}, {id: 'lalo_4311'}, {id: 'liurl_99123'}]}]}} + expect(ortb2).to.deep.equal(expectedOrtb2); + }); + + it('extracts segments and move them to the bidRequest.ortb2.user.data when user is undefined', function() { + bidRequestExample.bids[0].ortb2 = { source: {} } + const bidRequest = utils.deepClone(bidRequestExample); + + liveIntentRtdSubmodule.onBidRequestEvent(bidRequest); + const ortb2 = bidRequest.bids[0].ortb2; + const expectedOrtb2 = {source: {}, user: {data: [{name: 'liveintent.com', segment: [{id: 'asa_1231'}, {id: 'lalo_4311'}, {id: 'liurl_99123'}]}]}} + expect(ortb2).to.deep.equal(expectedOrtb2); + }); + + it('extracts segments and move them to the bidRequest.ortb2.user.data when data is undefined', function() { + bidRequestExample.bids[0].ortb2 = { + source: {}, + user: {} + } + const bidRequest = utils.deepClone(bidRequestExample); + + liveIntentRtdSubmodule.onBidRequestEvent(bidRequest); + const ortb2 = bidRequest.bids[0].ortb2; + const expectedOrtb2 = {source: {}, user: {data: [{name: 'liveintent.com', segment: [{id: 'asa_1231'}, {id: 'lalo_4311'}, {id: 'liurl_99123'}]}]}} + expect(ortb2).to.deep.equal(expectedOrtb2); + }); + + it('extracts segments and move them to the bidRequest.ortb2.user.data with the existing data', function() { + bidRequestExample.bids[0].ortb2 = { + source: {}, + user: { + data: [ + { + name: 'example.com', + segment: [ + { id: 'a_1231' }, + { id: 'b_4311' } + ] + } + ] + } + } + const bidRequest = utils.deepClone(bidRequestExample); + + liveIntentRtdSubmodule.onBidRequestEvent(bidRequest); + const ortb2 = bidRequest.bids[0].ortb2; + const expectedOrtb2 = {source: {}, user: {data: [{name: 'example.com', segment: [{id: 'a_1231'}, {id: 'b_4311'}]}, {name: 'liveintent.com', segment: [{id: 'asa_1231'}, {id: 'lalo_4311'}, {id: 'liurl_99123'}]}]}} + expect(ortb2).to.deep.equal(expectedOrtb2); + }); + }); +}); diff --git a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js index f82cc4c4f52..4b7f7bc2bac 100644 --- a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js +++ b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js @@ -59,6 +59,24 @@ const BID2 = Object.assign({}, BID1, { dealId: undefined }); +const BID2_2 = Object.assign({}, BID2, { + width: 320, + height: 320, + cpm: 10.0, + originalCpm: 20.0, + currency: 'USD', + originalCurrency: 'FOO', + timeToRespond: 300, + bidId: '3ecff0db240758', + requestId: '3ecff0db240757', + adId: '3ecff0db240758', + mediaType: 'video', + meta: { + data: 'value2_2' + }, + dealId: 'deal2_2' +}); + const BID3 = { bidId: '4ecff0db240757', requestId: '4ecff0db240757', @@ -99,7 +117,8 @@ const MOCK = { }, BID_RESPONSE: [ BID1, - BID2 + BID2, + BID2_2 ], AUCTION_END: { }, @@ -281,6 +300,7 @@ function performStandardAuction() { events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[2]); events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); @@ -341,7 +361,6 @@ describe('Livewrapped analytics adapter', function () { }); it('should build a batched message from prebid events', function () { - sandbox.stub(utils, 'getWindowTop').returns({}); performStandardAuction(); clock.tick(BID_WON_TIMEOUT + 1000); @@ -403,20 +422,6 @@ describe('Livewrapped analytics adapter', function () { expect(message.timeouts[0].adUnit).to.equal('panorama_d_1'); }); - it('should detect adblocker recovered request', function () { - sandbox.stub(utils, 'getWindowTop').returns({ I12C: { Morph: 1 } }); - performStandardAuction(); - - clock.tick(BID_WON_TIMEOUT + 1000); - - expect(server.requests.length).to.equal(1); - let request = server.requests[0]; - - let message = JSON.parse(request.requestBody); - - expect(message.rcv).to.equal(true); - }); - it('should forward GDPR data', function () { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, { @@ -623,4 +628,79 @@ describe('Livewrapped analytics adapter', function () { expect(request.url).to.equal('https://whitelabeled.com/analytics/10'); }); }); + + describe('when given extended options', function () { + adapterManager.registerAnalyticsAdapter({ + code: 'livewrapped', + adapter: livewrappedAnalyticsAdapter + }); + + beforeEach(function () { + adapterManager.enableAnalytics({ + provider: 'livewrapped', + options: { + publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', + ext: { + testparam: 123 + } + } + }); + }); + + afterEach(function () { + livewrappedAnalyticsAdapter.disableAnalytics(); + }); + + it('should forward the extended options', function () { + performStandardAuction(); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.ext).to.not.equal(null); + expect(message.ext.testparam).to.equal(123); + }); + + it('should forward the correct winning bid from a multi-bid response', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[2]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, Object.assign({}, BID2_2, { + 'status': 'rendered', + 'requestId': '3ecff0db240757' + })); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0]).to.deep.equal({ + timeStamp: 1519149562216, + adUnit: 'box_d_1', + adUnitId: 'adunitid', + bidder: 'livewrapped', + width: 320, + height: 320, + cpm: 10.0, + orgCpm: 200, + mediaType: 4, + dealId: 'deal2_2', + gdpr: 0, + auctionId: 0, + meta: { + data: 'value2_2' + } + }); + }); + }); }); diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index 5ab00859d81..f3125fec529 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -3,6 +3,9 @@ import {spec, storage} from 'modules/livewrappedBidAdapter.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { NATIVE, VIDEO } from 'src/mediaTypes.js'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; +import { getWinDimensions } from '../../../src/utils'; describe('Livewrapped adapter tests', function () { let sandbox, @@ -997,8 +1000,8 @@ describe('Livewrapped adapter tests', function () { url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, version: '1.4', - width: window.innerWidth, - height: window.innerHeight, + width: getWinDimensions().innerWidth, + height: getWinDimensions().innerHeight, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -1178,50 +1181,21 @@ describe('Livewrapped adapter tests', function () { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); - let origGetConfig = config.getConfig; - sandbox.stub(config, 'getConfig').callsFake(function (key) { - if (key === 'currency.adServerCurrency') { - return 'EUR'; - } - return origGetConfig.apply(config, arguments); - }); - + setCurrencyConfig({ adServerCurrency: 'EUR' }); let testbidRequest = clone(bidderRequest); let bids = testbidRequest.bids.map(b => { b.getFloor = function () { return { floor: 10, currency: 'EUR' }; } return b; }); - let result = spec.buildRequests(bids, testbidRequest); - let data = JSON.parse(result.data); - - expect(result.url).to.equal('https://lwadm.com/ad'); - - let expectedQuery = { - auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', - publisherId: '26947112-2289-405D-88C1-A7340C57E63E', - userId: 'user id', - url: 'https://www.domain.com', - seats: {'dsp': ['seat 1']}, - version: '1.4', - width: 100, - height: 100, - cookieSupport: true, - flrCur: 'EUR', - adRequests: [{ - adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', - callerAdUnitId: 'panorama_d_1', - bidId: '2ffb201a808da7', - rtbData: { - ext: { - tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' - }, - }, - formats: [{width: 980, height: 240}, {width: 980, height: 120}], - flr: 10 - }] - }; - expect(data).to.deep.equal(expectedQuery); + return addFPDToBidderRequest(testbidRequest).then(res => { + let result = spec.buildRequests(bids, res); + let data = JSON.parse(result.data); + expect(result.url).to.equal('https://lwadm.com/ad'); + expect(data.adRequests[0].flr).to.eql(10) + expect(data.flrCur).to.eql('EUR') + setCurrencyConfig({}); + }); }); it('getFloor returns valid floor - default currency', function() { diff --git a/test/spec/modules/lkqdBidAdapter_spec.js b/test/spec/modules/lkqdBidAdapter_spec.js index 7fee9bf6e41..1e05b9deeb3 100644 --- a/test/spec/modules/lkqdBidAdapter_spec.js +++ b/test/spec/modules/lkqdBidAdapter_spec.js @@ -46,12 +46,12 @@ describe('lkqdBidAdapter', () => { }); it('should return false when required params are not passed', () => { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { wrong: 'missing zone id' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -140,7 +140,7 @@ describe('lkqdBidAdapter', () => { }); it('should not populate unspecified parameters', () => { - const requests = spec.buildRequests(bidRequests); + const requests = spec.buildRequests(bidRequests, { timeout: 1000 }); const serverRequestObject = requests[0]; expect(serverRequestObject.data.device.dnt).to.be.a('undefined'); diff --git a/test/spec/modules/lm_kiviadsBidAdapter_spec.js b/test/spec/modules/lm_kiviadsBidAdapter_spec.js index 68ac73289cd..b6c4ae9bc4b 100644 --- a/test/spec/modules/lm_kiviadsBidAdapter_spec.js +++ b/test/spec/modules/lm_kiviadsBidAdapter_spec.js @@ -1,11 +1,13 @@ import {expect} from 'chai'; import {config} from 'src/config.js'; -import {spec, getBidFloor} from 'modules/lm_kiviadsBidAdapter.js'; +import {spec} from 'modules/lm_kiviadsBidAdapter.js'; import {deepClone} from 'src/utils'; +import {getBidFloor} from '../../../libraries/xeUtils/bidderUtils.js'; const ENDPOINT = 'https://pbjs.kiviads.live'; const defaultRequest = { + tmax: 0, adUnitCode: 'test', bidId: '1', requestId: 'qwerty', @@ -90,6 +92,7 @@ describe('lm_kiviadsBidAdapter', () => { it('should build basic request structure', function () { const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('tmax').and.to.equal(defaultRequest.tmax); expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); @@ -97,11 +100,9 @@ describe('lm_kiviadsBidAdapter', () => { expect(request).to.have.property('bc').and.to.equal(1); expect(request).to.have.property('floor').and.to.equal(null); expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); - expect(request).to.have.property('gdprApplies').and.to.equal(0); - expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('gdprConsent').and.to.deep.equal({}); expect(request).to.have.property('userEids').and.to.deep.equal([]); expect(request).to.have.property('usPrivacy').and.to.equal(''); - expect(request).to.have.property('coppa').and.to.equal(0); expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); expect(request).to.have.property('ext').and.to.deep.equal({}); expect(request).to.have.property('env').and.to.deep.equal({ @@ -194,18 +195,6 @@ describe('lm_kiviadsBidAdapter', () => { expect(request).to.have.property('floor').and.to.equal(5); }); - it('should build request with gdpr consent data if applies', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'qwerty' - } - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('gdprApplies').and.equals(1); - expect(request).to.have.property('consentString').and.equals('qwerty'); - }); - it('should build request with usp consent data if applies', function () { const bidderRequest = { uspConsent: '1YA-' @@ -214,14 +203,6 @@ describe('lm_kiviadsBidAdapter', () => { expect(request).to.have.property('usPrivacy').and.equals('1YA-'); }); - it('should build request with coppa 1', function () { - config.setConfig({ - coppa: true - }); - const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; - expect(request).to.have.property('coppa').and.equals(1); - }); - it('should build request with extended ids', function () { const idRequest = deepClone(defaultRequest); idRequest.userIdAsEids = [ @@ -452,4 +433,4 @@ describe('lm_kiviadsBidAdapter', () => { expect(result).to.equal(5); }); }); -}) +}); diff --git a/test/spec/modules/lmpIdSystem_spec.js b/test/spec/modules/lmpIdSystem_spec.js new file mode 100644 index 00000000000..b29195e4be9 --- /dev/null +++ b/test/spec/modules/lmpIdSystem_spec.js @@ -0,0 +1,134 @@ +import { expect } from 'chai'; +import { find } from 'src/polyfill.js'; +import { config } from 'src/config.js'; +import {init, startAuctionHook, setSubmoduleRegistry, resetUserIds} from 'modules/userId/index.js'; +import { storage, lmpIdSubmodule } from 'modules/lmpIdSystem.js'; +import { mockGdprConsent } from '../../helpers/consentData.js'; +import 'src/prebid.js'; + +function getConfigMock() { + return { + userSync: { + syncDelay: 0, + userIds: [{ + name: 'lmpid' + }] + } + } +} + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: { banner: {}, native: {} }, + sizes: [ + [300, 200], + [300, 600] + ], + bids: [{ + bidder: 'sampleBidder', + params: { placementId: 'banner-only-bidder' } + }] + }; +} + +describe('LMPID System', () => { + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let windowLmpidStub; + + beforeEach(() => { + window.__lmpid = undefined; + windowLmpidStub = sinon.stub(window, '__lmpid'); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + }); + + afterEach(() => { + getDataFromLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); + windowLmpidStub.restore(); + }); + + describe('LMPID: test "getId" method', () => { + it('prefers the window cached LMPID', () => { + localStorageIsEnabledStub.returns(true); + getDataFromLocalStorageStub.withArgs('__lmpid').returns('stored-lmpid'); + + windowLmpidStub.value('lmpid'); + expect(lmpIdSubmodule.getId()).to.deep.equal({ id: 'lmpid' }); + }); + + it('fallbacks on localStorage when window cache is falsy', () => { + localStorageIsEnabledStub.returns(true); + getDataFromLocalStorageStub.withArgs('__lmpid').returns('stored-lmpid'); + + windowLmpidStub.value(''); + expect(lmpIdSubmodule.getId()).to.deep.equal({ id: 'stored-lmpid' }); + + windowLmpidStub.value(false); + expect(lmpIdSubmodule.getId()).to.deep.equal({ id: 'stored-lmpid' }); + }); + + it('fallbacks only if localStorageIsEnabled', () => { + localStorageIsEnabledStub.returns(false); + getDataFromLocalStorageStub.withArgs('__lmpid').returns('stored-lmpid'); + + expect(lmpIdSubmodule.getId()).to.be.undefined; + }); + }); + + describe('LMPID: test "decode" method', () => { + it('provides the lmpid from a stored object', () => { + expect(lmpIdSubmodule.decode('lmpid')).to.deep.equal({ lmpid: 'lmpid' }); + }); + }); + + describe('LMPID: requestBids hook', () => { + let adUnits; + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockGdprConsent(sandbox); + adUnits = [getAdUnitMock()]; + init(config); + setSubmoduleRegistry([lmpIdSubmodule]); + getDataFromLocalStorageStub.withArgs('__lmpid').returns('stored-lmpid'); + localStorageIsEnabledStub.returns(true); + config.setConfig(getConfigMock()); + }); + + afterEach(() => { + sandbox.restore(); + config.resetConfig(); + }); + + after(() => { + init(config); + }) + + after(() => { + resetUserIds(); + }) + + it('when a stored LMPID exists it is added to bids', (done) => { + startAuctionHook(() => { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.lmpid'); + expect(bid.userId.lmpid).to.equal('stored-lmpid'); + const lmpidAsEid = find(bid.userIdAsEids, e => e.source == 'loblawmedia.ca'); + expect(lmpidAsEid).to.deep.equal({ + source: 'loblawmedia.ca', + uids: [{ + id: 'stored-lmpid', + atype: 3, + }] + }); + }); + }); + done(); + }, { adUnits }); + }); + }); +}); diff --git a/test/spec/modules/lockrAIMIdSystem_spec.js b/test/spec/modules/lockrAIMIdSystem_spec.js index c886fb305c2..193ec45c278 100644 --- a/test/spec/modules/lockrAIMIdSystem_spec.js +++ b/test/spec/modules/lockrAIMIdSystem_spec.js @@ -1,5 +1,5 @@ -/* eslint-disable no-console */ -/* eslint-disable quotes */ + + import * as lockrAIMSystem from "../../../modules/lockrAIMIdSystem.js"; import { hook } from "../../../src/hook.js"; import { expect } from "chai"; diff --git a/test/spec/modules/loganBidAdapter_spec.js b/test/spec/modules/loganBidAdapter_spec.js index a9859bbd4ae..f51f22580e2 100644 --- a/test/spec/modules/loganBidAdapter_spec.js +++ b/test/spec/modules/loganBidAdapter_spec.js @@ -79,7 +79,7 @@ describe('LoganBidAdapter', function () { expect(data).to.be.an('object'); let placement = data['placements'][0]; expect(placement).to.be.an('object'); - expect(placement).to.have.keys('placementId', 'bidId', 'adFormat', 'wPlayer', 'hPlayer', 'schain', 'minduration', 'maxduration', 'mimes', 'protocols', 'startdelay', 'placement', 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity', 'bidfloor'); + expect(placement).to.have.keys('placementId', 'bidId', 'adFormat', 'wPlayer', 'hPlayer', 'schain', 'minduration', 'maxduration', 'mimes', 'protocols', 'startdelay', 'placement', 'plcmt', 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity', 'bidfloor'); expect(placement.adFormat).to.equal(VIDEO); expect(placement.wPlayer).to.equal(playerSize[0]); expect(placement.hPlayer).to.equal(playerSize[1]); diff --git a/test/spec/modules/logicadBidAdapter_spec.js b/test/spec/modules/logicadBidAdapter_spec.js index 12e8ca31cbb..5c86ffc9325 100644 --- a/test/spec/modules/logicadBidAdapter_spec.js +++ b/test/spec/modules/logicadBidAdapter_spec.js @@ -182,7 +182,9 @@ describe('LogicadAdapter', function () { stack: [] }, auctionStart: 1563337198010, - fledgeEnabled: true + paapi: { + enabled: true + } }; const serverResponse = { body: { @@ -388,8 +390,8 @@ describe('LogicadAdapter', function () { const paapiRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; const paapiInterpretedResponse = spec.interpretResponse(paapiServerResponse, paapiRequest); expect(paapiInterpretedResponse).to.have.property('bids'); - expect(paapiInterpretedResponse).to.have.property('fledgeAuctionConfigs'); - expect(paapiInterpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal(paapiServerResponse.body.ext.fledgeAuctionConfigs[0]); + expect(paapiInterpretedResponse).to.have.property('paapi'); + expect(paapiInterpretedResponse.paapi[0]).to.deep.equal(paapiServerResponse.body.ext.fledgeAuctionConfigs[0]); // native const nativeRequest = spec.buildRequests(nativeBidRequests, bidderRequest)[0]; diff --git a/test/spec/modules/lotamePanoramaIdSystem_spec.js b/test/spec/modules/lotamePanoramaIdSystem_spec.js index ea538db08e1..0fa90cc6278 100644 --- a/test/spec/modules/lotamePanoramaIdSystem_spec.js +++ b/test/spec/modules/lotamePanoramaIdSystem_spec.js @@ -2,10 +2,11 @@ import { lotamePanoramaIdSubmodule, storage, } from 'modules/lotamePanoramaIdSystem.js'; -import { uspDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; import sinon from 'sinon'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; const responseHeader = { 'Content-Type': 'application/json' }; @@ -17,11 +18,9 @@ describe('LotameId', function() { let setLocalStorageStub; let removeFromLocalStorageStub; let timeStampStub; - let uspConsentDataStub; let requestHost; const nowTimestamp = new Date().getTime(); - beforeEach(function () { logErrorStub = sinon.stub(utils, 'logError'); getCookieStub = sinon.stub(storage, 'getCookie'); @@ -33,7 +32,6 @@ describe('LotameId', function() { 'removeDataFromLocalStorage' ); timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp); - uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); if (navigator.userAgent && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { requestHost = 'https://c.ltmsphrcl.net/id'; } else { @@ -49,7 +47,6 @@ describe('LotameId', function() { setLocalStorageStub.restore(); removeFromLocalStorageStub.restore(); timeStampStub.restore(); - uspConsentDataStub.restore(); }); describe('caching initial data received from the remote server', function () { @@ -420,8 +417,10 @@ describe('LotameId', function() { beforeEach(function () { let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, { - gdprApplies: true, - consentString: 'consentGiven' + gdpr: { + gdprApplies: true, + consentString: 'consentGiven' + } }).callback; submoduleCallback(callBackSpy); @@ -450,76 +449,14 @@ describe('LotameId', function() { }); }); - describe('when gdpr applies and falls back to eupubconsent cookie', function () { - let request; - let callBackSpy = sinon.spy(); - let consentData = { - gdprApplies: true, - consentString: undefined - }; - - beforeEach(function () { - getCookieStub - .withArgs('eupubconsent-v2') - .returns('consentGiven'); - - let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; - submoduleCallback(callBackSpy); - - // the contents of the response don't matter for this - request = server.requests[0]; - request.respond(200, responseHeader, ''); - }); - - it('should call the remote server when getId is called', function () { - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_applies=true&gdpr_consent=consentGiven` - ); - }); - }); - - describe('when gdpr applies and falls back to euconsent cookie', function () { - let request; - let callBackSpy = sinon.spy(); - let consentData = { - gdprApplies: true, - consentString: undefined - }; - - beforeEach(function () { - getCookieStub - .withArgs('euconsent-v2') - .returns('consentGiven'); - - let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; - submoduleCallback(callBackSpy); - - // the contents of the response don't matter for this - request = server.requests[0]; - request.respond(200, responseHeader, ''); - }); - - it('should call the remote server when getId is called', function () { - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_applies=true&gdpr_consent=consentGiven` - ); - }); - }); - describe('when gdpr applies but no consent string is available', function () { let request; let callBackSpy = sinon.spy(); let consentData = { - gdprApplies: true, - consentString: undefined + gdpr: { + gdprApplies: true, + consentString: undefined + } }; beforeEach(function () { @@ -542,64 +479,6 @@ describe('LotameId', function() { }); }); - describe('when no consentData and falls back to eupubconsent cookie', function () { - let request; - let callBackSpy = sinon.spy(); - let consentData; - - beforeEach(function () { - getCookieStub - .withArgs('eupubconsent-v2') - .returns('consentGiven'); - - let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; - submoduleCallback(callBackSpy); - - // the contents of the response don't matter for this - request = server.requests[0]; - request.respond(200, responseHeader, ''); - }); - - it('should call the remote server when getId is called', function () { - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_consent=consentGiven` - ); - }); - }); - - describe('when no consentData and falls back to euconsent cookie', function () { - let request; - let callBackSpy = sinon.spy(); - let consentData; - - beforeEach(function () { - getCookieStub - .withArgs('euconsent-v2') - .returns('consentGiven'); - - let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; - submoduleCallback(callBackSpy); - - // the contents of the response don't matter for this - request = server.requests[0]; - request.respond(200, responseHeader, ''); - }); - - it('should call the remote server when getId is called', function () { - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_consent=consentGiven` - ); - }); - }); - describe('when no consentData and no cookies', function () { let request; let callBackSpy = sinon.spy(); @@ -808,7 +687,6 @@ describe('LotameId', function() { let callBackSpy = sinon.spy(); beforeEach(function () { - uspConsentDataStub.returns('1NNN'); let submoduleCallback = lotamePanoramaIdSubmodule.getId( { params: { @@ -839,12 +717,6 @@ describe('LotameId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should pass the usp consent string and client id back', function () { - expect(request.url).to.be.eq( - `${requestHost}?gdpr_applies=false&us_privacy=1NNN&c=1234` - ); - }); - it('should NOT set an expiry for the client', function () { sinon.assert.neverCalledWith( setCookieStub, @@ -905,7 +777,9 @@ describe('LotameId', function() { }, }, { - gdprApplies: false, + gdpr: { + gdprApplies: false, + } } ).callback; submoduleCallback(callBackSpy); @@ -958,4 +832,20 @@ describe('LotameId', function() { }); }); }); + describe('eid', () => { + before(() => { + attachIdSystem(lotamePanoramaIdSubmodule); + }); + it('lotamePanoramaId', function () { + const userId = { + lotamePanoramaId: 'some-random-id-value', + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'crwdcntrl.net', + uids: [{ id: 'some-random-id-value', atype: 1 }], + }); + }); + }) }); diff --git a/test/spec/modules/loyalBidAdapter_spec .js b/test/spec/modules/loyalBidAdapter_spec.js similarity index 77% rename from test/spec/modules/loyalBidAdapter_spec .js rename to test/spec/modules/loyalBidAdapter_spec.js index 28e87fc7047..1c9106e3be8 100644 --- a/test/spec/modules/loyalBidAdapter_spec .js +++ b/test/spec/modules/loyalBidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/loyalBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'loyal' +const bidder = 'loyal'; describe('LoyalBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('LoyalBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('LoyalBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('LoyalBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -74,10 +87,19 @@ describe('LoyalBidAdapter', function () { const bidderRequest = { uspConsent: '1---', gdprConsent: { - consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw' + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -114,6 +136,7 @@ describe('LoyalBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -122,7 +145,11 @@ describe('LoyalBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -147,7 +174,56 @@ describe('LoyalBidAdapter', function () { 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.an('array'); + 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 + } + ]; + + let 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'); @@ -174,6 +250,9 @@ describe('LoyalBidAdapter', function () { let 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; }); @@ -188,12 +267,38 @@ describe('LoyalBidAdapter', function () { expect(data.ccpa).to.equal(bidderRequest.uspConsent); expect(data.gdpr).to.not.exist; }); + }); - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { diff --git a/test/spec/modules/luceadBidAdapter_spec.js b/test/spec/modules/luceadBidAdapter_spec.js old mode 100644 new mode 100755 index 72bc7cc2d6e..0543e9694f0 --- a/test/spec/modules/luceadBidAdapter_spec.js +++ b/test/spec/modules/luceadBidAdapter_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable prebid/validate-imports,no-undef */ + import { expect } from 'chai'; import { spec } from 'modules/luceadBidAdapter.js'; import sinon from 'sinon'; @@ -28,6 +28,7 @@ describe('Lucead Adapter', () => { bidder: 'lucead', params: { placementId: '1', + region: 'eu', }, }; }); @@ -39,7 +40,10 @@ describe('Lucead Adapter', () => { describe('onBidWon', function () { let sandbox; - const bid = { foo: 'bar', creativeId: 'ssp:improve' }; + const bids = [ + { foo: 'bar', creativeId: 'ssp:improve' }, + { foo: 'bar', creativeId: '123:456' }, + ]; beforeEach(function () { sandbox = sinon.sandbox.create(); @@ -47,8 +51,11 @@ describe('Lucead Adapter', () => { it('should trigger impression pixel', function () { sandbox.spy(ajax, 'fetch'); - spec.onBidWon(bid); - expect(ajax.fetch.args[0][0]).to.match(/report\/impression$/); + + for (const bid of bids) { + spec.onBidWon(bid); + expect(ajax?.fetch?.args[0][0]).to.match(/report\/impression$/); + } }); afterEach(function () { @@ -77,49 +84,62 @@ describe('Lucead Adapter', () => { it('should have a post method', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].method).to.equal('POST'); + expect(request.method).to.equal('POST'); }); it('should contains a request id equals to the bid id', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(JSON.parse(request[0].data).bid_id).to.equal(bidRequests[0].bidId); + expect(JSON.parse(request.data).bid_requests[0].bid_id).to.equal(bidRequests[0].bidId); }); - it('should have an url that contains sub keyword', function () { + it('should have an url that contains sra keyword', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].url).to.match(/sub/); + expect(request.url).to.contain('/prebid/sra'); }); }); describe('interpretResponse', function () { - const serverResponse = { - body: { - 'bid_id': '2daf899fbe4c52', - 'request_id': '13aaa3df18bfe4', - 'ad': 'Ad', - 'ad_id': '3890677904', - 'cpm': 3.02, - 'currency': 'USD', - 'time': 1707257712095, - 'size': {'width': 300, 'height': 250}, - } + const serverResponseBody = { + 'request_id': '17548f887fb722', + 'bids': [ + { + 'bid_id': '2d663fdd390b49', + 'ad': '\u003chtml lang="en"\u003e\u003cbody style="margin:0;background-color:#FFF"\u003e\u003ciframe src="urn:uuid:fb81a0f9-b83a-4f27-8676-26760d090f1c" style="width:300px;height:250px;border:none" seamless \u003e\u003c/iframe\u003e\u003c/body\u003e\u003c/html\u003e', + 'size': { + 'width': 300, + 'height': 250 + }, + 'ad_id': '1', + 'ig_id': '1', + 'cpm': 1, + 'currency': 'EUR', + 'time': 0, + 'ssp': '', + 'placement_id': '1', + 'is_pa': true + } + ] }; - const bidRequest = {data: JSON.stringify({ - 'request_id': '13aaa3df18bfe4', - 'domain': '7cdb-2a02-8429-e4a0-1701-bc69-d51c-86e-b279.ngrok-free.app', - 'bid_id': '2daf899fbe4c52', - 'sizes': [[300, 250]], - 'media_types': {'banner': {'sizes': [[300, 250]]}}, - 'fledge_enabled': true, - 'enable_contextual': true, - 'enable_pa': true, - 'params': {'placementId': '1'}, - })}; + const serverResponse = {body: serverResponseBody}; + + const bidRequest = { + data: JSON.stringify({ + 'request_id': '17548f887fb722', + 'domain': 'lucead.com', + 'bid_requests': [{ + 'bid_id': '2d663fdd390b49', + 'sizes': [[300, 250], [300, 150]], + 'media_types': {'banner': {'sizes': [[300, 250], [300, 150]]}}, + 'placement_id': '1' + }], + }), + }; it('should get correct bid response', function () { const result = spec.interpretResponse(serverResponse, bidRequest); + // noinspection JSCheckFunctionSignatures expect(Object.keys(result.bids[0])).to.have.members([ 'requestId', 'cpm', @@ -135,7 +155,7 @@ describe('Lucead Adapter', () => { }); it('should return bid empty response', function () { - const serverResponse = {body: {cpm: 0}}; + const serverResponse = {body: {bids: [{cpm: 0}]}}; const bidRequest = {data: '{}'}; const result = spec.interpretResponse(serverResponse, bidRequest); expect(result.bids[0].ad).to.be.equal(''); @@ -154,18 +174,11 @@ describe('Lucead Adapter', () => { expect(Object.keys(result.bids[0].meta)).to.include.members(['advertiserDomains']); }); - it('should support disabled contextual bids', function () { - const serverResponseWithDisabledContectual = deepClone(serverResponse); - serverResponseWithDisabledContectual.body.enable_contextual = false; - const result = spec.interpretResponse(serverResponseWithDisabledContectual, bidRequest); - expect(result.bids).to.be.null; - }); - - it('should support disabled Protected Audience', function () { - const serverResponseWithEnablePaFalse = deepClone(serverResponse); - serverResponseWithEnablePaFalse.body.enable_pa = false; - const result = spec.interpretResponse(serverResponseWithEnablePaFalse, bidRequest); - expect(result.fledgeAuctionConfigs).to.be.undefined; + it('should support enable_pa = false', function () { + serverResponse.body.enable_pa = false; + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.be.an('array'); + expect(result[0].cpm).to.be.greaterThan(0); }); }); }); diff --git a/test/spec/modules/lunamediahbBidAdapter_spec.js b/test/spec/modules/lunamediahbBidAdapter_spec.js index 181b6e75fe7..b715fb0d0c3 100644 --- a/test/spec/modules/lunamediahbBidAdapter_spec.js +++ b/test/spec/modules/lunamediahbBidAdapter_spec.js @@ -1,145 +1,257 @@ import {expect} from 'chai'; import {spec} from '../../../modules/lunamediahbBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'lunamediahb'; describe('LunamediaHBBidAdapter', function () { - const bid = { - bidId: '23fhj33i987f', - bidder: 'lunamediahb', + 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: { - placementId: 783, - traffic: BANNER + } - }; + } const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'test.com' - } + 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(bid)).to.be.true; + expect(spec.isBidRequestValid(bids[0])).to.be.true; }); it('Should return false if at least one of parameters is not present', function () { - delete bid.params.placementId; - expect(spec.isBidRequestValid(bid)).to.be.false; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; }); }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid], bidderRequest); + 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 valid URL', function () { expect(serverRequest.url).to.equal('https://balancer.lmgssp.com/?c=o&m=multi'); }); - it('Returns valid data if array of bids is valid', function () { + + it('Returns general data valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + 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.gdpr).to.not.exist; - expect(data.ccpa).to.not.exist; - let placement = data['placements'][0]; - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); - expect(placement.placementId).to.equal(783); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.traffic).to.equal(BANNER); - expect(placement.schain).to.be.an('object'); - expect(placement.sizes).to.be.an('array'); + 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 data for mediatype video', function () { - const playerSize = [300, 300]; - bid.mediaTypes = {}; - bid.params.traffic = VIDEO; - bid.mediaTypes[VIDEO] = { - playerSize - }; - serverRequest = spec.buildRequests([bid], bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - let placement = data['placements'][0]; - expect(placement).to.be.an('object'); - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain', 'videoContext'); - expect(placement.traffic).to.equal(VIDEO); - expect(placement.wPlayer).to.equal(playerSize[0]); - expect(placement.hPlayer).to.equal(playerSize[1]); - }); + 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); - it('Returns valid data for mediatype native', function () { - const native = { - title: { - required: true - }, - body: { - required: true - }, - icon: { - required: true, - size: [64, 64] + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); } - }; - - bid.mediaTypes = {}; - bid.params.traffic = NATIVE; - bid.mediaTypes[NATIVE] = native; - serverRequest = spec.buildRequests([bid], bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - let placement = data['placements'][0]; - expect(placement).to.be.an('object'); - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain'); - expect(placement.traffic).to.equal(NATIVE); - expect(placement.native).to.equal(native); + 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 () { - bidderRequest.gdprConsent = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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 = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { @@ -154,23 +266,28 @@ describe('LunamediaHBBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - dealId: '1' + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let bannerResponses = spec.interpretResponse(banner); expect(bannerResponses).to.be.an('array').that.is.not.empty; let dataItem = bannerResponses[0]; expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType'); - expect(dataItem.requestId).to.equal('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.width).to.equal(300); - expect(dataItem.height).to.equal(250); - expect(dataItem.ad).to.equal('Test'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); + '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('USD'); + 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 = { @@ -183,7 +300,11 @@ describe('LunamediaHBBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - dealId: '1' + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let videoResponses = spec.interpretResponse(video); @@ -191,7 +312,7 @@ describe('LunamediaHBBidAdapter', function () { let dataItem = videoResponses[0]; expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType'); + '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'); @@ -199,6 +320,7 @@ describe('LunamediaHBBidAdapter', function () { 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 = { @@ -216,13 +338,17 @@ describe('LunamediaHBBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let nativeResponses = spec.interpretResponse(native); expect(nativeResponses).to.be.an('array').that.is.not.empty; let dataItem = nativeResponses[0]; - expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + 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); @@ -235,6 +361,7 @@ describe('LunamediaHBBidAdapter', function () { 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 = { @@ -326,5 +453,17 @@ describe('LunamediaHBBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://cookie.lmgssp.com/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cookie.lmgssp.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/luponmediaBidAdapter_spec.js b/test/spec/modules/luponmediaBidAdapter_spec.js index 1441abc0fe8..3d9be5a40bf 100755 --- a/test/spec/modules/luponmediaBidAdapter_spec.js +++ b/test/spec/modules/luponmediaBidAdapter_spec.js @@ -21,12 +21,12 @@ describe('luponmediaBidAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'siteId': 12345 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -188,6 +188,9 @@ describe('luponmediaBidAdapter', function () { 'price': 0.43, 'adm': ' ', 'adid': '56380110', + 'adomain': [ + 'mi.betrivers.com' + ], 'cid': '44724710', 'crid': '443801010', 'w': 300, @@ -232,7 +235,15 @@ describe('luponmediaBidAdapter', function () { 'netRevenue': false, 'ttl': 300, 'referrer': '', - 'ad': ' ' + 'ad': ' ', + 'adomain': [ + 'mi.betrivers.com' + ], + 'meta': { + 'advertiserDomains': [ + 'mi.betrivers.com' + ] + } } ]; @@ -385,49 +396,4 @@ describe('luponmediaBidAdapter', function () { expect(checkSchain).to.equal(false); }); }); - - describe('onBidWon', function () { - const bidWonEvent = { - 'bidderCode': 'luponmedia', - 'width': 300, - 'height': 250, - 'statusMessage': 'Bid available', - 'adId': '105bbf8c54453ff', - 'requestId': '934b8752185955', - 'mediaType': 'banner', - 'source': 'client', - 'cpm': 0.364, - 'creativeId': '443801010', - 'currency': 'USD', - 'netRevenue': false, - 'ttl': 300, - 'referrer': '', - 'ad': '', - 'auctionId': '926a8ea3-3dd4-4bf2-95ab-c85c2ce7e99b', - 'responseTimestamp': 1598527728026, - 'requestTimestamp': 1598527727629, - 'bidder': 'luponmedia', - 'adUnitCode': 'div-gpt-ad-1533155193780-5', - 'timeToRespond': 397, - 'size': '300x250', - 'status': 'rendered' - }; - - let ajaxStub; - - beforeEach(() => { - ajaxStub = sinon.stub(spec, 'sendWinningsToServer') - }) - - afterEach(() => { - ajaxStub.restore() - }) - - it('calls luponmedia\'s callback endpoint', () => { - const result = spec.onBidWon(bidWonEvent); - expect(result).to.equal(undefined); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.deep.equal(JSON.stringify(bidWonEvent)); - }); - }); }); diff --git a/test/spec/modules/madsenseBidAdapter_spec.js b/test/spec/modules/madsenseBidAdapter_spec.js new file mode 100644 index 00000000000..eeb8c2d6f25 --- /dev/null +++ b/test/spec/modules/madsenseBidAdapter_spec.js @@ -0,0 +1,188 @@ +import { expect } from 'chai'; +import { spec } from 'modules/madsenseBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; +import { generateUUID } from '../../../src/utils.js'; + +const getCommonParams = () => ({ + company_id: '1234567' +}); + +const getVideoParams = () => ({ + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123, + }, + site: { + id: 1, + page: 'https://test.io', + referrer: 'http://test.io', + }, +}); + +const getBannerRequest = () => ({ + bidderCode: 'madsense', + auctionId: generateUUID(), + bidderRequestId: 'bidderRequestId', + bids: [ + { + bidder: 'madsense', + params: getCommonParams(), + auctionId: generateUUID(), + placementCode: 'dummy-placement-code', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bidId: 'a1b2c3d4', + bidderRequestId: 'bidderRequestId', + }, + ], + start: Date.now(), + auctionStart: Date.now() - 1, + timeout: 3000, +}); + +const getVideoBid = (bidId) => ({ + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + }, + }, + bidder: 'madsense', + sizes: [640, 480], + bidId, + adUnitCode: 'video1', + params: { + ...getVideoParams(), + ...getCommonParams(), + }, +}); + +const getVideoRequest = () => ({ + bidderCode: 'madsense', + auctionId: generateUUID(), + bidderRequestId: 'q1w2e3r4', + bids: [getVideoBid('i8u7y6t5'), getVideoBid('i8u7y6t5')], + auctionStart: Date.now(), + timeout: 5000, + start: Date.now() + 4, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'test.io', + }, +}); + +const getBidderResponse = () => ({ + headers: null, + body: { + id: 'bid-response', + seatbid: [ + { + bid: [ + { + id: 'a1b2c3d4', + impid: 'a1b2c3d4', + price: 0.18, + adm: '', + adid: '144762342', + adomain: ['https://test.io'], + iurl: 'iurl', + cid: '109', + crid: 'creativeId', + w: 300, + h: 250, + ext: { + prebid: { type: 'banner' }, + bidder: { + appnexus: { + brand_id: 321654987, + auction_id: 321654987000000, + bidder_id: 2, + bid_ad_type: 0, + }, + }, + }, + }, + ], + seat: 'madsense', + }, + ] + }, +}); + +describe('madsenseBidAdapter', function () { + describe('interpretResponse', function () { + context('when mediaType is banner', function () { + it('handles empty response', function () { + const bidderRequest = getBannerRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + const EMPTY_RESP = { ...getBidderResponse(), body: {} }; + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('validates banner bid', function () { + const bidderRequest = getBannerRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + const bidderResponse = getBidderResponse(); + const bids = spec.interpretResponse(bidderResponse, bidRequest); + + expect(bids).to.be.an('array').that.is.not.empty; + validateBid(bids[0], bidderResponse.body.seatbid[0].bid[0]); + }); + }); + + context('when mediaType is video', function () { + it('handles empty response', function () { + const bidderRequest = getVideoRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + const EMPTY_RESP = { ...getBidderResponse(), body: {} }; + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('returns no bids if required fields are missing', function () { + const bidderRequest = getVideoRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + const MISSING_FIELDS_RESP = { + ...getBidderResponse(), + body: { seatbid: [{ bid: [{ price: 6.01 }] }] }, + }; + const bids = spec.interpretResponse(MISSING_FIELDS_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + }); + }); +}); + +function validateBid(actualBid, expectedBid) { + expect(actualBid).to.include({ + currency: 'USD', + requestId: expectedBid.impid, + cpm: expectedBid.price, + width: expectedBid.w, + height: expectedBid.h, + ad: expectedBid.adm, + creativeId: expectedBid.crid, + ttl: 55, + netRevenue: true, + }); + expect(actualBid.meta).to.have.property('advertiserDomains'); +} diff --git a/test/spec/modules/madvertiseBidAdapter_spec.js b/test/spec/modules/madvertiseBidAdapter_spec.js index 466d30acdd3..8128bcc2d42 100644 --- a/test/spec/modules/madvertiseBidAdapter_spec.js +++ b/test/spec/modules/madvertiseBidAdapter_spec.js @@ -118,7 +118,7 @@ describe('madvertise adapater', () => { expect(req[0].url).to.contain(`&zoneId=test`); expect(req[0].url).to.contain(`&sizes[0]=728x90`); expect(req[0].url).to.contain(`&gdpr=1`); - expect(req[0].url).to.contain(`&consent[0][format]=IAB`); + expect(req[0].url).to.contain(`&consent[0][format]=iab`); expect(req[0].url).to.contain(`&consent[0][value]=CO_5mtSPHOmEIAsAkBFRBOCsAP_AAH_AAAqIHQgB7SrERyNAYWB5gusAKYlfQAQCA2AABAYdASgJQQBAMJYEkGAIuAnAACAKAAAEIHQAAAAlCCmABAEAAIABBSGMAQgABZAAIiAEEAATAABACAABGYCSCAIQjIAAAAEAgEKEAAoAQGBAAAEgBABAAAogACADAgXmACIKkQBAkBAYAkAYQAogAhAAAAAIAAAAAAAKAABAAAghAAQQAAAAAAAAAgAAAAABAAAAAAAAQAAAAAAAAABAAgAAAAAAAAAIAAAAAAAAAAAAAAAABAAAAAAAAAAAQCAKCgBgEQALgAqkJADAIgAXABVIaACAAERABAACKgAgABA`) }); diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 731b4ab1682..26a8f2ed313 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -26,6 +26,7 @@ const { BID_TIMEOUT, BILLABLE_EVENT, SEAT_NON_BID, + PBS_ANALYTICS, BID_REJECTED } = EVENTS; @@ -639,6 +640,45 @@ describe('magnite analytics adapter', function () { ]); }); + it('should pass along atag data', function () { + const PBS_ANALYTICS_EVENT = { + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + atag: [{ + 'stage': 'processed-auction-request', + 'module': 'mgni-timeout-optimization', + 'analyticstags': [{ + activities: [{ + name: 'optimize-tmax', + status: 'success', + results: [{ + status: 'success', + values: { + 'scenario': 'a', + 'rule': 'b', + 'tmax': 3 + } + }] + }] + }] + }] + } + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(PBS_ANALYTICS, PBS_ANALYTICS_EVENT) + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + + let message = JSON.parse(server.requests[0].requestBody); + expect(message.auctions[0].experiments[0]).to.deep.equal({ + name: 'a', + rule: 'b', + value: 3 + }); + }); + it('should pass along user ids', function () { let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); auctionInit.bidderRequests[0].bids[0].userId = { @@ -719,6 +759,34 @@ describe('magnite analytics adapter', function () { expect(message.auctions[0].adUnits[0].bids[0].bidResponse.networkId).to.equal(test.expected); }); }); + + // meta mediatype handler things + [ + { input: undefined, expected: 'banner', hasOg: false }, + { input: 'banner', expected: 'banner', hasOg: false }, + { input: 'video', expected: 'video', hasOg: true } + ].forEach((test, index) => { + it(`should handle meta mediaType stuff correctly - #${index + 1}`, function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + let bidResponse = utils.deepClone(MOCK.BID_RESPONSE); + bidResponse.meta = { + mediaType: test.input + }; + + events.emit(BID_RESPONSE, bidResponse); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(BID_WON, MOCK.BID_WON); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + + let message = JSON.parse(server.requests[0].requestBody); + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.mediaType).to.equal(test.expected); + if (test.hasOg) expect(message.auctions[0].adUnits[0].bids[0].bidResponse.ogMediaType).to.equal('banner'); + else expect(message.auctions[0].adUnits[0].bids[0].bidResponse).to.not.haveOwnProperty('ogMediaType'); + }); + }); }); describe('with session handling', function () { @@ -1758,16 +1826,32 @@ describe('magnite analytics adapter', function () { }); }); describe('cookieless', () => { - beforeEach(() => { - magniteAdapter.enableAnalytics({ - options: { - cookieles: undefined - } - }); - }) afterEach(() => { magniteAdapter.disableAnalytics(); }) + it('should not add cookieless and preserve original rule name', () => { + // Set the confs + config.setConfig({ + rubicon: { + wrapperName: '1001_general', + wrapperFamily: 'general', + rule_name: 'desktop-magnite.com', + } + }); + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.match(/\/\/localhost:9999\/event/); + + let message = JSON.parse(request.requestBody); + expect(message.wrapper).to.deep.equal({ + name: '1001_general', + family: 'general', + rule: 'desktop-magnite.com', + }); + }) it('should add sufix _cookieless to the wrapper.rule if ortb2.device.ext.cdep start with "treatment" or "control_2"', () => { // Set the confs config.setConfig({ @@ -2246,7 +2330,7 @@ describe('magnite analytics adapter', function () { const runNonBidAuction = () => { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - events.emit(SEAT_NON_BID, seatnonbid) + events.emit(PBS_ANALYTICS, seatnonbid) events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); events.emit(AUCTION_END, MOCK.AUCTION_END); clock.tick(rubiConf.analyticsBatchTimeout + 1000); diff --git a/test/spec/modules/mantisBidAdapter_spec.js b/test/spec/modules/mantisBidAdapter_spec.js index 579f41e620d..f0f453d32a0 100644 --- a/test/spec/modules/mantisBidAdapter_spec.js +++ b/test/spec/modules/mantisBidAdapter_spec.js @@ -35,10 +35,10 @@ describe('MantisAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/marsmediaBidAdapter_spec.js b/test/spec/modules/marsmediaBidAdapter_spec.js index 055b05700b2..ca3876703c8 100644 --- a/test/spec/modules/marsmediaBidAdapter_spec.js +++ b/test/spec/modules/marsmediaBidAdapter_spec.js @@ -1,6 +1,7 @@ import { spec } from 'modules/marsmediaBidAdapter.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; +import { internal, resetWinDimensions } from '../../../src/utils'; var marsAdapter = spec; @@ -32,7 +33,9 @@ describe('marsmedia adapter tests', function () { document: { visibilityState: 'visible' }, - + location: { + href: 'http://location' + }, innerWidth: 800, innerHeight: 600 }; @@ -520,10 +523,13 @@ describe('marsmedia adapter tests', function () { context('when element is partially in view', function() { it('returns percentage', function() { + sandbox.stub(internal, 'getWindowTop').returns(win); + resetWinDimensions(); Object.assign(element, { width: 800, height: 800 }); const request = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(request.data); expect(openrtbRequest.imp[0].ext.viewability).to.equal(75); + internal.getWindowTop.restore(); }); }); diff --git a/test/spec/modules/mathildeadsBidAdapter_spec.js b/test/spec/modules/mathildeadsBidAdapter_spec.js index 107906ec83d..eb4318199af 100644 --- a/test/spec/modules/mathildeadsBidAdapter_spec.js +++ b/test/spec/modules/mathildeadsBidAdapter_spec.js @@ -3,21 +3,32 @@ import { spec } from '../../../modules/mathildeadsBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'mathildeads' +const bidder = 'mathildeads'; describe('MathildeAdsBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), - bidder: bidder, + bidder, mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('MathildeAdsBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('MathildeAdsBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -66,14 +79,27 @@ describe('MathildeAdsBidAdapter', function () { sizes: [[300, 250]] } }, - params: {} + params: { + + } } const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -110,6 +136,7 @@ describe('MathildeAdsBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -118,7 +145,11 @@ describe('MathildeAdsBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -127,7 +158,7 @@ describe('MathildeAdsBidAdapter', function () { 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('string'); + 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); @@ -142,6 +173,8 @@ describe('MathildeAdsBidAdapter', function () { 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'); @@ -167,8 +200,10 @@ describe('MathildeAdsBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -183,12 +218,38 @@ describe('MathildeAdsBidAdapter', function () { 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -367,6 +428,7 @@ describe('MathildeAdsBidAdapter', function () { expect(serverResponses).to.be.an('array').that.is.empty; }); }); + describe('getUserSyncs', function() { it('Should return array of objects with proper sync config , include GDPR', function() { const syncData = spec.getUserSyncs({}, {}, { @@ -391,5 +453,17 @@ describe('MathildeAdsBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://cs2.mathilde-ads.com/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs2.mathilde-ads.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/mediaConsortiumBidAdapter_spec.js b/test/spec/modules/mediaConsortiumBidAdapter_spec.js new file mode 100644 index 00000000000..6a7ac6f5741 --- /dev/null +++ b/test/spec/modules/mediaConsortiumBidAdapter_spec.js @@ -0,0 +1,411 @@ +import { expect } from 'chai'; +import { spec, OPTIMIZATIONS_STORAGE_KEY, getOptimizationsFromLocalStorage } from 'modules/mediaConsortiumBidAdapter.js'; + +const BANNER_BID = { + adUnitCode: 'dfp_ban_atf', + bidId: '2f0d9715f60be8', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + } +} + +const VIDEO_BID = { + adUnitCode: 'video', + bidId: '2f0d9715f60be8', + mediaTypes: { + video: { + playerSize: [ + [300, 250] + ], + context: 'outstream' + } + } +} + +const VIDEO_BID_WITH_MISSING_CONTEXT = { + adUnitCode: 'video', + bidId: '2f0d9715f60be8', + mediaTypes: { + video: { + playerSize: [ + [300, 250] + ] + } + } +} + +const MULTI_MEDIATYPES_BID = { + adUnitCode: 'multi_type', + bidId: '2f0d9715f60be8', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + }, + video: { + playerSize: [ + [300, 250] + ], + context: 'outstream' + } + } +} + +const MULTI_MEDIATYPES_WITH_INVALID_VIDEO_CONTEXT = { + adUnitCode: 'multi_type', + bidId: '2f0d9715f60be8', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + }, + video: { + playerSize: [ + [300, 250] + ], + context: 'instream' + } + } +} + +describe('Media Consortium Bid Adapter', function () { + before(function () { + // The local storage variable is not cleaned in some other test so we need to do it ourselves here + localStorage.removeItem('ope_fpid') + }) + + beforeEach(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + mediaConsortium: { + storageAllowed: true + } + } + }) + + afterEach(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }) + + describe('buildRequests', function () { + const bidderRequest = { + auctionId: '98bb5f61-4140-4ced-8b0e-65a33d792ab8', + ortb2: { + device: { + w: 1200, + h: 400, + dnt: 0 + }, + site: { + page: 'http://localhost.com', + domain: 'localhost.com' + } + } + }; + + it('should build a banner request', function () { + const builtSyncRequest = { + gdpr: false, + ad_unit_codes: BANNER_BID.adUnitCode + } + + const builtBidRequest = { + id: '98bb5f61-4140-4ced-8b0e-65a33d792ab8', + impressions: [{ + id: BANNER_BID.bidId, + adUnitCode: BANNER_BID.adUnitCode, + mediaTypes: BANNER_BID.mediaTypes + }], + device: { + w: 1200, + h: 400, + dnt: 0 + }, + site: { + page: 'http://localhost.com', + domain: 'localhost.com' + }, + user: { + ids: {} + }, + options: { + useProfileApi: false + }, + regulations: { + gdpr: { + applies: false, + consentString: undefined + } + }, + timeout: 3600 + } + + const bids = [BANNER_BID] + const [syncRequest, auctionRequest] = spec.buildRequests(bids, {...bidderRequest, bids}); + + expect(syncRequest.data).to.deep.equal(builtSyncRequest) + expect(auctionRequest.data).to.deep.equal(builtBidRequest) + }) + + it('should build a video request', function () { + const builtSyncRequest = { + gdpr: false, + ad_unit_codes: VIDEO_BID.adUnitCode + } + + const builtBidRequest = { + id: '98bb5f61-4140-4ced-8b0e-65a33d792ab8', + impressions: [{ + id: VIDEO_BID.bidId, + adUnitCode: VIDEO_BID.adUnitCode, + mediaTypes: VIDEO_BID.mediaTypes + }], + device: { + w: 1200, + h: 400, + dnt: 0 + }, + site: { + page: 'http://localhost.com', + domain: 'localhost.com' + }, + user: { + ids: {} + }, + options: { + useProfileApi: false + }, + regulations: { + gdpr: { + applies: false, + consentString: undefined + } + }, + timeout: 3600 + } + + const bids = [VIDEO_BID] + const [syncRequest, auctionRequest] = spec.buildRequests(bids, {...bidderRequest, bids}); + + expect(syncRequest.data).to.deep.equal(builtSyncRequest) + expect(auctionRequest.data).to.deep.equal(builtBidRequest) + }) + + it('should build a request with multiple mediatypes', function () { + const builtSyncRequest = { + gdpr: false, + ad_unit_codes: MULTI_MEDIATYPES_BID.adUnitCode + } + + const builtBidRequest = { + id: '98bb5f61-4140-4ced-8b0e-65a33d792ab8', + impressions: [{ + id: MULTI_MEDIATYPES_BID.bidId, + adUnitCode: MULTI_MEDIATYPES_BID.adUnitCode, + mediaTypes: MULTI_MEDIATYPES_BID.mediaTypes + }], + device: { + w: 1200, + h: 400, + dnt: 0 + }, + site: { + page: 'http://localhost.com', + domain: 'localhost.com' + }, + user: { + ids: {} + }, + options: { + useProfileApi: false + }, + regulations: { + gdpr: { + applies: false, + consentString: undefined + } + }, + timeout: 3600 + } + + const bids = [MULTI_MEDIATYPES_BID] + const [syncRequest, auctionRequest] = spec.buildRequests(bids, {...bidderRequest, bids}); + + expect(syncRequest.data).to.deep.equal(builtSyncRequest) + expect(auctionRequest.data).to.deep.equal(builtBidRequest) + }) + + it('should not build a request if optimizations are there for the adunit code', function () { + const bids = [BANNER_BID] + const optimizations = { + [bids[0].adUnitCode]: {isEnabled: false, expiresAt: Date.now() + 600000} + } + + localStorage.setItem(OPTIMIZATIONS_STORAGE_KEY, JSON.stringify(optimizations)) + + const requests = spec.buildRequests(bids, {...bidderRequest, bids}); + + localStorage.removeItem(OPTIMIZATIONS_STORAGE_KEY) + + expect(requests).to.be.undefined + }) + + it('should exclude video requests where context is missing or not equal to outstream', function () { + const builtSyncRequest = { + gdpr: false, + ad_unit_codes: MULTI_MEDIATYPES_WITH_INVALID_VIDEO_CONTEXT.adUnitCode + } + + const builtBidRequest = { + id: '98bb5f61-4140-4ced-8b0e-65a33d792ab8', + impressions: [{ + id: MULTI_MEDIATYPES_WITH_INVALID_VIDEO_CONTEXT.bidId, + adUnitCode: MULTI_MEDIATYPES_WITH_INVALID_VIDEO_CONTEXT.adUnitCode, + mediaTypes: {banner: MULTI_MEDIATYPES_WITH_INVALID_VIDEO_CONTEXT.mediaTypes.banner} + }], + device: { + w: 1200, + h: 400, + dnt: 0 + }, + site: { + page: 'http://localhost.com', + domain: 'localhost.com' + }, + user: { + ids: {} + }, + options: { + useProfileApi: false + }, + regulations: { + gdpr: { + applies: false, + consentString: undefined + } + }, + timeout: 3600 + } + + const invalidVideoBids = [VIDEO_BID_WITH_MISSING_CONTEXT] + const multiMediatypesBidWithInvalidVideo = [MULTI_MEDIATYPES_WITH_INVALID_VIDEO_CONTEXT] + + expect(spec.buildRequests(invalidVideoBids, {...bidderRequest, bids: invalidVideoBids})).to.be.undefined + + const [syncRequest, auctionRequest] = spec.buildRequests(multiMediatypesBidWithInvalidVideo, {...bidderRequest, bids: multiMediatypesBidWithInvalidVideo}) + + expect(syncRequest.data).to.deep.equal(builtSyncRequest) + expect(auctionRequest.data).to.deep.equal(builtBidRequest) + }) + }) + + describe('interpretResponse', function () { + it('should return an empty array if the response is invalid', function () { + expect(spec.interpretResponse({body: 'INVALID_BODY'}, {})).to.deep.equal([]); + }) + + it('should return a formatted bid', function () { + const serverResponse = { + body: { + id: 'requestId', + bids: [{ + impressionId: '2f0d9715f60be8', + price: { + cpm: 1, + currency: 'JPY' + }, + dealId: 'TEST_DEAL_ID', + ad: { + creative: { + id: 'CREATIVE_ID', + mediaType: 'banner', + size: {width: 320, height: 250}, + markup: '
1
' + } + }, + ttl: 3600 + }], + optimizations: [ + { + adUnitCode: 'test_ad_unit_code', + isEnabled: false, + ttl: 12000 + }, + { + adUnitCode: 'test_ad_unit_code_2', + isEnabled: true, + ttl: 12000 + } + ] + } + } + + const formattedBid = { + requestId: '2f0d9715f60be8', + cpm: 1, + currency: 'JPY', + dealId: 'TEST_DEAL_ID', + ttl: 3600, + netRevenue: true, + creativeId: 'CREATIVE_ID', + mediaType: 'banner', + width: 320, + height: 250, + ad: '
1
', + adUrl: null + } + + const formattedResponse = spec.interpretResponse(serverResponse, {}) + const storedOptimizations = getOptimizationsFromLocalStorage() + + localStorage.removeItem(OPTIMIZATIONS_STORAGE_KEY) + + expect(formattedResponse).to.deep.equal([formattedBid]); + + expect(storedOptimizations['test_ad_unit_code']).to.exist + expect(storedOptimizations['test_ad_unit_code'].isEnabled).to.equal(false) + expect(storedOptimizations['test_ad_unit_code'].expiresAt).to.be.a('number') + + expect(storedOptimizations['test_ad_unit_code_2']).to.exist + expect(storedOptimizations['test_ad_unit_code_2'].isEnabled).to.equal(true) + expect(storedOptimizations['test_ad_unit_code_2'].expiresAt).to.be.a('number') + }) + }); + + describe('getUserSyncs', function () { + it('should return an empty response if the response is invalid or missing data', function () { + expect(spec.getUserSyncs(null, [{body: 'INVALID_BODY'}])).to.be.undefined; + expect(spec.getUserSyncs(null, [{body: 'INVALID_BODY'}, {body: 'INVALID_BODY'}])).to.be.undefined; + }) + + it('should return an array of user syncs', function () { + const serverResponses = [ + { + body: { + bidders: [ + {type: 'image', url: 'https://test-url.com'}, + {type: 'redirect', url: 'https://test-url.com'}, + {type: 'iframe', url: 'https://test-url.com'} + ] + } + }, + { + body: 'BID-RESPONSE-DATA' + } + ] + + const formattedUserSyncs = [ + {type: 'image', url: 'https://test-url.com'}, + {type: 'image', url: 'https://test-url.com'}, + {type: 'iframe', url: 'https://test-url.com'} + ] + + expect(spec.getUserSyncs(null, serverResponses)).to.deep.equal(formattedUserSyncs); + }) + }); +}); diff --git a/test/spec/modules/mediaeyesBidAdapter_spec.js b/test/spec/modules/mediaeyesBidAdapter_spec.js new file mode 100644 index 00000000000..b79ece092a2 --- /dev/null +++ b/test/spec/modules/mediaeyesBidAdapter_spec.js @@ -0,0 +1,183 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/mediaeyesBidAdapter.js'; +import * as utils from '../../../src/utils.js'; + +describe('mediaeyes adapter', function () { + let request; + let bannerResponse, invalidResponse; + + beforeEach(function () { + request = [ + { + bidder: 'mediaeyes', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + itemId: 'ec1d7389a4a5afa28a23c4', + bidFloor: 0.1 + } + } + ]; + bannerResponse = { + 'body': { + "id": "3c51f851-56d8-4513-b4bb-e5a1612cede3", + "seatbid": [ + { + "bid": [ + { + "impid": "3db1c7f2867eb3", + "adm": " ", + "iurl": "https://static.upremium.asia/n1191/ad/300x250_OWMrIjJQ.jpg", + "h": 250, + "w": 300, + "price": 0.25, + "crid": "6808551", + "adomain": [ + "google.com" + ], + "ext": { + "advertiser_name": "urekamedia", + "agency_name": "urekamedia" + } + } + ] + } + ] + } + }; + invalidResponse = { + 'body': { + + } + }; + }); + + describe('validations', function () { + it('isBidValid : itemId is passed', function () { + let bid = { + bidder: 'mediaeyes', + params: { + itemId: 'ec1d7389a4a5afa28a23c4', + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(true); + }); + it('isBidValid : itemId is not passed', function () { + let bid = { + bidder: 'mediaeyes', + params: { + + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + }); + describe('Validate Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(request), + bidRequest = spec.buildRequests(request); + expect(request).to.deep.equal(_Request); + }); + }); + + describe('responses processing', function () { + it('should return fully-initialized banner bid-response', function () { + let bidRequest = spec.buildRequests(request); + + let resp = spec.interpretResponse(bannerResponse, bidRequest[0])[0]; + expect(resp).to.have.property('requestId'); + expect(resp).to.have.property('cpm'); + expect(resp).to.have.property('width'); + expect(resp).to.have.property('height'); + expect(resp).to.have.property('creativeId'); + expect(resp).to.have.property('currency'); + expect(resp).to.have.property('ttl'); + expect(resp).to.have.property('ad'); + expect(resp).to.have.property('meta'); + }); + + it('no ads returned', function () { + let response = { + "body": { + "id": "0309d787-75cd-4e9d-a430-666fc76c1fbe", + "seatbid": [ + { + "bid": [] + } + ] + } + } + let bidderRequest; + + let result = spec.interpretResponse(response, {bidderRequest}); + expect(result.length).to.equal(0); + }); + }) + + describe('setting imp.floor using floorModule', function () { + let newRequest; + let floorModuleTestData; + let getFloor = function (req) { + return floorModuleTestData['banner']; + }; + + beforeEach(() => { + floorModuleTestData = { + 'banner': { + 'currency': 'USD', + 'floor': 1, + }, + }; + newRequest = utils.deepClone(request); + newRequest[0].getFloor = getFloor; + }); + + it('params bidfloor undefined', function () { + floorModuleTestData.banner.floor = 0; + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request[0].data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(0); + }); + + it('floormodule if floor is not number', function () { + floorModuleTestData.banner.floor = 'INR'; + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request[0].data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(0); + }); + + it('floormodule if currency is not matched', function () { + floorModuleTestData.banner.currency = 'INR'; + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request[0].data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(1); + }); + + it('bidFloor is not passed, use minimum from floorModule', function () { + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request[0].data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(1); + }); + + it('if params bidFloor is passed, priority use it', function () { + newRequest[0].params.bidFloor = 1; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request[0].data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/mediafuseBidAdapter_spec.js b/test/spec/modules/mediafuseBidAdapter_spec.js index dd2b5df70bd..1fb09265d56 100644 --- a/test/spec/modules/mediafuseBidAdapter_spec.js +++ b/test/spec/modules/mediafuseBidAdapter_spec.js @@ -35,23 +35,23 @@ describe('MediaFuseAdapter', function () { }); it('should return true when required params found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'member': '1234', 'invCode': 'ABCD' }; - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(spec.isBidRequestValid(invalidBid)).to.equal(true); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placementId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js index 6e58217b3d3..a84ffe06270 100644 --- a/test/spec/modules/mediagoBidAdapter_spec.js +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -3,14 +3,11 @@ import { spec, getPmgUID, storage, - getPageTitle, - getPageDescription, - getPageKeywords, - getConnectionDownLink, THIRD_PARTY_COOKIE_ORIGIN, COOKIE_KEY_MGUID, getCurrentTimeToUTCString } from 'modules/mediagoBidAdapter.js'; +import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink } from '../../../libraries/fpdUtils/pageInfo.js'; import * as utils from 'src/utils.js'; describe('mediago:BidAdapterTests', function () { @@ -59,7 +56,9 @@ describe('mediago:BidAdapterTests', function () { content: { keywords: 'video, source=streaming' }, - + publisher: { + domain: 'mediago.io' + }, }, user: { ext: { @@ -161,7 +160,8 @@ describe('mediago:BidAdapterTests', function () { spec.isBidRequestValid({ bidder: 'mediago', params: { - token: ['85a6b01e41ac36d49744fad726e3655d'] + token: ['85a6b01e41ac36d49744fad726e3655d'], + publisher: ['test_publisher'] } }) ).to.equal(true); @@ -260,43 +260,43 @@ describe('mediago:BidAdapterTests', function () { expect(bid.height).to.equal(250); expect(bid.currency).to.equal('USD'); }); +}); - describe('mediago: getUserSyncs', function() { - const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; - const IFRAME_ENABLED = { - iframeEnabled: true, - pixelEnabled: false, - }; - const IFRAME_DISABLED = { - iframeEnabled: false, - pixelEnabled: false, - }; - const GDPR_CONSENT = { - consentString: 'gdprConsentString', - gdprApplies: true - }; - const USP_CONSENT = { - consentString: 'uspConsentString' +describe('mediago: getUserSyncs', function() { + const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; + const IFRAME_ENABLED = { + iframeEnabled: true, + pixelEnabled: false, + }; + const IFRAME_DISABLED = { + iframeEnabled: false, + pixelEnabled: false, + }; + const GDPR_CONSENT = { + consentString: 'gdprConsentString', + gdprApplies: true + }; + const USP_CONSENT = { + consentString: 'uspConsentString' + } + + let syncParamUrl = `dm=${encodeURIComponent(location.origin || `https://${location.host}`)}`; + syncParamUrl += '&gdpr=1&gdpr_consent=gdprConsentString&ccpa_consent=uspConsentString'; + const expectedIframeSyncs = [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` } + ]; - let syncParamUrl = `dm=${encodeURIComponent(location.origin || `https://${location.host}`)}`; - syncParamUrl += '&gdpr=1&gdpr_consent=gdprConsentString&ccpa_consent=uspConsentString'; - const expectedIframeSyncs = [ - { - type: 'iframe', - url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` - } - ]; - - it('should return nothing if iframe is disabled', () => { - const userSyncs = spec.getUserSyncs(IFRAME_DISABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); - expect(userSyncs).to.be.undefined; - }); + it('should return nothing if iframe is disabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_DISABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.be.undefined; + }); - it('should do userSyncs if iframe is enabled', () => { - const userSyncs = spec.getUserSyncs(IFRAME_ENABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); - expect(userSyncs).to.deep.equal(expectedIframeSyncs); - }); + it('should do userSyncs if iframe is enabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_ENABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.deep.equal(expectedIframeSyncs); }); }); diff --git a/test/spec/modules/mediaimpactBidAdapter_spec.js b/test/spec/modules/mediaimpactBidAdapter_spec.js index 3d706e59c3f..5bf088c0334 100644 --- a/test/spec/modules/mediaimpactBidAdapter_spec.js +++ b/test/spec/modules/mediaimpactBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec, ENDPOINT_PROTOCOL, ENDPOINT_DOMAIN, ENDPOINT_PATH} from 'modules/mediaimpactBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import * as miUtils from 'libraries/mediaImpactUtils/index.js'; const BIDDER_CODE = 'mediaimpact'; @@ -117,7 +118,7 @@ describe('MediaimpactAdapter', function () { describe('joinSizesToString', function () { it('success convert sizes list to string', function () { - const sizesStr = spec.joinSizesToString([[300, 250], [300, 600]]); + const sizesStr = miUtils.joinSizesToString([[300, 250], [300, 600]]); expect(sizesStr).to.equal('300x250|300x600'); }); }); @@ -139,9 +140,9 @@ describe('MediaimpactAdapter', function () { 'width': 300, 'height': 250, 'creativeId': '8:123456', - 'adomain': [ - 'test.domain' - ], + 'meta': { + 'advertiserDomains': ['test.domain'] + }, 'syncs': [ {'type': 'image', 'url': 'https://test.domain/tracker_1.gif'}, {'type': 'image', 'url': 'https://test.domain/tracker_2.gif'}, @@ -200,9 +201,9 @@ describe('MediaimpactAdapter', function () { 'cpm': 0.01, 'currency': 'USD', 'netRevenue': true, - 'adomain': [ - 'test.domain' - ], + 'meta': { + 'advertiserDomains': ['test.domain'] + }, }; it('fill ad for response', function () { @@ -238,7 +239,7 @@ describe('MediaimpactAdapter', function () { let ajaxStub; beforeEach(() => { - ajaxStub = sinon.stub(spec, 'postRequest') + ajaxStub = sinon.stub(miUtils, 'postRequest') }) afterEach(() => { @@ -263,9 +264,9 @@ describe('MediaimpactAdapter', function () { 'width': 300, 'height': 250, 'creativeId': '8:123456', - 'adomain': [ - 'test.domain' - ], + 'meta': { + 'advertiserDomains': ['test.domain'] + }, 'syncs': [ {'type': 'image', 'link': 'https://test.domain/tracker_1.gif'}, {'type': 'image', 'link': 'https://test.domain/tracker_2.gif'}, diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js index 7c3cf88dace..fa50252dadc 100644 --- a/test/spec/modules/medianetAnalyticsAdapter_spec.js +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -1,116 +1,300 @@ -import { expect } from 'chai'; +import {expect} from 'chai'; import medianetAnalytics from 'modules/medianetAnalyticsAdapter.js'; import * as utils from 'src/utils.js'; -import { EVENTS } from 'src/constants.js'; +import {EVENTS} from 'src/constants.js'; import * as events from 'src/events.js'; import {clearEvents} from 'src/events.js'; +import {deepAccess} from 'src/utils.js'; +import 'src/prebid.js'; +import {config} from 'src/config.js'; +import {REJECTION_REASON} from 'src/constants.js'; +import {getGlobal} from 'src/prebidGlobal.js'; +import sinon from "sinon"; +import * as mnUtils from '../../../libraries/medianetUtils/utils.js'; const { - AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, NO_BID, BID_TIMEOUT, AUCTION_END, SET_TARGETING, BID_WON + AUCTION_INIT, + BID_REQUESTED, + BID_RESPONSE, + NO_BID, + BID_TIMEOUT, + AUCTION_END, + SET_TARGETING, + BID_WON, + AD_RENDER_FAILED, + AD_RENDER_SUCCEEDED, + STALE_RENDER, + BID_REJECTED } = EVENTS; -const ERROR_WINNING_BID_ABSENT = 'winning_bid_absent'; - -const MOCK = { - Ad_Units: [{'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, 'bids': [], 'ext': {'prop1': 'value1'}}], - MULTI_FORMAT_TWIN_AD_UNITS: [{'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 250]]}, 'native': {'image': {'required': true, 'sizes': [150, 50]}}}, 'bids': [], 'ext': {'prop1': 'value1'}}, {'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'video': {'playerSize': [640, 480], 'context': 'instream'}}, 'bids': [], 'ext': {'prop1': 'value1'}}], - TWIN_AD_UNITS: [{'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 100]]}}, 'ask': '300x100', 'bids': [{'bidder': 'bidder1', 'params': {'siteId': '451465'}}]}, {'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 250], [300, 100]]}}, 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}}]}, {'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, 'bids': [{'bidder': 'bidder1', 'params': {'siteId': '451466'}}]}], - AUCTION_INIT: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739, 'timeout': 6000}, - AUCTION_INIT_WITH_FLOOR: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739, 'timeout': 6000, 'bidderRequests': [{'bids': [{ 'floorData': {'enforcements': {'enforceJS': true}} }]}]}, - BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, - MULTI_FORMAT_BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]]}, 'video': {'playerSize': [640, 480], 'context': 'instream'}, 'native': {'image': {'required': true, 'sizes': [150, 50]}, 'title': {'required': true, 'len': 80}}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, - TWIN_AD_UNITS_BID_REQUESTED: [{'bidderCode': 'bidder1', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bidderRequestId': '16f0746ff657b5', 'bids': [{'bidder': 'bidder1', 'params': {'siteId': '451465'}, 'mediaTypes': {'banner': {'sizes': [[300, 100]]}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '9615b5d1-7a4f-4c65-9464-4178b91da9e3', 'sizes': [[300, 100]], 'bidId': '2984d18e18bdfe', 'bidderRequestId': '16f0746ff657b5', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client', 'bidRequestsCount': 3, 'bidderRequestsCount': 2, 'bidderWinsCount': 0}, {'bidder': 'bidder1', 'params': {'siteId': '451466'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '8bd7c9f2-0fe6-4ac5-8f2a-7f4a88af1b71', 'sizes': [[300, 250]], 'bidId': '3dced609066035', 'bidderRequestId': '16f0746ff657b5', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client', 'bidRequestsCount': 3, 'bidderRequestsCount': 2, 'bidderWinsCount': 0}], 'auctionStart': 1584563605739, 'timeout': 3000, 'start': 1584563605743}, {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bidderRequestId': '4b45d1de1fa8fe', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250], [300, 100]]}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '215c038e-3b6a-465b-8937-d32e2ad8de45', 'sizes': [[300, 250], [300, 100]], 'bidId': '58d34adcb09c99', 'bidderRequestId': '4b45d1de1fa8fe', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client', 'bidRequestsCount': 3, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}], 'auctionStart': 1584563605739, 'timeout': 3000, 'start': 1584563605743}], - BID_RESPONSE: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, - AUCTION_END: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'auctionEnd': 1584563605739}, - SET_TARGETING: {'div-gpt-ad-1460505748561-0': {'prebid_test': '1', 'hb_format': 'banner', 'hb_source': 'client', 'hb_size': '300x250', 'hb_pb': '2.00', 'hb_adid': '3e6e4bce5c8fb3', 'hb_bidder': 'medianet', 'hb_format_medianet': 'banner', 'hb_source_medianet': 'client', 'hb_size_medianet': '300x250', 'hb_pb_medianet': '2.00', 'hb_adid_medianet': '3e6e4bce5c8fb3', 'hb_bidder_medianet': 'medianet'}}, - NO_BID_SET_TARGETING: {'div-gpt-ad-1460505748561-0': {}}, - BID_WON: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, - BID_WON_2: {'bidderCode': 'appnexus', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aecd5', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'appnexus', 'hb_adid': '3e6e4bce5c8fb4', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'publisherId': 'test123', 'placementId': '451466393'}]}, - BID_WON_UNKNOWN: {'bidderCode': 'appnexus', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fkk', 'requestId': '28248b0e6aecd5', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'appnexus', 'hb_adid': '3e6e4bce5c8fb4', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'publisherId': 'test123', 'placementId': '451466393'}]}, - NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, - BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}], - BIDS_SAME_REQ_DIFF_CPM: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 1.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 278, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.29', 'pbAg': '1.25', 'pbDg': '1.29', 'pbCg': '1.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '1.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}], - BIDS_SAME_REQ_DIFF_CPM_SAME_TIME: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 1.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.29', 'pbAg': '1.25', 'pbDg': '1.29', 'pbCg': '1.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '1.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}], - BIDS_SAME_REQ_EQUAL_CPM: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 286, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}], - BID_RESPONSES: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'appnexus', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aecd5', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 1.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 278, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.29', 'pbAg': '1.25', 'pbDg': '1.29', 'pbCg': '1.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'appnexus', 'hb_adid': '3e6e4bce5c8fb4', 'hb_pb': '1.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'params': [{'publisherId': 'test123', 'placementId': '451466393'}]}], - BID_REQUESTS: [{'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, {'bidderCode': 'appnexus', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'appnexus', 'params': {'publisherId': 'TEST_CID', 'placementId': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aecd5', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}], - MULTI_BID_RESPONSES: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'bidA2', 'originalBidder': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aebecc', 'originalRequestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 3.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 3.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '3.00', 'pbMg': '3.20', 'pbHg': '3.29', 'pbAg': '3.25', 'pbDg': '3.29', 'pbCg': '3.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '3.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}] -}; - -function performAuctionWithFloorConfig() { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT_WITH_FLOOR, {adUnits: MOCK.Ad_Units})); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); - events.emit(BID_WON, MOCK.BID_WON); +function createBidResponse(bidderCode, requestId, cpm, adId = '3e6e4bce5c8fb3') { + return { + bidderCode, + width: 300, + height: 250, + adId, + requestId, + mediaType: 'banner', + source: 'client', + ext: {pvid: 123, crid: '321'}, + no_bid: false, + cpm, + ad: 'AD_CODE', + ttl: 180, + creativeId: 'Test1', + netRevenue: true, + currency: 'USD', + dfp_id: 'div-gpt-ad-1460505748561-0', + originalCpm: cpm, + originalCurrency: 'USD', + floorData: {floorValue: 1.10, floorRule: 'banner'}, + auctionId: '8e0d5245-deb3-406c-96ca-9b609e077ff7', + snm: 'SUCCESS', + responseTimestamp: 1584563606009, + requestTimestamp: 1584563605743, + bidder: 'medianet', + adUnitCode: 'div-gpt-ad-1460505748561-0', + timeToRespond: 266, + pbLg: '2.00', + pbMg: '2.20', + pbHg: '2.29', + pbAg: '2.25', + pbDg: '2.29', + pbCg: '2.00', + size: '300x250', + adserverTargeting: { + hb_bidder: 'medianet', + hb_adid: adId, + hb_pb: '1.8', + hb_size: '300x250', + hb_source: 'client', + hb_format: 'banner', + prebid_test: 1 + }, + params: [{cid: 'test123', crid: '451466393'}] + }; } -function performStandardAuctionWithWinner() { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); - events.emit(BID_WON, MOCK.BID_WON); +function createBidRequest(bidderCode, auctionId, bidId, adUnits) { + return { + bidderCode, + auctionId, + bids: adUnits.map(adUnit => ({ + bidder: bidderCode, + params: {cid: 'TEST_CID', crid: '451466393'}, + mediaTypes: adUnit.mediaTypes, + adUnitCode: adUnit.code, + sizes: [(adUnit.mediaTypes.banner?.sizes || []), (adUnit.mediaTypes.native?.image?.sizes || []), (adUnit.mediaTypes.video?.playerSize || [])], + bidId, + auctionId, + src: 'client' + })), + auctionStart: Date.now(), + timeout: 6000, + uspConsent: '1YY', + start: Date.now() + }; } -function performMultiFormatAuctionWithNoBid() { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.MULTI_FORMAT_TWIN_AD_UNITS})); - events.emit(BID_REQUESTED, MOCK.MULTI_FORMAT_BID_REQUESTED); - events.emit(NO_BID, MOCK.NO_BID); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); +function createNoBid(bidder, params) { + return { + bidder, + params, + mediaTypes: {banner: {sizes: [[300, 250]], ext: ['asdads']}}, + adUnitCode: 'div-gpt-ad-1460505748561-0', + transactionId: '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', + sizes: [[300, 250], [300, 600]], + bidId: '28248b0e6aece2', + bidderRequestId: '13fccf3809fe43', + auctionId: '8e0d5245-deb3-406c-96ca-9b609e077ff7', + src: 'client' + }; } -function performTwinAdUnitAuctionWithNoBid() { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.TWIN_AD_UNITS})); - MOCK.TWIN_AD_UNITS_BID_REQUESTED.forEach(bidRequested => events.emit(BID_REQUESTED, bidRequested)); - MOCK.TWIN_AD_UNITS_BID_REQUESTED.forEach(bidRequested => bidRequested.bids.forEach(noBid => events.emit(NO_BID, noBid))); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); +function createBidTimeout(bidId, bidder, auctionId, params) { + return [{ + bidId: '28248b0e6aece2', + bidder: 'medianet', + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId, + params, + timeout: 6 + }]; } -function performStandardAuctionWithNoBid() { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - events.emit(NO_BID, MOCK.NO_BID); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); +function createBidWon(bidderCode, adId, requestId, cpm) { + return { + bidderCode, + width: 300, + height: 250, + statusMessage: 'Bid available', + adId, + requestId, + mediaType: 'banner', + source: 'client', + no_bid: false, + cpm, + ad: 'AD_CODE', + ttl: 180, + creativeId: 'Test1', + netRevenue: true, + currency: 'USD', + dfp_id: 'div-gpt-ad-1460505748561-0', + originalCpm: 1.1495, + originalCurrency: 'USD', + auctionId: '8e0d5245-deb3-406c-96ca-9b609e077ff7', + responseTimestamp: 1584563606009, + requestTimestamp: 1584563605743, + bidder: 'medianet', + adUnitCode: 'div-gpt-ad-1460505748561-0', + snm: 'SUCCESS', + timeToRespond: 266, + pbLg: '2.00', + pbMg: '2.20', + pbHg: '2.29', + pbAg: '2.25', + pbDg: '2.29', + pbCg: '2.00', + size: '300x250', + adserverTargeting: { + hb_bidder: 'medianet', + hb_adid: adId, + hb_pb: '2.00', + hb_size: '300x250', + hb_source: 'client', + hb_format: 'banner', + prebid_test: 1 + }, + status: 'rendered', + params: [{cid: 'test123', crid: '451466393'}] + }; } -function performStandardAuctionWithTimeout() { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); -} +const BANNER_AD_UNIT = {code: 'div-gpt-ad-1460505748561-0', mediaTypes: {banner: {sizes: [[300, 250]]}}}; +const VIDEO_AD_UNIT = { + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: {video: {playerSize: [640, 480], context: 'outstream'}} +}; +const INSTREAM_VIDEO_AD_UNIT = { + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: {video: {playerSize: [640, 480], context: 'instream'}} +}; +const BANNER_NATIVE_AD_UNIT = { + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: {banner: {sizes: [[300, 250]]}, native: {image: {required: true, sizes: [150, 50]}}} +}; +const BANNER_NATIVE_VIDEO_AD_UNIT = { + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: {sizes: [[300, 250]]}, + video: {playerSize: [640, 480], context: 'outstream'}, + native: {image: {required: true, sizes: [150, 50]}, title: {required: true, len: 80}} + } +}; -function performStandardAuctionMultiBidWithSameRequestId(bidRespArray) { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - bidRespArray.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); - events.emit(BID_WON, MOCK.BID_WON); +function createS2SBidRequest(bidderCode, auctionId, bidId, adUnits) { + const bidRequest = createBidRequest(bidderCode, auctionId, bidId, adUnits); + // Update to mark as S2S + bidRequest.src = 's2s'; + bidRequest.bids.forEach(bid => { + bid.src = 's2s'; + }); + return bidRequest; } -function performStandardAuctionMultiBidResponseNoWin() { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); - MOCK.BID_REQUESTS.forEach(bidReq => events.emit(BID_REQUESTED, bidReq)); - MOCK.BID_RESPONSES.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); +function createS2SBidResponse(bidderCode, requestId, cpm, adId = '3e6e4bce5c8fb3') { + const bidResponse = createBidResponse(bidderCode, requestId, cpm, adId); + // Update to mark as S2S + bidResponse.source = 's2s'; + return bidResponse; } -function performMultiBidAuction() { - events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - MOCK.MULTI_BID_RESPONSES.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); - events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); -} +const MOCK = { + AD_UNITS: [BANNER_NATIVE_AD_UNIT, VIDEO_AD_UNIT], + AUCTION_INIT: { + auctionId: '8e0d5245-deb3-406c-96ca-9b609e077ff7', + timestamp: 1584563605739, + timeout: 6000, + bidderRequests: [{bids: [{floorData: {enforcements: {enforceJS: true}}}]}] + }, + MNET_BID_REQUESTED: createBidRequest('medianet', '8e0d5245-deb3-406c-96ca-9b609e077ff7', '28248b0e6aece2', [BANNER_NATIVE_VIDEO_AD_UNIT]), + MNET_BID_RESPONSE: createBidResponse('medianet', '28248b0e6aece2', 2.299), + COMMON_REQ_ID_BID_REQUESTS: [ + createBidRequest('medianet', '8e0d5245-deb3-406c-96ca-9b609e077ff7', '28248b0e6aece2', [BANNER_AD_UNIT]), + createBidRequest('appnexus', '8e0d5245-deb3-406c-96ca-9b609e077ff7', '28248b0e6aecd5', [BANNER_AD_UNIT]) + ], + COMMON_REQ_ID_BID_RESPONSES: [ + createBidResponse('medianet', '28248b0e6aece2', 2.299, '3e6e4bce5c8fb3'), + createBidResponse('appnexus', '28248b0e6aecd5', 1.299, '3e6e4bce5c8fb4') + ], + COMMON_REQ_ID_BID_RESPONSES_EQUAL_CPM: [ + createBidResponse('medianet', '28248b0e6aece2', 2.299, '3e6e4bce5c8fb3'), + createBidResponse('medianet', '28248b0e6aece2', 2.299, '3e6e4bce5c8fb4') + ], + MULTI_BID_RESPONSES: [ + createBidResponse('medianet', '28248b0e6aece2', 2.299, '3e6e4bce5c8fb3'), + Object.assign(createBidResponse('medianet', '28248b0e6aebecc', 3.299, '3e6e4bce5c8fb4'), + {originalBidder: 'bidA2', originalRequestId: '28248b0e6aece2'}), + ], + MNET_NO_BID: createNoBid('medianet', {cid: 'test123', crid: '451466393', site: {}}), + MNET_BID_TIMEOUT: createBidTimeout('28248b0e6aece2', 'medianet', '8e0d5245-deb3-406c-96ca-9b609e077ff7', [{ + cid: 'test123', + crid: '451466393', + site: {} + }, {cid: '8CUX0H51P', crid: '451466393', site: {}}]), + AUCTION_END: { + auctionId: '8e0d5245-deb3-406c-96ca-9b609e077ff7', + auctionEnd: 1584563605739, + bidderRequests: [createBidRequest('medianet', '8e0d5245-deb3-406c-96ca-9b609e077ff7', '28248b0e6aece2', [BANNER_NATIVE_VIDEO_AD_UNIT])] + }, + MNET_BID_WON: createBidWon('medianet', '3e6e4bce5c8fb3', '28248b0e6aece2', 2.299), + BID_WON_UNKNOWN: createBidWon('appnexus', '3e6e4bce5c8fkk', '28248b0e6aecd5', 2.299), + MNET_SET_TARGETING: { + 'div-gpt-ad-1460505748561-0': { + prebid_test: '1', + hb_format: 'banner', + hb_source: 'client', + hb_size: '300x250', + hb_pb: '1.8', + hb_adid: '3e6e4bce5c8fb3', + hb_bidder: 'medianet', + hb_format_medianet: 'banner', + hb_source_medianet: 'client', + hb_size_medianet: '300x250', + hb_pb_medianet: '2.00', + hb_adid_medianet: '3e6e4bce5c8fb3', + hb_bidder_medianet: 'medianet' + } + }, + NO_BID_SET_TARGETING: {'div-gpt-ad-1460505748561-0': {}}, + // S2S mocks + MNET_S2S_BID_REQUESTED: createS2SBidRequest('medianet', '8e0d5245-deb3-406c-96ca-9b609e077ff7', '28248b0e6aece2', [BANNER_AD_UNIT]), + MNET_S2S_BID_RESPONSE: createS2SBidResponse('medianet', '28248b0e6aece2', 2.299), + MNET_S2S_BID_WON: Object.assign({}, createBidWon('medianet', '3e6e4bce5c8fb3', '28248b0e6aece2', 2.299), {source: 's2s'}), + MNET_S2S_SET_TARGETING: { + 'div-gpt-ad-1460505748561-0': { + prebid_test: '1', + hb_format: 'banner', + hb_source: 's2s', + hb_size: '300x250', + hb_pb: '1.8', + hb_adid: '3e6e4bce5c8fb3', + hb_bidder: 'medianet', + hb_format_medianet: 'banner', + hb_source_medianet: 's2s', + hb_size_medianet: '300x250', + hb_pb_medianet: '2.00', + hb_adid_medianet: '3e6e4bce5c8fb3', + hb_bidder_medianet: 'medianet' + } + }, + // Currency conversion mocks + MNET_JPY_BID_RESPONSE: Object.assign({}, createBidResponse('medianet', '28248b0e6aece2', 250, '3e6e4bce5c8fb5'), { + currency: 'JPY', + originalCurrency: 'JPY', + originalCpm: 250 + }) +}; function getQueryData(url, decode = false) { const queryArgs = url.split('?')[1].split('&'); @@ -131,8 +315,91 @@ function getQueryData(url, decode = false) { }, {}); } -describe('Media.net Analytics Adapter', function() { +function waitForPromiseResolve(promise) { + return new Promise((resolve, reject) => { + promise.then(resolve).catch(reject); + }); +} + +function performNoBidAuction() { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_BID_REQUESTED); + events.emit(NO_BID, MOCK.MNET_NO_BID); + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, {bidderRequests: [MOCK.MNET_BID_REQUESTED]})); + events.emit(SET_TARGETING, MOCK.MNET_SET_TARGETING); +} + +function performBidWonAuction() { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.MNET_BID_RESPONSE); + events.emit(SET_TARGETING, MOCK.MNET_SET_TARGETING); + events.emit(BID_WON, MOCK.MNET_BID_WON); +} + +function performBidTimeoutAuction() { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_BID_REQUESTED); + events.emit(BID_TIMEOUT, MOCK.MNET_BID_TIMEOUT); + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, {bidderRequests: [MOCK.MNET_BID_REQUESTED]})); + events.emit(SET_TARGETING, MOCK.MNET_SET_TARGETING); +} + +function performAuctionWithSameRequestIdBids(bidRespArray) { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_BID_REQUESTED); + bidRespArray.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, {bidderRequests: [MOCK.MNET_BID_REQUESTED]})); + events.emit(SET_TARGETING, MOCK.MNET_SET_TARGETING); + events.emit(BID_WON, MOCK.MNET_BID_WON); +} + +function performAuctionNoWin() { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + MOCK.COMMON_REQ_ID_BID_REQUESTS.forEach(bidReq => events.emit(BID_REQUESTED, bidReq)); + MOCK.COMMON_REQ_ID_BID_RESPONSES.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, {bidderRequests: MOCK.COMMON_REQ_ID_BID_REQUESTS})); + events.emit(SET_TARGETING, MOCK.MNET_SET_TARGETING); +} + +function performMultiBidAuction() { + let bidRequest = createBidRequest('medianet', '8e0d5245-deb3-406c-96ca-9b609e077ff7', '28248b0e6aece2', [BANNER_AD_UNIT]); + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, bidRequest); + MOCK.MULTI_BID_RESPONSES.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, {bidderRequests: [bidRequest]})); + events.emit(SET_TARGETING, MOCK.MNET_SET_TARGETING); +} + +function performBidRejectedAuction() { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.MNET_BID_RESPONSE); + events.emit(BID_REJECTED, Object.assign({}, MOCK.MNET_BID_RESPONSE, { + rejectionReason: REJECTION_REASON.FLOOR_NOT_MET, + })); + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, {bidderRequests: [MOCK.MNET_BID_REQUESTED]})); +} + +function performS2SAuction() { + 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); + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, {bidderRequests: [MOCK.MNET_S2S_BID_REQUESTED]})); + events.emit(SET_TARGETING, MOCK.MNET_S2S_SET_TARGETING); + events.emit(BID_WON, MOCK.MNET_S2S_BID_WON); +} + +function performCurrencyConversionAuction() { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.MNET_JPY_BID_RESPONSE); + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, {bidderRequests: [MOCK.MNET_BID_REQUESTED]})); +} + +describe('Media.net Analytics Adapter', function () { let sandbox; + let clock; let CUSTOMER_ID = 'test123'; let VALID_CONFIGURATION = { options: { @@ -146,14 +413,16 @@ describe('Media.net Analytics Adapter', function() { beforeEach(function () { sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(); }); afterEach(function () { sandbox.restore(); + clock.restore(); }); - describe('Configuration', function() { - it('should log error if publisher id is not passed', function() { + describe('Configuration', function () { + it('should log error if publisher id is not passed', function () { sandbox.stub(utils, 'logError'); medianetAnalytics.enableAnalytics(); @@ -164,7 +433,7 @@ describe('Media.net Analytics Adapter', function() { ).to.be.true; }); - it('should not log error if valid config is passed', function() { + it('should not log error if valid config is passed', function () { sandbox.stub(utils, 'logError'); medianetAnalytics.enableAnalytics(VALID_CONFIGURATION); @@ -173,7 +442,7 @@ describe('Media.net Analytics Adapter', function() { }); }); - describe('Events', function() { + describe('VAST Tracking', function () { beforeEach(function () { medianetAnalytics.enableAnalytics({ options: { @@ -181,62 +450,329 @@ describe('Media.net Analytics Adapter', function() { } }); medianetAnalytics.clearlogsQueue(); + // Set config required for vastTrackerHandler + config.setConfig({ + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache' + } + }); }); + afterEach(function () { - medianetAnalytics.clearlogsQueue(); medianetAnalytics.disableAnalytics(); + config.resetConfig(); }); - it('should not log if only Auction Init', function() { - medianetAnalytics.clearlogsQueue(); - medianetAnalytics.track({ AUCTION_INIT }) - expect(medianetAnalytics.getlogsQueue().length).to.equal(0); + it('should generate valid tracking URL for video bids', function () { + const VIDEO_AUCTION = Object.assign({}, MOCK.AUCTION_INIT, {adUnits: [INSTREAM_VIDEO_AD_UNIT]}); + const VIDEO_BID_REQUESTED = createBidRequest('medianet', '8e0d5245-deb3-406c-96ca-9b609e077ff7', '28248b0e6aece2', [INSTREAM_VIDEO_AD_UNIT]); + const videoBidResponse = Object.assign({}, MOCK.MNET_BID_RESPONSE, { + mediaType: 'video', + playerSize: [640, 480] + }); + + events.emit(AUCTION_INIT, VIDEO_AUCTION); + events.emit(BID_REQUESTED, VIDEO_BID_REQUESTED); + events.emit(BID_RESPONSE, videoBidResponse); + + const trackers = medianetAnalytics.getVastTrackerHandler()(videoBidResponse, { + auction: VIDEO_AUCTION, + bidRequest: VIDEO_BID_REQUESTED.bids[0] + }); + + expect(trackers).to.be.an('array'); + expect(trackers.length).to.equal(1); + expect(trackers[0].event).to.equal('impressions'); + expect(trackers[0].url).to.include('https://'); + + const urlData = getQueryData(trackers[0].url); + expect(urlData.lgtp).to.equal('RA'); + expect(urlData.pvnm).to.include('medianet'); + expect(urlData.bdp).to.equal('2.299'); + expect(urlData.vplcmtt).to.equal('1'); }); - it('should have all applicable sizes in request', function() { - medianetAnalytics.clearlogsQueue(); - performMultiFormatAuctionWithNoBid(); - const noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log))[0]; + it('should return error tracker when auction is missing', function () { + const videoBidResponse = Object.assign({}, MOCK.MNET_BID_RESPONSE, { + mediaType: 'video', + vastUrl: 'https://vast.example.com/vast.xml', + videoCacheKey: 'video_cache_123', + auctionId: 'missing_auction_id' // This auction ID doesn't exist + }); + + const trackers = medianetAnalytics.getVastTrackerHandler()(videoBidResponse, { + bidRequest: MOCK.MNET_BID_REQUESTED.bids[0], + auction: MOCK.AUCTION_INIT + }); + + expect(trackers).to.be.an('array'); + expect(trackers.length).to.equal(1); + expect(trackers[0].url).to.include('vast_tracker_handler_missing_auction'); + }); + + it('should return error tracker when bidrequest is missing', function () { + const VIDEO_AUCTION = Object.assign({}, MOCK.AUCTION_INIT, {adUnits: [INSTREAM_VIDEO_AD_UNIT]}); + const VIDEO_BID_REQUESTED = createBidRequest('medianet', '8e0d5245-deb3-406c-96ca-9b609e077ff7', '28248b0e6aece2', [INSTREAM_VIDEO_AD_UNIT]); + const videoBidResponse = Object.assign({}, MOCK.MNET_BID_RESPONSE, { + mediaType: 'video', + playerSize: [640, 480], + }); + + events.emit(AUCTION_INIT, VIDEO_AUCTION); + + const trackers = medianetAnalytics.getVastTrackerHandler()(videoBidResponse, { + auction: VIDEO_AUCTION, + bidRequest: VIDEO_BID_REQUESTED.bids[0] + }); + expect(trackers).to.be.an('array'); + expect(trackers.length).to.equal(1); + expect(trackers[0].url).to.include('missing_bidrequest'); + }); + }); + + describe('Events', function () { + beforeEach(function () { + sandbox.stub(mnUtils, 'onHidden').callsFake(() => { + }) + medianetAnalytics.enableAnalytics({ + options: { + cid: 'test123' + } + }); medianetAnalytics.clearlogsQueue(); - expect(noBidLog.mtype).to.have.ordered.members([encodeURIComponent('banner|native|video'), encodeURIComponent('banner|video|native')]); - expect(noBidLog.szs).to.have.ordered.members([encodeURIComponent('300x250|1x1|640x480'), encodeURIComponent('300x250|1x1|640x480')]); - expect(noBidLog.vplcmtt).to.equal('instream'); + }); + afterEach(function () { + sandbox.restore(); + medianetAnalytics.disableAnalytics(); }); - it('twin ad units should have correct sizes', function() { - performTwinAdUnitAuctionWithNoBid(); - const noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log, true))[0]; - const banner = 'banner'; - expect(noBidLog.pvnm).to.have.ordered.members(['-2', 'bidder1', 'bidder1', 'medianet']); - expect(noBidLog.mtype).to.have.ordered.members([banner, banner, banner, banner]); - expect(noBidLog.status).to.have.ordered.members(['1', '2', '2', '2']); - expect(noBidLog.size).to.have.ordered.members(['', '', '', '']); - expect(noBidLog.szs).to.have.ordered.members(['300x100|300x250', '300x100', '300x250', '300x250|300x100']); + it('can handle multi bid module', function (done) { + performMultiBidAuction(); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + const queue = medianetAnalytics.getlogsQueue(); + expect(queue.length).equals(1); + const multiBidLog = queue.map((log) => getQueryData(log, true))[0]; + expect(multiBidLog.pvnm).to.have.ordered.members(['-2', 'medianet', 'medianet']); + expect(multiBidLog.status).to.have.ordered.members(['1', '1', '1']); + done(); + }).catch(done); }); - it('AP log should fire only once', function() { - performStandardAuctionWithNoBid(); - events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); - const logs = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log, true)); - expect(logs.length).to.equal(1); - expect(logs[0].lgtp).to.equal('APPR'); + it('should have all applicable sizes in request', function (done) { + performNoBidAuction(); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + const noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log))[0]; + expect(noBidLog.mtype).to.have.ordered.members([encodeURIComponent('banner|native|video'), encodeURIComponent('banner|video|native')]); + expect(noBidLog.szs).to.have.ordered.members([encodeURIComponent('300x250|1x1|640x480'), encodeURIComponent('300x250|1x1|640x480')]); + expect(noBidLog.vplcmtt).to.equal('6'); + done(); + }).catch(done); }); - it('should have winner log in standard auction', function() { - medianetAnalytics.clearlogsQueue(); - performStandardAuctionWithWinner(); - let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); - medianetAnalytics.clearlogsQueue(); + it('AP log should fire only once', function (done) { + performNoBidAuction(); + clock.tick(2000); - expect(winnerLog.length).to.equal(1); + waitForPromiseResolve(Promise.resolve()).then(() => { + const logs = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log, true)); + expect(logs[0]).to.exist; + expect(logs[0].lgtp).to.equal('APPR'); + done(); + }).catch(done); }); - it('should have correct values in winner log', function() { - medianetAnalytics.clearlogsQueue(); - performStandardAuctionWithWinner(); + it('should have no bid status', function (done) { + performNoBidAuction(); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + let noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); + noBidLog = noBidLog[0]; + + expect(noBidLog.pvnm).to.have.ordered.members(['-2', 'medianet']); + expect(noBidLog.iwb).to.have.ordered.members(['0', '0']); + expect(noBidLog.status).to.have.ordered.members(['1', '2']); + expect(noBidLog.src).to.have.ordered.members(['client', 'client']); + expect(noBidLog.curr).to.have.ordered.members(['', '']); + expect(noBidLog.mtype).to.have.ordered.members([encodeURIComponent('banner|native|video'), encodeURIComponent('banner|video|native')]); + expect(noBidLog.ogbdp).to.have.ordered.members(['', '']); + expect(noBidLog.mpvid).to.have.ordered.members(['', '']); + expect(noBidLog.crid).to.have.ordered.members(['', '451466393']); + done(); + }).catch(done); + }); + + it('should have timeout status', function (done) { + performBidTimeoutAuction(); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + let timeoutLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); + timeoutLog = timeoutLog[0]; + + expect(timeoutLog.pvnm).to.have.ordered.members(['-2', 'medianet']); + expect(timeoutLog.iwb).to.have.ordered.members(['0', '0']); + expect(timeoutLog.status).to.have.ordered.members(['3', '3']); + expect(timeoutLog.src).to.have.ordered.members(['client', 'client']); + expect(timeoutLog.curr).to.have.ordered.members(['', '']); + expect(timeoutLog.mtype).to.have.ordered.members([encodeURIComponent('banner|native|video'), encodeURIComponent('banner|video|native')]); + expect(timeoutLog.ogbdp).to.have.ordered.members(['', '']); + expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); + expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); + done(); + }).catch(done); + }); + + it('should have correct bid values after and before bidCmpAdjustment', function (done) { + const bidCopy = utils.deepClone(MOCK.MNET_BID_RESPONSE); + bidCopy.cpm = bidCopy.originalCpm * 0.8; // Simulate bidCpmAdjustment + + // Emit events to simulate an auction + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_BID_REQUESTED); + events.emit(BID_RESPONSE, bidCopy); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.MNET_SET_TARGETING); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + const adjustedLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log))[0]; + + expect(adjustedLog.bdp[1]).to.equal(String(bidCopy.cpm)); + expect(adjustedLog.ogbdp[1]).to.equal(String(bidCopy.originalCpm)); + expect(adjustedLog.cbdp[1]).to.equal(String(bidCopy.cpm)); + expect(adjustedLog.dfpbd[1]).to.equal(String(deepAccess(bidCopy, 'adserverTargeting.hb_pb'))); + done(); + }).catch(done); + }); + + it('should pick winning bid if multibids with same request id', function (done) { + sandbox.stub(getGlobal(), 'getAdserverTargetingForAdUnitCode').returns({ + hb_pb: '2.299', + hb_adid: '3e6e4bce5c8fb3', + hb_pb_medianet: '2.299', + hb_adid_medianet: '3e6e4bce5c8fb3', + hb_pb_appnexus: '1.299', + hb_adid_appnexus: '3e6e4bce5c8fb4', + }); + performAuctionWithSameRequestIdBids(MOCK.COMMON_REQ_ID_BID_RESPONSES); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner === '1')[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fb3'); + medianetAnalytics.clearlogsQueue(); + + performAuctionWithSameRequestIdBids([...MOCK.COMMON_REQ_ID_BID_RESPONSES].reverse()); + clock.tick(2000); + + return waitForPromiseResolve(Promise.resolve()); + }).then(() => { + let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner === '1')[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fb3'); + done(); + }).catch(done); + }); + + it('should pick winning bid if multibids with same request id and same time to respond', function (done) { + performAuctionWithSameRequestIdBids(MOCK.COMMON_REQ_ID_BID_RESPONSES_EQUAL_CPM); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner === '1')[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fb3'); + medianetAnalytics.clearlogsQueue(); + done(); + }).catch(done); + }); + + it('should ignore unknown winning bid and log error', function (done) { + performAuctionNoWin(); + events.emit(BID_WON, MOCK.BID_WON_UNKNOWN); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + let winningBids = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner); + let errors = medianetAnalytics.getErrorQueue().map((log) => getQueryData(log)); + expect(winningBids.length).equals(0); + expect(errors.length).equals(1); + expect(errors[0].event).equals('winning_bid_absent'); + done(); + }).catch(done); + }); + + it('should have correct bid status for bid rejected', function (done) { + performBidRejectedAuction(); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + let bidRejectedLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log))[0]; + expect(bidRejectedLog.pvnm).to.have.ordered.members(['-2', 'medianet', 'medianet', 'medianet']); + expect(bidRejectedLog.status).to.have.ordered.members(['1', '1', '12', '12']); + done(); + }).catch(done); + }); + + it('should handle S2S auction correctly', function (done) { + performS2SAuction(); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + const queue = medianetAnalytics.getlogsQueue(); + const s2sLog = queue.map((log) => getQueryData(log, true))[0]; + + // Verify S2S source is recorded correctly + expect(s2sLog.src).to.equal('s2s'); + expect(s2sLog.pvnm).to.include('medianet'); + expect(s2sLog.status).to.include('1'); + expect(s2sLog.winner).to.equal('1'); + + done(); + }).catch(done); + }); + + it('should handle currency conversion from JPY to USD', function (done) { + const prebidGlobal = getGlobal(); + prebidGlobal.convertCurrency = prebidGlobal.convertCurrency || function () { + }; + const convertStub = sandbox.stub(prebidGlobal, 'convertCurrency'); + convertStub.withArgs(250, 'JPY', 'USD').returns(2.25); + + performCurrencyConversionAuction(); + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + const queue = medianetAnalytics.getlogsQueue(); + expect(queue.length).equals(1); + const currencyLog = queue.map((log) => getQueryData(log, true))[0]; + + expect(currencyLog.curr).to.have.ordered.members(['', 'JPY', '']); + expect(currencyLog.ogbdp).to.have.ordered.members(['', '2.25', '']); + expect(currencyLog.bdp).to.have.ordered.members(['', '2.25', '']); + expect(currencyLog.bdp).to.have.ordered.members(['', '2.25', '']); + expect(convertStub.calledWith(250, 'JPY', 'USD')).to.be.true; + done(); + }).catch(done).finally(() => { + convertStub.restore(); + }); + }); + + it('should have winner log in standard auction', function () { + performBidWonAuction(); + let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); - medianetAnalytics.clearlogsQueue(); + expect(winnerLog.length).to.equal(1); + expect(winnerLog[0].lgtp).to.equal('RA'); + }); + + it('should have correct values in winner log', function () { + performBidWonAuction(); + let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); expect(winnerLog[0]).to.include({ winner: '1', pvnm: 'medianet', @@ -247,7 +783,7 @@ describe('Media.net Analytics Adapter', function() { gdpr: '0', cid: 'test123', lper: '1', - ogbdp: '1.1495', + ogbdp: '2.299', flt: '1', supcrid: 'div-gpt-ad-1460505748561-0', mpvid: '123', @@ -255,124 +791,73 @@ describe('Media.net Analytics Adapter', function() { }); }); - it('should have correct bid floor data in winner log', function() { - medianetAnalytics.clearlogsQueue(); - performAuctionWithFloorConfig(); + it('should have correct bid floor data in winner log', function (done) { + performBidWonAuction(); let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); - medianetAnalytics.clearlogsQueue(); - expect(winnerLog[0]).to.include({ winner: '1', curr: 'USD', - ogbdp: '1.1495', + ogbdp: '2.299', bidflr: '1.1', flrrule: 'banner', - flrdata: encodeURIComponent('ln=||skp=||enfj=true||enfd=||sr=||fs=') + flrdata: encodeURIComponent('ln=||skp=||sr=||fs=||enfj=true||enfd=') }); + done(); }); - it('should have no bid status', function() { - medianetAnalytics.clearlogsQueue(); - performStandardAuctionWithNoBid(); - let noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); - noBidLog = noBidLog[0]; - - medianetAnalytics.clearlogsQueue(); - expect(noBidLog.pvnm).to.have.ordered.members(['-2', 'medianet']); - expect(noBidLog.iwb).to.have.ordered.members(['0', '0']); - expect(noBidLog.status).to.have.ordered.members(['1', '2']); - expect(noBidLog.src).to.have.ordered.members(['client', 'client']); - expect(noBidLog.curr).to.have.ordered.members(['', '']); - expect(noBidLog.mtype).to.have.ordered.members(['banner', 'banner']); - expect(noBidLog.ogbdp).to.have.ordered.members(['', '']); - expect(noBidLog.mpvid).to.have.ordered.members(['', '']); - expect(noBidLog.crid).to.have.ordered.members(['', '451466393']); - }); - - it('should have timeout status', function() { - medianetAnalytics.clearlogsQueue(); - performStandardAuctionWithTimeout(); - let timeoutLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); - timeoutLog = timeoutLog[0]; - - medianetAnalytics.clearlogsQueue(); - expect(timeoutLog.pvnm).to.have.ordered.members(['-2', 'medianet']); - expect(timeoutLog.iwb).to.have.ordered.members(['0', '0']); - expect(timeoutLog.status).to.have.ordered.members(['1', '3']); - expect(timeoutLog.src).to.have.ordered.members(['client', 'client']); - expect(timeoutLog.curr).to.have.ordered.members(['', '']); - expect(timeoutLog.mtype).to.have.ordered.members(['banner', 'banner']); - expect(timeoutLog.ogbdp).to.have.ordered.members(['', '']); - expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); - expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); - }); - - it('should pick winning bid if multibids with same request id', function() { - performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_DIFF_CPM); - let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; - expect(winningBid.adid).equals('3e6e4bce5c8fb3'); - medianetAnalytics.clearlogsQueue(); - - const reversedResponseArray = [].concat(MOCK.BIDS_SAME_REQ_DIFF_CPM).reverse(); - performStandardAuctionMultiBidWithSameRequestId(reversedResponseArray); - winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; - expect(winningBid.adid).equals('3e6e4bce5c8fb3'); - }); - - it('should pick winning bid if multibids with same request id and same time to respond', function() { - performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_DIFF_CPM_SAME_TIME); - let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; - expect(winningBid.adid).equals('3e6e4bce5c8fb3'); - medianetAnalytics.clearlogsQueue(); + it('should log error on AD_RENDER_FAILED event', function () { + const errorData = { + reason: 'timeout', + message: 'Ad failed to render', + bid: { + auctionId: '8e0d5245-deb3-406c-96ca-9b609e077ff7', + adUnitCode: 'div-gpt-ad-1460505748561-0', + bidder: 'medianet', + creativeId: 'Test1' + } + }; + events.emit(AD_RENDER_FAILED, errorData); + + const errors = medianetAnalytics.getErrorQueue().map((log) => getQueryData(log)); + expect(errors.length).to.equal(1); + const relatedData = JSON.parse(decodeURIComponent(errors[0].rd)); + expect(errors[0].event).to.equal(AD_RENDER_FAILED); + expect(relatedData.reason).to.equal('timeout'); + expect(relatedData.message).to.equal('Ad failed to render'); }); - it('should pick winning bid if multibids with same request id and equal cpm', function() { - performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_EQUAL_CPM); - let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; - expect(winningBid.adid).equals('3e6e4bce5c8fb3'); - medianetAnalytics.clearlogsQueue(); + it('should log success on AD_RENDER_SUCCEEDED event', function () { + const successData = { + bid: { + auctionId: '8e0d5245-deb3-406c-96ca-9b609e077ff7', + adUnitCode: 'div-gpt-ad-1460505748561-0', + bidder: 'medianet', + creativeId: 'Test1' + } + }; + events.emit(AD_RENDER_SUCCEEDED, successData); - const reversedResponseArray = [].concat(MOCK.BIDS_SAME_REQ_EQUAL_CPM).reverse(); - performStandardAuctionMultiBidWithSameRequestId(reversedResponseArray); - winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; - expect(winningBid.adid).equals('3e6e4bce5c8fb3'); + const logs = medianetAnalytics.getErrorQueue().map((log) => getQueryData(log)); + expect(logs.length).to.equal(1); + expect(logs[0].event).to.equal(AD_RENDER_SUCCEEDED); }); - it('should pick single winning bid per bid won', function() { - performStandardAuctionMultiBidResponseNoWin(); - const queue = medianetAnalytics.getlogsQueue(); - queue.length = 0; - - events.emit(BID_WON, MOCK.BID_WON); - let winningBids = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); - expect(winningBids[0].adid).equals(MOCK.BID_WON.adId); - expect(winningBids.length).equals(1); - events.emit(BID_WON, MOCK.BID_WON_2); - winningBids = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); - expect(winningBids[1].adid).equals(MOCK.BID_WON_2.adId); - expect(winningBids.length).equals(2); + it('should log error on STALE_RENDER event', function () { + const staleData = { + auctionId: '8e0d5245-deb3-406c-96ca-9b609e077ff7', + adUnitCode: 'div-gpt-ad-1460505748561-0', + bidder: 'medianet', + creativeId: 'Test1', + adId: '3e6e4bce5c8fb3', + cpm: 2.299 + }; + events.emit(STALE_RENDER, staleData); + + const errors = medianetAnalytics.getErrorQueue().map((log) => getQueryData(log)); + expect(errors.length).to.equal(1); + const relatedData = JSON.parse(decodeURIComponent(errors[0].rd)); + expect(errors[0].event).to.equal(STALE_RENDER); + expect(relatedData.adId).to.equal('3e6e4bce5c8fb3'); }); - - it('should ignore unknown winning bid and log error', function() { - performStandardAuctionMultiBidResponseNoWin(); - const queue = medianetAnalytics.getlogsQueue(); - queue.length = 0; - - events.emit(BID_WON, MOCK.BID_WON_UNKNOWN); - let winningBids = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); - let errors = medianetAnalytics.getErrorQueue().map((log) => getQueryData(log)); - expect(winningBids.length).equals(0); - expect(errors.length).equals(1); - expect(errors[0].event).equals(ERROR_WINNING_BID_ABSENT); - }); - - it('can handle multi bid module', function () { - performMultiBidAuction(); - const queue = medianetAnalytics.getlogsQueue(); - expect(queue.length).equals(1); - const multiBidLog = queue.map((log) => getQueryData(log, true))[0]; - expect(multiBidLog.pvnm).to.have.ordered.members(['-2', 'medianet', 'medianet']); - expect(multiBidLog.status).to.have.ordered.members(['1', '1', '1']); - }) }); }); diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js index 4a221e97444..b5b81f713e6 100644 --- a/test/spec/modules/medianetBidAdapter_spec.js +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -1,7 +1,10 @@ import {expect, assert} from 'chai'; -import {spec} from 'modules/medianetBidAdapter.js'; +import {spec, EVENTS} from '../../../modules/medianetBidAdapter.js'; +import {POST_ENDPOINT} from '../../../libraries/medianetUtils/constants.js'; import { makeSlot } from '../integration/faker/googletag.js'; -import { config } from 'src/config.js'; +import { config } from '../../../src/config.js'; +import {server} from '../../mocks/xhr.js'; +import {resetWinDimensions} from '../../../src/utils.js'; $$PREBID_GLOBAL$$.version = $$PREBID_GLOBAL$$.version || 'version'; let VALID_BID_REQUEST = [{ @@ -29,7 +32,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '28f8f8130a583e', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1, }, { 'bidder': 'medianet', 'params': { @@ -56,7 +59,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '3f97ca71b1e5c2', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }], VALID_BID_REQUEST_WITH_CRID = [{ @@ -86,7 +89,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '28f8f8130a583e', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }, { 'bidder': 'medianet', 'params': { @@ -114,7 +117,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '3f97ca71b1e5c2', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }], VALID_BID_REQUEST_WITH_ORTB2 = [{ 'bidder': 'medianet', @@ -143,7 +146,7 @@ let VALID_BID_REQUEST = [{ 'data': {'pbadslot': '/12345/my-gpt-tag-0'} } }, - 'bidRequestsCount': 1 + 'auctionsCount': 1 }, { 'bidder': 'medianet', 'params': { @@ -172,8 +175,39 @@ let VALID_BID_REQUEST = [{ 'data': {'pbadslot': '/12345/my-gpt-tag-0'} } }, - 'bidRequestsCount': 1 + 'auctionsCount': 1 }], + // Protected Audience API Request + VALID_BID_REQUEST_WITH_AE_IN_ORTB2IMP = [{ + 'bidder': 'medianet', + 'params': { + 'crid': 'crid', + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]], + } + }, + 'bidId': '28f8f8130a583e', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'ortb2Imp': { + 'ext': { + 'tid': '277b631f-92f5-4844-8b19-ea13c095d3f1', + 'ae': 1 + } + }, + 'auctionsCount': 1 + }], + VALID_BID_REQUEST_WITH_USERID = [{ 'bidder': 'medianet', 'params': { @@ -203,7 +237,73 @@ let VALID_BID_REQUEST = [{ 'bidId': '28f8f8130a583e', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 + }, { + 'bidder': 'medianet', + 'params': { + 'crid': 'crid', + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-123', + ortb2Imp: { + ext: { + tid: 'c52a5c62-3c2b-4b90-9ff8-ec1487754822', + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 251]], + } + }, + 'sizes': [[300, 251]], + 'bidId': '3f97ca71b1e5c2', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'auctionsCount': 1 + }], + VALID_BID_REQUEST_WITH_USERIDASEIDS = [{ + 'bidder': 'medianet', + 'params': { + 'crid': 'crid', + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + }, + userIdAsEids: [{ + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'VZME3l9ycFFORncwaGJRVUNtUzB1UVhpWVd5TElrR3A5SHVSWXAwSFVPJTJCWiUyRnV2UTBPWjZOZ1ZrWnN4SldxcWUlMkJhUnFmUVNzUVg4N1NsdW84SGpUU1BsUllQSnN5bERMdFdPM2pWVXAlMkZVSSUyQkZsJTJGcktlenpZaHp0YXlvU25INWRQQ2tXciUyRk9PQmdac3RHeG9adDNKVzlRWE51ZyUzRCUzRA', + 'atype': 1 + } + ] + } + ], + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + ortb2Imp: { + ext: { + tid: '277b631f-92f5-4844-8b19-ea13c095d3f1', + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]], + } + }, + 'bidId': '28f8f8130a583e', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'auctionsCount': 1 }, { 'bidder': 'medianet', 'params': { @@ -231,7 +331,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '3f97ca71b1e5c2', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }], VALID_BID_REQUEST_INVALID_BIDFLOOR = [{ @@ -261,7 +361,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '28f8f8130a583e', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }, { 'bidder': 'medianet', 'params': { @@ -288,7 +388,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '3f97ca71b1e5c2', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }], VALID_NATIVE_BID_REQUEST = [{ 'bidder': 'medianet', @@ -316,7 +416,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '28f8f8130a583e', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1, + 'auctionsCount': 1, 'nativeParams': { 'image': { 'required': true, @@ -373,7 +473,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '3f97ca71b1e5c2', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1, + 'auctionsCount': 1, 'nativeParams': { 'image': { 'required': true, @@ -426,13 +526,23 @@ let VALID_BID_REQUEST = [{ }, 'ext': { 'customer_id': 'customer_id', - 'prebid_version': $$PREBID_GLOBAL$$.version, + 'prebid_version': 'v' + '$prebid.version$', 'gdpr_applies': false, 'usp_applies': false, 'coppa_applies': false, 'screen': { 'w': 1000, 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } } }, 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', @@ -504,6 +614,7 @@ let VALID_BID_REQUEST = [{ } } }], + 'ortb2': {}, 'tmax': config.getConfig('bidderTimeout') }, VALID_PAYLOAD_NATIVE = { @@ -516,13 +627,23 @@ let VALID_BID_REQUEST = [{ }, 'ext': { 'customer_id': 'customer_id', - 'prebid_version': $$PREBID_GLOBAL$$.version, + 'prebid_version': 'v' + '$prebid.version$', 'gdpr_applies': false, 'usp_applies': false, 'coppa_applies': false, 'screen': { 'w': 1000, 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } } }, 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', @@ -595,6 +716,7 @@ let VALID_BID_REQUEST = [{ } } }], + 'ortb2': {}, 'tmax': config.getConfig('bidderTimeout') }, VALID_PAYLOAD = { @@ -607,13 +729,23 @@ let VALID_BID_REQUEST = [{ }, 'ext': { 'customer_id': 'customer_id', - 'prebid_version': $$PREBID_GLOBAL$$.version, + 'prebid_version': 'v' + '$prebid.version$', 'gdpr_applies': false, 'usp_applies': false, 'coppa_applies': false, 'screen': { 'w': 1000, 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } } }, 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', @@ -684,6 +816,7 @@ let VALID_BID_REQUEST = [{ } } }], + 'ortb2': {}, 'tmax': config.getConfig('bidderTimeout') }, VALID_PAYLOAD_WITH_USERID = { @@ -696,7 +829,7 @@ let VALID_BID_REQUEST = [{ }, 'ext': { 'customer_id': 'customer_id', - 'prebid_version': $$PREBID_GLOBAL$$.version, + 'prebid_version': 'v' + '$prebid.version$', 'gdpr_applies': false, 'user_id': { britepoolid: '82efd5e1-816b-4f87-97f8-044f407e2911' @@ -706,6 +839,16 @@ let VALID_BID_REQUEST = [{ 'screen': { 'w': 1000, 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } } }, 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', @@ -780,6 +923,124 @@ let VALID_BID_REQUEST = [{ } } }], + 'ortb2': {}, + 'tmax': config.getConfig('bidderTimeout') + }, + VALID_PAYLOAD_WITH_USERIDASEIDS = { + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'topMostLocation': 'http://media.net/topmost', + 'isTop': true + }, + 'ext': { + 'customer_id': 'customer_id', + 'prebid_version': 'v' + '$prebid.version$', + 'gdpr_applies': false, + 'usp_applies': false, + 'coppa_applies': false, + 'screen': { + 'w': 1000, + 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } + } + }, + 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'imp': [{ + 'id': '28f8f8130a583e', + ortb2Imp: VALID_BID_REQUEST_WITH_USERID[0].ortb2Imp, + 'transactionId': '277b631f-92f5-4844-8b19-ea13c095d3f1', + 'tagid': 'crid', + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-0', + 'visibility': 1, + 'viewability': 1, + 'coordinates': { + 'top_left': { + x: 50, + y: 50 + }, + 'bottom_right': { + x: 100, + y: 100 + } + }, + 'display_count': 1 + }, + 'banner': [{ + 'w': 300, + 'h': 250 + }], + 'all': { + 'cid': 'customer_id', + 'crid': 'crid', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + } + }, { + 'id': '3f97ca71b1e5c2', + ortb2Imp: VALID_BID_REQUEST_WITH_USERID[1].ortb2Imp, + 'transactionId': 'c52a5c62-3c2b-4b90-9ff8-ec1487754822', + 'tagid': 'crid', + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-123', + 'visibility': 1, + 'viewability': 1, + 'coordinates': { + 'top_left': { + x: 50, + y: 50 + }, + 'bottom_right': { + x: 100, + y: 100 + } + }, + 'display_count': 1 + }, + 'banner': [{ + 'w': 300, + 'h': 251 + }], + 'all': { + 'cid': 'customer_id', + 'crid': 'crid', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + } + }], + 'ortb2': { + 'user': { + 'ext': { + 'eids': [{ + 'source': 'criteo.com', + 'uids': [{ + 'id': 'VZME3l9ycFFORncwaGJRVUNtUzB1UVhpWVd5TElrR3A5SHVSWXAwSFVPJTJCWiUyRnV2UTBPWjZOZ1ZrWnN4SldxcWUlMkJhUnFmUVNzUVg4N1NsdW84SGpUU1BsUllQSnN5bERMdFdPM2pWVXAlMkZVSSUyQkZsJTJGcktlenpZaHp0YXlvU25INWRQQ2tXciUyRk9PQmdac3RHeG9adDNKVzlRWE51ZyUzRCUzRA', + 'atype': 1 + } + ] + }] + } + }, + }, 'tmax': config.getConfig('bidderTimeout') }, VALID_PAYLOAD_WITH_CRID = { @@ -792,13 +1053,23 @@ let VALID_BID_REQUEST = [{ }, 'ext': { 'customer_id': 'customer_id', - 'prebid_version': $$PREBID_GLOBAL$$.version, + 'prebid_version': 'v' + '$prebid.version$', 'gdpr_applies': false, 'usp_applies': false, 'coppa_applies': true, 'screen': { 'w': 1000, 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } } }, 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', @@ -873,8 +1144,89 @@ let VALID_BID_REQUEST = [{ } } }], + 'ortb2': {}, 'tmax': config.getConfig('bidderTimeout') }, + // Protected Audience API Valid Payload + VALID_PAYLOAD_PAAPI = { + 'site': { + 'domain': 'media.net', + 'page': 'http://media.net/prebidtest', + 'ref': 'http://media.net/prebidtest', + 'topMostLocation': 'http://media.net/topmost', + 'isTop': true + }, + 'ext': { + 'customer_id': 'customer_id', + 'prebid_version': 'v' + '$prebid.version$', + 'gdpr_applies': false, + 'usp_applies': false, + 'coppa_applies': false, + 'screen': { + 'w': 1000, + 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } + } + }, + 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'imp': [ + { + 'id': '28f8f8130a583e', + 'transactionId': '277b631f-92f5-4844-8b19-ea13c095d3f1', + 'ext': { + 'ae': 1, + 'dfp_id': 'div-gpt-ad-1460505748561-0', + 'display_count': 1, + 'coordinates': { + 'top_left': { + 'x': 50, + 'y': 50 + }, + 'bottom_right': { + 'x': 100, + 'y': 100 + } + }, + 'viewability': 1, + 'visibility': 1 + }, + 'all': { + 'cid': 'customer_id', + 'crid': 'crid', + 'site': { + 'domain': 'media.net', + 'isTop': true, + 'page': 'http://media.net/prebidtest', + 'ref': 'http://media.net/prebidtest' + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '277b631f-92f5-4844-8b19-ea13c095d3f1', + 'ae': 1 + } + }, + 'banner': [ + { + 'w': 300, + 'h': 250 + } + ], + 'tagid': 'crid' + } + ], + 'ortb2': {}, + 'tmax': 3000 + }, VALID_VIDEO_BID_REQUEST = [{ 'bidder': 'medianet', @@ -894,7 +1246,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '28f8f8130a583e', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }], VALID_PAYLOAD_PAGE_META = (() => { @@ -904,8 +1256,6 @@ let VALID_BID_REQUEST = [{ } catch (e) {} PAGE_META.site = Object.assign(PAGE_META.site, { 'canonical_url': 'http://localhost:9999/canonical-test', - 'twitter_url': 'http://localhost:9999/twitter-test', - 'og_url': 'http://localhost:9999/fb-test' }); return PAGE_META; })(), @@ -1104,6 +1454,126 @@ let VALID_BID_REQUEST = [{ } } }, + // Protected Audience API Response + SERVER_RESPONSE_PAAPI = { + body: { + 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'bidList': [{ + 'no_bid': false, + 'requestId': '28f8f8130a583e', + 'ad': 'ad', + 'width': 300, + 'height': 250, + 'creativeId': 'crid', + 'netRevenue': true, + 'cpm': 0.1 + }], + 'ext': { + 'paApiAuctionConfigs': [ + { + 'bidId': '28f8f8130a583e', + 'config': { + 'seller': 'https://hbx.test.media.net', + 'decisionLogicUrl': 'https://hbx.test.media.net/decision-logic.js', + 'interestGroupBuyers': ['https://buyer.test.media.net'], + 'auctionSignals': { + 'logging_params': { + 'cid': 'customer_id', + 'crid': 'crid', + 'bid_uuid': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'browser_id': 2, + 'dfpid': 'div-gpt-ad-1460505748561-0' + }, + 'pvidLookup': { + 'https://buyer.test.media.net': { + 'pvid': '172', + 'seat': 'quantcast-qc1' + } + }, + 'bidFlr': 0.0 + }, + 'sellerTimout': 1000, + 'sellerSignals': { + 'callbackURL': 'https://test.com/paapi/v1/abcd' + }, + 'perBuyerSignals': { + 'https://buyer.test.media.net': [ 'test_buyer_signals' ] + }, + 'perBuyerTimeouts': { + '*': 200 + } + } + } + ], + 'csUrl': [{ + 'type': 'iframe', + 'url': 'http://contextual.media.net/checksync.php?&vsSync=1' + }] + } + } + }, + // Protected Audience API OpenRTB Response + SERVER_RESPONSE_PAAPI_ORTB = { + body: { + 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'bidList': [{ + 'no_bid': false, + 'requestId': '28f8f8130a583e', + 'ad': 'ad', + 'width': 300, + 'height': 250, + 'creativeId': 'crid', + 'netRevenue': true, + 'cpm': 0.1 + }], + 'ext': { + 'igi': [{ + 'igs': [ + { + 'impid': '28f8f8130a583e', + 'bidId': '28f8f8130a583e', + 'config': { + 'seller': 'https://hbx.test.media.net', + 'decisionLogicUrl': 'https://hbx.test.media.net/decision-logic.js', + 'interestGroupBuyers': ['https://buyer.test.media.net'], + 'auctionSignals': { + 'logging_params': { + 'cid': 'customer_id', + 'crid': 'crid', + 'bid_uuid': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'browser_id': 2, + 'dfpid': 'div-gpt-ad-1460505748561-0' + }, + 'pvidLookup': { + 'https://buyer.test.media.net': { + 'pvid': '172', + 'seat': 'quantcast-qc1' + } + }, + 'bidFlr': 0.0 + }, + 'sellerTimout': 1000, + 'sellerSignals': { + 'callbackURL': 'https://test.com/paapi/v1/abcd' + }, + 'perBuyerSignals': { + 'https://buyer.test.media.net': [ 'test_buyer_signals' ] + }, + 'perBuyerTimeouts': { + '*': 200 + } + } + } + ], + }], + 'csUrl': [{ + 'type': 'iframe', + 'url': 'http://contextual.media.net/checksync.php?&vsSync=1' + }] + } + } + }, + SERVER_VIDEO_OUTSTREAM_RESPONSE_VALID_BID = { body: { 'id': 'd90ca32f-3877-424a-b2f2-6a68988df57a', @@ -1176,7 +1646,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '28f8f8130a583e', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }, { 'bidder': 'medianet', 'params': { @@ -1203,7 +1673,7 @@ let VALID_BID_REQUEST = [{ 'bidId': '3f97ca71b1e5c2', 'bidderRequestId': '1e9b1f07797c1c', 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', - 'bidRequestsCount': 1 + 'auctionsCount': 1 }], VALID_BIDDER_REQUEST_WITH_GDPR = { 'gdprConsent': { @@ -1231,7 +1701,7 @@ let VALID_BID_REQUEST = [{ }, 'ext': { 'customer_id': 'customer_id', - 'prebid_version': $$PREBID_GLOBAL$$.version, + 'prebid_version': 'v' + '$prebid.version$', 'gdpr_consent_string': 'consentString', 'gdpr_applies': true, 'usp_applies': true, @@ -1240,6 +1710,16 @@ let VALID_BID_REQUEST = [{ 'screen': { 'w': 1000, 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } } }, 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', @@ -1310,6 +1790,7 @@ let VALID_BID_REQUEST = [{ } } }], + 'ortb2': {}, 'tmax': 3000, }, VALID_BIDDER_REQUEST_WITH_GPP_IN_ORTB2 = { @@ -1339,13 +1820,23 @@ let VALID_BID_REQUEST = [{ }, 'ext': { 'customer_id': 'customer_id', - 'prebid_version': $$PREBID_GLOBAL$$.version, + 'prebid_version': 'v' + '$prebid.version$', 'gdpr_applies': false, 'usp_applies': false, 'coppa_applies': false, 'screen': { 'w': 1000, 'h': 1000 + }, + 'vcoords': { + 'top_left': { + 'x': 50, + 'y': 100 + }, + 'bottom_right': { + 'x': 490, + 'y': 880 + } } }, 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', @@ -1428,9 +1919,14 @@ describe('Media.net bid adapter', function () { let sandbox; beforeEach(function () { sandbox = sinon.sandbox.create(); + sandbox.stub(window.top, 'innerHeight').value(780) + sandbox.stub(window.top, 'innerWidth').value(440) + sandbox.stub(window.top, 'scrollY').value(100) + sandbox.stub(window.top, 'scrollX').value(50) }); afterEach(function () { + resetWinDimensions(); sandbox.restore(); }); @@ -1547,9 +2043,27 @@ describe('Media.net bid adapter', function () { expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_WITH_USERID); }); + it('should have userIdAsEids in bid request', function () { + let bidReq = spec.buildRequests(VALID_BID_REQUEST_WITH_USERIDASEIDS, VALID_AUCTIONDATA); + expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_WITH_USERIDASEIDS); + }); + + it('should have valid payload when PAAPI is enabled', function () { + let bidReq = spec.buildRequests(VALID_BID_REQUEST_WITH_AE_IN_ORTB2IMP, {...VALID_AUCTIONDATA, paapi: {enabled: true}}); + expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_PAAPI); + }); + + it('should send whatever is set in ortb2imp.ext.ae in all bid requests when PAAPI is enabled', function () { + let bidReq = spec.buildRequests(VALID_BID_REQUEST_WITH_AE_IN_ORTB2IMP, {...VALID_AUCTIONDATA, paapi: {enabled: true}}); + let data = JSON.parse(bidReq.data); + expect(data).to.deep.equal(VALID_PAYLOAD_PAAPI); + expect(data.imp[0].ext).to.have.property('ae'); + expect(data.imp[0].ext.ae).to.equal(1); + }); + describe('build requests: when page meta-data is available', () => { beforeEach(() => { - spec.clearMnData(); + spec.clearPageMeta(); }); it('should pass canonical, twitter and fb paramters if available', () => { let documentStub = sandbox.stub(window.top.document, 'querySelector'); @@ -1721,19 +2235,140 @@ describe('Media.net bid adapter', function () { let bids = spec.interpretResponse(SERVER_RESPONSE_EMPTY_BIDLIST, []); expect(bids).to.deep.equal(validBids); }); + + it('should return paapi if PAAPI response is received', function() { + let response = spec.interpretResponse(SERVER_RESPONSE_PAAPI, []); + expect(response).to.have.property('bids'); + expect(response).to.have.property('paapi'); + expect(response.paapi[0]).to.deep.equal(SERVER_RESPONSE_PAAPI.body.ext.paApiAuctionConfigs[0]); + }); + + it('should return paapi if openRTB PAAPI response received', function () { + let response = spec.interpretResponse(SERVER_RESPONSE_PAAPI_ORTB, []); + expect(response).to.have.property('bids'); + expect(response).to.have.property('paapi'); + expect(response.paapi[0]).to.deep.equal(SERVER_RESPONSE_PAAPI_ORTB.body.ext.igi[0].igs[0]) + }); + + it('should have the correlation between paapi[0].bidId and bidreq.imp[0].id', function() { + let bidReq = spec.buildRequests(VALID_BID_REQUEST_WITH_AE_IN_ORTB2IMP, {...VALID_AUCTIONDATA, paapi: {enabled: true}}); + let bidRes = spec.interpretResponse(SERVER_RESPONSE_PAAPI, []); + expect(bidRes.paapi[0].bidId).to.equal(JSON.parse(bidReq.data).imp[0].id) + }); + + it('should have the correlation between paapi[0].bidId and bidreq.imp[0].id for openRTB response', function() { + let bidReq = spec.buildRequests(VALID_BID_REQUEST_WITH_AE_IN_ORTB2IMP, {...VALID_AUCTIONDATA, paapi: {enabled: true}}); + let bidRes = spec.interpretResponse(SERVER_RESPONSE_PAAPI_ORTB, []); + expect(bidRes.paapi[0].bidId).to.equal(JSON.parse(bidReq.data).imp[0].id) + }); }); describe('onTimeout', function () { - it('should have valid timeout data', function() { - let response = spec.onTimeout({}); - expect(response).to.deep.equal(undefined); + it('onTimeout exist as a function', () => { + assert.typeOf(spec.onTimeout, 'function'); + }); + it('should send timeout data correctly', function () { + const timeoutData = [{ + bidder: 'medianet', + bidId: 'mnet-4644-442a-b5e0-93f268cf8d19', + adUnitCode: 'adUnit-code', + timeout: 3000, + auctionId: '12a34b56c' + }]; + sandbox.stub(window.navigator, 'sendBeacon').returns(false); + + spec.onTimeout(timeoutData); + const reqBody = new URLSearchParams(server.requests[0].requestBody); + + assert.equal(server.requests[0].method, 'POST'); + assert.equal(server.requests[0].url, POST_ENDPOINT); + assert.equal(reqBody.get('event'), EVENTS.TIMEOUT_EVENT_NAME); + assert.equal(reqBody.get('rd'), timeoutData[0].timeout.toString()); + assert.equal(reqBody.get('acid[]'), timeoutData[0].auctionId); }); }); describe('onBidWon', function () { - it('should have valid bid data', function() { - let response = spec.onBidWon(undefined); - expect(response).to.deep.equal(undefined); + it('onBidWon exist as a function', () => { + assert.typeOf(spec.onBidWon, 'function'); + }); + it('should send winning bid data correctly', function () { + const bid = { + bidder: 'medianet', + bidId: 'mnet-4644-442a-b5e0-93f268cf8d19', + adUnitCode: 'adUnit-code', + timeout: 3000, + auctionId: '12a34b56c', + cpm: 12.24 + }; + sandbox.stub(window.navigator, 'sendBeacon').returns(false); + + spec.onBidWon(bid); + const reqBody = new URLSearchParams(server.requests[0].requestBody); + + assert.equal(server.requests[0].method, 'POST'); + assert.equal(server.requests[0].url, POST_ENDPOINT); + assert.equal(reqBody.get('event'), EVENTS.BID_WON_EVENT_NAME); + assert.equal(reqBody.get('value'), bid.cpm.toString()); + assert.equal(reqBody.get('acid[]'), bid.auctionId); + }); + }); + + describe('onSetTargeting', function () { + it('onSetTargeting exist as a function', () => { + assert.typeOf(spec.onSetTargeting, 'function'); + }); + it('should send targeting data correctly', function () { + const bid = { + bidder: 'medianet', + bidId: 'mnet-4644-442a-b5e0-93f268cf8d19', + adUnitCode: 'adUnit-code', + timeout: 3000, + auctionId: '12a34b56c', + cpm: 12.24 + }; + sandbox.stub(window.navigator, 'sendBeacon').returns(false); + sandbox.stub(config, 'getConfig').withArgs('enableSendAllBids').returns(false); + + spec.onSetTargeting(bid); + const reqBody = new URLSearchParams(server.requests[0].requestBody); + + assert.equal(server.requests[0].method, 'POST'); + assert.equal(server.requests[0].url, POST_ENDPOINT); + assert.equal(reqBody.get('event'), EVENTS.SET_TARGETING); + assert.equal(reqBody.get('value'), bid.cpm.toString()); + assert.equal(reqBody.get('acid[]'), bid.auctionId); + }); + }); + + describe('onBidderError', function () { + it('onBidderError exist as a function', () => { + assert.typeOf(spec.onBidderError, 'function'); + }); + it('should send bidderError data correctly', function () { + const error = { + reason: {message: 'Failed to fetch', status: 500}, + timedOut: true, + status: 0 + } + const bids = [{ + bidder: 'medianet', + bidId: 'mnet-4644-442a-b5e0-93f268cf8d19', + adUnitCode: 'adUnit-code', + timeout: 3000, + auctionId: '12a34b56c', + cpm: 12.24 + }]; + sandbox.stub(window.navigator, 'sendBeacon').returns(false); + + spec.onBidderError({error, bidderRequest: {bids}}); + const reqBody = new URLSearchParams(server.requests[0].requestBody); + + assert.equal(server.requests[0].method, 'POST'); + assert.equal(server.requests[0].url, POST_ENDPOINT); + assert.equal(reqBody.get('event'), EVENTS.BIDDER_ERROR); + assert.equal(reqBody.get('rd'), `timedOut:${error.timedOut}|status:${error.status}|message:${error.reason.message}`); + assert.equal(reqBody.get('acid[]'), bids[0].auctionId); }); }); diff --git a/test/spec/modules/merkleIdSystem_spec.js b/test/spec/modules/merkleIdSystem_spec.js index 82c17336d20..a74bbefdca7 100644 --- a/test/spec/modules/merkleIdSystem_spec.js +++ b/test/spec/modules/merkleIdSystem_spec.js @@ -3,6 +3,8 @@ import * as utils from 'src/utils.js'; import {merkleIdSubmodule} from 'modules/merkleIdSystem.js'; import sinon from 'sinon'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; let expect = require('chai').expect; @@ -157,7 +159,7 @@ describe('Merkle System', function () { storage: STORAGE_PARAMS }; - let submoduleCallback = merkleIdSubmodule.getId(config, { gdprApplies: true }); + let submoduleCallback = merkleIdSubmodule.getId(config, {gdpr: {gdprApplies: true}}); expect(submoduleCallback).to.be.undefined; expect(utils.logError.args[0][0]).to.exist.and.to.equal('User ID - merkleId submodule does not currently handle consent strings'); }); @@ -248,4 +250,71 @@ describe('Merkle System', function () { expect(callbackSpy.calledOnce).to.be.true; }); }); + + describe('eid', () => { + before(() => { + attachIdSystem(merkleIdSubmodule); + }); + it('merkleId (legacy) - supports single id', function() { + const userId = { + merkleId: { + id: 'some-random-id-value', keyID: 1 + } + }; + const newEids = createEidsArray(userId); + + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'merkleinc.com', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { keyID: 1 } + }] + }); + }); + + it('merkleId supports multiple source providers', function() { + const userId = { + merkleId: [{ + id: 'some-random-id-value', ext: { enc: 1, keyID: 16, idName: 'pamId', ssp: 'ssp1' } + }, { + id: 'another-random-id-value', + ext: { + enc: 1, + idName: 'pamId', + third: 4, + ssp: 'ssp2' + } + }] + } + + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(2); + expect(newEids[0]).to.deep.equal({ + source: 'ssp1.merkleinc.com', + uids: [{id: 'some-random-id-value', + atype: 3, + ext: { + enc: 1, + keyID: 16, + idName: 'pamId', + ssp: 'ssp1' + } + }] + }); + expect(newEids[1]).to.deep.equal({ + source: 'ssp2.merkleinc.com', + uids: [{id: 'another-random-id-value', + atype: 3, + ext: { + third: 4, + enc: 1, + idName: 'pamId', + ssp: 'ssp2' + } + }] + }); + }); + }) }); diff --git a/test/spec/modules/mgidBidAdapter_spec.js b/test/spec/modules/mgidBidAdapter_spec.js index f9bb1fb91e1..180b0dc723e 100644 --- a/test/spec/modules/mgidBidAdapter_spec.js +++ b/test/spec/modules/mgidBidAdapter_spec.js @@ -320,6 +320,14 @@ describe('Mgid bid adapter', function () { let abid = { adUnitCode: 'div', bidder: 'mgid', + ortb2Imp: { + ext: { + gpid: '/1111/gpid', + data: { + pbadslot: '/1111/gpid', + } + } + }, params: { accountId: '1', placementId: '2', @@ -447,12 +455,13 @@ describe('Mgid bid adapter', function () { expect(data.device.w).equal(screenWidth); expect(data.device.language).to.deep.equal(lang); expect(data.imp[0].tagid).to.deep.equal('2/div'); + expect(data.imp[0].ext.gpid).to.deep.equal('/1111/gpid'); expect(data.imp[0].banner).to.deep.equal({w: 300, h: 250}); expect(data.imp[0].secure).to.deep.equal(secure); expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"banner":{"w":300,"h":250}}],"tmax":3000}`, + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"ext":{"gpid":"/1111/gpid"},"banner":{"w":300,"h":250}}],"tmax":3000}`, }); }); it('should not return native imp if minimum asset list not requested', function () { @@ -496,12 +505,13 @@ describe('Mgid bid adapter', function () { expect(data.device.w).equal(screenWidth); expect(data.device.language).to.deep.equal(lang); expect(data.imp[0].tagid).to.deep.equal('2/div'); + expect(data.imp[0].ext.gpid).to.deep.equal('/1111/gpid'); expect(data.imp[0].native).is.a('object').and.to.deep.equal({'request': {'assets': [{'id': 1, 'required': 1, 'title': {'len': 80}}, {'id': 2, 'img': {'h': 80, 'type': 3, 'w': 80}, 'required': 0}, {'data': {'type': 1}, 'id': 11, 'required': 0}], 'plcmtcnt': 1}}); expect(data.imp[0].secure).to.deep.equal(secure); expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":80,"h":80}},{"id":11,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"ext":{"gpid":"/1111/gpid"},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":80,"h":80}},{"id":11,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, }); }); it('should return proper native imp with image altered', function () { @@ -538,7 +548,7 @@ describe('Mgid bid adapter', function () { expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":1,"img":{"type":3,"w":492,"h":328,"wmin":50,"hmin":50}},{"id":3,"required":0,"img":{"type":1,"w":50,"h":50}},{"id":11,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"ext":{"gpid":"/1111/gpid"},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":1,"img":{"type":3,"w":492,"h":328,"wmin":50,"hmin":50}},{"id":3,"required":0,"img":{"type":1,"w":50,"h":50}},{"id":11,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, }); }); it('should return proper native imp with sponsoredBy', function () { @@ -574,7 +584,7 @@ describe('Mgid bid adapter', function () { expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":80,"h":80}},{"id":4,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"ext":{"gpid":"/1111/gpid"},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":80,"h":80}},{"id":4,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, }); }); it('should return proper banner request', function () { @@ -608,7 +618,7 @@ describe('Mgid bid adapter', function () { expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"banner":{"w":300,"h":600,"format":[{"w":300,"h":600},{"w":300,"h":250}],"pos":1}}],"tmax":3000}`, + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"ext":{"gpid":"/1111/gpid"},"banner":{"w":300,"h":600,"format":[{"w":300,"h":600},{"w":300,"h":250}],"pos":1}}],"tmax":3000}`, }); }); it('should proper handle ortb2 data', function () { diff --git a/test/spec/modules/mgidXBidAdapter_spec.js b/test/spec/modules/mgidXBidAdapter_spec.js index e0b1e1a84e9..f933a61ee55 100644 --- a/test/spec/modules/mgidXBidAdapter_spec.js +++ b/test/spec/modules/mgidXBidAdapter_spec.js @@ -5,9 +5,19 @@ import { getUniqueIdentifierStr } from '../../../src/utils.js'; import { config } from '../../../src/config'; import { USERSYNC_DEFAULT_CONFIG } from '../../../src/userSync'; -const bidder = 'mgidX' +const bidder = 'mgidX'; describe('MGIDXBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -19,8 +29,9 @@ describe('MGIDXBidAdapter', function () { }, params: { region: 'eu', - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -33,8 +44,9 @@ describe('MGIDXBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -57,8 +69,9 @@ describe('MGIDXBidAdapter', function () { }, params: { region: 'eu', - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -82,8 +95,17 @@ describe('MGIDXBidAdapter', function () { vendorData: {} }, refererInfo: { - referer: 'https://test.com' - } + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } + }, + timeout: 500 }; describe('isBidRequestValid', function () { @@ -112,7 +134,7 @@ describe('MGIDXBidAdapter', function () { it('Returns valid EU URL', function () { bids[0].params.region = 'eu'; serverRequest = spec.buildRequests(bids, bidderRequest); - expect(serverRequest.url).to.equal('https://eu.mgid.com/pbjs'); + expect(serverRequest.url).to.equal('https://eu-x.mgid.com/pbjs'); }); it('Returns valid EAST URL', function () { @@ -126,6 +148,7 @@ describe('MGIDXBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -134,7 +157,11 @@ describe('MGIDXBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -159,6 +186,56 @@ describe('MGIDXBidAdapter', function () { 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 + } + ]; + + let 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'); @@ -204,6 +281,38 @@ describe('MGIDXBidAdapter', function () { }); }); + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { diff --git a/test/spec/modules/michaoBidAdapter_spec.js b/test/spec/modules/michaoBidAdapter_spec.js new file mode 100644 index 00000000000..4aefa0b01c0 --- /dev/null +++ b/test/spec/modules/michaoBidAdapter_spec.js @@ -0,0 +1,651 @@ +import { cloneDeep } from 'lodash'; +import { domainLogger, spec } from '../../../modules/michaoBidAdapter'; +import * as utils from '../../../src/utils.js'; + +describe('Michao Bid Adapter', () => { + let bannerBidRequest; + let videoBidRequest; + let nativeBidRequest; + let videoServerResponse; + let bannerServerResponse; + let domainLoggerMock; + let sandbox; + let triggerPixelSpy; + + beforeEach(() => { + bannerBidRequest = cloneDeep(_bannerBidRequest); + videoBidRequest = cloneDeep(_videoBidRequest); + nativeBidRequest = cloneDeep(_nativeBidRequest); + videoServerResponse = cloneDeep(_videoServerResponse); + bannerServerResponse = cloneDeep(_bannerServerResponse); + sandbox = sinon.sandbox.create(); + domainLoggerMock = sandbox.stub(domainLogger); + triggerPixelSpy = sandbox.spy(utils, 'triggerPixel'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('`isBidRequestValid`', () => { + describe('Required parameter behavior', () => { + it('passes when siteId is a number', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + site: 123, + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.true; + expect(domainLoggerMock.invalidSiteError.calledOnce).to.be.false; + }); + + it('detects invalid input when siteId is not a number', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + site: '123', + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.false; + expect(domainLoggerMock.invalidSiteError.calledOnce).to.be.true; + }); + + it('passes when placementId is a string', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + placement: '123', + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.true; + expect(domainLoggerMock.invalidPlacementError.calledOnce).to.be.false; + }); + + it('detects invalid input when placementId is not a string', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + placement: 123, + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.false; + expect(domainLoggerMock.invalidPlacementError.calledOnce).to.be.true; + }); + }); + + describe('Optional parameter behavior', () => { + it('passes when partnerId is not specified', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + partner: undefined, + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.true; + expect(domainLoggerMock.invalidPartnerError.calledOnce).to.be.false; + }); + + it('passes when partnerId is a number', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + partner: 6789, + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.true; + expect(domainLoggerMock.invalidPartnerError.calledOnce).to.be.false; + }); + + it('detects invalid input when partnerId is not a number', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + partner: '6789', + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.false; + expect(domainLoggerMock.invalidPartnerError.calledOnce).to.be.true; + }); + + it('passes when test is not specified', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + test: undefined, + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.true; + expect(domainLoggerMock.invalidTestParamError.calledOnce).to.be.false; + }); + + it('passes when test is a boolean', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + test: false, + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.true; + expect(domainLoggerMock.invalidTestParamError.calledOnce).to.be.false; + }); + + it('detects invalid input when test is not a boolean', () => { + bannerBidRequest.params = { + ...bannerBidRequest.params, + test: 'trueee', + }; + + const result = spec.isBidRequestValid(bannerBidRequest); + + expect(result).to.be.false; + expect(domainLoggerMock.invalidTestParamError.calledOnce).to.be.true; + }); + }); + }); + + describe('`buildRequest`', () => { + describe('Bid request format behavior', () => { + it('creates banner-specific bid request from bid request containing one banner format', () => { + const bidderRequest = { + bids: [bannerBidRequest], + auctionId: bannerBidRequest.auctionId, + bidderRequestId: bannerBidRequest.bidderRequestId, + }; + + const result = spec.buildRequests([bannerBidRequest], bidderRequest); + + expect(result.length).to.equal(1); + expect(result[0].data.imp.length).to.equal(1); + expect(result[0].data.imp[0]).to.have.property( + 'banner' + ); + }); + + it('creates video-specific bid request from bid request containing one video format', () => { + const bidderRequest = { + bids: [videoBidRequest], + auctionId: videoBidRequest.auctionId, + bidderRequestId: videoBidRequest.bidderRequestId, + }; + + const result = spec.buildRequests([videoBidRequest], bidderRequest); + + expect(result.length).to.equal(1); + expect(result[0].data.imp.length).to.equal(1); + }); + + it('creates native-specific bid request from bid request containing one native format', () => { + const bidderRequest = { + bids: [nativeBidRequest], + auctionId: nativeBidRequest.auctionId, + bidderRequestId: nativeBidRequest.bidderRequestId, + }; + + const result = spec.buildRequests([nativeBidRequest], bidderRequest); + + expect(result.length).to.equal(1); + expect(result[0].data.imp.length).to.equal(1); + }); + }); + + describe('Multiple format combination behavior', () => { + it('creates banner and video bid request with two impressions from bid request containing both banner and video formats', () => { + const multiFormatRequest = { + ...bannerBidRequest, + mediaTypes: { + ...bannerBidRequest.mediaTypes, + video: videoBidRequest.mediaTypes.video, + }, + }; + + const bidderRequest = { + bids: [multiFormatRequest], + auctionId: multiFormatRequest.auctionId, + bidderRequestId: multiFormatRequest.bidderRequestId, + }; + + const result = spec.buildRequests([multiFormatRequest], bidderRequest); + + expect(result.length).to.equal(1); + expect(result[0].data.imp.length).to.equal(2); + expect(result[0].data.imp[0]).to.have.property( + 'banner' + ); + }); + + it('creates banner and native bid request with two impressions from bid request containing both banner and native formats', () => { + const multiFormatRequest = { + ...bannerBidRequest, + mediaTypes: { + ...bannerBidRequest.mediaTypes, + native: nativeBidRequest.mediaTypes.native, + }, + }; + + const bidderRequest = { + bids: [multiFormatRequest], + auctionId: multiFormatRequest.auctionId, + bidderRequestId: multiFormatRequest.bidderRequestId, + }; + + const result = spec.buildRequests([multiFormatRequest], bidderRequest); + + expect(result.length).to.equal(1); + expect(result[0].data.imp.length).to.equal(2); + expect(result[0].data.imp[0]).to.have.property( + 'banner' + ); + }); + + it('creates video and native bid request with two impressions from bid request containing both video and native formats', () => { + const multiFormatRequest = { + ...videoBidRequest, + mediaTypes: { + ...videoBidRequest.mediaTypes, + native: nativeBidRequest.mediaTypes.native, + }, + }; + + const bidderRequest = { + bids: [multiFormatRequest], + auctionId: multiFormatRequest.auctionId, + bidderRequestId: multiFormatRequest.bidderRequestId, + }; + + const result = spec.buildRequests([multiFormatRequest], bidderRequest); + + expect(result.length).to.equal(1); + expect(result[0].data.imp.length).to.equal(2); + }); + + it('creates banner, video and native bid request with three impressions from bid request containing all three formats', () => { + const multiFormatRequest = { + ...bannerBidRequest, + mediaTypes: { + ...bannerBidRequest.mediaTypes, + video: videoBidRequest.mediaTypes.video, + native: nativeBidRequest.mediaTypes.native, + }, + }; + + const bidderRequest = { + bids: [multiFormatRequest], + auctionId: multiFormatRequest.auctionId, + bidderRequestId: multiFormatRequest.bidderRequestId, + }; + + const result = spec.buildRequests([multiFormatRequest], bidderRequest); + + expect(result.length).to.equal(1); + expect(result[0].data.imp.length).to.equal(3); + expect(result[0].data.imp[0]).to.have.property( + 'banner' + ); + }); + }); + + describe('Required parameter behavior', () => { + it('sets siteId in site object', () => { + bannerBidRequest.params = { + site: 456, + placement: '123', + }; + const bidderRequest = { + bids: [bannerBidRequest], + auctionId: bannerBidRequest.auctionId, + bidderRequestId: bannerBidRequest.bidderRequestId, + }; + + const result = spec.buildRequests([bannerBidRequest], bidderRequest); + + expect(result[0].data.site.ext.michao.site).to.equal('456'); + }); + + it('sets placementId in impression object', () => { + bannerBidRequest.params = { + site: 456, + placement: '123', + }; + const bidderRequest = { + bids: [bannerBidRequest], + auctionId: bannerBidRequest.auctionId, + bidderRequestId: bannerBidRequest.bidderRequestId, + }; + + const result = spec.buildRequests([bannerBidRequest], bidderRequest); + + expect(result[0].data.imp[0].ext.michao.placement).to.equal('123'); + }); + }); + + describe('Optional parameter behavior', () => { + it('sets partnerId in publisher when specified', () => { + bannerBidRequest.params = { + site: 456, + placement: '123', + partner: 123, + }; + const bidderRequest = { + bids: [bannerBidRequest], + auctionId: bannerBidRequest.auctionId, + bidderRequestId: bannerBidRequest.bidderRequestId, + }; + + const result = spec.buildRequests([bannerBidRequest], bidderRequest); + + expect(result[0].data.site.publisher.ext.michao.partner).to.equal('123'); + }); + + it('sets test in publisher when specified', () => { + bannerBidRequest.params = { + site: 456, + placement: '123', + test: true, + }; + const bidderRequest = { + bids: [bannerBidRequest], + auctionId: bannerBidRequest.auctionId, + bidderRequestId: bannerBidRequest.bidderRequestId, + }; + + const result = spec.buildRequests([bannerBidRequest], bidderRequest); + + expect(result[0].data.test).to.equal(1); + }); + }); + }); + + describe('`interpretResponse`', () => { + it('sets renderer for video bid response when bid request was outstream', () => { + videoBidRequest.mediaTypes.video = { + ...videoBidRequest.mediaTypes.video, + context: 'outstream', + }; + const bidderRequest = { + bids: [videoBidRequest], + auctionId: videoBidRequest.auctionId, + bidderCode: 'michao', + bidderRequestId: videoBidRequest.bidderRequestId, + }; + const request = spec.buildRequests([videoBidRequest], bidderRequest); + + const result = spec.interpretResponse(videoServerResponse, request[0]); + + expect(result[0].renderer.url).to.equal( + 'https://cdn.jsdelivr.net/npm/in-renderer-js@1/dist/in-video-renderer.umd.min.js' + ); + }); + + it('does not set renderer for video bid response when bid request was instream', () => { + videoBidRequest.mediaTypes.video = { + ...videoBidRequest.mediaTypes.video, + context: 'instream', + }; + const bidderRequest = { + bids: [videoBidRequest], + auctionId: videoBidRequest.auctionId, + bidderCode: 'michao', + bidderRequestId: videoBidRequest.bidderRequestId, + }; + const request = spec.buildRequests([videoBidRequest], bidderRequest); + + const result = spec.interpretResponse(videoServerResponse, request[0]); + + expect(result[0].renderer).to.be.undefined; + }); + + it('does not set renderer for banner bid response', () => { + const bidderRequest = { + bids: [bannerBidRequest], + auctionId: bannerBidRequest.auctionId, + bidderCode: 'michao', + bidderRequestId: bannerBidRequest.bidderRequestId, + }; + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + + const result = spec.interpretResponse(bannerServerResponse, request[0]); + + expect(result[0].renderer).to.be.undefined; + }); + }); + + describe('`getUserSyncs`', () => { + it('performs iframe user sync when iframe is enabled', () => { + const syncOptions = { + iframeEnabled: true, + }; + + const result = spec.getUserSyncs(syncOptions, {}, {}, {}); + + expect(result[0].url).to.equal( + 'https://sync.michao-ssp.com/cookie-syncs?' + ); + expect(result[0].type).to.equal('iframe'); + }); + + it('does not perform iframe user sync when iframe is disabled', () => { + const syncOptions = { + iframeEnabled: false, + }; + + const result = spec.getUserSyncs(syncOptions, {}, {}, {}); + + expect(result.length).to.equal(0); + }); + + it('sets GDPR parameters in user sync URL when GDPR applies', () => { + const syncOptions = { + iframeEnabled: true, + }; + const gdprConsent = { + gdprApplies: true, + consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + }; + + const result = spec.getUserSyncs(syncOptions, {}, gdprConsent, {}); + + expect(result[0].url).to.equal( + 'https://sync.michao-ssp.com/cookie-syncs?gdpr=1&gdpr_consent=BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA' + ); + expect(result[0].type).to.equal('iframe'); + }); + + it('does not set GDPR parameters in user sync URL when GDPR does not apply', () => { + const syncOptions = { + iframeEnabled: true, + }; + const gdrpConsent = { + gdrpApplies: false, + }; + + const result = spec.getUserSyncs(syncOptions, {}, gdrpConsent, {}); + + expect(result[0].url).to.equal( + 'https://sync.michao-ssp.com/cookie-syncs?' + ); + expect(result[0].type).to.equal('iframe'); + }); + }); + + describe('`onBidBillable`', () => { + it('does not generate billing when billing URL is not included in bid', () => { + const bid = {}; + + spec.onBidBillable(bid); + + expect(triggerPixelSpy.calledOnce).to.be.false; + }); + + it('calls billing url when billing URL is a string', () => { + const bid = { + burl: 'https://example.com/burls', + cpm: 1, + }; + + spec.onBidBillable(bid); + + expect(triggerPixelSpy.calledOnce).to.be.true; + }); + + it('calls bidder billing url when billing URL includes bidder burl', () => { + const bid = { + burl: 'https://example.com/burl?burl=https://bidder.example.com/burl', + cpm: 1, + }; + + spec.onBidBillable(bid); + + expect(triggerPixelSpy.calledTwice).to.be.true; + }); + + it('does not calls billing url when billing URL is not a string', () => { + const bid = { + burl: 123, + }; + + spec.onBidBillable(bid); + + expect(triggerPixelSpy.calledOnce).to.be.false; + }); + }); +}); + +const _bannerBidRequest = { + adUnitCode: 'test-div', + auctionId: 'banner-auction-id', + bidId: 'banner-bid-id', + bidder: 'michao', + bidderRequestId: 'banner-bidder-request-id', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + mediaTypes: { banner: [[300, 250]] }, + params: { + site: 123, + placement: '456', + }, +}; + +const _videoBidRequest = { + adUnitCode: 'test-div', + auctionId: 'video-auction-request-id', + bidId: 'video-bid-id', + bidder: 'michao', + bidderRequestId: 'video-bidder-request-id', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + mimes: ['video/mp4'], + minduration: 0, + maxduration: 120, + protocols: [2] + }, + }, + params: { + site: 123, + placement: '456', + }, +}; + +const _nativeBidRequest = { + adUnitCode: 'test-div', + auctionId: 'native-auction-id', + bidId: 'native-bid-id', + bidder: 'michao', + bidderRequestId: 'native-bidder-request-id', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + mediaTypes: { + native: { + ortb: { + assets: [ + { + id: 1, + title: { + len: 30, + }, + }, + ], + }, + }, + }, + params: { + site: 123, + placement: '456', + }, +}; + +const _videoServerResponse = { + headers: null, + body: { + id: 'video-server-response-id', + seatbid: [ + { + bid: [ + { + id: 'video-bid-id', + impid: 'video-bid-id', + price: 0.18, + adm: '', + adid: '144762342', + adomain: ['https://dummydomain.com'], + iurl: 'iurl', + cid: '109', + crid: 'creativeId', + cat: [], + w: 300, + h: 250, + mtype: 2, + }, + ], + seat: 'seat', + }, + ], + cur: 'USD', + }, +}; + +const _bannerServerResponse = { + headers: null, + body: { + id: 'banner-server-response-id', + seatbid: [ + { + bid: [ + { + id: 'banner-bid-id', + impid: 'banner-bid-id', + price: 0.18, + adm: '
ad
', + adid: '144762342', + adomain: ['https://dummydomain.com'], + iurl: 'iurl', + cid: '109', + crid: 'creativeId', + cat: [], + w: 300, + h: 250, + mtype: 1, + }, + ], + seat: 'seat', + }, + ], + cur: 'USD', + }, +}; diff --git a/test/spec/modules/microadBidAdapter_spec.js b/test/spec/modules/microadBidAdapter_spec.js index 9eb36d2fa6c..ac1738685db 100644 --- a/test/spec/modules/microadBidAdapter_spec.js +++ b/test/spec/modules/microadBidAdapter_spec.js @@ -301,10 +301,6 @@ describe('microadBidAdapter', () => { userId: {novatiq: {snowflake: 'novatiq-sample'}}, expected: {aids: JSON.stringify([{type: 10, id: 'novatiq-sample'}])} }, - 'Parrable ID': { - userId: {parrableId: {eid: 'parrable-sample'}}, - expected: {aids: JSON.stringify([{type: 11, id: 'parrable-sample'}])} - }, 'AudienceOne User ID': { userId: {dacId: {id: 'audience-one-sample'}}, expected: {aids: JSON.stringify([{type: 12, id: 'audience-one-sample'}])} diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js index cf50ad2cd0a..4e5cd4883d3 100644 --- a/test/spec/modules/minutemediaBidAdapter_spec.js +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -2,8 +2,9 @@ import { expect } from 'chai'; import { spec } from 'modules/minutemediaBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; import * as utils from 'src/utils.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native'; const ENDPOINT = 'https://hb.minutemedia-prebid.com/hb-mm-multi'; const TEST_ENDPOINT = 'https://hb.minutemedia-prebid.com/hb-multi-mm-test'; @@ -63,7 +64,6 @@ describe('minutemediaAdapter', function () { 'plcmt': 1 } }, - 'vastXml': '"..."' }, { 'bidder': spec.code, @@ -80,7 +80,59 @@ describe('minutemediaAdapter', function () { 'banner': { } }, - 'ad': '""' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ 300, 250 ] + ] + }, + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream', + 'plcmt': 1 + }, + 'native': { + 'ortb': { + 'assets': [ + { + 'id': 1, + 'required': 1, + 'img': { + 'type': 3, + 'w': 300, + 'h': 200, + } + }, + { + 'id': 2, + 'required': 1, + 'title': { + 'len': 80 + } + }, + { + 'id': 3, + 'required': 1, + 'data': { + 'type': 1 + } + } + ] + } + }, + }, } ]; @@ -151,10 +203,10 @@ describe('minutemediaAdapter', function () { }); it('should send the correct mimes array', function () { - bidRequests[1].mediaTypes.banner.mimes = mimes; + bidRequests[0].mediaTypes.video.mimes = mimes; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bids[1].mimes).to.be.an('array'); - expect(request.data.bids[1].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); + expect(request.data.bids[0].mimes).to.be.an('array'); + expect(request.data.bids[0].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); }); it('should send the correct protocols array', function () { @@ -170,12 +222,21 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) expect(request.data.bids[1].sizes).to.be.an('array'); expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + expect(request.data.bids[2].sizes).to.be.an('array'); + expect(request.data.bids[2].sizes).to.eql(bidRequests[2].sizes) + }); + + it('should send nativeOrtbRequest in native bid request', function () { + decorateAdUnitsWithNativeParams(bidRequests) + const request = spec.buildRequests(bidRequests, bidderRequest); + assert.deepEqual(request.data.bids[2].nativeOrtbRequest, bidRequests[2].mediaTypes.native.ortb) }); it('should send the correct media type', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.bids[0].mediaType).to.equal(VIDEO) expect(request.data.bids[1].mediaType).to.equal(BANNER) + expect(request.data.bids[2].mediaType.split(',')).to.include.members([VIDEO, NATIVE, BANNER]) }); it('should respect syncEnabled option', function() { @@ -440,6 +501,8 @@ describe('minutemediaAdapter', function () { width: 640, height: 480, requestId: '21e12606d47ba7', + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', adomain: ['abc.com'], mediaType: VIDEO }, @@ -449,8 +512,32 @@ describe('minutemediaAdapter', function () { width: 300, height: 250, requestId: '21e12606d47ba7', + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', adomain: ['abc.com'], mediaType: BANNER + }, + { + cpm: 12.5, + width: 300, + height: 200, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + native: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg' + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } }] }; @@ -461,7 +548,7 @@ describe('minutemediaAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -476,10 +563,10 @@ describe('minutemediaAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -490,10 +577,42 @@ describe('minutemediaAdapter', function () { ad: '""' }; + const expectedNativeResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 300, + height: 200, + ttl: TTL, + creativeId: 'creative-id', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + meta: { + mediaType: NATIVE, + advertiserDomains: ['abc.com'] + }, + native: { + ortb: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg', + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } + }, + }; + it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); + expect(result[2]).to.deep.equal(expectedNativeResponse); }); it('video type should have vastXml key', function () { @@ -505,6 +624,11 @@ describe('minutemediaAdapter', function () { const result = spec.interpretResponse({ body: response }); expect(result[1].ad).to.equal(expectedBannerResponse.ad) }); + + it('native type should have native key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[2].native).to.eql(expectedNativeResponse.native) + }); }) describe('getUserSyncs', function() { diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js index ab1fbdcc074..e95903d4791 100644 --- a/test/spec/modules/missenaBidAdapter_spec.js +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -1,10 +1,15 @@ import { expect } from 'chai'; import { spec, storage } from 'modules/missenaBidAdapter.js'; import { BANNER } from '../../../src/mediaTypes.js'; +import { config } from 'src/config.js'; +import * as autoplay from 'libraries/autoplayDetection/autoplay.js'; +import { getWinDimensions } from '../../../src/utils.js'; const REFERRER = 'https://referer'; const REFERRER2 = 'https://referer2'; const COOKIE_DEPRECATION_LABEL = 'test'; +const CONSENT_STRING = 'AAAAAAAAA=='; +const API_KEY = 'PA-XXXXXX'; describe('Missena Adapter', function () { $$PREBID_GLOBAL$$.bidderSettings = { @@ -12,12 +17,15 @@ describe('Missena Adapter', function () { storageAllowed: true, }, }; + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); + sandbox.stub(autoplay, 'isAutoplayEnabled').returns(false); + const viewport = { width: getWinDimensions().innerWidth, height: getWinDimensions().innerHeight }; const bidId = 'abc'; const bid = { bidder: 'missena', bidId: bidId, - sizes: [[1, 1]], mediaTypes: { banner: { sizes: [[1, 1]] } }, ortb2: { device: { @@ -25,10 +33,16 @@ describe('Missena Adapter', function () { }, }, params: { - apiKey: 'PA-34745704', + apiKey: API_KEY, placement: 'sticky', formats: ['sticky-banner'], }, + schain: { + validation: 'strict', + config: { + ver: '1.0', + }, + }, getFloor: (inputParams) => { if (inputParams.mediaType === BANNER) { return { @@ -43,25 +57,35 @@ describe('Missena Adapter', function () { const bidWithoutFloor = { bidder: 'missena', bidId: bidId, - sizes: [[1, 1]], - mediaTypes: { banner: { sizes: [[1, 1]] } }, + mediaTypes: { banner: { sizes: [1, 1] } }, params: { - apiKey: 'PA-34745704', + apiKey: API_KEY, placement: 'sticky', formats: ['sticky-banner'], }, }; - const consentString = 'AAAAAAAAA=='; const bidderRequest = { gdprConsent: { - consentString: consentString, + consentString: CONSENT_STRING, gdprApplies: true, }, + uspConsent: 'IDO', refererInfo: { topmostLocation: REFERRER, canonicalUrl: 'https://canonical', }, + ortb2: { + regs: { coppa: 1, ext: { gdpr: 1 }, us_privacy: 'IDO' }, + user: { + ext: { consent: CONSENT_STRING }, + }, + device: { + w: screen.width, + h: screen.height, + ext: { cdep: COOKIE_DEPRECATION_LABEL }, + }, + }, }; const bids = [bid, bidWithoutFloor]; @@ -100,6 +124,23 @@ describe('Missena Adapter', function () { const payload = JSON.parse(request.data); const payloadNoFloor = JSON.parse(requests[1].data); + it('should send disabled autoplay', function () { + expect(payload.autoplay).to.equal(0); + }); + + it('should contain coppa', function () { + expect(payload.ortb2.regs.coppa).to.equal(1); + }); + sandbox.restore(); + + it('should contain uspConsent', function () { + expect(payload.ortb2.regs.us_privacy).to.equal('IDO'); + }); + + it('should contain schain', function () { + expect(payload.schain.config.ver).to.equal('1.0'); + }); + it('should return as many server requests as bidder requests', function () { expect(requests.length).to.equal(2); }); @@ -113,21 +154,21 @@ describe('Missena Adapter', function () { }); it('should send placement', function () { - expect(payload.placement).to.equal('sticky'); + expect(payload.params.placement).to.equal('sticky'); }); it('should send formats', function () { - expect(payload.formats).to.eql(['sticky-banner']); + expect(payload.params.formats).to.eql(['sticky-banner']); }); - it('should send referer information to the request', function () { - expect(payload.referer).to.equal(REFERRER); - expect(payload.referer_canonical).to.equal('https://canonical'); + it('should send viewport', function () { + expect(payload.viewport.width).to.equal(viewport.width); + expect(payload.viewport.height).to.equal(viewport.height); }); it('should send gdpr consent information to the request', function () { - expect(payload.consent_string).to.equal(consentString); - expect(payload.consent_required).to.equal(true); + expect(payload.ortb2.user.ext.consent).to.equal(CONSENT_STRING); + expect(payload.ortb2.regs.ext.gdpr).to.equal(1); }); it('should send floor data', function () { expect(payload.floor).to.equal(3.5); @@ -142,6 +183,21 @@ describe('Missena Adapter', function () { expect(payload.ik).to.equal(window.msna_ik); }); + it('should send screen', function () { + expect(payload.ortb2.device.w).to.equal(screen.width); + expect(payload.ortb2.device.h).to.equal(screen.height); + }); + + it('should send size', function () { + expect(payload.sizes[0].width).to.equal(1); + expect(payload.sizes[0].height).to.equal(1); + }); + + it('should send single size', function () { + expect(payloadNoFloor.sizes[0].width).to.equal(1); + expect(payloadNoFloor.sizes[0].height).to.equal(1); + }); + getDataFromLocalStorageStub.restore(); getDataFromLocalStorageStub = sinon.stub( storage, @@ -199,7 +255,7 @@ describe('Missena Adapter', function () { }); it('should send cookie deprecation', function () { - expect(payload.cdep).to.equal(COOKIE_DEPRECATION_LABEL); + expect(payload.ortb2.device.ext.cdep).to.equal(COOKIE_DEPRECATION_LABEL); }); }); @@ -269,7 +325,7 @@ describe('Missena Adapter', function () { expect(userSync.length).to.be.equal(1); expect(userSync[0].type).to.be.equal('iframe'); - expect(userSync[0].url).to.be.equal(syncFrameUrl); + expect(userSync[0].url).to.be.equal(`${syncFrameUrl}?t=${API_KEY}`); }); it('should return empty array when iframeEnabled is false', function () { @@ -282,7 +338,7 @@ describe('Missena Adapter', function () { gdprApplies: true, consentString, }); - const expectedUrl = `${syncFrameUrl}?gdpr=1&gdpr_consent=${consentString}`; + const expectedUrl = `${syncFrameUrl}?t=${API_KEY}&gdpr=1&gdpr_consent=${consentString}`; expect(userSync.length).to.be.equal(1); expect(userSync[0].type).to.be.equal('iframe'); expect(userSync[0].url).to.be.equal(expectedUrl); @@ -292,7 +348,7 @@ describe('Missena Adapter', function () { gdprApplies: false, consentString, }); - const expectedUrl = `${syncFrameUrl}?gdpr=0&gdpr_consent=${consentString}`; + const expectedUrl = `${syncFrameUrl}?t=${API_KEY}&gdpr=0&gdpr_consent=${consentString}`; expect(userSync.length).to.be.equal(1); expect(userSync[0].type).to.be.equal('iframe'); expect(userSync[0].url).to.be.equal(expectedUrl); diff --git a/test/spec/modules/mobfoxpbBidAdapter_spec.js b/test/spec/modules/mobfoxpbBidAdapter_spec.js index a4e58afbd1b..c926c2c9bfc 100644 --- a/test/spec/modules/mobfoxpbBidAdapter_spec.js +++ b/test/spec/modules/mobfoxpbBidAdapter_spec.js @@ -1,148 +1,273 @@ -import {expect} from 'chai'; -import {spec} from '../../../modules/mobfoxpbBidAdapter.js'; +import { expect } from 'chai'; +import { spec } from '../../../modules/mobfoxpbBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'mobfoxpb'; describe('MobfoxHBBidAdapter', function () { - const bid = { - bidId: '23fhj33i987f', - bidder: 'mobfoxpb', + 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: { - placementId: 783, - traffic: BANNER + } - }; + } const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'test.com' + referer: 'https://test.com', + page: 'https://test.com' }, - ortb2: {} + 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(bid)).to.be.true; + expect(spec.isBidRequestValid(bids[0])).to.be.true; }); it('Should return false if at least one of parameters is not present', function () { - delete bid.params.placementId; - expect(spec.isBidRequestValid(bid)).to.be.false; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; }); }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid], bidderRequest); + 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 valid URL', function () { expect(serverRequest.url).to.equal('https://bes.mobfox.com/pbjs'); }); - it('Returns valid data if array of bids is valid', function () { + + it('Returns general data valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + 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.gdpr).to.not.exist; - expect(data.ccpa).to.not.exist; - let placement = data['placements'][0]; - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain', 'bidfloor'); - expect(placement.placementId).to.equal(783); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.traffic).to.equal(BANNER); - expect(placement.schain).to.be.an('object'); - expect(placement.sizes).to.be.an('array'); - expect(placement.bidfloor).to.equal(0); + 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 data for mediatype video', function () { - const playerSize = [300, 300]; - bid.mediaTypes = {}; - bid.params.traffic = VIDEO; - bid.mediaTypes[VIDEO] = { - playerSize - }; - serverRequest = spec.buildRequests([bid], bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - let placement = data['placements'][0]; - expect(placement).to.be.an('object'); - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'playerSize', 'wPlayer', 'hPlayer', 'schain', 'bidfloor', - 'minduration', 'maxduration', 'mimes', 'protocols', 'startdelay', 'placement', - 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'); - expect(placement.traffic).to.equal(VIDEO); - expect(placement.wPlayer).to.equal(playerSize[0]); - expect(placement.hPlayer).to.equal(playerSize[1]); + 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 data for mediatype native', function () { - const native = { - title: { - required: true - }, - body: { - required: true - }, - icon: { - required: true, - size: [64, 64] + it('Returns valid endpoints', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + endpointId: 'testBanner', + }, + userIdAsEids } - }; + ]; - bid.mediaTypes = {}; - bid.params.traffic = NATIVE; - bid.mediaTypes[NATIVE] = native; - serverRequest = spec.buildRequests([bid], bidderRequest); - let data = serverRequest.data; - expect(data).to.be.an('object'); - let placement = data['placements'][0]; - expect(placement).to.be.an('object'); - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain', 'bidfloor'); - expect(placement.traffic).to.equal(NATIVE); - expect(placement.native).to.equal(native); + let 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.traffic) { + 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 () { - bidderRequest.gdprConsent = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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 = 'test'; - serverRequest = spec.buildRequests([bid], bidderRequest); + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); let 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; }); - - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); - let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); }); describe('gpp consent', function () { @@ -152,7 +277,7 @@ describe('MobfoxHBBidAdapter', function () { applicableSections: [8] }; - let serverRequest = spec.buildRequests([bid], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data).to.be.an('object'); expect(data).to.have.property('gpp'); @@ -162,15 +287,18 @@ describe('MobfoxHBBidAdapter', function () { }) 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]; - let serverRequest = spec.buildRequests([bid], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data).to.be.an('object'); expect(data).to.have.property('gpp'); expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; }) }); @@ -188,7 +316,11 @@ describe('MobfoxHBBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - dealId: '1' + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let bannerResponses = spec.interpretResponse(banner); @@ -196,15 +328,16 @@ describe('MobfoxHBBidAdapter', function () { let 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('23fhj33i987f'); - expect(dataItem.cpm).to.equal(0.4); - expect(dataItem.width).to.equal(300); - expect(dataItem.height).to.equal(250); - expect(dataItem.ad).to.equal('Test'); - expect(dataItem.ttl).to.equal(120); - expect(dataItem.creativeId).to.equal('2'); + 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('USD'); + 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 = { @@ -217,7 +350,11 @@ describe('MobfoxHBBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', - dealId: '1' + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let videoResponses = spec.interpretResponse(video); @@ -233,6 +370,7 @@ describe('MobfoxHBBidAdapter', function () { 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 = { @@ -250,6 +388,10 @@ describe('MobfoxHBBidAdapter', function () { creativeId: '2', netRevenue: true, currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } }] }; let nativeResponses = spec.interpretResponse(native); @@ -269,6 +411,7 @@ describe('MobfoxHBBidAdapter', function () { 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 = { diff --git a/test/spec/modules/mobianRtdProvider_spec.js b/test/spec/modules/mobianRtdProvider_spec.js new file mode 100644 index 00000000000..0794e99151d --- /dev/null +++ b/test/spec/modules/mobianRtdProvider_spec.js @@ -0,0 +1,281 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as ajax from 'src/ajax.js'; +import * as gptUtils from 'libraries/gptUtils/gptUtils.js'; +import { + CONTEXT_KEYS, + AP_VALUES, + CATEGORIES, + EMOTIONS, + GENRES, + RISK, + SENTIMENT, + THEMES, + TONES, + extendBidRequestConfig, + fetchContextData, + getConfig, + getContextData, + makeContextDataToKeyValuesReducer, + makeDataFromResponse, + setTargeting, +} from 'modules/mobianRtdProvider.js'; + +describe('Mobian RTD Submodule', function () { + let ajaxStub; + let bidReqConfig; + let setKeyValueSpy; + + const mockResponse = JSON.stringify({ + meta: { + url: 'https://example.com', + has_results: true + }, + results: { + ap: { a0: [], a1: [2313, 12], p0: [1231231, 212], p1: [231, 419] }, + mobianContentCategories: [], + mobianEmotions: ['affection'], + mobianGenres: [], + mobianRisk: 'low', + mobianSentiment: 'positive', + mobianThemes: [], + mobianTones: [], + } + }); + + const mockContextData = { + [AP_VALUES]: { a0: [], a1: [2313, 12], p0: [1231231, 212], p1: [231, 419] }, + [CATEGORIES]: [], + [EMOTIONS]: ['affection'], + [GENRES]: [], + [RISK]: 'low', + [SENTIMENT]: 'positive', + [THEMES]: [], + [TONES]: [], + } + + const mockKeyValues = { + 'mobian_ap_a1': ['2313', '12'], + 'mobian_ap_p0': ['1231231', '212'], + 'mobian_ap_p1': ['231', '419'], + 'mobian_emotions': ['affection'], + 'mobian_risk': 'low', + 'mobian_sentiment': 'positive', + } + + const mockConfig = { + prefix: 'mobian', + publisherTargeting: [AP_VALUES, EMOTIONS, RISK, SENTIMENT, THEMES, TONES, GENRES], + advertiserTargeting: [AP_VALUES, EMOTIONS, RISK, SENTIMENT, THEMES, TONES, GENRES], + } + + beforeEach(function () { + bidReqConfig = { + ortb2Fragments: { + global: { + site: { + ext: { + data: {} + } + } + } + } + }; + + setKeyValueSpy = sinon.spy(gptUtils, 'setKeyValue'); + }); + + afterEach(function () { + ajaxStub.restore(); + setKeyValueSpy.restore(); + }); + + describe('fetchContextData', function () { + it('should return fetched context data', async function () { + ajaxStub = sinon.stub(ajax, 'ajaxBuilder').returns(function(url, callbacks) { + callbacks.success(mockResponse); + }); + + const contextData = await fetchContextData(); + expect(contextData).to.deep.equal(mockResponse); + }); + }); + + describe('makeDataFromResponse', function () { + it('should format context data response', async function () { + const data = makeDataFromResponse(mockResponse); + expect(data).to.deep.equal(mockContextData); + }); + }); + + describe('getContextData', function () { + it('should return formatted context data', async function () { + ajaxStub = sinon.stub(ajax, 'ajaxBuilder').returns(function(url, callbacks) { + callbacks.success(mockResponse); + }); + + const data = await getContextData(); + expect(data).to.deep.equal(mockContextData); + }); + }); + + describe('setTargeting', function () { + it('should set targeting key-value pairs as per config', function () { + const parsedConfig = { + prefix: 'mobian', + publisherTargeting: [AP_VALUES, EMOTIONS, RISK, SENTIMENT, THEMES, TONES, GENRES], + }; + setTargeting(parsedConfig, mockContextData); + + expect(setKeyValueSpy.callCount).to.equal(6); + expect(setKeyValueSpy.calledWith('mobian_ap_a1', ['2313', '12'])).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_ap_p0', ['1231231', '212'])).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_ap_p1', ['231', '419'])).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_emotions', ['affection'])).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_risk', 'low')).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_sentiment', 'positive')).to.equal(true); + + expect(setKeyValueSpy.calledWith('mobian_ap_a0')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_themes')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_tones')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_genres')).to.equal(false); + }); + + it('should not set key-value pairs if context data is empty', function () { + const parsedConfig = { + prefix: 'mobian', + publisherTargeting: [AP_VALUES, EMOTIONS, RISK, SENTIMENT, THEMES, TONES, GENRES], + }; + setTargeting(parsedConfig, {}); + + expect(setKeyValueSpy.callCount).to.equal(0); + }); + + it('should only set key-value pairs for the keys specified in config', function () { + const parsedConfig = { + prefix: 'mobian', + publisherTargeting: [EMOTIONS, RISK], + }; + + setTargeting(parsedConfig, mockContextData); + + expect(setKeyValueSpy.callCount).to.equal(2); + expect(setKeyValueSpy.calledWith('mobian_emotions', ['affection'])).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_risk', 'low')).to.equal(true); + + expect(setKeyValueSpy.calledWith('mobian_ap_a0')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_ap_a1')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_ap_p0')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_ap_p1')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_themes')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_tones')).to.equal(false); + expect(setKeyValueSpy.calledWith('mobian_genres')).to.equal(false); + }); + }); + + describe('extendBidRequestConfig', function () { + it('should extend bid request config with context data', function () { + const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData, mockConfig); + expect(extendedConfig.ortb2Fragments.global.site.ext.data).to.deep.equal(mockKeyValues); + }); + + it('should not override existing data', function () { + bidReqConfig.ortb2Fragments.global.site.ext.data = { + existing: 'data' + }; + + const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData, mockConfig); + expect(extendedConfig.ortb2Fragments.global.site.ext.data).to.deep.equal({ + existing: 'data', + ...mockKeyValues + }); + }); + + it('should create data object if missing', function () { + delete bidReqConfig.ortb2Fragments.global.site.ext.data; + const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData, mockConfig); + expect(extendedConfig.ortb2Fragments.global.site.ext.data).to.deep.equal(mockKeyValues); + }); + }); + + describe('getConfig', function () { + it('should return config with correct keys', function () { + const config = getConfig({ + name: 'mobianBrandSafety', + params: { + prefix: 'mobiantest', + publisherTargeting: [AP_VALUES], + advertiserTargeting: [EMOTIONS], + } + }); + expect(config).to.deep.equal({ + prefix: 'mobiantest', + publisherTargeting: [AP_VALUES], + advertiserTargeting: [EMOTIONS], + }); + }); + + it('should set default values for configs not set', function () { + const config = getConfig({ + name: 'mobianBrandSafety', + params: { + publisherTargeting: [AP_VALUES], + } + }); + expect(config).to.deep.equal({ + prefix: 'mobian', + publisherTargeting: [AP_VALUES], + advertiserTargeting: [], + }); + }); + + it('should set default values if not provided', function () { + const config = getConfig({}); + expect(config).to.deep.equal({ + prefix: 'mobian', + publisherTargeting: [], + advertiserTargeting: [], + }); + }); + + it('should set default values if no config is provided', function () { + const config = getConfig(); + expect(config).to.deep.equal({ + prefix: 'mobian', + publisherTargeting: [], + advertiserTargeting: [], + }); + }); + + it('should set all tarteging values if value is true', function () { + const config = getConfig({ + name: 'mobianBrandSafety', + params: { + publisherTargeting: true, + advertiserTargeting: true, + } + }); + expect(config).to.deep.equal({ + prefix: 'mobian', + publisherTargeting: CONTEXT_KEYS, + advertiserTargeting: CONTEXT_KEYS, + }); + }); + }); + + describe('makeContextDataToKeyValuesReducer', function () { + it('should format context data to key-value pairs', function () { + const config = getConfig({ + name: 'mobianBrandSafety', + params: { + prefix: 'mobian', + publisherTargeting: true, + advertiserTargeting: true, + } + }); + const keyValues = Object.entries(mockContextData).reduce(makeContextDataToKeyValuesReducer(config), []); + const keyValuesObject = Object.fromEntries(keyValues); + expect(keyValuesObject).to.deep.equal(mockKeyValues); + }); + }); +}); diff --git a/test/spec/modules/mobkoiAnalyticsAdapter_spec.js b/test/spec/modules/mobkoiAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9122b5e49f4 --- /dev/null +++ b/test/spec/modules/mobkoiAnalyticsAdapter_spec.js @@ -0,0 +1,508 @@ +import mobkoiAnalyticsAdapter, { DEBUG_EVENT_LEVELS, utils, SUB_PAYLOAD_UNIQUE_FIELDS_LOOKUP, SUB_PAYLOAD_TYPES } from 'modules/mobkoiAnalyticsAdapter.js'; +import {internal} from '../../../src/utils.js'; +import adapterManager from '../../../src/adapterManager.js'; +import * as events from 'src/events.js'; +import { EVENTS } from 'src/constants.js'; +import sinon from 'sinon'; + +const defaultTimeout = 10000; +const requestId = 'test-request-id' +const publisherId = 'mobkoiPublisherId' +const bidId = 'test-bid-id' +const bidderCode = 'mobkoi' +const transactionId = 'test-transaction-id' +const impressionId = 'test-impression-id' +const adUnitId = 'test-ad-unit-id' +const auctionId = 'test-auction-id' +const adServerBaseUrl = 'http://adServerBaseUrl'; + +const adm = '
test ad
'; +const lurl = 'test.com/loss'; +const nurl = 'test.com/win'; + +const performStandardAuction = (auctionEvents) => { + auctionEvents.forEach(auctionEvent => { + events.emit(auctionEvent.event, auctionEvent.data); + }); +} + +const getOrtb2 = () => ({ + site: { + publisher: { + id: publisherId, + ext: { adServerBaseUrl } + } + } +}) + +const getBidderResponse = () => ({ + body: { + id: bidId, + cur: 'USD', + seatbid: [ + { + seat: 'mobkoi_debug', + bid: [ + { + id: bidId, + impid: impressionId, + cid: 'campaign_1', + crid: 'creative_1', + price: 1, + cur: [ + 'USD' + ], + adomain: [ + 'advertiser.com' + ], + adm, + w: 300, + h: 250, + mtype: 1, + lurl, + nurl + } + ] + } + ], + } +}) + +const getMockEvents = () => { + const sizes = [800, 300]; + const timestamp = Date.now(); + const auctionOrBidError = {timestamp, error: 'error', bidderRequest: { bidderRequestId: requestId }} + + return { + AUCTION_TIMEOUT: auctionOrBidError, + AUCTION_INIT: { + timestamp, + auctionId, + auctionStatus: 'inProgress', + adUnits: [{ + adUnitId: adUnitId, + code: 'banner-ad', + mediaTypes: { banner: { sizes: [sizes] } }, + transactionId, + }], + bidderRequests: [{ + bidderRequestId: requestId, + bids: [getBidRequest()], + ortb2: getOrtb2() + }] + }, + BID_RESPONSE: { + auctionId, + timestamp, + requestId: bidId, + bidId, + ortbId: requestId, + cpm: 1.5, + currency: 'USD', + ortbBidResponse: { + id: requestId, + impid: bidId, + price: 1.5 + } + }, + NO_BID: auctionOrBidError, + BIDDER_DONE: { + timestamp, + auctionId, + bidderRequestId: requestId, + bids: [getBidRequest()], + ortb2: getOrtb2() + }, + BID_WON: { + timestamp, + auctionId, + requestId, + bidId, + ortbBidResponse: { + id: requestId, + impid: bidId + } + }, + AUCTION_END: { + timestamp, + auctionId, + auctionStatus: 'completed' + }, + AD_RENDER_SUCCEEDED: { + bid: { + timestamp, + requestId: bidId, + bidId, + ortbId: requestId, + ad: '
test ad
' + }, + doc: { visibilityState: 'visible' } + }, + AD_RENDER_FAILED: { + bid: { + timestamp, + requestId: bidId, + bidId, + ortbId: requestId, + ad: '
test ad
' + }, + reason: 'error', + message: 'error' + }, + BIDDER_ERROR: auctionOrBidError, + BID_REJECTED: { + timestamp, + error: 'error', + bidderRequestId: requestId + } + } +} + +const getBidRequest = () => ({ + bidder: bidderCode, + adUnitCode: 'banner-ad', + transactionId, + adUnitId, + bidId: bidId, + bidderRequestId: requestId, + auctionId, + ortb2: getOrtb2() +}) + +const getBidderRequest = () => ({ + bidderCode, + auctionId, + bidderRequestId: requestId, + bids: [getBidRequest()], + ortb2: getOrtb2() +}) + +describe('mobkoiAnalyticsAdapter', function () { + it('should registers with the adapter manager', function () { + // should refer to the BIDDER_CODE in the mobkoiAnalyticsAdapter + const adapter = adapterManager.getAnalyticsAdapter('mobkoi'); + expect(adapter).to.exist; + // should refer to the GVL_ID in the mobkoiAnalyticsAdapter + expect(adapter.gvlid).to.equal(898); + expect(adapter.adapter).to.equal(mobkoiAnalyticsAdapter); + }); + + describe('Tracks events', function () { + let adapter; + let sandbox; + let pushEventSpy; + let flushEventsSpy; + let triggerBeaconSpy; + let postAjaxStub; + let sendGetRequestStub; + + beforeEach(function () { + adapter = mobkoiAnalyticsAdapter; + sandbox = sinon.createSandbox({ + useFakeTimers: { + now: new Date(2025, 0, 8, 0, 1, 33, 425), + }, + }); + + // Disable then reenable the adapter in order to have a fresh context + adapter.disableAnalytics(); + adapter.enableAnalytics({ + options: { + endpoint: adServerBaseUrl, + pid: 'test-pid', + timeout: defaultTimeout, + } + }); + + sandbox.stub(internal, 'logInfo'); + sandbox.stub(internal, 'logWarn'); + sandbox.stub(internal, 'logError'); + + // Create spies after enabling analytics to ensure localContext exists + postAjaxStub = sandbox.stub(utils, 'postAjax'); + sendGetRequestStub = sandbox.stub(utils, 'sendGetRequest'); + pushEventSpy = sandbox.spy(adapter.localContext, 'pushEventToAllBidContexts'); + flushEventsSpy = sandbox.spy(adapter.localContext, 'flushAllDebugEvents'); + triggerBeaconSpy = sandbox.spy(adapter.localContext, 'triggerAllLossBidLossBeacon'); + }); + + afterEach(function () { + adapter.disableAnalytics(); + sandbox.restore(); + postAjaxStub.reset(); + sendGetRequestStub.reset(); + }); + + it('should call sendGetRequest while tracking BIDDER_DONE / BID_WON events', function () { + const { AUCTION_INIT, BID_RESPONSE, BID_WON } = getMockEvents(); + const bidResponse = { + ...BID_RESPONSE, + ortbBidResponse: { + ...BID_RESPONSE.ortbBidResponse, + lurl, + bidWin: false, + lurlTriggered: false + } + }; + const eventSequence = [ + { event: EVENTS.AUCTION_INIT, data: AUCTION_INIT }, + { event: EVENTS.BID_RESPONSE, data: bidResponse }, + { event: EVENTS.BID_WON, data: BID_WON }, + ] + + performStandardAuction(eventSequence); + + expect(sendGetRequestStub.callCount).to.equal(1); + expect(sendGetRequestStub.firstCall.args[0]).to.equal(lurl); + }) + + it('should call postAjax while tracking BIDDER_DONE event', function () { + const { AUCTION_INIT, BID_RESPONSE, BIDDER_DONE } = getMockEvents(); + + const eventSequence = [ + { event: EVENTS.AUCTION_INIT, data: AUCTION_INIT }, + { event: EVENTS.BID_RESPONSE, data: BID_RESPONSE }, + { event: EVENTS.BIDDER_DONE, data: BIDDER_DONE } + ]; + + performStandardAuction(eventSequence); + + expect(postAjaxStub.calledOnce).to.be.true; + expect(postAjaxStub.firstCall.args[0]).to.equal(`${adServerBaseUrl}/debug`); + }) + + it('should track complete auction workflow in correct sequence and trigger a loss beacon', function () { + const { AUCTION_INIT, BID_RESPONSE, AUCTION_END, AD_RENDER_SUCCEEDED, BIDDER_DONE } = getMockEvents(); + + const eventSequence = [ + { event: EVENTS.AUCTION_INIT, data: AUCTION_INIT }, + { event: EVENTS.BID_RESPONSE, data: BID_RESPONSE }, + { event: EVENTS.AD_RENDER_SUCCEEDED, data: AD_RENDER_SUCCEEDED }, + { event: EVENTS.AUCTION_END, data: AUCTION_END }, + { event: EVENTS.BIDDER_DONE, data: BIDDER_DONE } + ]; + + performStandardAuction(eventSequence); + expect(pushEventSpy.callCount).to.equal(3); // AUCTION_INIT, AUCTION_END, BIDDER_DONE + expect(flushEventsSpy.callCount).to.equal(1); + expect(triggerBeaconSpy.callCount).to.equal(1); // BIDDER_DONE + }); + + it('should track errors events', function () { + const { AUCTION_TIMEOUT, NO_BID, BID_REJECTED, BIDDER_ERROR, AD_RENDER_FAILED } = getMockEvents(); + + const eventSequence = [ + { event: EVENTS.AUCTION_TIMEOUT, data: AUCTION_TIMEOUT }, + { event: EVENTS.NO_BID, data: NO_BID }, + { event: EVENTS.BID_REJECTED, data: BID_REJECTED }, + { event: EVENTS.BIDDER_ERROR, data: BIDDER_ERROR }, + { event: EVENTS.AD_RENDER_FAILED, data: AD_RENDER_FAILED } + ]; + + performStandardAuction(eventSequence); + + expect(pushEventSpy.callCount).to.equal(3); // AUCTION_TIMEOUT, NO_BID, BIDDER_ERROR + }); + + it('should push unexpected error events to the localContext', async function () { + const { AUCTION_INIT } = getMockEvents(); + delete AUCTION_INIT.auctionStatus; + try { + await adapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: AUCTION_INIT + }); + } catch { + expect(pushEventSpy.calledOnce).to.be.true; + const errorEventCall = pushEventSpy.getCall(0); + + expect(errorEventCall.args[0]).to.deep.include({ + eventType: EVENTS.AUCTION_INIT, + level: DEBUG_EVENT_LEVELS.error, + note: 'Error occurred when processing this event.' + }); + + const errorPayload = errorEventCall.args[0].subPayloads[`errorInEvent_${EVENTS.AUCTION_INIT}`]; + expect(errorPayload).to.exist; + expect(errorPayload.error).to.include('Unable to determine track args type'); + } + }); + }) + + describe('utils', function () { + let bidderRequest; + + beforeEach(function () { + bidderRequest = getBidderRequest(); + }); + + describe('isMobkoiBid', function () { + it('should return true when the bid is from mobkoi', function () { + expect(utils.isMobkoiBid(bidderRequest)).to.be.true; + }); + it('should return false when the bid is not from mobkoi', function () { + bidderRequest.bidderCode = 'anything'; + expect(utils.isMobkoiBid(bidderRequest)).to.be.false; + }); + }); + + describe('getOrtbId', function () { + it('should return the ortbId from the prebid request object (i.e bidderRequestId)', function () { + expect(utils.getOrtbId(bidderRequest)).to.equal(bidderRequest.bidderRequestId); + }); + + it('should return the ortbId from the prebid response object (i.e seatBidId)', function () { + const customBidRequest = { ...bidderRequest, seatBidId: bidderRequest.bidderRequestId }; + delete customBidRequest.bidderRequestId; + expect(utils.getOrtbId(customBidRequest)).to.equal(bidderRequest.bidderRequestId); + }); + + it('should return the ortbId from the interpreted prebid response object (i.e ortbId)', function () { + const customBidRequest = { ...bidderRequest, ortbId: bidderRequest.bidderRequestId }; + delete customBidRequest.bidderRequestId; + expect(utils.getOrtbId(customBidRequest)).to.equal(bidderRequest.bidderRequestId); + }); + + it('should return the ortbId from the ORTB request object (i.e has imp)', function () { + const customBidRequest = { ...bidderRequest, imp: {}, id: bidderRequest.bidderRequestId }; + delete customBidRequest.bidderRequestId; + expect(utils.getOrtbId(customBidRequest)).to.equal(bidderRequest.bidderRequestId); + }); + + it('should throw error when ortbId is missing', function () { + delete bidderRequest.bidderRequestId; + expect(() => { + utils.getOrtbId(bidderRequest); + }).to.throw(); + }); + }) + + describe('getImpId', function () { + let bidResponse; + beforeEach(function () { + const bidderResponse = getBidderResponse(); + bidResponse = bidderResponse.body.seatbid[0].bid[0]; + }); + + it('should return the impId from the impid field', function () { + expect(utils.getImpId(bidResponse)).to.equal(bidResponse.impid); + }); + + it('should return the impId from the requestId field', function () { + const customBidResponse = { ...bidResponse, requestId: bidResponse.impid }; + delete customBidResponse.impid; + expect(utils.getImpId(customBidResponse)).to.equal(bidResponse.impid); + }); + + it('should return the impId from the bidId field', function () { + const customBidResponse = { ...bidResponse, bidId: bidResponse.impid }; + delete customBidResponse.impid; + expect(utils.getImpId(customBidResponse)).to.equal(bidResponse.impid); + }); + + it('should return null if impId is missing', function () { + expect(utils.getImpId({})).to.be.null; + }); + }) + + describe('getPublisherId', function () { + it('should return the publisherId from the given object', function () { + expect(utils.getPublisherId(bidderRequest)).to.equal(bidderRequest.ortb2.site.publisher.id); + }); + + it('should throw error when publisherId is missing', function () { + delete bidderRequest.ortb2.site.publisher.id; + expect(() => { + utils.getPublisherId(bidderRequest); + }).to.throw(); + }); + }) + + describe('getAdServerEndpointBaseUrl', function () { + it('should return the adServerBaseUrl from the given object', function () { + expect(utils.getAdServerEndpointBaseUrl(bidderRequest)) + .to.equal(adServerBaseUrl); + }); + + it('should throw error when adServerBaseUrl is missing', function () { + delete bidderRequest.ortb2.site.publisher.ext.adServerBaseUrl; + + expect(() => { + utils.getAdServerEndpointBaseUrl(bidderRequest); + }).to.throw(); + }); + }) + + describe('determineObjType', function () { + [null, undefined, 123, 'string', true].forEach(value => { + it(`should throw an error when input is ${value}`, function() { + expect(() => { + utils.determineObjType(value); + }).to.throw(); + }); + }); + + it('should throw an error if the object type could not be determined', function () { + expect(() => { + utils.determineObjType({dumbAttribute: 'bid'}) + }).to.throw(); + }); + + Object.values(SUB_PAYLOAD_TYPES).forEach(type => { + it(`should return the ${type} type`, function () { + const eventArgs = {} + const uniqueFields = SUB_PAYLOAD_UNIQUE_FIELDS_LOOKUP[type] + uniqueFields.forEach(field => { + eventArgs[field] = 'random-value' + }) + expect(utils.determineObjType(eventArgs)).to.equal(type); + }) + }) + }) + + describe('mergePayloadAndCustomFields', function () { + it('should throw an error when the target is not an object', function () { + expect(() => { + utils.mergePayloadAndCustomFields(123, {}) + }).to.throw(); + }) + + it('should throw an error when the new values are not an object', function () { + expect(() => { + utils.mergePayloadAndCustomFields({}, 123) + }).to.throw(); + }) + + it('should throw an error if custom fields are provided and one of them is not a string', () => { + const customFields = {impid: 'bid-123', bidId: 123} + expect(() => { + utils.mergePayloadAndCustomFields({}, customFields) + }).to.throw(); + }) + }) + + describe('validateSubPayloads', function () { + it('should throw an error if the sub payloads required fields are not the correct type', function () { + expect(() => { + utils.validateSubPayloads({ + [SUB_PAYLOAD_TYPES.ORTB_BID]: { + impid: 123, + publisherId: 456 + } + }) + }).to.throw(); + }); + + it('should not throw when sub payloads have valid required fields', function () { + expect(() => { + utils.validateSubPayloads({ + [SUB_PAYLOAD_TYPES.ORTB_BID]: { + impid: '123', + publisherId: 'publisher-123' + } + }) + }).to.not.throw(); + }); + }); + }) +}); diff --git a/test/spec/modules/mobkoiBidAdapter_spec.js b/test/spec/modules/mobkoiBidAdapter_spec.js new file mode 100644 index 00000000000..3b1f9552c7c --- /dev/null +++ b/test/spec/modules/mobkoiBidAdapter_spec.js @@ -0,0 +1,226 @@ +import { + spec, + utils, + DEFAULT_AD_SERVER_BASE_URL +} from 'modules/mobkoiBidAdapter.js'; + +describe('Mobkoi bidding Adapter', function () { + const testAdServerBaseUrl = 'http://test.adServerBaseUrl.com'; + const testRequestId = 'test-request-id'; + const testPlacementId = 'mobkoiPlacementId'; + const testBidId = 'test-bid-id'; + const bidderCode = 'mobkoi'; + const testTransactionId = 'test-transaction-id'; + const testAdUnitId = 'test-ad-unit-id'; + const testAuctionId = 'test-auction-id'; + + const getOrtb2 = () => ({ + site: { + publisher: { + ext: { adServerBaseUrl: testAdServerBaseUrl } + } + } + }) + + const getBidRequest = () => ({ + bidder: bidderCode, + adUnitCode: 'banner-ad', + transactionId: testTransactionId, + adUnitId: testAdUnitId, + bidId: testBidId, + bidderRequestId: testRequestId, + auctionId: testAuctionId, + ortb2: getOrtb2(), + params: { + adServerBaseUrl: testAdServerBaseUrl, + placementId: testPlacementId + } + }) + + const getBidderRequest = () => ({ + bidderCode, + auctionId: testAuctionId, + bidderRequestId: testRequestId, + bids: [getBidRequest()], + ortb2: getOrtb2() + }) + + const getConvertedBidRequest = () => ({ + id: testRequestId, + imp: [{ + id: testBidId, + tagid: testPlacementId, + }], + ...getOrtb2(), + test: 0 + }) + + const adm = '
test ad
'; + const lurl = 'test.com/loss'; + const nurl = 'test.com/win'; + + const getBidderResponse = () => ({ + body: { + id: testBidId, + cur: 'USD', + seatbid: [ + { + seat: 'mobkoi_debug', + bid: [ + { + id: testBidId, + impid: testBidId, + cid: 'campaign_1', + crid: 'creative_1', + price: 1, + cur: [ + 'USD' + ], + adomain: [ + 'advertiser.com' + ], + adm, + w: 300, + h: 250, + mtype: 1, + lurl, + nurl + } + ] + } + ], + } + }) + + describe('isBidRequestValid', function () { + let bid; + + beforeEach(function () { + bid = getBidderRequest().bids[0]; + }); + + it('should return true when placement id exist in ad unit params', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when placement id is missing in ad unit params', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidderRequest, convertedBidRequest; + + beforeEach(function () { + bidderRequest = getBidderRequest(); + convertedBidRequest = getConvertedBidRequest(); + }); + + it('should include converted ORTB data in request', function () { + const request = spec.buildRequests(bidderRequest.bids, bidderRequest); + const ortbData = request.data; + + expect(ortbData.id).to.equal(bidderRequest.bidderRequestId); + }); + + it('should obtain adServerBaseUrl from ad unit params if the value does not exist in ortb2', function () { + delete bidderRequest.ortb2.site.publisher.ext.adServerBaseUrl; + const request = spec.buildRequests(bidderRequest.bids, bidderRequest); + const ortbData = request.data; + + expect(ortbData.site.publisher.ext.adServerBaseUrl).to.equal(bidderRequest.bids[0].params.adServerBaseUrl); + }); + + it('should use the pro server url when the ad server base url is not set', function () { + delete bidderRequest.ortb2.site.publisher.ext.adServerBaseUrl; + delete bidderRequest.bids[0].params.adServerBaseUrl; + + const request = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.url).to.equal(DEFAULT_AD_SERVER_BASE_URL + '/bid'); + }); + }); + + describe('interpretResponse', function () { + let bidderRequest, bidRequest, bidderResponse; + + beforeEach(function () { + bidderRequest = getBidderRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('should return empty array when response is empty', function () { + expect(spec.interpretResponse({}, {})).to.deep.equal([]); + }); + + it('should interpret valid bid response', function () { + const bidsResponse = spec.interpretResponse(bidderResponse, bidRequest); + expect(bidsResponse).to.not.be.empty; + const bid = bidsResponse[0]; + + expect(bid.ad).to.include(adm); + expect(bid.requestId).to.equal(bidderResponse.body.seatbid[0].bid[0].impid); + expect(bid.cpm).to.equal(bidderResponse.body.seatbid[0].bid[0].price); + expect(bid.width).to.equal(bidderResponse.body.seatbid[0].bid[0].w); + expect(bid.height).to.equal(bidderResponse.body.seatbid[0].bid[0].h); + expect(bid.creativeId).to.equal(bidderResponse.body.seatbid[0].bid[0].crid); + expect(bid.currency).to.equal(bidderResponse.body.cur); + expect(bid.netRevenue).to.be.true; + expect(bid.ttl).to.equal(30); + }); + }) + + describe('utils', function () { + let bidderRequest; + + beforeEach(function () { + bidderRequest = getBidderRequest(); + }); + + describe('getAdServerEndpointBaseUrl', function () { + it('should return the adServerBaseUrl from the given object', function () { + expect(utils.getAdServerEndpointBaseUrl(bidderRequest)) + .to.equal(testAdServerBaseUrl); + }); + + it('should return default prod ad server url when adServerBaseUrl is missing in params and ortb2', function () { + delete bidderRequest.ortb2.site.publisher.ext.adServerBaseUrl; + delete bidderRequest.bids[0].params.adServerBaseUrl; + + expect(utils.getAdServerEndpointBaseUrl(bidderRequest)).to.equal(DEFAULT_AD_SERVER_BASE_URL); + }); + }) + + describe('getOrtbId', function () { + it('should return the ortbId from the prebid request object (i.e bidderRequestId)', function () { + expect(utils.getOrtbId(bidderRequest)).to.equal(bidderRequest.bidderRequestId); + }); + + it('should return the ortbId from the prebid response object (i.e seatBidId)', function () { + const customBidRequest = { ...bidderRequest, seatBidId: bidderRequest.bidderRequestId }; + delete customBidRequest.bidderRequestId; + expect(utils.getOrtbId(customBidRequest)).to.equal(bidderRequest.bidderRequestId); + }); + + it('should return the ortbId from the interpreted prebid response object (i.e ortbId)', function () { + const customBidRequest = { ...bidderRequest, ortbId: bidderRequest.bidderRequestId }; + delete customBidRequest.bidderRequestId; + expect(utils.getOrtbId(customBidRequest)).to.equal(bidderRequest.bidderRequestId); + }); + + it('should return the ortbId from the ORTB request object (i.e has imp)', function () { + const customBidRequest = { ...bidderRequest, imp: {}, id: bidderRequest.bidderRequestId }; + delete customBidRequest.bidderRequestId; + expect(utils.getOrtbId(customBidRequest)).to.equal(bidderRequest.bidderRequestId); + }); + + it('should throw error when ortbId is missing', function () { + delete bidderRequest.bidderRequestId; + expect(() => { + utils.getOrtbId(bidderRequest); + }).to.throw(); + }); + }) + }) +}) diff --git a/test/spec/modules/mobkoiIdSystem_spec.js b/test/spec/modules/mobkoiIdSystem_spec.js new file mode 100644 index 00000000000..4ca5acec686 --- /dev/null +++ b/test/spec/modules/mobkoiIdSystem_spec.js @@ -0,0 +1,224 @@ +import sinon from 'sinon'; +import { + mobkoiIdSubmodule, + storage, + PROD_AD_SERVER_BASE_URL, + EQUATIV_NETWORK_ID, + utils as mobkoiUtils +} from 'modules/mobkoiIdSystem'; +import * as prebidUtils from 'src/utils'; + +const TEST_SAS_ID = 'test-sas-id'; +const TEST_AD_SERVER_BASE_URL = 'https://mocha.test.adserver.com'; +const TEST_CONSENT_STRING = 'test-consent-string'; + +function decodeFullUrl(url) { + return decodeURIComponent(url); +} + +describe('mobkoiIdSystem', function () { + let sandbox, + getCookieStub, + setCookieStub, + cookiesAreEnabledStub, + insertUserSyncIframeStub; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + sandbox.stub(prebidUtils, 'logInfo'); + sandbox.stub(prebidUtils, 'logError'); + + insertUserSyncIframeStub = sandbox.stub(prebidUtils, 'insertUserSyncIframe'); + getCookieStub = sandbox.stub(storage, 'getCookie'); + setCookieStub = sandbox.stub(storage, 'setCookie'); + cookiesAreEnabledStub = sandbox.stub(storage, 'cookiesAreEnabled'); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('decode', function () { + it('should return undefined if value is empty', function () { + expect(mobkoiIdSubmodule.decode()).to.be.undefined; + }); + + it('should return an object with the module name as key if value is provided', function () { + const value = 'test-value'; + expect(mobkoiIdSubmodule.decode(value)).to.deep.equal({ mobkoiId: value }); + }); + }); + + describe('getId', function () { + const userSyncOptions = { + storage: { + type: 'cookie', + name: '_mobkoi_Id', + expires: 30, // days + } + }; + + it('should return null id if cookies are not enabled', function () { + cookiesAreEnabledStub.returns(false); + const result = mobkoiIdSubmodule.getId(userSyncOptions); + expect(result).to.deep.equal({ id: null }); + }); + + it('should return existing id from cookie if available in cookie', function () { + const testId = 'existing-id'; + cookiesAreEnabledStub.returns(true); + getCookieStub.returns(testId); + const result = mobkoiIdSubmodule.getId(userSyncOptions); + + expect(result).to.deep.equal({ id: testId }); + }); + + it('should return a callback function if id is not available in cookie', function () { + cookiesAreEnabledStub.returns(true); + getCookieStub.returns(null); + const result = mobkoiIdSubmodule.getId(userSyncOptions); + + expect(result).to.have.property('callback').that.is.a('function'); + }); + + it('should the callback function should return a SAS ID', function () { + cookiesAreEnabledStub.returns(true); + getCookieStub.returns(null); + + const requestEquativSasIdStub = sandbox.stub(mobkoiUtils, 'requestEquativSasId') + .callsFake((_syncUserOptions, _gdprConsent, onCompleteCallback) => { + onCompleteCallback(TEST_SAS_ID); + return TEST_SAS_ID; + }); + + const callback = mobkoiIdSubmodule.getId(userSyncOptions).callback; + return callback().then(result => { + expect(setCookieStub.calledOnce).to.be.true; + expect(result).to.deep.equal({ id: TEST_SAS_ID }); + expect(requestEquativSasIdStub.calledOnce).to.be.true; + }); + }); + }); + + describe('utils.requestEquativSasId', function () { + let buildEquativPixelUrlStub; + + beforeEach(function () { + buildEquativPixelUrlStub = sandbox.stub(mobkoiUtils, 'buildEquativPixelUrl'); + }); + + it('should call insertUserSyncIframe with the correctly encoded URL', function () { + const syncUserOptions = {}; + const gdprConsent = {}; + const onCompleteCallback = sinon.spy(); + const testPixelUrl = 'https://equativ.test.pixel.url?uid=[sas_uid]'; + buildEquativPixelUrlStub.returns(testPixelUrl); + + mobkoiUtils.requestEquativSasId(syncUserOptions, gdprConsent, onCompleteCallback); + + const expectedEncodedUrl = encodeURIComponent(testPixelUrl); + expect(insertUserSyncIframeStub.calledOnce).to.be.true; + expect(insertUserSyncIframeStub.firstCall.args[0]).to.include('pixelUrl=' + expectedEncodedUrl); + }); + }); + + describe('utils.buildEquativPixelUrl', function () { + it('should use the provided adServerBaseUrl URL from syncUserOptions', function () { + const gdprConsent = { + gdprApplies: true, + consentString: TEST_CONSENT_STRING + }; + const syncUserOptions = { + params: { + adServerBaseUrl: TEST_AD_SERVER_BASE_URL + } + }; + + const url = mobkoiUtils.buildEquativPixelUrl(syncUserOptions, gdprConsent); + const decodedUrl = decodeFullUrl(url); + + expect(decodedUrl).to.include(TEST_AD_SERVER_BASE_URL); + }); + + it('should use the PROD ad server endpoint if adServerBaseUrl is not provided', function () { + const syncUserOptions = {}; + const gdprConsent = { + gdprApplies: true, + consentString: TEST_CONSENT_STRING + }; + + const url = mobkoiUtils.buildEquativPixelUrl(syncUserOptions, gdprConsent); + const decodedUrl = decodeFullUrl(url); + + expect(decodedUrl).to.include(PROD_AD_SERVER_BASE_URL); + }); + + it('should contains the Equativ network ID', function () { + const syncUserOptions = {}; + const gdprConsent = { + gdprApplies: true, + consentString: TEST_CONSENT_STRING + }; + + const url = mobkoiUtils.buildEquativPixelUrl(syncUserOptions, gdprConsent); + + expect(url).to.include(`nwid=${EQUATIV_NETWORK_ID}`); + }); + + it('should contain a consent string', function () { + const syncUserOptions = { + params: { + adServerBaseUrl: TEST_AD_SERVER_BASE_URL + } + }; + const gdprConsent = { + gdprApplies: true, + consentString: TEST_CONSENT_STRING + }; + + const url = mobkoiUtils.buildEquativPixelUrl(syncUserOptions, gdprConsent); + const decodedUrl = decodeFullUrl(url); + + expect(decodedUrl).to.include( + `gdpr_consent=${TEST_CONSENT_STRING}` + ); + }); + + it('should set empty string to gdpr_consent when GDPR is not applies', function () { + const syncUserOptions = { + params: { + adServerBaseUrl: TEST_AD_SERVER_BASE_URL + } + }; + const gdprConsent = { + gdprApplies: true, + consentString: TEST_CONSENT_STRING + }; + + const url = mobkoiUtils.buildEquativPixelUrl(syncUserOptions, gdprConsent); + + expect(url).to.include( + 'gdpr_consent=' // no value + ); + }); + + it('should contain SAS ID marco', function () { + const syncUserOptions = { + params: { + adServerBaseUrl: TEST_AD_SERVER_BASE_URL + } + }; + const gdprConsent = { + gdprApplies: true, + consentString: TEST_CONSENT_STRING + }; + + const url = mobkoiUtils.buildEquativPixelUrl(syncUserOptions, gdprConsent); + const decodedUrl = decodeFullUrl(url); + + expect(decodedUrl).to.include( + 'value=[sas_uid]' + ); + }); + }); +}); diff --git a/test/spec/modules/mygaruIdSystem_spec.js b/test/spec/modules/mygaruIdSystem_spec.js index 2bfb5fdd4af..1d8035277d8 100644 --- a/test/spec/modules/mygaruIdSystem_spec.js +++ b/test/spec/modules/mygaruIdSystem_spec.js @@ -53,8 +53,10 @@ describe('MygaruID module', function () { }) it('should buildUrl with consent data', () => { const result = mygaruIdSubmodule.getId({}, { - gdprApplies: true, - consentString: 'consentString' + gdpr: { + gdprApplies: true, + consentString: 'consentString' + } }); expect(result.url).to.eq('https://ident.mygaru.com/v2/id?gdprApplies=1&gdprConsentString=consentString'); diff --git a/test/spec/modules/mytargetBidAdapter_spec.js b/test/spec/modules/mytargetBidAdapter_spec.js deleted file mode 100644 index 8880efd3d7c..00000000000 --- a/test/spec/modules/mytargetBidAdapter_spec.js +++ /dev/null @@ -1,199 +0,0 @@ -import { expect } from 'chai'; -import { spec } from 'modules/mytargetBidAdapter'; - -describe('MyTarget Adapter', function() { - describe('isBidRequestValid', function () { - it('should return true when required params found', function () { - let validBid = { - bidder: 'mytarget', - params: { - placementId: '1' - } - }; - - expect(spec.isBidRequestValid(validBid)).to.equal(true); - }); - - it('should return false for when required params are not passed', function () { - let invalidBid = { - bidder: 'mytarget', - params: {} - }; - - expect(spec.isBidRequestValid(invalidBid)).to.equal(false); - }); - }); - - describe('buildRequests', function () { - let bidRequests = [ - { - bidId: 'bid1', - bidder: 'mytarget', - params: { - placementId: '1' - } - }, - { - bidId: 'bid2', - bidder: 'mytarget', - params: { - placementId: '2', - position: 1, - response: 1, - bidfloor: 10000 - } - } - ]; - let bidderRequest = { - refererInfo: { - page: 'https://example.com?param=value' - } - }; - - let bidRequest = spec.buildRequests(bidRequests, bidderRequest); - - it('should build single POST request for multiple bids', function() { - expect(bidRequest.method).to.equal('POST'); - expect(bidRequest.url).to.equal('//ad.mail.ru/hbid_prebid/'); - expect(bidRequest.data).to.be.an('object'); - expect(bidRequest.data.places).to.be.an('array'); - expect(bidRequest.data.places).to.have.lengthOf(2); - }); - - it('should pass bid parameters', function() { - let place1 = bidRequest.data.places[0]; - let place2 = bidRequest.data.places[1]; - - expect(place1.placementId).to.equal('1'); - expect(place2.placementId).to.equal('2'); - expect(place1.id).to.equal('bid1'); - expect(place2.id).to.equal('bid2'); - }); - - it('should pass default position and response type', function() { - let place = bidRequest.data.places[0]; - - expect(place.position).to.equal(0); - expect(place.response).to.equal(0); - }); - - it('should pass provided position and response type', function() { - let place = bidRequest.data.places[1]; - - expect(place.position).to.equal(1); - expect(place.response).to.equal(1); - }); - - it('should not pass default bidfloor', function() { - let place = bidRequest.data.places[0]; - - expect(place.bidfloor).not.to.exist; - }); - - it('should not pass provided bidfloor', function() { - let place = bidRequest.data.places[1]; - - expect(place.bidfloor).to.exist; - expect(place.bidfloor).to.equal(10000); - }); - - it('should pass site parameters', function() { - let site = bidRequest.data.site; - - expect(site).to.be.an('object'); - expect(site.sitename).to.equal('example.com'); - expect(site.page).to.equal('https://example.com?param=value'); - }); - - it('should pass settings', function() { - let settings = bidRequest.data.settings; - - expect(settings).to.be.an('object'); - expect(settings.currency).to.equal('RUB'); - expect(settings.windowSize).to.be.an('object'); - expect(settings.windowSize.width).to.equal(window.screen.width); - expect(settings.windowSize.height).to.equal(window.screen.height); - }); - }); - - describe('interpretResponse', function () { - let serverResponse = { - body: { - 'bidder_status': - [ - { - 'bidder': 'mail.ru', - 'response_time_ms': 100, - 'num_bids': 2 - } - ], - 'bids': - [ - { - 'displayUrl': 'https://ad.mail.ru/hbid_imp/12345', - 'size': - { - 'height': '400', - 'width': '240' - }, - 'id': '1', - 'currency': 'RUB', - 'price': 100, - 'ttl': 360, - 'creativeId': '123456' - }, - { - 'adm': '

Ad

', - 'size': - { - 'height': '250', - 'width': '300' - }, - 'id': '2', - 'price': 200 - } - ] - } - }; - - let bids = spec.interpretResponse(serverResponse); - - it('should return empty array for response with no bids', function() { - let emptyBids = spec.interpretResponse({ body: {} }); - - expect(emptyBids).to.have.lengthOf(0); - }); - - it('should parse all bids from response', function() { - expect(bids).to.have.lengthOf(2); - }); - - it('should parse bid with ad url', function() { - expect(bids[0].requestId).to.equal('1'); - expect(bids[0].cpm).to.equal(100); - expect(bids[0].width).to.equal('240'); - expect(bids[0].height).to.equal('400'); - expect(bids[0].ttl).to.equal(360); - expect(bids[0].currency).to.equal('RUB'); - expect(bids[0]).to.have.property('creativeId'); - expect(bids[0].creativeId).to.equal('123456'); - expect(bids[0].netRevenue).to.equal(true); - expect(bids[0].adUrl).to.equal('https://ad.mail.ru/hbid_imp/12345'); - expect(bids[0]).to.not.have.property('ad'); - }); - - it('should parse bid with ad markup', function() { - expect(bids[1].requestId).to.equal('2'); - expect(bids[1].cpm).to.equal(200); - expect(bids[1].width).to.equal('300'); - expect(bids[1].height).to.equal('250'); - expect(bids[1].ttl).to.equal(180); - expect(bids[1].currency).to.equal('RUB'); - expect(bids[1]).to.have.property('creativeId'); - expect(bids[1].creativeId).not.to.equal('123456'); - expect(bids[1].netRevenue).to.equal(true); - expect(bids[1].ad).to.equal('

Ad

'); - expect(bids[1]).to.not.have.property('adUrl'); - }); - }); -}); diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 75fb357b196..349051cb48e 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -221,6 +221,7 @@ describe('interpretResponse', function () { meta: { advertiserDomains: ['test.com'], }, + mediaType: 'banner', }, ] @@ -681,16 +682,24 @@ describe('hasProtocol', () => { describe('addProtocol', () => { it('www.testpage.com', () => { - expect(addProtocol('www.testpage.com')).to.be.equal('https://www.testpage.com') + expect(addProtocol('www.testpage.com')).to.be.equal( + 'https://www.testpage.com' + ) }) it('//www.testpage.com', () => { - expect(addProtocol('//www.testpage.com')).to.be.equal('https://www.testpage.com') + expect(addProtocol('//www.testpage.com')).to.be.equal( + 'https://www.testpage.com' + ) }) it('http://www.testpage.com', () => { - expect(addProtocol('http://www.testpage.com')).to.be.equal('http://www.testpage.com') + expect(addProtocol('http://www.testpage.com')).to.be.equal( + 'http://www.testpage.com' + ) }) it('https://www.testpage.com', () => { - expect(addProtocol('https://www.testpage.com')).to.be.equal('https://www.testpage.com') + expect(addProtocol('https://www.testpage.com')).to.be.equal( + 'https://www.testpage.com' + ) }) }) @@ -786,7 +795,7 @@ describe('RequestData', () => { describe('UserEIDs', () => { const userEids = new UserEIDs() - const eids = [{ 'testId': 1111 }] + const eids = [{ testId: 1111 }] describe('processBidRequestData', () => { it('Processes bid request without eids', () => { @@ -810,7 +819,7 @@ describe('UserEIDs', () => { expect(qs).to.include('ntv_pb_eid=') try { expect(JSON.parse(value)).to.be.equal(eids) - } catch (err) { } + } catch (err) {} }) }) }) @@ -828,12 +837,83 @@ describe('buildRequestUrl', () => { }) it('Returns baseUrl + QS params if QS strings passed', () => { - const url = buildRequestUrl(baseUrl, ['ntv_ptd=123456&ntv_test=true', 'ntv_foo=bar']) - expect(url).to.be.equal(`${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar`) + const url = buildRequestUrl(baseUrl, [ + 'ntv_ptd=123456&ntv_test=true', + 'ntv_foo=bar', + ]) + expect(url).to.be.equal( + `${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar` + ) }) it('Returns baseUrl + QS params if mixed QS strings passed', () => { - const url = buildRequestUrl(baseUrl, ['ntv_ptd=123456&ntv_test=true', '', '', 'ntv_foo=bar']) - expect(url).to.be.equal(`${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar`) + const url = buildRequestUrl(baseUrl, [ + 'ntv_ptd=123456&ntv_test=true', + '', + '', + 'ntv_foo=bar', + ]) + expect(url).to.be.equal( + `${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar` + ) + }) +}) + +describe('Prebid Video', function () { + it('should handle video bid requests', function () { + const videoBidRequest = { + bidder: 'nativo', + params: { + video: { + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + playbackmethod: [1, 2], + skip: 1, + skipafter: 5, + }, + }, + } + + const isValid = spec.isBidRequestValid(videoBidRequest) + expect(isValid).to.be.true + }) +}) + +describe('Prebid Native', function () { + it('should handle native bid requests', function () { + const nativeBidRequest = { + bidder: 'nativo', + params: { + native: { + title: { + required: true, + len: 80, + }, + image: { + required: true, + sizes: [150, 50], + }, + sponsoredBy: { + required: true, + }, + clickUrl: { + required: true, + }, + privacyLink: { + required: false, + }, + body: { + required: true, + }, + icon: { + required: true, + sizes: [50, 50], + }, + }, + }, + } + + const isValid = spec.isBidRequestValid(nativeBidRequest) + expect(isValid).to.be.true }) }) diff --git a/test/spec/modules/naveggIdSystem_spec.js b/test/spec/modules/naveggIdSystem_spec.js index 2c4f1cda859..4907a63abde 100644 --- a/test/spec/modules/naveggIdSystem_spec.js +++ b/test/spec/modules/naveggIdSystem_spec.js @@ -1,45 +1,155 @@ -import { naveggIdSubmodule, storage } from 'modules/naveggIdSystem.js'; +import { naveggIdSubmodule, storage, getIdFromAPI } from 'modules/naveggIdSystem.js'; +import { server } from 'test/mocks/xhr.js'; +import * as ajaxLib from 'src/ajax.js'; -describe('naveggId', function () { - let sandbox; - beforeEach(() => { - sandbox = sinon.sandbox.create(); - sandbox.stub(storage, 'getDataFromLocalStorage'); +const NAVEGGID_CONFIG_COOKIE_HTML5 = { + storage: { + name: 'nvggid', + type: 'cookie&html5', + expires: 8 + } +} + +const MOCK_RESPONSE = { + nvggid: 'test_nvggid' +} + +const MOCK_RESPONSE_NULL = { + nvggid: null +} + +function mockResponse(responseText, isSuccess = true) { + return function(url, callbacks) { + if (isSuccess) { + callbacks.success(responseText) + } else { + callbacks.error(new Error('Mock Error')) + } + } +} + +function deleteAllCookies() { + document.cookie.split(';').forEach(cookie => { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie; + document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; + }); +} + +function setLocalStorage() { + storage.setDataInLocalStorage('nvggid', 'localstorage_value'); +} + +describe('getId', function () { + let ajaxStub, ajaxBuilderStub; + + beforeEach(function() { + ajaxStub = sinon.stub(); + ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').returns(ajaxStub); + }); + + afterEach(function() { + ajaxBuilderStub.restore(); + deleteAllCookies(); + storage.removeDataFromLocalStorage('nvggid'); + }); + + it('should get the value from the existing localstorage', function() { + setLocalStorage(); + + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; + + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) + + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('localstorage_value')).to.be.true; }); - afterEach(() => { - sandbox.restore(); + + it('should get the value from a nid cookie', function() { + storage.setCookie('nid', 'old_nid_cookie', storage.expires) + + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; + + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) + + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('old_nid_cookie')).to.be.true; }); - it('should NOT find navegg id', function () { - let id = naveggIdSubmodule.getId(); + it('should get the value from a nav cookie', function() { + storage.setCookie('navId', 'old_nav_cookie', storage.expires) + + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; - expect(id).to.be.undefined; + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) + + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('old_nav_cookie')).to.be.true; }); - it('getId() should return "test-nid" id from cookie OLD_NAVEGG_ID', function() { - sinon.stub(storage, 'getCookie').withArgs('nid').returns('test-nid'); - let id = naveggIdSubmodule.getId(); - expect(id).to.be.deep.equal({id: 'test-nid'}) - }) + it('should get the value from an old nvg cookie', function() { + storage.setCookie('nvgid', 'old_nvg_cookie', storage.expires) + + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; + + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) - it('getId() should return "test-nvggid" id from local storage NAVEGG_ID', function() { - storage.getDataFromLocalStorage.callsFake(() => 'test-ninvggidd') + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('old_nvg_cookie')).to.be.true; + }); - let id = naveggIdSubmodule.getId(); - expect(id).to.be.deep.equal({id: 'test-ninvggidd'}) - }) + it('should return correct value from API response', function(done) { + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; - it('getId() should return "test-nvggid" id from local storage NAV0', function() { - storage.getDataFromLocalStorage.callsFake(() => 'nvgid-nav0') + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE)); + } + }); + apiCallback(callback) - let id = naveggIdSubmodule.getId(); - expect(id).to.be.deep.equal({id: 'nvgid-nav0'}) - }) + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('test_nvggid')).to.be.true; + done(); + }); - it('getId() should return "test-nvggid" id from local storage NVG0', function() { - storage.getDataFromLocalStorage.callsFake(() => 'nvgid-nvg0') + it('should return no value from API response', function(done) { + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; - let id = naveggIdSubmodule.getId(); - expect(id).to.be.deep.equal({id: 'nvgid-nvg0'}) - }) + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith(undefined)).to.be.true; + done(); + }); }); diff --git a/test/spec/modules/netIdSystem_spec.js b/test/spec/modules/netIdSystem_spec.js new file mode 100644 index 00000000000..bbf59c39f32 --- /dev/null +++ b/test/spec/modules/netIdSystem_spec.js @@ -0,0 +1,23 @@ +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {netIdSubmodule} from '../../../modules/netIdSystem.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; + +describe('Net ID', () => { + describe('eid', () => { + before(() => { + attachIdSystem(netIdSubmodule); + }); + it('NetId', function () { + const userId = { + netId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'netid.de', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + }); +}); diff --git a/test/spec/modules/newspassidBidAdapter_spec.js b/test/spec/modules/newspassidBidAdapter_spec.js index 6468d4f530a..dec630f3f6a 100644 --- a/test/spec/modules/newspassidBidAdapter_spec.js +++ b/test/spec/modules/newspassidBidAdapter_spec.js @@ -1,1799 +1,346 @@ -import { expect } from 'chai'; -import { spec, defaultSize } from 'modules/newspassidBidAdapter.js'; +import { spec } from 'modules/newspassidBidAdapter.js'; import { config } from 'src/config.js'; -import {getGranularityKeyName, getGranularityObject} from '../../../modules/newspassidBidAdapter.js'; -import * as utils from '../../../src/utils.js'; -const NEWSPASSURI = 'https://bidder.newspassid.com/openrtb2/auction'; -const BIDDER_CODE = 'newspassid'; -var validBidRequests = [ - { - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff8', - bidRequestsCount: 1, +import { deepClone } from 'src/utils.js'; +import { resolveNewpassidPublisherId } from '../../../modules/newspassidBidAdapter'; + +describe('newspassidBidAdapter', function () { + const TEST_PUBLISHER_ID = '123456'; + const TEST_PLACEMENT_ID = 'test-group1'; + + const validBidRequest = { bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, - sizes: [[300, 250], [300, 600]], - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' - } -]; -var validBidRequestsNoCustomData = [ - { - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff8', - bidRequestsCount: 1, - bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, - sizes: [[300, 250], [300, 600]], - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' - } -]; -var validBidRequestsMulti = [ - { - testId: 1, - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff8', - bidRequestsCount: 1, - bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, - sizes: [[300, 250], [300, 600]], - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' - }, - { - testId: 2, - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff0', - bidRequestsCount: 1, - bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c0', - crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, - sizes: [[300, 250], [300, 600]], - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' - } -]; -var validBidRequestsWithUserIdData = [ - { - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff8', - bidRequestsCount: 1, - bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, - sizes: [[300, 250], [300, 600]], - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87', - userId: { - 'pubcid': '12345678', - 'tdid': '1111tdid', - 'id5id': { uid: '1111', ext: { linkType: 2, abTestingControlGroup: false } }, - 'criteoId': '1111criteoId', - 'idl_env': 'liverampId', - 'lipb': {'lipbid': 'lipbidId123'}, - 'parrableId': {'eid': '01.5678.parrableid'}, - 'sharedid': {'id': '01EAJWWNEPN3CYMM5N8M5VXY22', 'third': '01EAJWWNEPN3CYMM5N8M5VXY22'} - }, - userIdAsEids: [ - { - 'source': 'pubcid.org', - 'uids': [ - { - 'id': '12345678', - 'atype': 1 - } - ] - }, - { - 'source': 'adserver.org', - 'uids': [{ - 'id': '1111tdid', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - }] - }, - { - 'source': 'id5-sync.com', - 'uids': [{ - 'id': 'ID5-someId', - 'atype': 1, - }] - }, - { - 'source': 'criteoId', - 'uids': [{ - 'id': '1111criteoId', - 'atype': 1, - }] - }, - { - 'source': 'idl_env', - 'uids': [{ - 'id': 'liverampId', - 'atype': 1, - }] - }, - { - 'source': 'lipb', - 'uids': [{ - 'id': {'lipbid': 'lipbidId123'}, - 'atype': 1, - }] - }, - { - 'source': 'parrableId', - 'uids': [{ - 'id': {'eid': '01.5678.parrableid'}, - 'atype': 1, - }] - } - ] - } -]; -var validBidRequestsMinimal = [ - { - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff8', - bidRequestsCount: 1, - bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - params: { publisherId: '9876abcd12-3', placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, - sizes: [[300, 250], [300, 600]], - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' - } -]; -var validBidRequestsNoSizes = [ - { - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff8', - bidRequestsCount: 1, - bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' - } -]; -var validBidRequestsWithBannerMediaType = [ - { - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff8', - bidRequestsCount: 1, - bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, - mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}, - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' - } -]; -var validBidRequestsIsThisCamelCaseEnough = [ - { - 'bidder': 'newspassid', - 'testname': 'validBidRequestsIsThisCamelCaseEnough', - 'params': { - 'publisherId': 'newspassRUP0001', - 'placementId': '8000000009', - 'siteId': '4204204201', - 'customData': [ - { - 'settings': {}, - 'targeting': { - 'sens': 'f', - 'pt1': '/uk', - 'pt2': 'uk', - 'pt3': 'network-front', - 'pt4': 'ng', - 'pt5': [ - 'uk' - ], - 'pt7': 'desktop', - 'pt8': [ - 'tfmqxwj7q', - 'penl4dfdk', - 'sek9ghqwi' - ], - 'pt9': '|k0xw2vqzp33kklb3j5w4|||' - } - } - ], - 'userId': { - 'pubcid': '2ada6ae6-aeca-4e07-8922-a99b3aaf8a56' - }, - 'userIdAsEids': [ - { - 'source': 'pubcid.org', - 'uids': [ - { - 'id': '2ada6ae6-aeca-4e07-8922-a99b3aaf8a56', - 'atype': 1 - } - ] - } - ] - }, - mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}, - 'adUnitCode': 'some-ad', - 'transactionId': '02c1ea7d-0bf2-451b-a122-1420040d1cf8', - 'bidId': '2899ec066a91ff8', - 'bidderRequestId': '1c1586b27a1b5c8', - 'auctionId': '0456c9b7-5ab2-4fec-9e10-f418d3d1f04c', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - } -]; -var validBidderRequest = { - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - auctionStart: 1536838908986, - bidderCode: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - bids: [{ - adUnitCode: 'div-gpt-ad-1460505748561-0', - auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', - bidId: '2899ec066a91ff8', - bidRequestsCount: 1, - bidder: 'newspassid', - bidderRequestId: '1c1586b27a1b5c8', - crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { banner: { topframe: 1, w: 300, h: 250, format: [{ w: 300, h: 250 }, { w: 300, h: 600 }] }, id: '2899ec066a91ff8', secure: 1, tagid: 'undefined' } ] }, - sizes: [[300, 250], [300, 600]], - transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' - }], - doneCbCallCount: 1, - start: 1536838908987, - timeout: 3000 -}; -var emptyObject = {}; -var validResponse = { - 'body': { - 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', - 'seatbid': [ - { - 'bid': [ - { - 'id': '677903815252395017', - 'impid': '2899ec066a91ff8', - 'price': 0.5, - 'adm': '', - 'adid': '98493581', - 'adomain': [ - 'http://prebid.org' - ], - 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', - 'cid': '9325', - 'crid': '98493581', - 'cat': [ - 'IAB3-1' - ], - 'w': 300, - 'h': 600, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 555545, - 'auction_id': 6500448734132353000, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - } - ], - 'seat': 'appnexus' - } - ], - 'cur': 'GBP', /* NOTE - this is where cur is, not in the seatbids. */ - 'ext': { - 'responsetimemillis': { - 'appnexus': 47, - 'openx': 30 - } - }, - 'timing': { - 'start': 1536848078.089177, - 'end': 1536848078.142203, - 'TimeTaken': 0.05302619934082031 - } - }, - 'headers': {} -}; -var validResponse2BidsSameAdunit = { - 'body': { - 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', - 'seatbid': [ - { - 'bid': [ - { - 'id': '677903815252395017', - 'impid': '2899ec066a91ff8', - 'price': 0.5, - 'adm': '', - 'adid': '98493581', - 'adomain': [ - 'http://prebid.org' - ], - 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', - 'cid': '9325', - 'crid': '98493581', - 'cat': [ - 'IAB3-1' - ], - 'w': 300, - 'h': 600, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 555545, - 'auction_id': 6500448734132353000, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - }, - { - 'id': '677903815252395010', - 'impid': '2899ec066a91ff8', - 'price': 0.9, - 'adm': '', - 'adid': '98493580', - 'adomain': [ - 'http://prebid.org' - ], - 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', - 'cid': '9320', - 'crid': '98493580', - 'cat': [ - 'IAB3-1' - ], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 555540, - 'auction_id': 6500448734132353000, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - } ], - 'seat': 'npappnexus' - } - ], - 'cur': 'GBP', /* NOTE - this is where cur is, not in the seatbids. */ - 'ext': { - 'responsetimemillis': { - 'appnexus': 47, - 'openx': 30 - } - }, - 'timing': { - 'start': 1536848078.089177, - 'end': 1536848078.142203, - 'TimeTaken': 0.05302619934082031 - } - }, - 'headers': {} -}; -var validBidResponse1adWith2Bidders = { - 'body': { - 'id': '91221f96-b931-4acc-8f05-c2a1186fa5ac', - 'seatbid': [ - { - 'bid': [ - { - 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', - 'impid': '2899ec066a91ff8', - 'price': 0.36754, - 'adm': '', - 'adid': '134928661', - 'adomain': [ - 'somecompany.com' - ], - 'iurl': 'https:\/\/ams1-ib.adnxs.com\/cr?id=134928661', - 'cid': '8825', - 'crid': '134928661', - 'cat': [ - 'IAB8-15', - 'IAB8-16', - 'IAB8-4', - 'IAB8-1', - 'IAB8-14', - 'IAB8-6', - 'IAB8-13', - 'IAB8-3', - 'IAB8-17', - 'IAB8-12', - 'IAB8-8', - 'IAB8-7', - 'IAB8-2', - 'IAB8-9', - 'IAB8', - 'IAB8-11' - ], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 14640, - 'auction_id': 1.8369641905139e+18, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - } - ], - 'seat': 'appnexus' - }, - { - 'bid': [ - { - 'id': '75665207-a1ca-49db-ba0e-a5e9c7d26f32', - 'impid': '37fff511779365a', - 'price': 1.046, - 'adm': '
removed
', - 'adomain': [ - 'kx.com' - ], - 'crid': '13005', - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - } - } - } - ], - 'seat': 'openx' - } - ], - 'ext': { - 'responsetimemillis': { - 'appnexus': 91, - 'openx': 109, - 'npappnexus': 46, - 'npbeeswax': 2, - 'pangaea': 91 - } - } - }, - 'headers': {} -}; -var multiRequest1 = [ - { - 'bidder': 'newspassid', - 'params': { - 'publisherId': 'newspassRUP0001', - 'siteId': '4204204201', - 'placementId': '0420420421', - 'customData': [ - { - 'settings': {}, - 'targeting': { - 'sens': 'f', - 'pt1': '/uk', - 'pt2': 'uk', - 'pt3': 'network-front', - 'pt4': 'ng', - 'pt5': [ - 'uk' - ], - 'pt7': 'desktop', - 'pt8': [ - 'tfmqxwj7q', - 'penl4dfdk', - 'uayf5jmv3', - 't8nyiude5', - 'sek9ghqwi' - ], - 'pt9': '|k0xw2vqzp33kklb3j5w4|||' - } - } - ] + params: { + publisherId: TEST_PUBLISHER_ID, + placementId: TEST_PLACEMENT_ID }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] - ] + mediaTypes: { + banner: { + sizes: [[300, 250]] } }, - 'adUnitCode': 'mpu', - 'transactionId': '6480bac7-31b5-4723-9145-ad8966660651', - 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] - ], - 'bidId': '2d30e86db743a8', - 'bidderRequestId': '1d03a1dfc563fc', - 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }, - { - 'bidder': 'newspassid', - 'params': { - 'publisherId': 'newspassRUP0001', - 'siteId': '4204204201', - 'placementId': '0420420421', - 'customData': [ - { - 'settings': {}, - 'targeting': { - 'sens': 'f', - 'pt1': '/uk', - 'pt2': 'uk', - 'pt3': 'network-front', - 'pt4': 'ng', - 'pt5': [ - 'uk' - ], - 'pt7': 'desktop', - 'pt8': [ - 'tfmqxwj7q', - 'penl4dfdk', - 't8nxz6qzd', - 't8nyiude5', - 'sek9ghqwi' - ], - 'pt9': '|k0xw2vqzp33kklb3j5w4|||' - } - } - ] - }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 728, - 90 - ], - [ - 970, - 250 - ] - ] - } - }, - 'adUnitCode': 'leaderboard', - 'transactionId': 'a49988e6-ae7c-46c4-9598-f18db49892a0', - 'sizes': [ - [ - 728, - 90 - ], - [ - 970, - 250 - ] - ], - 'bidId': '3025f169863b7f8', - 'bidderRequestId': '1d03a1dfc563fc', - 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - } -]; -var multiBidderRequest1 = { - bidderRequest: { - 'bidderCode': 'newspassid', - 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', - 'bidderRequestId': '1d03a1dfc563fc', - 'bids': [ - { - 'bidder': 'newspassid', - 'params': { - 'publisherId': 'newspassRUP0001', - 'siteId': '4204204201', - 'placementId': '0420420421', - 'customData': [ - { - 'settings': {}, - 'targeting': { - 'sens': 'f', - 'pt1': '/uk', - 'pt2': 'uk', - 'pt3': 'network-front', - 'pt4': 'ng', - 'pt5': [ - 'uk' - ], - 'pt7': 'desktop', - 'pt8': [ - 'tfmqxwj7q', - 'txeh7uyo0', - 't8nxz6qzd', - 't8nyiude5', - 'sek9ghqwi' - ], - 'pt9': '|k0xw2vqzp33kklb3j5w4|||' - } - } - ] - }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] - ] - } - }, - 'adUnitCode': 'mpu', - 'transactionId': '6480bac7-31b5-4723-9145-ad8966660651', - 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] - ], - 'bidId': '2d30e86db743a8', - 'bidderRequestId': '1d03a1dfc563fc', - 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }, - { - 'bidder': 'newspassid', - 'params': { - 'publisherId': 'newspassRUP0001', - 'siteId': '4204204201', - 'placementId': '0420420421', - 'customData': [ - { - 'settings': {}, - 'targeting': { - 'sens': 'f', - 'pt1': '/uk', - 'pt2': 'uk', - 'pt3': 'network-front', - 'pt4': 'ng', - 'pt5': [ - 'uk' - ], - 'pt7': 'desktop', - 'pt8': [ - 'tfmqxwj7q', - 'penl4dfdk', - 't8nxz6qzd', - 't8nyiude5', - 'sek9ghqwi' - ], - 'pt9': '|k0xw2vqzp33kklb3j5w4|||' - } - } - ] - }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 728, - 90 - ], - [ - 970, - 250 - ] - ] - } - }, - 'adUnitCode': 'leaderboard', - 'transactionId': 'a49988e6-ae7c-46c4-9598-f18db49892a0', - 'sizes': [ - [ - 728, - 90 - ], - [ - 970, - 250 - ] - ], - 'bidId': '3025f169863b7f8', - 'bidderRequestId': '1d03a1dfc563fc', - 'auctionId': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - } - ], - 'auctionStart': 1592918645574, - 'timeout': 3000, - 'refererInfo': { - 'referer': 'http://some.referrer.com', - 'reachedTop': true, - 'numIframes': 0, - 'stack': [ - 'http://some.referrer.com' - ] + adUnitCode: 'test-div', + transactionId: '123456', + bidId: '789', + bidderRequestId: 'abc', + auctionId: 'xyz' + }; + + const validBidderRequest = { + bidderCode: 'newspassid', + auctionId: 'xyz', + bidderRequestId: 'abc', + bids: [validBidRequest], + gdprConsent: { + gdprApplies: true, + consentString: 'consent123' }, - 'start': 1592918645578 - } -}; -var multiResponse1 = { - 'body': { - 'id': '592ee33b-fb2e-4c00-b2d5-383e99cac57f', - 'seatbid': [ - { - 'bid': [ - { - 'id': '4419718600113204943', - 'impid': '2d30e86db743a8', - 'price': 0.2484, - 'adm': '', - 'adid': '119683582', - 'adomain': [ - 'https://someurl.com' - ], - 'iurl': 'https://ams1-ib.adnxs.com/cr?id=119683582', - 'cid': '9979', - 'crid': '119683582', - 'cat': [ - 'IAB3' - ], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'newspassid': {}, - 'appnexus': { - 'brand_id': 734921, - 'auction_id': 2995348111857539600, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - }, - 'cpm': 0.2484, - 'bidId': '2d30e86db743a8', - 'requestId': '2d30e86db743a8', - 'width': 300, - 'height': 250, - 'ad': '', - 'netRevenue': true, - 'creativeId': '119683582', - 'currency': 'USD', - 'ttl': 300, - 'originalCpm': 0.2484, - 'originalCurrency': 'USD' - }, - { - 'id': '18552976939844681', - 'impid': '3025f169863b7f8', - 'price': 0.0621, - 'adm': '', - 'adid': '120179216', - 'adomain': [ - 'appnexus.com' - ], - 'iurl': 'https://ams1-ib.adnxs.com/cr?id=120179216', - 'cid': '9979', - 'crid': '120179216', - 'w': 970, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'newspassid': {}, - 'appnexus': { - 'brand_id': 1, - 'auction_id': 3449036134472542700, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - }, - 'cpm': 0.0621, - 'bidId': '3025f169863b7f8', - 'requestId': '3025f169863b7f8', - 'width': 970, - 'height': 250, - 'ad': '', - 'netRevenue': true, - 'creativeId': '120179216', - 'currency': 'USD', - 'ttl': 300, - 'originalCpm': 0.0621, - 'originalCurrency': 'USD' - }, - { - 'id': '18552976939844999', - 'impid': '3025f169863b7f8', - 'price': 0.521, - 'adm': '', - 'adid': '120179216', - 'adomain': [ - 'appnexus.com' - ], - 'iurl': 'https://ams1-ib.adnxs.com/cr?id=120179216', - 'cid': '9999', - 'crid': '120179299', - 'w': 728, - 'h': 90, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'newspassid': {}, - 'appnexus': { - 'brand_id': 1, - 'auction_id': 3449036134472542700, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - }, - 'cpm': 0.521, - 'bidId': '3025f169863b7f8', - 'requestId': '3025f169863b7f8', - 'width': 728, - 'height': 90, - 'ad': '', - 'netRevenue': true, - 'creativeId': '120179299', - 'currency': 'USD', - 'ttl': 300, - 'originalCpm': 0.0621, - 'originalCurrency': 'USD' - } - ], - 'seat': 'npappnexus' - }, - { - 'bid': [ - { - 'id': '1c605e8a-4992-4ec6-8a5c-f82e2938c2db', - 'impid': '2d30e86db743a8', - 'price': 0.01, - 'adm': '
', - 'crid': '540463358', - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'newspassid': {} - } - }, - 'cpm': 0.01, - 'bidId': '2d30e86db743a8', - 'requestId': '2d30e86db743a8', - 'width': 300, - 'height': 250, - 'ad': '
', - 'netRevenue': true, - 'creativeId': '540463358', - 'currency': 'USD', - 'ttl': 300, - 'originalCpm': 0.01, - 'originalCurrency': 'USD' - }, - { - 'id': '3edeb4f7-d91d-44e2-8aeb-4a2f6d295ce5', - 'impid': '3025f169863b7f8', - 'price': 0.01, - 'adm': '
', - 'crid': '540221061', - 'w': 970, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'newspassid': {} - } - }, - 'cpm': 0.01, - 'bidId': '3025f169863b7f8', - 'requestId': '3025f169863b7f8', - 'width': 970, - 'height': 250, - 'ad': '
', - 'netRevenue': true, - 'creativeId': '540221061', - 'currency': 'USD', - 'ttl': 300, - 'originalCpm': 0.01, - 'originalCurrency': 'USD' - } - ], - 'seat': 'openx' - } - ], - 'ext': { - 'debug': {}, - 'responsetimemillis': { - 'beeswax': 6, - 'openx': 91, - 'npappnexus': 40, - 'npbeeswax': 6 - } + refererInfo: { + page: 'http://example.com' } - }, - 'headers': {} -}; -describe('newspassid Adapter', function () { - describe('isBidRequestValid', function () { - let validBidReq = { - bidder: BIDDER_CODE, - params: { - placementId: '1310000099', - publisherId: '9876abcd12-3', - siteId: '1234567890' - } - }; - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(validBidReq)).to.equal(true); - }); - var validBidReq2 = { - bidder: BIDDER_CODE, - params: { - placementId: '1310000099', - publisherId: '9876abcd12-3', - siteId: '1234567890', - customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}] - }, - siteId: 1234567890 + }; + + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + impid: '789', + price: 2.5, + w: 300, + h: 250, + crid: 'creative123', + adm: '
ad
', + adomain: ['advertiser.com'] + }] + }], + cur: 'USD' } - it('should return true when required params found and all optional params are valid', function () { - expect(spec.isBidRequestValid(validBidReq2)).to.equal(true); - }); - var xEmptyPlacement = { - bidder: BIDDER_CODE, - params: { - placementId: '', - publisherId: '9876abcd12-3', - siteId: '1234567890' - } - }; - it('should not validate empty placementId', function () { - expect(spec.isBidRequestValid(xEmptyPlacement)).to.equal(false); - }); - var xMissingPlacement = { - bidder: BIDDER_CODE, - params: { - publisherId: '9876abcd12-3', - siteId: '1234567890' - } - }; - it('should not validate missing placementId', function () { - expect(spec.isBidRequestValid(xMissingPlacement)).to.equal(false); - }); - var xBadPlacement = { - bidder: BIDDER_CODE, - params: { - placementId: '123X45', - publisherId: '9876abcd12-3', - siteId: '1234567890' - } - }; - it('should not validate placementId with a non-numeric value', function () { - expect(spec.isBidRequestValid(xBadPlacement)).to.equal(false); - }); - var xBadPlacementTooShort = { - bidder: BIDDER_CODE, - params: { - placementId: 123456789, /* should be exactly 10 chars */ - publisherId: '9876abcd12-3', - siteId: '1234567890' - } - }; - it('should not validate placementId with a numeric value of wrong length', function () { - expect(spec.isBidRequestValid(xBadPlacementTooShort)).to.equal(false); - }); - var xBadPlacementTooLong = { - bidder: BIDDER_CODE, - params: { - placementId: 12345678901, /* should be exactly 10 chars */ - publisherId: '9876abcd12-3', - siteId: '1234567890' - } - }; - it('should not validate placementId with a numeric value of wrong length', function () { - expect(spec.isBidRequestValid(xBadPlacementTooLong)).to.equal(false); - }); - var xMissingPublisher = { - bidder: BIDDER_CODE, - params: { - placementId: '1234567890', - siteId: '1234567890' - } - }; - it('should not validate missing publisherId', function () { - expect(spec.isBidRequestValid(xMissingPublisher)).to.equal(false); - }); - var xMissingSiteId = { - bidder: BIDDER_CODE, - params: { - publisherId: '9876abcd12-3', - placementId: '1234567890', - } - }; - it('should not validate missing sitetId', function () { - expect(spec.isBidRequestValid(xMissingSiteId)).to.equal(false); - }); - var xBadPublisherTooShort = { - bidder: BIDDER_CODE, - params: { - placementId: '1234567890', - publisherId: '9876abcd12a', - siteId: '1234567890' - } - }; - it('should not validate publisherId being too short', function () { - expect(spec.isBidRequestValid(xBadPublisherTooShort)).to.equal(false); - }); - var xBadPublisherTooLong = { - bidder: BIDDER_CODE, - params: { - placementId: '1234567890', - publisherId: '9876abcd12abc', - siteId: '1234567890' - } - }; - it('should not validate publisherId being too long', function () { - expect(spec.isBidRequestValid(xBadPublisherTooLong)).to.equal(false); - }); - var publisherNumericOk = { - bidder: BIDDER_CODE, - params: { - placementId: '1234567890', - publisherId: 123456789012, - siteId: '1234567890' - } - }; - it('should validate publisherId being 12 digits', function () { - expect(spec.isBidRequestValid(publisherNumericOk)).to.equal(true); - }); - var xEmptyPublisher = { - bidder: BIDDER_CODE, - params: { - placementId: '1234567890', - publisherId: '', - siteId: '1234567890' - } - }; - it('should not validate empty publisherId', function () { - expect(spec.isBidRequestValid(xEmptyPublisher)).to.equal(false); - }); - var xBadSite = { - bidder: BIDDER_CODE, - params: { - placementId: '1234567890', - publisherId: '9876abcd12-3', - siteId: '12345Z' - } - }; - it('should not validate bad siteId', function () { - expect(spec.isBidRequestValid(xBadSite)).to.equal(false); - }); - it('should not validate siteId too long', function () { - expect(spec.isBidRequestValid(xBadSite)).to.equal(false); - }); - it('should not validate siteId too short', function () { - expect(spec.isBidRequestValid(xBadSite)).to.equal(false); - }); - var allNonStrings = { - bidder: BIDDER_CODE, - params: { - placementId: 1234567890, - publisherId: '9876abcd12-3', - siteId: 1234567890 - } - }; - it('should validate all numeric values being sent as non-string numbers', function () { - expect(spec.isBidRequestValid(allNonStrings)).to.equal(true); - }); - var emptySiteId = { - bidder: BIDDER_CODE, - params: { - placementId: 1234567890, - publisherId: '9876abcd12-3', - siteId: '' - } - }; - it('should not validate siteId being empty string (it is required now)', function () { - expect(spec.isBidRequestValid(emptySiteId)).to.equal(false); - }); - var xBadCustomData = { - bidder: BIDDER_CODE, - params: { - 'placementId': '1234567890', - 'publisherId': '9876abcd12-3', - 'siteId': '1234567890', - 'customData': 'this aint gonna work' - } - }; - it('should not validate customData not being an array', function () { - expect(spec.isBidRequestValid(xBadCustomData)).to.equal(false); - }); - var xBadCustomDataOldCustomdataValue = { - bidder: BIDDER_CODE, - params: { - 'placementId': '1234567890', - 'publisherId': '9876abcd12-3', - 'siteId': '1234567890', - 'customData': {'gender': 'bart', 'age': 'low'} - } - }; - it('should not validate customData being an object, not an array', function () { - expect(spec.isBidRequestValid(xBadCustomDataOldCustomdataValue)).to.equal(false); - }); - var xBadCustomDataZerocd = { - bidder: BIDDER_CODE, - params: { - 'placementId': '1111111110', - 'publisherId': '9876abcd12-3', - 'siteId': '1234567890', - 'customData': [] - } - }; - it('should not validate customData array having no elements', function () { - expect(spec.isBidRequestValid(xBadCustomDataZerocd)).to.equal(false); - }); - var xBadCustomDataNotargeting = { - bidder: BIDDER_CODE, - params: { - 'placementId': '1234567890', - 'publisherId': '9876abcd12-3', - 'customData': [{'settings': {}, 'xx': {'gender': 'bart', 'age': 'low'}}], - siteId: '1234567890' - } - }; - it('should not validate customData[] having no "targeting"', function () { - expect(spec.isBidRequestValid(xBadCustomDataNotargeting)).to.equal(false); - }); - var xBadCustomDataTgtNotObj = { - bidder: BIDDER_CODE, - params: { - 'placementId': '1234567890', - 'publisherId': '9876abcd12-3', - 'customData': [{'settings': {}, 'targeting': 'this should be an object'}], - siteId: '1234567890' - } - }; - it('should not validate customData[0].targeting not being an object', function () { - expect(spec.isBidRequestValid(xBadCustomDataTgtNotObj)).to.equal(false); - }); - var xBadCustomParams = { - bidder: BIDDER_CODE, - params: { - 'placementId': '1234567890', - 'publisherId': '9876abcd12-3', - 'siteId': '1234567890', - 'customParams': 'this key is no longer valid' - } - }; - it('should not validate customParams - this is a renamed key', function () { - expect(spec.isBidRequestValid(xBadCustomParams)).to.equal(false); + }; + + describe('gvlid', function() { + it('should expose gvlid', function() { + expect(spec.gvlid).to.equal(1317); }); }); - describe('buildRequests', function () { - it('sends bid request to NEWSPASSURI via POST', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - expect(request.url).to.equal(NEWSPASSURI); - expect(request.method).to.equal('POST'); - }); - it('sends data as a string', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - expect(request.data).to.be.a('string'); - }); - it('sends all bid parameters', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); - }); - it('adds all parameters inside the ext object only', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - expect(request.data).to.be.a('string'); - var data = JSON.parse(request.data); - expect(data.imp[0].ext.newspassid.customData).to.be.an('array'); - expect(request).not.to.have.key('lotameData'); - expect(request).not.to.have.key('customData'); - }); - it('adds all parameters inside the ext object only - lightning', function () { - let localBidReq = JSON.parse(JSON.stringify(validBidRequests)); - const request = spec.buildRequests(localBidReq, validBidderRequest); - expect(request.data).to.be.a('string'); - var data = JSON.parse(request.data); - expect(data.imp[0].ext.newspassid.customData).to.be.an('array'); - expect(request).not.to.have.key('lotameData'); - expect(request).not.to.have.key('customData'); - }); - it('has correct bidder', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - expect(request.bidderRequest.bids[0].bidder).to.equal(BIDDER_CODE); - }); - it('handles mediaTypes element correctly', function () { - const request = spec.buildRequests(validBidRequestsWithBannerMediaType, validBidderRequest); - expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); - }); - it('handles no newspassid or custom data', function () { - const request = spec.buildRequests(validBidRequestsMinimal, validBidderRequest); - expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); - }); - it('should not crash when there is no sizes element at all', function () { - const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); - expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); - }); - it('should be able to handle non-single requests', function () { - config.setConfig({'newspassid': {'singleRequest': false}}); - const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); - expect(request).to.be.a('array'); - expect(request[0]).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); - config.setConfig({'newspassid': {'singleRequest': true}}); - }); - it('should not have imp[N].ext.newspassid.userId', function () { - let bidderRequest = validBidderRequest; - let bidRequests = validBidRequests; - bidRequests[0]['userId'] = { - 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - 'id5id': { uid: '1111', ext: { linkType: 2, abTestingControlGroup: false } }, - 'idl_env': '3333', - 'parrableid': 'eidVersion.encryptionKeyReference.encryptedValue', - 'pubcid': '5555', - 'tdid': '6666', - 'sharedid': {'id': '01EAJWWNEPN3CYMM5N8M5VXY22', 'third': '01EAJWWNEPN3CYMM5N8M5VXY22'} - }; - bidRequests[0]['userIdAsEids'] = validBidRequestsWithUserIdData[0]['userIdAsEids']; - const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = JSON.parse(request.data); - let firstBid = payload.imp[0].ext.newspassid; - expect(firstBid).to.not.have.property('userId'); - delete validBidRequests[0].userId; // tidy up now, else it will screw with other tests - }); - it('should pick up the value of pubcid when built using the pubCommonId module (not userId)', function () { - let bidRequests = validBidRequests; - bidRequests[0]['userId'] = { - 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - 'id5id': { uid: '1111', ext: { linkType: 2, abTestingControlGroup: false } }, - 'idl_env': '3333', - 'parrableid': 'eidVersion.encryptionKeyReference.encryptedValue', - 'tdid': '6666', - 'sharedid': {'id': '01EAJWWNEPN3CYMM5N8M5VXY22', 'third': '01EAJWWNEPN3CYMM5N8M5VXY22'} - }; - bidRequests[0]['userIdAsEids'] = validBidRequestsWithUserIdData[0]['userIdAsEids']; - const request = spec.buildRequests(bidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - expect(payload.ext.newspassid.pubcid).to.equal(bidRequests[0]['crumbs']['pubcid']); - delete validBidRequests[0].userId; // tidy up now, else it will screw with other tests - }); - it('should add a user.ext.eids object to contain user ID data in the new location (Nov 2019) Updated Aug 2020', function() { - const request = spec.buildRequests(validBidRequestsWithUserIdData, validBidderRequest); - const payload = JSON.parse(request.data); - expect(payload.user).to.exist; - expect(payload.user.ext).to.exist; - expect(payload.user.ext.eids).to.exist; - expect(payload.user.ext.eids[0]['source']).to.equal('pubcid.org'); - expect(payload.user.ext.eids[0]['uids'][0]['id']).to.equal('12345678'); - expect(payload.user.ext.eids[1]['source']).to.equal('adserver.org'); - expect(payload.user.ext.eids[1]['uids'][0]['id']).to.equal('1111tdid'); - expect(payload.user.ext.eids[2]['source']).to.equal('id5-sync.com'); - expect(payload.user.ext.eids[2]['uids'][0]['id']).to.equal('ID5-someId'); - expect(payload.user.ext.eids[3]['source']).to.equal('criteoId'); - expect(payload.user.ext.eids[3]['uids'][0]['id']).to.equal('1111criteoId'); - expect(payload.user.ext.eids[4]['source']).to.equal('idl_env'); - expect(payload.user.ext.eids[4]['uids'][0]['id']).to.equal('liverampId'); - expect(payload.user.ext.eids[5]['source']).to.equal('lipb'); - expect(payload.user.ext.eids[5]['uids'][0]['id']['lipbid']).to.equal('lipbidId123'); - expect(payload.user.ext.eids[6]['source']).to.equal('parrableId'); - expect(payload.user.ext.eids[6]['uids'][0]['id']['eid']).to.equal('01.5678.parrableid'); - }); - it('replaces the auction url for a config override', function () { - spec.propertyBag.config = null; - let fakeOrigin = 'http://sometestendpoint'; - config.setConfig({'newspassid': {'endpointOverride': {'origin': fakeOrigin}}}); - const request = spec.buildRequests(validBidRequests, validBidderRequest); - expect(request.url).to.equal(fakeOrigin + '/openrtb2/auction'); - expect(request.method).to.equal('POST'); - const data = JSON.parse(request.data); - expect(data.ext.newspassid.origin).to.equal(fakeOrigin); - config.setConfig({'newspassid': {'kvpPrefix': null, 'endpointOverride': null}}); - }); - it('replaces the FULL auction url for a config override', function () { - spec.propertyBag.config = null; - let fakeurl = 'http://sometestendpoint/myfullurl'; - config.setConfig({'newspassid': {'endpointOverride': {'auctionUrl': fakeurl}}}); - const request = spec.buildRequests(validBidRequests, validBidderRequest); - expect(request.url).to.equal(fakeurl); - expect(request.method).to.equal('POST'); - const data = JSON.parse(request.data); - expect(data.ext.newspassid.origin).to.equal(fakeurl); - config.setConfig({'newspassid': {'kvpPrefix': null, 'endpointOverride': null}}); - }); - it('should ignore kvpPrefix', function () { - spec.propertyBag.config = null; - config.setConfig({'newspassid': {'kvpPrefix': 'np'}}); - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validResponse, request); - expect(result[0].adserverTargeting).to.have.own.property('np_appnexus_crid'); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_appnexus_crid')).to.equal('98493581'); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_adId')).to.equal('2899ec066a91ff8-0-np-0'); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_size')).to.equal('300x600'); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_pb_r')).to.equal('0.50'); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_bid')).to.equal('true'); + + describe('resolveNewpassidPublisherId', function() { + afterEach(() => { config.resetConfig(); }); - it('should create a meta object on each bid returned', function () { - spec.propertyBag.config = null; - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validResponse, request); - expect(result[0]).to.have.own.property('meta'); - expect(result[0].meta.advertiserDomains[0]).to.equal('http://prebid.org'); - config.resetConfig(); - }); - it('should use nptestmode GET value if set', function() { - var specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {'nptestmode': 'mytestvalue_123'}; - }; - const request = specMock.buildRequests(validBidRequests, validBidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].ext.newspassid.customData).to.be.an('array'); - expect(data.imp[0].ext.newspassid.customData[0].targeting.nptestmode).to.equal('mytestvalue_123'); - }); - it('should pass through GET params if present: npf, nppf, nprp, npip', function() { - var specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {npf: '1', nppf: '0', nprp: '2', npip: '123'}; - }; - const request = specMock.buildRequests(validBidRequests, validBidderRequest); - const data = JSON.parse(request.data); - expect(data.ext.newspassid.npf).to.equal(1); - expect(data.ext.newspassid.nppf).to.equal(0); - expect(data.ext.newspassid.nprp).to.equal(2); - expect(data.ext.newspassid.npip).to.equal(123); - }); - it('should pass through GET params if present: npf, nppf, nprp, npip with alternative values', function() { - var specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {npf: 'false', nppf: 'true', nprp: 'xyz', npip: 'hello'}; - }; - const request = specMock.buildRequests(validBidRequests, validBidderRequest); - const data = JSON.parse(request.data); - expect(data.ext.newspassid.npf).to.equal(0); - expect(data.ext.newspassid.nppf).to.equal(1); - expect(data.ext.newspassid).to.not.haveOwnProperty('nprp'); - expect(data.ext.newspassid).to.not.haveOwnProperty('npip'); + + it('should return null if no bidrequest object or no global publisherId set', function() { + expect(resolveNewpassidPublisherId()).to.equal(null); }); - it('should use nptestmode GET value if set, even if there is no customdata in config', function() { - var specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {'nptestmode': 'mytestvalue_123'}; - }; - const request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].ext.newspassid.customData).to.be.an('array'); - expect(data.imp[0].ext.newspassid.customData[0].targeting.nptestmode).to.equal('mytestvalue_123'); + + it('should return global publisherId if no bidrequest object and global publisherId set', function() { + config.setConfig({ + newspassid: { + publisherId: TEST_PUBLISHER_ID + } + }); + expect(resolveNewpassidPublisherId()).to.equal(TEST_PUBLISHER_ID); }); - it('should use GET values auction=[encoded URL] & cookiesync=[encoded url] if set', function() { - spec.propertyBag.config = null; - var specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {}; - }; - let request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest); - let url = request.url; - expect(url).to.equal('https://bidder.newspassid.com/openrtb2/auction'); - let cookieUrl = specMock.getCookieSyncUrl(); - expect(cookieUrl).to.equal('https://bidder.newspassid.com/static/load-cookie.html'); - specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {'auction': 'https://www.someurl.com/auction', 'cookiesync': 'https://www.someurl.com/sync'}; - }; - request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest); - url = request.url; - expect(url).to.equal('https://www.someurl.com/auction'); - cookieUrl = specMock.getCookieSyncUrl(); - expect(cookieUrl).to.equal('https://www.someurl.com/sync'); + }); + + describe('isBidRequestValid', function() { + it('should return true when required params are present', function() { + expect(spec.isBidRequestValid(validBidRequest)).to.be.true; + }); + + it('should return false when publisherId is missing', function() { + const bid = deepClone(validBidRequest); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when placementId is missing', function() { + const bid = deepClone(validBidRequest); + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; }); - it('should use a valid npstoredrequest GET value if set to override the placementId values, and set np_rw if we find it', function() { - var specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {'npstoredrequest': '1122334455'}; // 10 digits are valid + }); + + describe('buildRequests', function() { + it('should create request data', function() { + const requests = spec.buildRequests([validBidRequest], validBidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.equal('https://npid.amspbs.com/v0/bid/request'); + expect(requests[0].options.withCredentials).to.be.true; + }); + + it('should include bidder params in ortb2 request', function() { + const requests = spec.buildRequests([validBidRequest], validBidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.newspassid.publisher).to.equal(TEST_PUBLISHER_ID); + expect(data.imp[0].ext.newspassid.placementId).to.equal(TEST_PLACEMENT_ID); + }); + + it('should use global publisherId when not set in bid params', function() { + const validBidRequestWithoutPublisherId = { + ...validBidRequest, + params: { + placementId: TEST_PLACEMENT_ID + }, }; - const request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest); - const data = JSON.parse(request.data); - expect(data.ext.newspassid.np_rw).to.equal(1); - expect(data.imp[0].ext.prebid.storedrequest.id).to.equal('1122334455'); - }); - it('should NOT use an invalid npstoredrequest GET value if set to override the placementId values, and set np_rw to 0', function() { - var specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {'npstoredrequest': 'BADVAL'}; // 10 digits are valid + config.setConfig({ + newspassid: { + publisherId: TEST_PUBLISHER_ID + } + }); + const requests = spec.buildRequests([validBidRequestWithoutPublisherId], validBidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.newspassid.publisher).to.equal(TEST_PUBLISHER_ID); + expect(data.imp[0].ext.newspassid.placementId).to.equal(TEST_PLACEMENT_ID); + }); + + it('should use publisherId from bidRequest first over global publisherId', function() { + config.setConfig({ + newspassid: { + publisherId: TEST_PUBLISHER_ID + } + }); + const validBidRequestWithDifferentPublisherId = { + ...validBidRequest, + params: { + publisherId: 'publisherId123' + } }; - const request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest); - const data = JSON.parse(request.data); - expect(data.ext.newspassid.np_rw).to.equal(0); - expect(data.imp[0].ext.prebid.storedrequest.id).to.equal('1310000099'); - }); - it('should pick up the config value of coppa & set it in the request', function () { - config.setConfig({'coppa': true}); - const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); - const payload = JSON.parse(request.data); - expect(payload.regs).to.include.keys('coppa'); - expect(payload.regs.coppa).to.equal(1); - config.resetConfig(); - }); - it('should pick up the config value of coppa & only set it in the request if its true', function () { - config.setConfig({'coppa': false}); - const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); - const payload = JSON.parse(request.data); - expect(utils.deepAccess(payload, 'regs.coppa')).to.be.undefined; - config.resetConfig(); - }); - it('should should contain a unique page view id in the auction request which persists across calls', function () { - let request = spec.buildRequests(validBidRequests, validBidderRequest); - let payload = JSON.parse(request.data); - expect(utils.deepAccess(payload, 'ext.newspassid.pv')).to.be.a('string'); - request = spec.buildRequests(validBidRequestsIsThisCamelCaseEnough, validBidderRequest); - let payload2 = JSON.parse(request.data); - expect(utils.deepAccess(payload2, 'ext.newspassid.pv')).to.be.a('string'); - expect(utils.deepAccess(payload2, 'ext.newspassid.pv')).to.equal(utils.deepAccess(payload, 'ext.newspassid.pv')); + const requests = spec.buildRequests([validBidRequestWithDifferentPublisherId], validBidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.newspassid.publisher).to.equal('publisherId123'); + }); + + it('should handle multiple bid requests', function() { + const secondBidRequest = deepClone(validBidRequest); + secondBidRequest.bidId = '790'; + const requests = spec.buildRequests([validBidRequest, secondBidRequest], validBidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].data.imp).to.have.lengthOf(2); }); - it('should indicate that the whitelist was used when it contains valid data', function () { - config.setConfig({'newspassid': {'np_whitelist_adserver_keys': ['np_appnexus_pb', 'np_appnexus_imp_id']}}); - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - expect(payload.ext.newspassid.np_kvp_rw).to.equal(1); - config.resetConfig(); + }); + + describe('interpretResponse', function() { + it('should return empty array if no valid bids', function() { + const invalidResponse = {body: {}}; + const bids = spec.interpretResponse(invalidResponse); + expect(bids).to.be.empty; + }); + + it('should return empty array if no seatbid', function() { + const noSeatbidResponse = {body: {cur: 'USD'}}; + const bids = spec.interpretResponse(noSeatbidResponse); + expect(bids).to.be.empty; + }); + + it('should interpret valid server response', function() { + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(1); + expect(bids[0]).to.deep.equal({ + requestId: '789', + cpm: 2.5, + width: 300, + height: 250, + creativeId: 'creative123', + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: '
ad
', + meta: { + advertiserDomains: ['advertiser.com'] + } + }); }); - it('should indicate that the whitelist was not used when it contains no data', function () { - config.setConfig({'newspassid': {'np_whitelist_adserver_keys': []}}); - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - expect(payload.ext.newspassid.np_kvp_rw).to.equal(0); + }); + + describe('getUserSyncs', function() { + afterEach(() => { config.resetConfig(); }); - it('should indicate that the whitelist was not used when it is not set in the config', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const payload = JSON.parse(request.data); - expect(payload.ext.newspassid.np_kvp_rw).to.equal(0); - }); - it('should handle ortb2 site data', function () { - let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); - bidderRequest.ortb2 = { - 'site': { - 'name': 'example_ortb2_name', - 'domain': 'page.example.com', - 'cat': ['IAB2'], - 'sectioncat': ['IAB2-2'], - 'pagecat': ['IAB2-2'], - 'page': 'https://page.example.com/here.html', - 'ref': 'https://ref.example.com', - 'keywords': 'power tools, drills', - 'search': 'drill' + + it('should expect correct host', function() { + const syncs = spec.getUserSyncs({iframeEnabled: true}, [], {}, '', {}); + const url = new URL(syncs[0].url); + expect(url.host).to.equal('npid.amspbs.com'); + }); + + it('should expect correct pathname', function() { + const syncs = spec.getUserSyncs({iframeEnabled: true}, [], {}, '', {}); + const url = new URL(syncs[0].url); + expect(url.pathname).to.equal('/v0/user/sync'); + }); + + it('should return empty array when iframe sync option is disabled', function() { + const syncs = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(syncs).to.be.empty; + }); + + it('should use iframe sync when iframe enabled', function() { + const syncs = spec.getUserSyncs({iframeEnabled: true}); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://npid.amspbs.com/v0/user/sync?gdpr=0&gdpr_consent=&gpp=&gpp_sid=&us_privacy='); + }); + + it('should include GDPR params if purpose 1 is true', function() { + const consentString = 'CQO03QAQO03QAPoABABGBiEIAIAAAIAAAACQKSwAQKSgpLABApKAAAAA.QKSwAQKSgAAA.IAAA'; // purpose 1 true + const gdprConsent = { + gdprApplies: true, + consentString: consentString, + vendorData: { + purpose: { + consents: { + 1: true + } + } } }; - const request = spec.buildRequests(validBidRequests, bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.imp[0].ext.newspassid.customData[0].targeting.name).to.equal('example_ortb2_name'); - expect(payload.user.ext).to.not.have.property('gender'); - }); - it('should add ortb2 site data when there is no customData already created', function () { - let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); - bidderRequest.ortb2 = { - 'site': { - 'name': 'example_ortb2_name', - 'domain': 'page.example.com', - 'cat': ['IAB2'], - 'sectioncat': ['IAB2-2'], - 'pagecat': ['IAB2-2'], - 'page': 'https://page.example.com/here.html', - 'ref': 'https://ref.example.com', - 'keywords': 'power tools, drills', - 'search': 'drill' + const syncs = spec.getUserSyncs({iframeEnabled: true}, [], gdprConsent); + const url = new URL(syncs[0].url); + expect(url.searchParams.get('gdpr')).to.equal('1'); + expect(url.searchParams.get('gdpr_consent')).to.equal(encodeURIComponent(consentString)); + }); + + it('should disable user sync when purpose 1 is false', function() { + const consentString = 'CQO03QAQO03QAPoABABGBiEIAHAAAHAAAACQKSwAQKSgpLABApKAAAAA.QKSwAQKSgAAA.IAAA'; // purpose 1 false + const gdprConsent = { + gdprApplies: true, + consentString: consentString, + vendorData: { + purpose: { + consents: { + 1: false + } + } } }; - const request = spec.buildRequests(validBidRequestsNoCustomData, bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.imp[0].ext.newspassid.customData[0].targeting.name).to.equal('example_ortb2_name'); - expect(payload.imp[0].ext.newspassid.customData[0].targeting).to.not.have.property('gender') - }); - it('should add ortb2 user data to the user object', function () { - let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); - bidderRequest.ortb2 = { - 'user': { - 'gender': 'I identify as a box of rocks' - } + const syncs = spec.getUserSyncs({iframeEnabled: true}, [], gdprConsent); + expect(syncs).to.be.empty; + }); + + it('should include correct us_privacy param', function() { + const uspConsent = '1YNN'; + const syncs = spec.getUserSyncs({iframeEnabled: true}, [], {}, uspConsent, {}); + const url = new URL(syncs[0].url); + expect(url.searchParams.get('gdpr')).to.equal('0'); + expect(url.searchParams.get('gdpr_consent')).to.equal(''); + expect(url.searchParams.get('gpp')).to.equal(''); + expect(url.searchParams.get('gpp_sid')).to.equal(''); + expect(url.searchParams.get('us_privacy')).to.equal(uspConsent); + }); + + it('should include correct GPP params', function() { + const gppConsentString = 'DBABMA~1YNN'; + const gppSections = '2,6'; + const gppConsent = { + gppApplies: true, + gppString: gppConsentString, + applicableSections: gppSections }; - const request = spec.buildRequests(validBidRequests, bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.user.gender).to.equal('I identify as a box of rocks'); - }); - it('handles schain object in each bidrequest (will be the same in each br)', function () { - let br = JSON.parse(JSON.stringify(validBidRequests)); - let schainConfigObject = { - 'ver': '1.0', - 'complete': 1, - 'nodes': [ - { - 'asi': 'bidderA.com', - 'sid': '00001', - 'hp': 1 + const syncs = spec.getUserSyncs({iframeEnabled: true}, [], {}, '', gppConsent); + const url = new URL(syncs[0].url); + expect(url.searchParams.get('gdpr')).to.equal('0'); + expect(url.searchParams.get('gdpr_consent')).to.equal(''); + expect(url.searchParams.get('gpp')).to.equal(encodeURIComponent(gppConsentString)); + expect(url.searchParams.get('gpp_sid')).to.equal(encodeURIComponent(gppSections)); + expect(url.searchParams.get('us_privacy')).to.equal(''); + }); + + it('should include publisher param when publisherId is set in config', function() { + config.setConfig({ + newspassid: { + publisherId: TEST_PUBLISHER_ID + } + }); + const syncs = spec.getUserSyncs({iframeEnabled: true}); + const url = new URL(syncs[0].url); + expect(url.searchParams.get('gdpr')).to.equal('0'); + expect(url.searchParams.get('gdpr_consent')).to.equal(''); + expect(url.searchParams.get('gpp')).to.equal(''); + expect(url.searchParams.get('gpp_sid')).to.equal(''); + expect(url.searchParams.get('us_privacy')).to.equal(''); + expect(url.searchParams.get('publisher')).to.equal(encodeURIComponent(TEST_PUBLISHER_ID)); + }); + + it('should have zero user syncs if coppa is true', function() { + config.setConfig({coppa: true}); + const syncs = spec.getUserSyncs({iframeEnabled: true}); + expect(syncs).to.be.empty; + }); + + it('should include all params when all are present', function() { + const consentString = 'CQO03QAQO03QAPoABABGBiEIAIAAAIAAAACQKSwAQKSgpLABApKAAAAA.QKSwAQKSgAAA.IAAA'; // purpose 1 true + const gdprConsent = { + gdprApplies: true, + consentString: consentString, + vendorData: { + purpose: { + consents: { + 1: true + } } - ] + } }; - br[0]['schain'] = schainConfigObject; - const request = spec.buildRequests(br, validBidderRequest); - const data = JSON.parse(request.data); - expect(data.source.ext).to.haveOwnProperty('schain'); - expect(data.source.ext.schain).to.deep.equal(schainConfigObject); // .deep.equal() : Target object deeply (but not strictly) equals `{a: 1}` - }); - }); - describe('interpretResponse', function () { - it('should build bid array', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validResponse, request); - expect(result.length).to.equal(1); - }); - it('should have all relevant fields', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validResponse, request); - const bid = result[0]; - expect(bid.cpm).to.equal(validResponse.body.seatbid[0].bid[0].cpm); - expect(bid.width).to.equal(validResponse.body.seatbid[0].bid[0].width); - expect(bid.height).to.equal(validResponse.body.seatbid[0].bid[0].height); - }); - it('should build bid array with usp/CCPA', function () { - let validBR = JSON.parse(JSON.stringify(validBidderRequest)); - validBR.uspConsent = '1YNY'; - const request = spec.buildRequests(validBidRequests, validBR); - const payload = JSON.parse(request.data); - expect(payload.user.ext.uspConsent).not.to.exist; - expect(payload.regs.ext.us_privacy).to.equal('1YNY'); - }); - it('should fail ok if no seatbid in server response', function () { - const result = spec.interpretResponse({}, {}); - expect(result).to.be.an('array'); - expect(result).to.be.empty; - }); - it('should fail ok if seatbid is not an array', function () { - const result = spec.interpretResponse({'body': {'seatbid': 'nothing_here'}}, {}); - expect(result).to.be.an('array'); - expect(result).to.be.empty; - }); - it('should correctly parse response where there are more bidders than ad slots', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validBidResponse1adWith2Bidders, request); - expect(result.length).to.equal(2); - }); - it('should have a ttl of 600', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validResponse, request); - expect(result[0].ttl).to.equal(300); - }); - it('should handle a valid whitelist, removing items not on the list & leaving others', function () { - config.setConfig({'newspassid': {'np_whitelist_adserver_keys': ['np_appnexus_crid', 'np_appnexus_adId']}}); - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validResponse, request); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_appnexus_adv')).to.be.undefined; - expect(utils.deepAccess(result[0].adserverTargeting, 'np_appnexus_adId')).to.equal('2899ec066a91ff8-0-np-0'); - config.resetConfig(); - }); - it('should ignore a whitelist if enhancedAdserverTargeting is false', function () { - config.setConfig({'newspassid': {'np_whitelist_adserver_keys': ['np_appnexus_crid', 'np_appnexus_imp_id'], 'enhancedAdserverTargeting': false}}); - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validResponse, request); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_appnexus_adv')).to.be.undefined; - expect(utils.deepAccess(result[0].adserverTargeting, 'np_appnexus_imp_id')).to.be.undefined; - config.resetConfig(); - }); - it('should correctly handle enhancedAdserverTargeting being false', function () { - config.setConfig({'newspassid': {'enhancedAdserverTargeting': false}}); - const request = spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.interpretResponse(validResponse, request); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_appnexus_adv')).to.be.undefined; - expect(utils.deepAccess(result[0].adserverTargeting, 'np_appnexus_imp_id')).to.be.undefined; - config.resetConfig(); - }); - it('should add unique adId values to each bid', function() { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - let validres = JSON.parse(JSON.stringify(validResponse2BidsSameAdunit)); - const result = spec.interpretResponse(validres, request); - expect(result.length).to.equal(1); - expect(result[0]['price']).to.equal(0.9); - expect(result[0]['adserverTargeting']['np_npappnexus_adId']).to.equal('2899ec066a91ff8-0-np-1'); - }); - it('should add np_auc_id (response id value)', function () { - const request = spec.buildRequests(validBidRequests, validBidderRequest); - let validres = JSON.parse(JSON.stringify(validBidResponse1adWith2Bidders)); - const result = spec.interpretResponse(validres, request); - expect(utils.deepAccess(result[0].adserverTargeting, 'np_auc_id')).to.equal(validBidResponse1adWith2Bidders.body.id); - }); - it('should correctly process an auction with 2 adunits & multiple bidders one of which bids for both adslots', function() { - let validres = JSON.parse(JSON.stringify(multiResponse1)); - let request = spec.buildRequests(multiRequest1, multiBidderRequest1.bidderRequest); - let result = spec.interpretResponse(validres, request); - expect(result.length).to.equal(4); // one of the 5 bids will have been removed - expect(result[1]['price']).to.equal(0.521); - expect(result[1]['impid']).to.equal('3025f169863b7f8'); - expect(result[1]['id']).to.equal('18552976939844999'); - expect(result[1]['adserverTargeting']['np_npappnexus_adId']).to.equal('3025f169863b7f8-0-np-2'); - validres = JSON.parse(JSON.stringify(multiResponse1)); - validres.body.seatbid[0].bid[1].price = 1.1; - validres.body.seatbid[0].bid[1].cpm = 1.1; - request = spec.buildRequests(multiRequest1, multiBidderRequest1.bidderRequest); - result = spec.interpretResponse(validres, request); - expect(result[1]['price']).to.equal(1.1); - expect(result[1]['impid']).to.equal('3025f169863b7f8'); - expect(result[1]['id']).to.equal('18552976939844681'); - expect(result[1]['adserverTargeting']['np_npappnexus_adId']).to.equal('3025f169863b7f8-0-np-1'); - }); - }); - describe('userSyncs', function () { - it('should fail gracefully if no server response', function () { - const result = spec.getUserSyncs('bad', false, emptyObject); - expect(result).to.be.empty; - }); - it('should fail gracefully if server response is empty', function () { - const result = spec.getUserSyncs('bad', [], emptyObject); - expect(result).to.be.empty; - }); - it('should append the various values if they exist', function() { - spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.getUserSyncs({iframeEnabled: true}, 'good server response', emptyObject); - expect(result).to.be.an('array'); - expect(result[0].url).to.include('publisherId=9876abcd12-3'); - expect(result[0].url).to.include('siteId=1234567890'); - }); - it('should append ccpa (usp data)', function() { - spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.getUserSyncs({iframeEnabled: true}, 'good server response', emptyObject, '1YYN'); - expect(result).to.be.an('array'); - expect(result[0].url).to.include('usp_consent=1YYN'); - }); - it('should use "" if no usp is sent to cookieSync', function() { - spec.buildRequests(validBidRequests, validBidderRequest); - const result = spec.getUserSyncs({iframeEnabled: true}, 'good server response', emptyObject); - expect(result).to.be.an('array'); - expect(result[0].url).to.include('usp_consent=&'); - }); - }); - describe('default size', function () { - it('should should return default sizes if no obj is sent', function () { - let obj = ''; - const result = defaultSize(obj); - expect(result.defaultHeight).to.equal(250); - expect(result.defaultWidth).to.equal(300); - }); - }); - describe('getGranularityKeyName', function() { - it('should return a string granularity as-is', function() { - const result = getGranularityKeyName('', 'this is it', ''); - expect(result).to.equal('this is it'); - }); - it('should return "custom" for a mediaTypeGranularity object', function() { - const result = getGranularityKeyName('', {}, ''); - expect(result).to.equal('custom'); - }); - it('should return "custom" for a mediaTypeGranularity object', function() { - const result = getGranularityKeyName('', false, 'string buckets'); - expect(result).to.equal('string buckets'); - }); - }); - describe('getGranularityObject', function() { - it('should return an object as-is', function() { - const result = getGranularityObject('', {'name': 'mark'}, '', ''); - expect(result.name).to.equal('mark'); - }); - it('should return an object as-is', function() { - const result = getGranularityObject('', false, 'custom', {'name': 'rupert'}); - expect(result.name).to.equal('rupert'); - }); - }); - describe('blockTheRequest', function() { - it('should return true if np_request is false', function() { - config.setConfig({'newspassid': {'np_request': false}}); - let result = spec.blockTheRequest(); - expect(result).to.be.true; - config.resetConfig(); - }); - it('should return false if np_request is true', function() { - config.setConfig({'newspassid': {'np_request': true}}); - let result = spec.blockTheRequest(); - expect(result).to.be.false; - config.resetConfig(); - }); - }); - describe('getPageId', function() { - it('should return the same Page ID for multiple calls', function () { - let result = spec.getPageId(); - expect(result).to.be.a('string'); - let result2 = spec.getPageId(); - expect(result2).to.equal(result); - }); - }); - describe('getBidRequestForBidId', function() { - it('should locate a bid inside a bid array', function () { - let result = spec.getBidRequestForBidId('2899ec066a91ff8', validBidRequestsMulti); - expect(result.testId).to.equal(1); - result = spec.getBidRequestForBidId('2899ec066a91ff0', validBidRequestsMulti); - expect(result.testId).to.equal(2); - }); - }); - describe('removeSingleBidderMultipleBids', function() { - it('should remove the multi bid by npappnexus for adslot 2d30e86db743a8', function() { - let validres = JSON.parse(JSON.stringify(multiResponse1)); - expect(validres.body.seatbid[0].bid.length).to.equal(3); - expect(validres.body.seatbid[0].seat).to.equal('npappnexus'); - let response = spec.removeSingleBidderMultipleBids(validres.body.seatbid); - expect(response.length).to.equal(2); - expect(response[0].bid.length).to.equal(2); - expect(response[0].seat).to.equal('npappnexus'); - expect(response[1].bid.length).to.equal(2); + const uspConsent = '1YNN'; + const gppConsentString = 'DBABMA~1YNN'; + const gppSections = '2,6'; + const gppConsent = { + gppApplies: true, + gppString: gppConsentString, + applicableSections: gppSections + }; + config.setConfig({ + newspassid: { + publisherId: TEST_PUBLISHER_ID + } + }); + const syncs = spec.getUserSyncs({iframeEnabled: true}, [], gdprConsent, uspConsent, gppConsent); + const url = new URL(syncs[0].url); + expect(url.searchParams.get('gdpr')).to.equal('1'); + expect(url.searchParams.get('gdpr_consent')).to.equal(encodeURIComponent(consentString)); + expect(url.searchParams.get('gpp')).to.equal(encodeURIComponent(gppConsentString)); + expect(url.searchParams.get('gpp_sid')).to.equal(encodeURIComponent(gppSections)); + expect(url.searchParams.get('us_privacy')).to.equal(encodeURIComponent(uspConsent)); + expect(url.searchParams.get('publisher')).to.equal(encodeURIComponent(TEST_PUBLISHER_ID)); }); }); }); diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index ff58671b17b..c7fabd562c7 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -1,6 +1,8 @@ import { expect } from 'chai'; import { getImp, + setImpPos, + getSourceObj, replaceUsersyncMacros, setConsentStrings, setOrtb2Parameters, @@ -15,9 +17,11 @@ describe('nextMillenniumBidAdapterTests', () => { title: 'imp - banner', data: { id: '123', + postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, bid: { mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, adUnitCode: 'test-banner-1', + bidId: 'e36ea395f67f', }, mediaTypes: { @@ -25,16 +29,22 @@ describe('nextMillenniumBidAdapterTests', () => { data: {sizes: [[300, 250], [320, 250]]}, bidfloorcur: 'EUR', bidfloor: 1.11, + pos: 3, }, }, }, expected: { - id: 'test-banner-1', + id: 'e36ea395f67f', bidfloorcur: 'EUR', bidfloor: 1.11, ext: {prebid: {storedrequest: {id: '123'}}}, - banner: {w: 300, h: 250, format: [{w: 300, h: 250}, {w: 320, h: 250}]}, + banner: { + pos: 3, + w: 300, + h: 250, + format: [{w: 300, h: 250}, {w: 320, h: 250}], + }, }, }, @@ -42,21 +52,24 @@ describe('nextMillenniumBidAdapterTests', () => { title: 'imp - video', data: { id: '234', + postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, bid: { mediaTypes: {video: {playerSize: [400, 300], api: [2], placement: 1, plcmt: 1}}, adUnitCode: 'test-video-1', + bidId: 'e36ea395f67f', }, mediaTypes: { video: { data: {playerSize: [400, 300], api: [2], placement: 1, plcmt: 1}, bidfloorcur: 'USD', + pos: 0, }, }, }, expected: { - id: 'test-video-1', + id: 'e36ea395f67f', bidfloorcur: 'USD', ext: {prebid: {storedrequest: {id: '234'}}}, video: { @@ -66,6 +79,7 @@ describe('nextMillenniumBidAdapterTests', () => { plcmt: 1, w: 400, h: 300, + pos: 0, }, }, }, @@ -74,9 +88,10 @@ describe('nextMillenniumBidAdapterTests', () => { title: 'imp - mediaTypes.video is empty', data: { id: '234', + postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, bid: { mediaTypes: {video: {w: 640, h: 480}}, - adUnitCode: 'test-video-2', + bidId: 'e36ea395f67f', }, mediaTypes: { @@ -88,23 +103,224 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { - id: 'test-video-2', + id: 'e36ea395f67f', bidfloorcur: 'USD', ext: {prebid: {storedrequest: {id: '234'}}}, video: {w: 640, h: 480, mimes: ['video/mp4', 'video/x-ms-wmv', 'application/javascript']}, }, }, + + { + title: 'imp with gpid', + data: { + id: '123', + postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, + bid: { + mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, + adUnitCode: 'test-gpid-1', + bidId: 'e36ea395f67a', + ortb2Imp: {ext: {gpid: 'imp-gpid-123'}}, + }, + + mediaTypes: { + banner: { + data: {sizes: [[300, 250], [320, 250]]}, + }, + }, + }, + + expected: { + id: 'e36ea395f67a', + ext: { + prebid: {storedrequest: {id: '123'}}, + gpid: 'imp-gpid-123' + }, + banner: {w: 300, h: 250, format: [{w: 300, h: 250}, {w: 320, h: 250}]}, + }, + }, + + { + title: 'imp with pbadslot', + data: { + id: '123', + postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, + bid: { + mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, + adUnitCode: 'test-gpid-1', + bidId: 'e36ea395f67a', + ortb2Imp: { + ext: { + data: { + pbadslot: 'slot-123' + } + } + }, + }, + + mediaTypes: { + banner: { + data: {sizes: [[300, 250], [320, 250]]}, + }, + }, + }, + + expected: { + id: 'e36ea395f67a', + ext: { + prebid: {storedrequest: {id: '123'}}, + data: { + pbadslot: 'slot-123' + } + }, + banner: {w: 300, h: 250, format: [{w: 300, h: 250}, {w: 320, h: 250}]}, + }, + }, ]; for (let {title, data, expected} of dataTests) { it(title, () => { - const {bid, id, mediaTypes} = data; - const imp = getImp(bid, id, mediaTypes); + const {bid, id, mediaTypes, postBody} = data; + const imp = getImp(bid, id, mediaTypes, postBody); expect(imp).to.deep.equal(expected); }); } }); + describe('function setImpPos', () => { + const tests = [ + { + title: 'position is - 1', + pos: 0, + expected: {pos: 0}, + }, + + { + title: 'position is - 2', + pos: 7, + expected: {pos: 7}, + }, + + { + title: 'position is empty', + expected: {}, + }, + ]; + + for (const {title, pos, expected} of tests) { + it(title, () => { + const obj = {}; + setImpPos(obj, pos); + expect(obj).to.deep.equal(expected); + }); + }; + }); + + describe('function getSourceObj', () => { + const dataTests = [ + { + title: 'schain is empty', + validBidRequests: [{}], + bidderRequest: {}, + expected: undefined, + }, + + { + title: 'schain is validBidReequest', + bidderRequest: {}, + validBidRequests: [{ + schain: { + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [{asi: 'test.test', sid: '00001', hp: 1}], + }, + }, + }], + + expected: { + schain: { + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [{asi: 'test.test', sid: '00001', hp: 1}], + }, + }, + }, + }, + + { + title: 'schain is bidderReequest.ortb2.source.schain', + bidderRequest: { + ortb2: { + source: { + schain: { + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [{asi: 'test.test', sid: '00001', hp: 1}], + }, + }, + }, + }, + }, + + validBidRequests: [{}], + expected: { + schain: { + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [{asi: 'test.test', sid: '00001', hp: 1}], + }, + }, + }, + }, + + { + title: 'schain is bidderReequest.ortb2.source.ext.schain', + bidderRequest: { + ortb2: { + source: { + ext: { + schain: { + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [{asi: 'test.test', sid: '00001', hp: 1}], + }, + }, + }, + }, + }, + }, + + validBidRequests: [{}], + expected: { + schain: { + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [{asi: 'test.test', sid: '00001', hp: 1}], + }, + }, + }, + }, + ]; + + for (let {title, validBidRequests, bidderRequest, expected} of dataTests) { + it(title, () => { + const source = getSourceObj(validBidRequests, bidderRequest); + expect(source).to.deep.equal(expected); + }); + } + }); + describe('function setConsentStrings', () => { const dataTests = [ { @@ -115,16 +331,18 @@ describe('nextMillenniumBidAdapterTests', () => { uspConsent: '1---', gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, - ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10]}}, + ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10], coppa: 1}}, }, }, expected: { - user: {ext: {consent: 'kjfdniwjnifwenrif3'}}, + user: {consent: 'kjfdniwjnifwenrif3'}, regs: { gpp: 'DBACNYA~CPXxRfAPXxR', gpp_sid: [7], - ext: {gdpr: 1, us_privacy: '1---'}, + gdpr: 1, + us_privacy: '1---', + coppa: 1 }, }, }, @@ -135,16 +353,17 @@ describe('nextMillenniumBidAdapterTests', () => { postBody: {}, bidderRequest: { gdprConsent: {consentString: 'ewtewbefbawyadexv', gdprApplies: false}, - ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10]}}, + ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10], coppa: 0}}, }, }, expected: { - user: {ext: {consent: 'ewtewbefbawyadexv'}}, + user: {consent: 'ewtewbefbawyadexv'}, regs: { gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10], - ext: {gdpr: 0}, + gdpr: 0, + coppa: 0, }, }, }, @@ -157,7 +376,7 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { - regs: {ext: {gdpr: 0}}, + regs: {gdpr: 0}, }, }, @@ -411,16 +630,27 @@ describe('nextMillenniumBidAdapterTests', () => { title: 'site.pagecat, site.content.cat and site.content.language', data: { postBody: {}, - ortb2: {site: { + ortb2: { + bcat: ['IAB1-3', 'IAB1-4'], + badv: ['domain1.com', 'domain2.com'], + wlang: ['en', 'fr', 'de'], + wlangb: ['en', 'fr', 'de'], + site: { + pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], + content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, + } + }, + }, + + expected: { + bcat: ['IAB1-3', 'IAB1-4'], + badv: ['domain1.com', 'domain2.com'], + wlang: ['en', 'fr', 'de'], + site: { pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, - }}, + } }, - - expected: {site: { - pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], - content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, - }}, }, { @@ -428,6 +658,7 @@ describe('nextMillenniumBidAdapterTests', () => { data: { postBody: {}, ortb2: { + wlangb: ['en', 'fr', 'de'], user: {keywords: 'key7,key8,key9'}, site: { keywords: 'key1,key2,key3', @@ -437,6 +668,7 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { + wlangb: ['en', 'fr', 'de'], user: {keywords: 'key7,key8,key9'}, site: { keywords: 'key1,key2,key3', @@ -485,9 +717,9 @@ describe('nextMillenniumBidAdapterTests', () => { title: 'setEids - userIdAsEids is empty', data: { postBody: {}, - bid: { + bids: [{ userIdAsEids: undefined, - }, + }], }, expected: {}, @@ -497,9 +729,9 @@ describe('nextMillenniumBidAdapterTests', () => { title: 'setEids - userIdAsEids - array is empty', data: { postBody: {}, - bid: { + bids: [{ userIdAsEids: [], - }, + }], }, expected: {}, @@ -509,19 +741,34 @@ describe('nextMillenniumBidAdapterTests', () => { title: 'setEids - userIdAsEids is', data: { postBody: {}, - bid: { - userIdAsEids: [ - { - source: '33across.com', - uids: [{id: 'some-random-id-value', atype: 1}], - }, + bids: [ + { + userIdAsEids: [], + }, - { - source: 'utiq.com', - uids: [{id: 'some-random-id-value', atype: 1}], - }, - ], - }, + { + userIdAsEids: [ + { + source: '33across.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + + { + source: 'utiq.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + ], + }, + + { + userIdAsEids: [ + { + source: 'test.test', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + ], + }, + ], }, expected: { @@ -544,273 +791,145 @@ describe('nextMillenniumBidAdapterTests', () => { for (let { title, data, expected } of dataTests) { it(title, () => { - const { postBody, bid } = data; - setEids(postBody, bid); + const { postBody, bids } = data; + setEids(postBody, bids); expect(postBody).to.deep.equal(expected); }); } }); - const bidRequestData = [{ - adUnitCode: 'test-div', - bidId: 'bid1234', - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidder: 'nextMillennium', - params: { placement_id: '-1' }, - sizes: [[300, 250]], - uspConsent: '1---', - gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, - gdprConsent: { - consentString: 'kjfdniwjnifwenrif3', - gdprApplies: true - }, - - ortb2: { - device: { - w: 1500, - h: 1000 - }, - - site: { - domain: 'example.com', - page: 'http://example.com' - } - } - }]; - - const serverResponse = { - body: { - id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', - seatbid: [ - { - bid: [ - { - id: '7457329903666272789', - price: 0.5, - adm: 'Hello! It\'s a test ad!', - adid: '96846035', - adomain: ['test.addomain.com'], - w: 300, - h: 250 - } - ] - } - ], - cur: 'USD', - ext: { - sync: { - image: ['urlA?gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}'], - iframe: ['urlB'], - } - } - } - }; - - const bidRequestDataGI = [ - { - adUnitCode: 'test-banner-gi', - bidId: 'bid1234', - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidder: 'nextMillennium', - params: { group_id: '1234' }, - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - }, + const bidRequestDataGI = getBidRequestDataGI(); + function getBidRequestDataGI(adUnitCodes = ['test-banner-gi', 'test-banner-gi', 'test-video-gi']) { + return [ + { + adUnitCode: adUnitCodes[0], + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, - sizes: [[300, 250]], - uspConsent: '1---', - gdprConsent: { - consentString: 'kjfdniwjnifwenrif3', - gdprApplies: true - } - }, - - { - adUnitCode: 'test-banner-gi', - bidId: 'bid1234', - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidder: 'nextMillennium', - params: { group_id: '1234' }, - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 300]] + sizes: [[300, 250]], + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true } }, - sizes: [[300, 250], [300, 300]], - uspConsent: '1---', - gdprConsent: { - consentString: 'kjfdniwjnifwenrif3', - gdprApplies: true - } - }, - - { - adUnitCode: 'test-video-gi', - bidId: 'bid1234', - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidder: 'nextMillennium', - params: { group_id: '1234' }, - mediaTypes: { - video: { - playerSize: [640, 480], + { + adUnitCode: adUnitCodes[1], + bidId: 'bid1235', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 300]] + } + }, + + sizes: [[300, 250], [300, 300]], + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true } }, - uspConsent: '1---', - gdprConsent: { - consentString: 'kjfdniwjnifwenrif3', - gdprApplies: true - } - }, - ]; + { + adUnitCode: adUnitCodes[2], + bidId: 'bid1236', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + video: { + playerSize: [640, 480], + } + }, - it('validate_generated_params', function() { - const request = spec.buildRequests(bidRequestData, {bidderRequestId: 'mock-uuid'}); - expect(request[0].bidId).to.equal('bid1234'); - expect(JSON.parse(request[0].data).id).to.exist; - }); + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }, + ]; + } - it('use parameters group_id', function() { + describe('check parameters group_id or placement_id', function() { + let numberTest = 0 for (let test of bidRequestDataGI) { - const request = spec.buildRequests([test]); - const requestData = JSON.parse(request[0].data); - const storeRequestId = requestData.ext.prebid.storedrequest.id; - const templateRE = /^g[1-9]\d*;(?:[1-9]\d*x[1-9]\d*\|)*[1-9]\d*x[1-9]\d*;/; - expect(templateRE.test(storeRequestId)).to.be.true; - }; - }); - - it('Check if refresh_count param is incremented', function() { - const request = spec.buildRequests(bidRequestData); - expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(1); - }); - - it('Check if domain was added', function() { - const request = spec.buildRequests(bidRequestData) - expect(JSON.parse(request[0].data).site.domain).to.exist - }) - - it('Check if elOffsets was added', function() { - const request = spec.buildRequests(bidRequestData) - expect(JSON.parse(request[0].data).ext.nextMillennium.elOffsets).to.be.an('object') - }) - - it('Check if imp object was added', function() { - const request = spec.buildRequests(bidRequestData) - expect(JSON.parse(request[0].data).imp).to.be.an('array') - }); - - it('Check if imp prebid stored id is correct', function() { - const request = spec.buildRequests(bidRequestData) - const requestData = JSON.parse(request[0].data); - const storedReqId = requestData.ext.prebid.storedrequest.id; - expect(requestData.imp[0].ext.prebid.storedrequest.id).to.equal(storedReqId) - }); - - it('validate_response_params', function() { - let bids = spec.interpretResponse(serverResponse, bidRequestData[0]); - expect(bids).to.have.lengthOf(1); - - let bid = bids[0]; - - expect(bid.creativeId).to.equal('96846035'); - expect(bid.ad).to.equal('Hello! It\'s a test ad!'); - expect(bid.cpm).to.equal(0.5); - expect(bid.width).to.equal(300); - expect(bid.height).to.equal(250); - expect(bid.currency).to.equal('USD'); - }); + it(`test - ${++numberTest}`, () => { + const request = spec.buildRequests([test]); + const requestData = JSON.parse(request[0].data); + const storeRequestId = (requestData.imp[0].ext.prebid.storedrequest.id || ''); + expect(storeRequestId.length).to.be.not.equal(0); + + const srId = storeRequestId.split(';'); + const isGroupId = (/^g[1-9]\d*/).test(srId[0]); + if (isGroupId) { + expect(srId.length).to.be.equal(3); + expect((/^g[1-9]\d*/).test(srId[0])).to.be.true; + const sizes = srId[1].split('|'); + for (let size of sizes) { + if (!(/^[1-9]\d*[xX,][1-9]\d*$/).test(size)) { + expect(storeRequestId).to.be.equal(''); + } - it('validate_videowrapper_response_params', function() { - const serverResponse = { - body: { - id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', - seatbid: [ - { - bid: [ - { - id: '7457329903666272789', - price: 0.5, - adm: 'https://some_vast_host.com/vast.xml', - adid: '96846035', - adomain: ['test.addomain.com'], - w: 300, - h: 250, - ext: { - prebid: { - type: 'video' - } - } - } - ] + expect((/^[1-9]\d*[xX,][1-9]\d*$/).test(size)).to.be.true; } - ], - cur: 'USD' - } + } else { + expect(srId.length).to.be.equal(1); + expect((/^[1-9]\d*/).test(srId[0])).to.be.true; + }; + }); }; - - let bids = spec.interpretResponse(serverResponse, bidRequestData[0]); - expect(bids).to.have.lengthOf(1); - - let bid = bids[0]; - - expect(bid.creativeId).to.equal('96846035'); - expect(bid.vastUrl).to.equal('https://some_vast_host.com/vast.xml'); - expect(bid.cpm).to.equal(0.5); - expect(bid.width).to.equal(300); - expect(bid.height).to.equal(250); - expect(bid.currency).to.equal('USD'); }); - it('validate_videoxml_response_params', function() { - const serverResponse = { - body: { - id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', - seatbid: [ - { - bid: [ - { - id: '7457329903666272789', - price: 0.5, - adm: '', - adid: '96846035', - adomain: ['test.addomain.com'], - w: 300, - h: 250, - ext: { - prebid: { - type: 'video' - } - } - } - ] - } - ], - cur: 'USD' - } - }; + describe('Check ext.next_mil_imps', function() { + const expectedNextMilImps = [ + { + impId: 'bid1234', + nextMillennium: {refresh_count: 1}, + }, - let bids = spec.interpretResponse(serverResponse, bidRequestData[0]); - expect(bids).to.have.lengthOf(1); + { + impId: 'bid1235', + nextMillennium: {refresh_count: 1}, + }, - let bid = bids[0]; + { + impId: 'bid1236', + nextMillennium: {refresh_count: 1}, + }, + ]; - expect(bid.creativeId).to.equal('96846035'); - expect(bid.vastXml).to.equal(''); - expect(bid.cpm).to.equal(0.5); - expect(bid.width).to.equal(300); - expect(bid.height).to.equal(250); - expect(bid.currency).to.equal('USD'); + const dataForRequest = getBidRequestDataGI(expectedNextMilImps.map(el => el.impId)); + for (let j = 0; j < 2; j++) { + const request = spec.buildRequests(dataForRequest); + const bidRequest = JSON.parse(request[0].data); + for (let i = 0; i < bidRequest.ext.next_mil_imps.length; i++) { + it(`test - ${j * i + 1}`, () => { + const nextMilImp = bidRequest.ext.next_mil_imps[i]; + expect(nextMilImp.impId).to.deep.equal(expectedNextMilImps[i].impId); + expect(nextMilImp.nextMillennium.refresh_count).to.deep.equal(expectedNextMilImps[i].nextMillennium.refresh_count + j); + }) + }; + }; }); - it('Check function of getting URL for sending statistics data', function() { + describe('function spec._getUrlPixelMetric', function() { const dataForTests = [ { + title: 'Check function of getting URL for sending statistics data - 1', eventName: 'bidRequested', - bid: { + bids: { bidderCode: 'appnexus', bids: [{bidder: 'appnexus', params: {}}], }, @@ -819,8 +938,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 2', eventName: 'bidRequested', - bid: { + bids: { bidderCode: 'appnexus', bids: [{bidder: 'appnexus', params: {placement_id: '807'}}], }, @@ -829,8 +949,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 3', eventName: 'bidRequested', - bid: { + bids: { bidderCode: 'nextMillennium', bids: [{bidder: 'nextMillennium', params: {placement_id: '807'}}], }, @@ -839,8 +960,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 4', eventName: 'bidRequested', - bid: { + bids: { bidderCode: 'nextMillennium', bids: [ {bidder: 'nextMillennium', params: {placement_id: '807'}}, @@ -852,8 +974,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 5', eventName: 'bidRequested', - bid: { + bids: { bidderCode: 'nextMillennium', bids: [{bidder: 'nextMillennium', params: {placement_id: '807', group_id: '123'}}], }, @@ -862,8 +985,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 6', eventName: 'bidRequested', - bid: { + bids: { bidderCode: 'nextMillennium', bids: [ {bidder: 'nextMillennium', params: {placement_id: '807', group_id: '123'}}, @@ -876,8 +1000,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 7', eventName: 'bidResponse', - bid: { + bids: { bidderCode: 'appnexus', }, @@ -885,8 +1010,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 8', eventName: 'bidResponse', - bid: { + bids: { bidderCode: 'nextMillennium', params: {placement_id: '807'}, }, @@ -895,8 +1021,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 9', eventName: 'noBid', - bid: { + bids: { bidder: 'appnexus', }, @@ -904,8 +1031,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 10', eventName: 'noBid', - bid: { + bids: { bidder: 'nextMillennium', params: {placement_id: '807'}, }, @@ -914,8 +1042,9 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 11', eventName: 'bidTimeout', - bid: { + bids: { bidder: 'appnexus', }, @@ -923,19 +1052,250 @@ describe('nextMillenniumBidAdapterTests', () => { }, { + title: 'Check function of getting URL for sending statistics data - 12', eventName: 'bidTimeout', - bid: { + bids: { bidder: 'nextMillennium', params: {placement_id: '807'}, }, expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=bidTimeout&bidder=nextMillennium&source=pbjs&placements=807', }, + + { + title: 'Check function of getting URL for sending statistics data - 13', + eventName: 'bidRequested', + bids: [ + { + bidderCode: 'nextMillennium', + bids: [ + {bidder: 'nextMillennium', params: {placement_id: '807', group_id: '123'}}, + {bidder: 'nextMillennium', params: {group_id: '456'}}, + {bidder: 'nextMillennium', params: {placement_id: '222'}}, + ], + }, + + { + bidderCode: 'nextMillennium', + params: {group_id: '7777'}, + }, + + { + bidderCode: 'nextMillennium', + params: {placement_id: '8888'}, + }, + ], + + expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=bidRequested&bidder=nextMillennium&source=pbjs&groups=123;456;7777&placements=222;8888', + }, + ]; + + for (let {title, eventName, bids, expected} of dataForTests) { + it(title, () => { + const url = spec._getUrlPixelMetric(eventName, bids); + expect(url).to.equal(expected); + }); + }; + }); + + describe('check function buildRequests', () => { + const tests = [ + { + title: 'test - 1', + bidderRequest: {bidderRequestId: 'mock-uuid', timeout: 1200}, + bidRequests: [ + { + adUnitCode: 'test-div', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { placement_id: '-1' }, + sizes: [[300, 250]], + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + }, + + ortb2: { + device: { + w: 1500, + h: 1000 + }, + + site: { + domain: 'example.com', + page: 'http://example.com' + } + } + }, + + { + adUnitCode: 'test-div-2', + bidId: 'bid1235', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { placement_id: '333' }, + sizes: [[300, 250]], + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + }, + + ortb2: { + device: { + w: 1500, + h: 1000 + }, + + site: { + domain: 'example.com', + page: 'http://example.com' + } + }, + }, + ], + + expected: { + id: 'mock-uuid', + impSize: 2, + requestSize: 1, + domain: 'example.com', + tmax: 1200, + }, + }, + ]; + + for (let {title, bidRequests, bidderRequest, expected} of tests) { + it(title, () => { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.length).to.equal(expected.requestSize); + + const requestData = JSON.parse(request[0].data); + expect(requestData.id).to.equal(expected.id); + expect(requestData.tmax).to.equal(expected.tmax); + expect(requestData?.imp?.length).to.equal(expected.impSize); + }); + }; + }); + + describe('check function interpretResponse', () => { + const tests = [ + { + title: 'test - 1', + serverResponse: { + body: { + id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', + seatbid: [ + { + bid: [ + { + id: '7457329903666272789-0', + impid: '700ce0a43f72', + price: 0.5, + adm: 'Hello! It\'s a test ad!', + adid: '96846035-0', + adomain: ['test.addomain.com'], + w: 300, + h: 250, + }, + + { + id: '7457329903666272789-1', + impid: '700ce0a43f73', + price: 0.7, + adm: 'https://some_vast_host.com/vast.xml', + adid: '96846035-1', + adomain: ['test.addomain.com'], + w: 400, + h: 300, + ext: {prebid: {type: 'video'}}, + }, + + { + id: '7457329903666272789-2', + impid: '700ce0a43f74', + price: 1.0, + adm: '', + adid: '96846035-3', + adomain: ['test.addomain.com'], + w: 640, + h: 480, + ext: {prebid: {type: 'video'}}, + }, + ], + }, + ], + cur: 'USD', + }, + }, + + expected: [ + { + title: 'banner', + requestId: '700ce0a43f72', + creativeId: '96846035-0', + ad: 'Hello! It\'s a test ad!', + vastUrl: undefined, + vastXml: undefined, + cpm: 0.5, + width: 300, + height: 250, + currency: 'USD', + }, + + { + title: 'video - vastUrl', + requestId: '700ce0a43f73', + creativeId: '96846035-1', + ad: undefined, + vastUrl: 'https://some_vast_host.com/vast.xml', + vastXml: undefined, + cpm: 0.7, + width: 400, + height: 300, + currency: 'USD', + }, + + { + title: 'video - vastXml', + requestId: '700ce0a43f74', + creativeId: '96846035-3', + ad: undefined, + vastUrl: undefined, + vastXml: '', + cpm: 1.0, + width: 640, + height: 480, + currency: 'USD', + }, + ], + }, ]; - for (let {eventName, bid, expected} of dataForTests) { - const url = spec._getUrlPixelMetric(eventName, bid); - expect(url).to.equal(expected); + for (let {title, serverResponse, bidRequest, expected} of tests) { + describe(title, () => { + const bids = spec.interpretResponse(serverResponse, bidRequest); + for (let i = 0; i < bids.length; i++) { + it(expected[i].title, () => { + expect(bids).to.have.lengthOf(expected.length); + + const bid = bids[i] + expect(bid.creativeId).to.equal(expected[i].creativeId); + expect(bid.requestId).to.equal(expected[i].requestId); + expect(bid.ad).to.equal(expected[i].ad); + expect(bid.vastUrl).to.equal(expected[i].vastUrl); + expect(bid.vastXml).to.equal(expected[i].vastXml); + expect(bid.cpm).to.equal(expected[i].cpm); + expect(bid.width).to.equal(expected[i].width); + expect(bid.height).to.equal(expected[i].height); + expect(bid.currency).to.equal(expected[i].currency); + }); + }; + }); }; - }) + }); }); diff --git a/test/spec/modules/nexverseBidAdapter_spec.js b/test/spec/modules/nexverseBidAdapter_spec.js new file mode 100644 index 00000000000..fd20a37cc0d --- /dev/null +++ b/test/spec/modules/nexverseBidAdapter_spec.js @@ -0,0 +1,203 @@ +import { expect } from 'chai'; +import { spec } from 'modules/nexverseBidAdapter.js'; +import { getDeviceModel, buildEndpointUrl, parseNativeResponse } from '../../../libraries/nexverseUtils/index.js'; +import { getOsVersion } from '../../../libraries/advangUtils/index.js'; + +const BIDDER_ENDPOINT = 'https://rtb.nexverse.ai'; + +describe('nexverseBidAdapterTests', () => { + describe('isBidRequestValid', function () { + let sbid = { + 'adUnitCode': 'div', + 'bidder': 'nexverse', + 'params': { + 'uid': '77d4a2eb3d209ce6c7691dc79fcab358', + 'pubId': '24051' + }, + }; + + it('should not accept bid without required params', function () { + let isValid = spec.isBidRequestValid(sbid); + expect(isValid).to.equal(false); + }); + + it('should return false when params are not passed', function () { + let bid = Object.assign({}, sbid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when valid params are not passed', function () { + let bid = Object.assign({}, sbid); + delete bid.params; + bid.params = {uid: '', pubId: '', pubEpid: ''}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when valid params are not passed', function () { + let bid = Object.assign({}, sbid); + delete bid.params; + bid.adUnitCode = ''; + bid.mediaTypes = { + banner: { + sizes: [[300, 250]] + } + }; + bid.params = {uid: '77d4a2eb3d209ce6c7691dc79fcab358', pubId: '24051'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return true when valid params are passed as nums', function () { + let bid = Object.assign({}, sbid); + delete bid.params; + bid.mediaTypes = { + banner: { + sizes: [[300, 250]] + } + }; + bid.params = {uid: '77d4a2eb3d209ce6c7691dc79fcab358', pubId: '24051', pubEpid: '34561'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('getDeviceModel', () => { + it('should return "iPhone" for iPhone userAgent', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)', configurable: true }); + expect(getDeviceModel()).to.equal('iPhone'); + }); + + it('should return "iPad" for iPad userAgent', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X)', configurable: true }); + expect(getDeviceModel()).to.equal('iPad'); + }); + + it('should return the Android device name for Android userAgent', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (Linux; Android 10; Pixel 3 Build/QQ2A.200305.003) AppleWebKit/537.36', configurable: true }); + expect(getDeviceModel()).to.equal('Pixel 3'); + }); + + it('should return "Unknown Android Device" if device name is missing in Android userAgent', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (Linux; Android 10;) AppleWebKit/537.36', configurable: true }); + expect(getDeviceModel()).to.equal('Unknown Android Device'); + }); + + it('should return "Mac" for Mac userAgent', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', configurable: true }); + expect(getDeviceModel()).to.equal('Mac'); + }); + + it('should return "Linux" for Linux userAgent', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (X11; Linux x86_64)', configurable: true }); + expect(getDeviceModel()).to.equal('Linux'); + }); + + it('should return "Windows PC" for Windows userAgent', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', configurable: true }); + expect(getDeviceModel()).to.equal('Windows PC'); + }); + + it('should return "Unknown Device" for an unrecognized userAgent', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (Unknown OS)', configurable: true }); + expect(getDeviceModel()).to.equal(''); + }); + }); + + describe('buildEndpointUrl', () => { + it('should construct the URL with uid, pubId, and pubEpid', function () { + const bid = { + params: { + uid: '12345', + pubId: '67890', + pubEpid: 'abcdef' + }, + isDebug: false + }; + const expectedUrl = `${BIDDER_ENDPOINT}?uid=12345&pub_id=67890&pub_epid=abcdef`; + expect(buildEndpointUrl(BIDDER_ENDPOINT, bid)).to.equal(expectedUrl); + }); + }); + + describe('buildEndpointUrl', () => { + it('should construct the test URL with uid, pubId, and pubEpid', function () { + const bid = { + params: { + uid: '12345', + pubId: '67890', + pubEpid: 'abcdef' + }, + isDebug: true + }; + const expectedUrl = `${BIDDER_ENDPOINT}?uid=12345&pub_id=67890&pub_epid=abcdef&test=1`; + expect(buildEndpointUrl(BIDDER_ENDPOINT, bid)).to.equal(expectedUrl); + }); + }); + + describe('parseNativeResponse', () => { + it('should parse and return the empty json object from a invalid JSON string', function () { + const adm = 'Nexverse test ad'; + const result = parseNativeResponse(adm); + expect(result).to.deep.equal({}); + }); + it('should parse and return the native object from a valid JSON string', function () { + const adm = '{"native": "sample native ad"}'; // JSON string + const result = parseNativeResponse(adm); + expect(result).to.deep.equal('sample native ad'); + }); + }); + + describe('getOsVersion', () => { + it('should detect Android OS', function () { + Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (Linux; Android 10; Pixel 3 Build/QQ2A.200305.003) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36', configurable: true }); + expect(getOsVersion()).to.equal('Android'); + }); + it('should detect iOS', function () { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', + configurable: true, + }); + expect(getOsVersion()).to.equal('iOS'); + }); + it('should detect Mac OS X', function () { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + configurable: true, + }); + expect(getOsVersion()).to.equal('Mac OS X'); + }); + it('should detect Windows 10', function () { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + configurable: true, + }); + expect(getOsVersion()).to.equal('Windows 10'); + }); + it('should detect Linux', function () { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + configurable: true, + }); + expect(getOsVersion()).to.equal('Linux'); + }); + it('should detect Windows 7', function () { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + configurable: true, + }); + expect(getOsVersion()).to.equal('Windows 7'); + }); + it('should detect Search Bot', function () { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + configurable: true, + }); + expect(getOsVersion()).to.equal('Search Bot'); + }); + it('should return unknown for an unrecognized user agent', function () { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Unknown OS) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + configurable: true, + }); + expect(getOsVersion()).to.equal('unknown'); + }); + }); +}); diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index f18e0365226..b8bed5f01de 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -3,242 +3,182 @@ import { spec, storage, getNexx360LocalStorage, } from 'modules/nexx360BidAdapter.js'; import { sandbox } from 'sinon'; +import { getAmxId } from '../../../modules/nexx360BidAdapter'; -const instreamResponse = { - 'id': '2be64380-ba0c-405a-ab53-51f51c7bde51', - 'cur': 'USD', - 'seatbid': [ - { - 'bid': [ - { - 'id': '8275140264321181514', - 'impid': '263cba3b8bfb72', - 'price': 5, - 'adomain': [ - 'appnexus.com' - ], - 'crid': '97517771', - 'h': 1, - 'w': 1, - 'adm': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ', - 'ext': { - 'mediaType': 'instream', - 'ssp': 'appnexus', - 'divId': 'video1', - 'adUnitCode': 'video1', - } - } - ], - 'seat': 'appnexus' - } - ], - 'ext': { - 'cookies': [] - } -}; - -describe('Nexx360 bid adapter tests', function () { - const DISPLAY_BID_REQUEST = { - 'id': '77b3f21a-e0df-4495-8bce-4e8a1d2309c1', - 'imp': [ - {'id': '2b4d8fc1c1c7ea', - 'tagid': 'div-1', - 'ext': {'divId': 'div-1', 'nexx360': {'account': '1067', 'tag_id': 'luvxjvgn'}}, - 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}], 'topframe': 1}}, {'id': '38fc428ab96638', 'tagid': 'div-2', 'ext': {'divId': 'div-2', 'nexx360': {'account': '1067', 'tag_id': 'luvxjvgn'}}, 'banner': {'format': [{'w': 728, 'h': 90}, {'w': 970, 'h': 250}], 'topframe': 1}}], - 'cur': ['USD'], - 'at': 1, - 'tmax': 3000, - 'site': {'page': 'https://test.nexx360.io/adapter/index.html?nexx360_test=1', 'domain': 'test.nexx360.io'}, - 'regs': {'coppa': 0, 'ext': {'gdpr': 1}}, - 'device': { - 'dnt': 0, - 'h': 844, - 'w': 390, - 'ua': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', - 'language': 'fr' - }, - 'user': { - 'ext': { - 'consent': 'CPgocUAPgocUAAKAsAENCkCsAP_AAH_AAAqIJDtd_H__bW9r-f5_aft0eY1P9_r37uQzDhfNk-8F3L_W_LwX52E7NF36tq4KmR4ku1LBIUNlHMHUDUmwaokVryHsak2cpzNKJ7BEknMZOydYGF9vmxtj-QKY7_5_d3bx2D-t_9v239z3z81Xn3d53-_03LCdV5_9Dfn9fR_bc9KPt_58v8v8_____3_e__3_7994JEAEmGrcQBdmWODNoGEUCIEYVhIVQKACCgGFogMAHBwU7KwCfWECABAKAIwIgQ4AowIBAAAJAEhEAEgRYIAAARAIAAQAIhEIAGBgEFgBYGAQAAgGgYohQACBIQZEBEUpgQFQJBAa2VCCUF0hphAHWWAFBIjYqABEEgIrAAEBYOAYIkBKxYIEmKN8gBGCFAKJUK1EAAAA.YAAAAAAAAAAA', - 'ConsentedProvidersSettings': {'consented_providers': '1~39.43.46.55.61.70.83.89.93.108.117.122.124.131.135.136.143.144.147.149.159.162.167.171.192.196.202.211.218.228.230.239.241.259.266.272.286.291.311.317.322.323.326.327.338.367.371.385.389.394.397.407.413.415.424.430.436.445.449.453.482.486.491.494.495.501.503.505.522.523.540.550.559.560.568.574.576.584.587.591.733.737.745.787.802.803.817.820.821.829.839.864.867.874.899.904.922.931.938.979.981.985.1003.1024.1027.1031.1033.1040.1046.1051.1053.1067.1085.1092.1095.1097.1099.1107.1127.1135.1143.1149.1152.1162.1166.1186.1188.1201.1205.1211.1215.1226.1227.1230.1252.1268.1270.1276.1284.1286.1290.1301.1307.1312.1345.1356.1364.1365.1375.1403.1415.1416.1419.1440.1442.1449.1455.1456.1465.1495.1512.1516.1525.1540.1548.1555.1558.1564.1570.1577.1579.1583.1584.1591.1603.1616.1638.1651.1653.1665.1667.1677.1678.1682.1697.1699.1703.1712.1716.1721.1725.1732.1745.1750.1765.1769.1782.1786.1800.1808.1810.1825.1827.1832.1838.1840.1842.1843.1845.1859.1866.1870.1878.1880.1889.1899.1917.1929.1942.1944.1962.1963.1964.1967.1968.1969.1978.2003.2007.2008.2027.2035.2039.2044.2047.2052.2056.2064.2068.2070.2072.2074.2088.2090.2103.2107.2109.2115.2124.2130.2133.2137.2140.2145.2147.2150.2156.2166.2177.2183.2186.2202.2205.2216.2219.2220.2222.2225.2234.2253.2264.2279.2282.2292.2299.2305.2309.2312.2316.2322.2325.2328.2331.2334.2335.2336.2337.2343.2354.2357.2358.2359.2370.2376.2377.2387.2392.2394.2400.2403.2405.2407.2411.2414.2416.2418.2425.2440.2447.2459.2461.2462.2465.2468.2472.2477.2481.2484.2486.2488.2493.2496.2497.2498.2499.2501.2510.2511.2517.2526.2527.2532.2534.2535.2542.2552.2563.2564.2567.2568.2569.2571.2572.2575.2577.2583.2584.2596.2601.2604.2605.2608.2609.2610.2612.2614.2621.2628.2629.2633.2634.2636.2642.2643.2645.2646.2647.2650.2651.2652.2656.2657.2658.2660.2661.2669.2670.2677.2681.2684.2686.2687.2690.2695.2698.2707.2713.2714.2729.2739.2767.2768.2770.2772.2784.2787.2791.2792.2798.2801.2805.2812.2813.2816.2817.2818.2821.2822.2827.2830.2831.2834.2838.2839.2840.2844.2846.2847.2849.2850.2852.2854.2856.2860.2862.2863.2865.2867.2869.2873.2874.2875.2876.2878.2880.2881.2882.2883.2884.2886.2887.2888.2889.2891.2893.2894.2895.2897.2898.2900.2901.2908.2909.2911.2912.2913.2914.2916.2917.2918.2919.2920.2922.2923.2924.2927.2929.2930.2931.2939.2940.2941.2947.2949.2950.2956.2961.2962.2963.2964.2965.2966.2968.2970.2973.2974.2975.2979.2980.2981.2983.2985.2986.2987.2991.2994.2995.2997.2999.3000.3002.3003.3005.3008.3009.3010.3012.3016.3017.3018.3019.3024.3025.3028.3034.3037.3038.3043.3045.3048.3052.3053.3055.3058.3059.3063.3065.3066.3068.3070.3072.3073.3074.3075.3076.3077.3078.3089.3090.3093.3094.3095.3097.3099.3104.3106.3109.3112.3117.3118.3119.3120.3124.3126.3127.3128.3130.3135.3136.3145.3149.3150.3151.3154.3155.3162.3163.3167.3172.3173.3180.3182.3183.3184.3185.3187.3188.3189.3190.3194.3196.3197.3209.3210.3211.3214.3215.3217.3219.3222.3223.3225.3226.3227.3228.3230.3231.3232.3234.3235.3236.3237.3238.3240.3241.3244.3245.3250.3251.3253.3257.3260.3268.3270.3272.3281.3288.3290.3292.3293.3295.3296.3300.3306.3307.3308.3314.3315.3316.3318.3324.3327.3328.3330'}, - 'eids': [{'source': 'id5-sync.com', - 'uids': [{'id': 'ID5*tdrSpYbccONIbxmulXFRLEil1aozZGGVMo9eEZgydgYoYFZQRYoae3wJyY0YtmXGKGJ7uXIQByQ6f7uzcpy9Oyhj1jGRzCf0BCoI4VkkKZIoZBubolUKUXXxOIdQOz7ZKGV0E3sqi9Zut0BbOuoJAihpLbgfNgDJ0xRmQw04rDooaxn7_TIPzEX5_L5ohNkUKG01Gnh2djvcrcPigKlk7ChwnauCwHIetHYI32yYAnAocYyqoM9XkoVOHtyOTC_UKHIR0qVBVIzJ1Nn_g7kLqyhzfosadKVvf7RQCsE6QrYodtpOJKg7i72-tnMXkzgmKHjh98aEDfTQrZOkKebmAyh6GlOHtYn_sZBFjJwtWp4oe9j2QTNbzK3G0jp1PlJqKHxiu4LawFEKJ3yi5-NFUyh-YkEalJUWyl1cDlWo5NQogAy2HM8N_w0qrVQgNbrTKIHK3KzTXztH7WzBgYrk8g', - 'atype': 1, - 'ext': {'linkType': 2}}]}, - {'source': 'domain.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 1, 'ext': {'stype': 'ppuid'}}]}]}}, - 'ext': { - 'source': 'prebid.js', - 'version': '7.20.0-pre', - 'pageViewId': '5b970aba-51e9-4e0a-8299-f3f5618c695e' - }} - - const VIDEO_BID_REQUEST = [ - { - 'bidder': 'nexx360', - 'params': { - 'account': '1067', - 'tagId': 'yqsc1tfj' - }, - 'mediaTypes': { - 'video': { - 'context': 'instream', - 'playerSize': [[640, 480]], - 'mimes': ['video/mp4'], - 'protocols': [1, 2, 3, 4, 5, 6], - 'playbackmethod': [2], - 'skip': 1 - } - }, - 'adUnitCode': 'video1', - 'transactionId': '5434c81c-7210-44ae-9014-67c75dee48d0', - 'sizes': [[640, 480]], - 'bidId': '22f90541e576a3', - 'bidderRequestId': '1d4549243f3bfd', - 'auctionId': 'ed21b528-bcab-47e2-8605-ec9b71000c89', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - } - ] - +describe('Nexx360 bid adapter tests', () => { const DEFAULT_OPTIONS = { gdprConsent: { gdprApplies: true, consentString: 'BOzZdA0OzZdA0AGABBENDJ-AAAAvh7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__79__3z3_9pxP78k89r7337Mw_v-_v-b7JCPN_Y3v-8Kg', - vendorData: {} + vendorData: {}, }, refererInfo: { referer: 'https://www.prebid.org', - canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page', }, uspConsent: '111222333', - userId: { 'id5id': { uid: '1111' } }, + userId: { id5id: { uid: '1111' } }, schain: { - 'ver': '1.0', - 'complete': 1, - 'nodes': [{ - 'asi': 'exchange1.com', - 'sid': '1234', - 'hp': 1, - 'rid': 'bid-request-1', - 'name': 'publisher', - 'domain': 'publisher.com' - }] + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com', + }], }, }; + - describe('isBidRequestValid()', function() { + describe('isBidRequestValid()', () => { let bannerBid; - beforeEach(function () { + beforeEach(() => { bannerBid = { - 'bidder': 'nexx360', - 'mediaTypes': {'banner': {'sizes': [[300, 250], [300, 600]]}}, - 'adUnitCode': 'div-1', - 'transactionId': '70bdc37e-9475-4b27-8c74-4634bdc2ee66', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '4906582fc87d0c', - 'bidderRequestId': '332fda16002dbe', - 'auctionId': '98932591-c822-42e3-850e-4b3cf748d063', + bidder: 'nexx360', + mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + adUnitCode: 'div-1', + transactionId: '70bdc37e-9475-4b27-8c74-4634bdc2ee66', + sizes: [[300, 250], [300, 600]], + bidId: '4906582fc87d0c', + bidderRequestId: '332fda16002dbe', + auctionId: '98932591-c822-42e3-850e-4b3cf748d063', } }); - it('We verify isBidRequestValid with unvalid adUnitName', function() { + it('We verify isBidRequestValid with unvalid adUnitName', () => { bannerBid.params = { adUnitName: 1 }; expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); }); - it('We verify isBidRequestValid with empty adUnitName', function() { + it('We verify isBidRequestValid with empty adUnitName', () => { bannerBid.params = { adUnitName: '' }; expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); }); - it('We verify isBidRequestValid with unvalid adUnitPath', function() { + it('We verify isBidRequestValid with unvalid adUnitPath', () => { bannerBid.params = { adUnitPath: 1 }; expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); }); - it('We verify isBidRequestValid with unvalid divId', function() { + it('We verify isBidRequestValid with unvalid divId', () => { bannerBid.params = { divId: 1 }; expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); }); - it('We verify isBidRequestValid unvalid allBids', function() { + it('We verify isBidRequestValid unvalid allBids', () => { bannerBid.params = { allBids: 1 }; expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); }); - it('We verify isBidRequestValid with uncorrect tagid', function() { + it('We verify isBidRequestValid with uncorrect tagid', () => { bannerBid.params = { 'tagid': 'luvxjvgn' }; expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); }); - it('We verify isBidRequestValid with correct tagId', function() { + it('We verify isBidRequestValid with correct tagId', () => { bannerBid.params = { 'tagId': 'luvxjvgn' }; expect(spec.isBidRequestValid(bannerBid)).to.be.equal(true); }); + + it('We verify isBidRequestValid with correct placement', () => { + bannerBid.params = { 'placement': 'testad' }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(true); + }); }); - describe('getNexx360LocalStorage disabled', function () { - before(function () { + describe('getNexx360LocalStorage disabled', () => { + before(() => { sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => false); }); - it('We test if we get the nexx360Id', function() { + it('We test if we get the nexx360Id', () => { const output = getNexx360LocalStorage(); expect(output).to.be.eql(false); }); - after(function () { + after(() => { sandbox.restore() }); }) - describe('getNexx360LocalStorage enabled but nothing', function () { - before(function () { + describe('getNexx360LocalStorage enabled but nothing', () => { + before(() => { sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); sandbox.stub(storage, 'setDataInLocalStorage'); sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => null); }); - it('We test if we get the nexx360Id', function() { + it('We test if we get the nexx360Id', () => { const output = getNexx360LocalStorage(); expect(typeof output.nexx360Id).to.be.eql('string'); }); - after(function () { + after(() => { sandbox.restore() }); }) - describe('getNexx360LocalStorage enabled but wrong payload', function () { - before(function () { + describe('getNexx360LocalStorage enabled but wrong payload', () => { + before(() => { sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); sandbox.stub(storage, 'setDataInLocalStorage'); sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => '{"nexx360Id":"5ad89a6e-7801-48e7-97bb-fe6f251f6cb4",}'); }); - it('We test if we get the nexx360Id', function() { + it('We test if we get the nexx360Id', () => { const output = getNexx360LocalStorage(); expect(output).to.be.eql(false); }); - after(function () { + after(() => { sandbox.restore() }); - }) + }); - describe('getNexx360LocalStorage enabled', function () { - before(function () { + describe('getNexx360LocalStorage enabled', () => { + before(() => { sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); sandbox.stub(storage, 'setDataInLocalStorage'); sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => '{"nexx360Id":"5ad89a6e-7801-48e7-97bb-fe6f251f6cb4"}'); }); - it('We test if we get the nexx360Id', function() { + it('We test if we get the nexx360Id', () => { const output = getNexx360LocalStorage(); expect(output.nexx360Id).to.be.eql('5ad89a6e-7801-48e7-97bb-fe6f251f6cb4'); }); - after(function () { + after(() => { sandbox.restore() }); - }) + }); + + describe('getAmxId() with localStorage enabled and data not set', () => { + before(() => { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => null); + }); + it('We test if we get the amxId', () => { + const output = getAmxId(); + expect(output).to.be.eql(false); + }); + after(() => { + sandbox.restore() + }); + }); - describe('buildRequests()', function() { - before(function () { + describe('getAmxId() with localStorage enabled and data set', () => { + before(() => { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => 'abcdef'); + }); + it('We test if we get the amxId', () => { + const output = getAmxId(); + expect(output).to.be.eql('abcdef'); + }); + after(() => { + sandbox.restore() + }); + }); + + describe('buildRequests()', () => { + before(() => { const documentStub = sandbox.stub(document, 'getElementById'); documentStub.withArgs('div-1').returns({ offsetWidth: 200, @@ -249,7 +189,7 @@ describe('Nexx360 bid adapter tests', function () { } }); }); - describe('We test with a multiple display bids', function() { + describe('We test with a multiple display bids', () => { const sampleBids = [ { bidder: 'nexx360', @@ -259,6 +199,11 @@ describe('Nexx360 bid adapter tests', function () { adUnitName: 'header-ad', adUnitPath: '/12345/nexx360/Homepage/HP/Header-Ad', }, + ortb2Imp: { + ext: { + gpid: '/12345/nexx360/Homepage/HP/Header-Ad', + } + }, adUnitCode: 'header-ad-1234', transactionId: '469a570d-f187-488d-b1cb-48c1a2009be9', sizes: [[300, 250], [300, 600]], @@ -295,7 +240,7 @@ describe('Nexx360 bid adapter tests', function () { { bidder: 'nexx360', params: { - tagId: 'luvxjvgn', + placement: 'testPlacement', allBids: true, }, mediaTypes: { @@ -303,6 +248,7 @@ describe('Nexx360 bid adapter tests', function () { sizes: [[728, 90], [970, 250]] } }, + adUnitCode: 'div-2-abcd', transactionId: '6196885d-4e76-40dc-a09c-906ed232626b', sizes: [[728, 90], [970, 250]], @@ -344,8 +290,8 @@ describe('Nexx360 bid adapter tests', function () { consentString: 'CPhdLUAPhdLUAAKAsAENCmCsAP_AAE7AAAqIJFNd_H__bW9r-f5_aft0eY1P9_r37uQzDhfNk-8F3L_W_LwX52E7NF36tq4KmR4ku1LBIUNlHMHUDUmwaokVryHsak2cpzNKJ7BEknMZOydYGF9vmxtj-QKY7_5_d3bx2D-t_9v239z3z81Xn3d53-_03LCdV5_9Dfn9fR_bc9KPt_58v8v8_____3_e__3_7997BIiAaADgAJYBnwEeAJXAXmAwQBj4DtgHcgPBAeKBIgAA.YAAAAAAAAAAA', } }; - it('We perform a test with 2 display adunits', function() { - const displayBids = [...sampleBids]; + it('We perform a test with 2 display adunits', () => { + const displayBids = structuredClone(sampleBids); displayBids[0].mediaTypes = { banner: { sizes: [[300, 250], [300, 600]] @@ -354,33 +300,76 @@ describe('Nexx360 bid adapter tests', function () { const request = spec.buildRequests(displayBids, bidderRequest); const requestContent = request.data; expect(request).to.have.property('method').and.to.equal('POST'); - expect(requestContent.cur[0]).to.be.eql('USD'); - expect(requestContent.imp.length).to.be.eql(2); - expect(requestContent.imp[0].id).to.be.eql('44a2706ac3574'); - expect(requestContent.imp[0].tagid).to.be.eql('header-ad'); - expect(requestContent.imp[0].ext.divId).to.be.eql('div-1'); - expect(requestContent.imp[0].ext.adUnitCode).to.be.eql('header-ad-1234'); - expect(requestContent.imp[0].ext.adUnitName).to.be.eql('header-ad'); - expect(requestContent.imp[0].ext.adUnitPath).to.be.eql('/12345/nexx360/Homepage/HP/Header-Ad'); - expect(requestContent.imp[0].ext.dimensions.slotW).to.be.eql(200); - expect(requestContent.imp[0].ext.dimensions.slotH).to.be.eql(250); - expect(requestContent.imp[0].ext.dimensions.cssMaxW).to.be.eql('400px'); - expect(requestContent.imp[0].ext.dimensions.cssMaxH).to.be.eql('350px'); - expect(requestContent.imp[0].ext.nexx360.tagId).to.be.eql('luvxjvgn'); - expect(requestContent.imp[0].banner.format.length).to.be.eql(2); - expect(requestContent.imp[0].banner.format[0].w).to.be.eql(300); - expect(requestContent.imp[0].banner.format[0].h).to.be.eql(250); - expect(requestContent.imp[1].ext.nexx360.allBids).to.be.eql(true); - expect(requestContent.imp[1].tagid).to.be.eql('div-2-abcd'); - expect(requestContent.imp[1].ext.adUnitCode).to.be.eql('div-2-abcd'); - expect(requestContent.imp[1].ext.divId).to.be.eql('div-2-abcd'); - expect(requestContent.ext.bidderVersion).to.be.eql('4.0'); - expect(requestContent.ext.source).to.be.eql('prebid.js'); + const expectedRequest = { + imp: [ + { + id: '44a2706ac3574', + banner: { + topframe: 0, + format: [ + { w: 300, h: 250 }, + { w: 300, h: 600 }, + ], + }, + secure: 1, + tagid: 'header-ad-1234', + ext: { + adUnitCode: 'header-ad-1234', + gpid: '/12345/nexx360/Homepage/HP/Header-Ad', + divId: 'div-1', + dimensions: { + slotW: 200, + slotH: 250, + cssMaxW: '400px', + cssMaxH: '350px', + }, + nexx360: { + tagId: 'luvxjvgn', + }, + adUnitName: 'header-ad', + adUnitPath: '/12345/nexx360/Homepage/HP/Header-Ad', + }, + }, + { + id: '5ba94555219a03', + banner: { + topframe: 0, + format: [ + { w: 728, h: 90 }, + { w: 970, h: 250 }, + ], + }, + secure: 1, + tagid: 'div-2-abcd', + ext: { + adUnitCode: 'div-2-abcd', + divId: 'div-2-abcd', + nexx360: { + placement: 'testPlacement', + allBids: true, + }, + }, + }, + ], + id: requestContent.id, + test: 0, + ext: { + version: requestContent.ext.version, + source: 'prebid.js', + pageViewId: requestContent.ext.pageViewId, + bidderVersion: '5.0', + }, + cur: [ + 'USD', + ], + user: {}, + }; + expect(requestContent).to.be.eql(expectedRequest); }); if (FEATURES.VIDEO) { - it('We perform a test with a multiformat adunit', function() { - const multiformatBids = [...sampleBids]; + it('We perform a test with a multiformat adunit', () => { + const multiformatBids = structuredClone(sampleBids); multiformatBids[0].mediaTypes = { banner: { sizes: [[300, 250], [300, 600]] @@ -396,13 +385,24 @@ describe('Nexx360 bid adapter tests', function () { } }; const request = spec.buildRequests(multiformatBids, bidderRequest); - const requestContent = request.data; - expect(requestContent.imp[0].video.ext.context).to.be.eql('outstream'); - expect(requestContent.imp[0].video.playbackmethod[0]).to.be.eql(2); + const video = request.data.imp[0].video; + const expectedVideo = { + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1, + w: 640, + h: 480, + ext: { + playerSize: [640, 480], + context: 'outstream', + }, + }; + expect(video).to.eql(expectedVideo); }); - it('We perform a test with a instream adunit', function() { - const videoBids = [sampleBids[0]]; + it('We perform a test with a instream adunit', () => { + const videoBids = structuredClone(sampleBids); videoBids[0].mediaTypes = { video: { context: 'instream', @@ -418,265 +418,372 @@ describe('Nexx360 bid adapter tests', function () { expect(request).to.have.property('method').and.to.equal('POST'); expect(requestContent.imp[0].video.ext.context).to.be.eql('instream'); expect(requestContent.imp[0].video.playbackmethod[0]).to.be.eql(2); - }) + }); } }); - after(function () { + after(() => { sandbox.restore() }); }); - describe('interpretResponse()', function() { - it('empty response', function() { + describe('We test intepretResponse', () => { + it('empty response', () => { const response = { body: '' }; const output = spec.interpretResponse(response); expect(output.length).to.be.eql(0); }); - it('banner responses with adUrl only', function() { + it('banner responses with adUrl only', () => { const response = { body: { - 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', - 'cur': 'USD', - 'seatbid': [ + id: 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', + cur: 'USD', + seatbid: [ { - 'bid': [ + bid: [ { - 'id': '4427551302944024629', - 'impid': '226175918ebeda', - 'price': 1.5, - 'adomain': [ - 'http://prebid.org' + id: '4427551302944024629', + impid: '226175918ebeda', + price: 1.5, + adomain: [ + 'http://prebid.org', ], - 'crid': '98493581', - 'ssp': 'appnexus', - 'h': 600, - 'w': 300, - 'cat': [ - 'IAB3-1' + crid: '98493581', + ssp: 'appnexus', + h: 600, + w: 300, + dealid: 'testdeal', + cat: [ + 'IAB3-1', ], - 'ext': { - 'adUnitCode': 'div-1', - 'mediaType': 'banner', - 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', - 'ssp': 'appnexus', - } - } + ext: { + adUnitCode: 'div-1', + mediaType: 'banner', + adUrl: 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + ssp: 'appnexus', + }, + }, ], - 'seat': 'appnexus' - } + seat: 'appnexus', + }, ], - 'ext': { - 'id': 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', - 'cookies': [] + ext: { + id: 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', + cookies: [], }, - } + }, }; + const output = spec.interpretResponse(response); - expect(output[0].adUrl).to.be.eql(response.body.seatbid[0].bid[0].ext.adUrl); - expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].ext.mediaType); - expect(output[0].currency).to.be.eql(response.body.cur); - expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); - }); - it('banner responses with adm', function() { + const expectedOutput = [{ + requestId: '226175918ebeda', + cpm: 1.5, + width: 300, + height: 600, + creativeId: '98493581', + currency: 'USD', + netRevenue: true, + dealid: 'testdeal', + ttl: 120, + mediaType: 'banner', + meta: { advertiserDomains: ['http://prebid.org'], demandSource: 'appnexus' }, + adUrl: 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + }]; + expect(output).to.eql(expectedOutput); + }); + it('banner responses with adm', () => { const response = { body: { - 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', - 'cur': 'USD', - 'seatbid': [ + id: 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', + cur: 'USD', + seatbid: [ { - 'bid': [ + bid: [ { - 'id': '4427551302944024629', - 'impid': '226175918ebeda', - 'price': 1.5, - 'adomain': [ - 'http://prebid.org' + id: '4427551302944024629', + impid: '226175918ebeda', + price: 1.5, + adomain: [ + 'http://prebid.org', ], - 'crid': '98493581', - 'ssp': 'appnexus', - 'h': 600, - 'w': 300, - 'adm': '
TestAd
', - 'cat': [ - 'IAB3-1' + crid: '98493581', + ssp: 'appnexus', + h: 600, + w: 300, + adm: '
TestAd
', + cat: [ + 'IAB3-1', ], - 'ext': { - 'adUnitCode': 'div-1', - 'mediaType': 'banner', - 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', - 'ssp': 'appnexus', - } - } + ext: { + adUnitCode: 'div-1', + mediaType: 'banner', + adUrl: 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + ssp: 'appnexus', + }, + }, ], - 'seat': 'appnexus' - } + seat: 'appnexus', + }, ], - 'ext': { - 'id': 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', - 'cookies': [] + ext: { + id: 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', + cookies: [], }, - } + }, }; const output = spec.interpretResponse(response); - expect(output[0].ad).to.be.eql(response.body.seatbid[0].bid[0].adm); - expect(output[0].adUrl).to.be.eql(undefined); - expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].ext.mediaType); - expect(output[0].currency).to.be.eql(response.body.cur); - expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); + const expectedOutput = [{ + requestId: '226175918ebeda', + cpm: 1.5, + width: 300, + height: 600, + creativeId: '98493581', + currency: 'USD', + netRevenue: true, + ttl: 120, + mediaType: 'banner', + meta: { + advertiserDomains: [ + 'http://prebid.org', + ], + demandSource: 'appnexus', + }, + ad: '
TestAd
', + }]; + expect(output).to.eql(expectedOutput); }); - it('instream responses', function() { + + it('instream responses', () => { const response = { body: { - 'id': '2be64380-ba0c-405a-ab53-51f51c7bde51', - 'cur': 'USD', - 'seatbid': [ + id: '2be64380-ba0c-405a-ab53-51f51c7bde51', + cur: 'USD', + seatbid: [ { - 'bid': [ + bid: [ { - 'id': '8275140264321181514', - 'impid': '263cba3b8bfb72', - 'price': 5, - 'adomain': [ - 'appnexus.com' + id: '8275140264321181514', + impid: '263cba3b8bfb72', + price: 5, + adomain: [ + 'appnexus.com', ], - 'crid': '97517771', - 'h': 1, - 'w': 1, - 'ext': { - 'mediaType': 'instream', - 'ssp': 'appnexus', - 'adUnitCode': 'video1', - 'vastXml': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' - } - } + crid: '97517771', + h: 1, + w: 1, + adm: 'vast', + ext: { + mediaType: 'instream', + ssp: 'appnexus', + adUnitCode: 'video1', + }, + }, ], - 'seat': 'appnexus' - } + seat: 'appnexus', + }, ], - 'ext': { - 'cookies': [] - } - } + ext: { + cookies: [], + }, + }, }; + const output = spec.interpretResponse(response); - expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].adm); - expect(output[0].mediaType).to.be.eql('video'); - expect(output[0].currency).to.be.eql(response.body.cur); - expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); + const expectedOutput = [{ + requestId: '263cba3b8bfb72', + cpm: 5, + width: 1, + height: 1, + creativeId: '97517771', + currency: 'USD', + netRevenue: true, + ttl: 120, + mediaType: 'video', + meta: { advertiserDomains: ['appnexus.com'], demandSource: 'appnexus' }, + vastXml: 'vast', + }]; + expect(output).to.eql(expectedOutput); }); - it('outstream responses', function() { + it('outstream responses', () => { const response = { body: { - 'id': '40c23932-135e-4602-9701-ca36f8d80c07', - 'cur': 'USD', - 'seatbid': [ + id: '40c23932-135e-4602-9701-ca36f8d80c07', + cur: 'USD', + seatbid: [ { - 'bid': [ + bid: [ { - 'id': '1186971142548769361', - 'impid': '4ce809b61a3928', - 'price': 5, - 'adomain': [ - 'appnexus.com' + id: '1186971142548769361', + impid: '4ce809b61a3928', + price: 5, + adomain: [ + 'appnexus.com', ], - 'crid': '97517771', - 'h': 1, - 'w': 1, - 'adm': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ', - 'ext': { - 'mediaType': 'outstream', - 'ssp': 'appnexus', - 'adUnitCode': 'div-1', - } - } + crid: '97517771', + h: 1, + w: 1, + adm: 'vast', + ext: { + mediaType: 'outstream', + ssp: 'appnexus', + adUnitCode: 'div-1', + }, + }, ], - 'seat': 'appnexus' - } + seat: 'appnexus', + }, ], - 'ext': { - 'cookies': [] - } - } + ext: { + cookies: [], + }, + }, }; + const output = spec.interpretResponse(response); - expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].adm); - expect(output[0].mediaType).to.be.eql('video'); - expect(output[0].currency).to.be.eql(response.body.cur); - expect(typeof output[0].renderer).to.be.eql('object'); - expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); + const expectedOutut = [{ + requestId: '4ce809b61a3928', + cpm: 5, + width: 1, + height: 1, + creativeId: '97517771', + currency: 'USD', + netRevenue: true, + ttl: 120, + mediaType: 'video', + meta: { advertiserDomains: ['appnexus.com'], demandSource: 'appnexus' }, + vastXml: 'vast', + renderer: output[0].renderer, + }]; + expect(output).to.eql(expectedOutut); }); - it('native responses', function() { + it('native responses', () => { const response = { body: { - 'id': '3c0290c1-6e75-4ef7-9e37-17f5ebf3bfa3', - 'cur': 'USD', - 'seatbid': [ + id: '3c0290c1-6e75-4ef7-9e37-17f5ebf3bfa3', + cur: 'USD', + seatbid: [ { - 'bid': [ + bid: [ { - 'id': '6624930625245272225', - 'impid': '23e11d845514bb', - 'price': 10, - 'adomain': [ - 'prebid.org' + id: '6624930625245272225', + impid: '23e11d845514bb', + price: 10, + adomain: [ + 'prebid.org', ], - 'crid': '97494204', - 'h': 1, - 'w': 1, - 'cat': [ - 'IAB3-1' + crid: '97494204', + h: 1, + w: 1, + cat: [ + 'IAB3-1', ], - 'ext': { - 'mediaType': 'native', - 'ssp': 'appnexus', - 'adUnitCode': '/19968336/prebid_native_example_1' + ext: { + mediaType: 'native', + ssp: 'appnexus', + adUnitCode: '/19968336/prebid_native_example_1', }, - 'adm': '{"ver":"1.2","assets":[{"id":1,"img":{"url":"https:\\/\\/vcdn.adnxs.com\\/p\\/creative-image\\/f8\\/7f\\/0f\\/13\\/f87f0f13-230c-4f05-8087-db9216e393de.jpg","w":989,"h":742,"ext":{"appnexus":{"prevent_crop":0}}}},{"id":0,"title":{"text":"This is a Prebid Native Creative"}},{"id":2,"data":{"value":"Prebid.org"}}],"link":{"url":"https:\\/\\/ams3-ib.adnxs.com\\/click?AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQKZS4ZZl5vVbR6p-A-MwnyTZ7QVkAAAAAOLoyQBtJAAAbSQAAAIAAAC8pM8FnPgWAAAAAABVU0QAVVNEAAEAAQBNXQAAAAABAgMCAAAAALoAURe69gAAAAA.\\/bcr=AAAAAAAA8D8=\\/pp=${AUCTION_PRICE}\\/cnd=%21JBC72Aj8-LwKELzJvi4YnPFbIAQoADEAAAAAAAAkQDoJQU1TMzo2MTM1QNAwSQAAAAAAAPA_UQAAAAAAAAAAWQAAAAAAAAAAYQAAAAAAAAAAaQAAAAAAAAAAcQAAAAAAAAAAeACJAQAAAAAAAAAA\\/cca=OTMyNSNBTVMzOjYxMzU=\\/bn=97062\\/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html"},"eventtrackers":[{"event":1,"method":1,"url":"https:\\/\\/ams3-ib.adnxs.com\\/it?an_audit=0&referrer=https%3A%2F%2Ftest.nexx360.io%2Fadapter%2Fnative%2Ftest.html&e=wqT_3QKJCqAJBQAAAwDWAAUBCNnbl6AGEKalhbfZzPn6WxjH1PqbsJzMzyQqNgkAAAECCCRAEQEHEAAAJEAZEQkAIREJACkRCQAxEQmoMOLRpwY47UhA7UhIAlC8yb4uWJzxW2AAaM26dXim9gWAAQGKAQNVU0SSAQEG9F4BmAEBoAEBqAEBsAEAuAECwAEDyAEC0AEJ2AEA4AEA8AEAigIpdWYoJ2EnLCAyNTI5ODg1LCAwKTt1ZigncicsIDk3NDk0MjA0LCAwKTuSAvEDIS0xRDNJQWo4LUx3S0VMekp2aTRZQUNDYzhWc3dBRGdBUUFSSTdVaFE0dEduQmxnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFIenJXcWtBQUFrUU1FQjg2MXFwQUFBSkVESkFYSUtWbWViSmZJXzJRRUFBQUFBQUFEd1AtQUJBUFVCQUFBQUFKZ0NBS0FDQUxVQ0FBQUFBTDBDQUFBQUFNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpnREFib0RDVUZOVXpNNk5qRXpOZUFEMERDSUJBQ1FCQUNZQkFIQkJBQUFBQUFBQUFBQXlRUUFBCQscQUFOZ0VBUEURlSxBQUFDSUJmY3ZxUVUBDQRBQQGoCDdFRgEKCQEMREJCUQkKAQEAeRUoAUwyKAAAWi4oALg0QVhBaEQzd0JhTEQzd0w0QmQyMG1nR0NCZ05WVTBTSUJnQ1FCZ0dZQmdDaEJnQQFONEFBQ1JBcUFZQnNnWWtDHXQARR0MAEcdDABJHQw8dUFZS5oClQEhSkJDNzJBajL1ASRuUEZiSUFRb0FEFfhUa1FEb0pRVTFUTXpvMk1UTTFRTkF3UxFRDFBBX1URDAxBQUFXHQwAWR0MAGEdDABjHQwQZUFDSkEdEMjYAvfpA-ACrZhI6gIwaHR0cHM6Ly90ZXN0Lm5leHgzNjAuaW8vYWRhcHRlci9uYXRpdmUJH_CaaHRtbIADAIgDAZADAJgDFKADAaoDAMAD4KgByAMA2AMA4AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAqAQAsgQMCAAQABgAIAAwADgAuAQAwASA2rgiyAQA0gQOOTMyNSNBTVMzOjYxMzXaBAIIAeAEAPAEvMm-LvoEEgkAAABAPG1IQBEAAACgV8oCQIgFAZgFAKAF______8BBbABqgUkM2MwMjkwYzEtNmU3NS00ZWY3LTllMzctMTdmNWViZjNiZmEzwAUAyQWJFxTwP9IFCQkJDHgAANgFAeAFAfAFmfQh-gUECAAQAJAGAZgGALgGAMEGCSUo8D_QBvUv2gYWChAJERkBAdpg4AYM8gYCCACABwGIBwCgB0HIB6b2BdIHDRVkASYI2gcGAV1oGADgBwDqBwIIAPAHAIoIAhAAlQgAAIA_mAgB&s=ccf63f2e483a37091d2475d895e7cf7c911d1a78&pp=${AUCTION_PRICE}"}]}' - } + adm: '{"ver":"1.2","assets":[{"id":1,"img":{"url":"https:\\/\\/vcdn.adnxs.com\\/p\\/creative-image\\/f8\\/7f\\/0f\\/13\\/f87f0f13-230c-4f05-8087-db9216e393de.jpg","w":989,"h":742,"ext":{"appnexus":{"prevent_crop":0}}}},{"id":0,"title":{"text":"This is a Prebid Native Creative"}},{"id":2,"data":{"value":"Prebid.org"}}],"link":{"url":"https:\\/\\/ams3-ib.adnxs.com\\/click?AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQKZS4ZZl5vVbR6p-A-MwnyTZ7QVkAAAAAOLoyQBtJAAAbSQAAAIAAAC8pM8FnPgWAAAAAABVU0QAVVNEAAEAAQBNXQAAAAABAgMCAAAAALoAURe69gAAAAA.\\/bcr=AAAAAAAA8D8=\\/pp=${AUCTION_PRICE}\\/cnd=%21JBC72Aj8-LwKELzJvi4YnPFbIAQoADEAAAAAAAAkQDoJQU1TMzo2MTM1QNAwSQAAAAAAAPA_UQAAAAAAAAAAWQAAAAAAAAAAYQAAAAAAAAAAaQAAAAAAAAAAcQAAAAAAAAAAeACJAQAAAAAAAAAA\\/cca=OTMyNSNBTVMzOjYxMzU=\\/bn=97062\\/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html"},"eventtrackers":[{"event":1,"method":1,"url":"https:\\/\\/ams3-ib.adnxs.com\\/it?an_audit=0&referrer=https%3A%2F%2Ftest.nexx360.io%2Fadapter%2Fnative%2Ftest.html&e=wqT_3QKJCqAJBQAAAwDWAAUBCNnbl6AGEKalhbfZzPn6WxjH1PqbsJzMzyQqNgkAAAECCCRAEQEHEAAAJEAZEQkAIREJACkRCQAxEQmoMOLRpwY47UhA7UhIAlC8yb4uWJzxW2AAaM26dXim9gWAAQGKAQNVU0SSAQEG9F4BmAEBoAEBqAEBsAEAuAECwAEDyAEC0AEJ2AEA4AEA8AEAigIpdWYoJ2EnLCAyNTI5ODg1LCAwKTt1ZigncicsIDk3NDk0MjA0LCAwKTuSAvEDIS0xRDNJQWo4LUx3S0VMekp2aTRZQUNDYzhWc3dBRGdBUUFSSTdVaFE0dEduQmxnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFIenJXcWtBQUFrUU1FQjg2MXFwQUFBSkVESkFYSUtWbWViSmZJXzJRRUFBQUFBQUFEd1AtQUJBUFVCQUFBQUFKZ0NBS0FDQUxVQ0FBQUFBTDBDQUFBQUFNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpnREFib0RDVUZOVXpNNk5qRXpOZUFEMERDSUJBQ1FCQUNZQkFIQkJBQUFBQUFBQUFBQXlRUUFBCQscQUFOZ0VBUEURlSxBQUFDSUJmY3ZxUVUBDQRBQQGoCDdFRgEKCQEMREJCUQkKAQEAeRUoAUwyKAAAWi4oALg0QVhBaEQzd0JhTEQzd0w0QmQyMG1nR0NCZ05WVTBTSUJnQ1FCZ0dZQmdDaEJnQQFONEFBQ1JBcUFZQnNnWWtDHXQARR0MAEcdDABJHQw8dUFZS5oClQEhSkJDNzJBajL1ASRuUEZiSUFRb0FEFfhUa1FEb0pRVTFUTXpvMk1UTTFRTkF3UxFRDFBBX1URDAxBQUFXHQwAWR0MAGEdDABjHQwQZUFDSkEdEMjYAvfpA-ACrZhI6gIwaHR0cHM6Ly90ZXN0Lm5leHgzNjAuaW8vYWRhcHRlci9uYXRpdmUJH_CaaHRtbIADAIgDAZADAJgDFKADAaoDAMAD4KgByAMA2AMA4AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAqAQAsgQMCAAQABgAIAAwADgAuAQAwASA2rgiyAQA0gQOOTMyNSNBTVMzOjYxMzXaBAIIAeAEAPAEvMm-LvoEEgkAAABAPG1IQBEAAACgV8oCQIgFAZgFAKAF______8BBbABqgUkM2MwMjkwYzEtNmU3NS00ZWY3LTllMzctMTdmNWViZjNiZmEzwAUAyQWJFxTwP9IFCQkJDHgAANgFAeAFAfAFmfQh-gUECAAQAJAGAZgGALgGAMEGCSUo8D_QBvUv2gYWChAJERkBAdpg4AYM8gYCCACABwGIBwCgB0HIB6b2BdIHDRVkASYI2gcGAV1oGADgBwDqBwIIAPAHAIoIAhAAlQgAAIA_mAgB&s=ccf63f2e483a37091d2475d895e7cf7c911d1a78&pp=${AUCTION_PRICE}"}]}', + }, ], - 'seat': 'appnexus' - } + seat: 'appnexus', + }, ], - 'ext': { - 'cookies': [], - } - } + ext: { + cookies: [], + }, + }, }; + const output = spec.interpretResponse(response); - expect(output[0].native.ortb.ver).to.be.eql('1.2'); - expect(output[0].native.ortb.assets[0].id).to.be.eql(1); - expect(output[0].mediaType).to.be.eql('native'); + const expectOutput = [{ + requestId: '23e11d845514bb', + cpm: 10, + width: 1, + height: 1, + creativeId: '97494204', + currency: 'USD', + netRevenue: true, + ttl: 120, + mediaType: 'native', + meta: { + advertiserDomains: [ + 'prebid.org', + ], + demandSource: 'appnexus', + }, + native: { + ortb: { + ver: '1.2', + assets: [ + { + id: 1, + img: { + url: 'https://vcdn.adnxs.com/p/creative-image/f8/7f/0f/13/f87f0f13-230c-4f05-8087-db9216e393de.jpg', + w: 989, + h: 742, + ext: { + appnexus: { + prevent_crop: 0, + }, + }, + }, + }, + { + id: 0, + title: { + text: 'This is a Prebid Native Creative', + }, + }, + { + id: 2, + data: { + value: 'Prebid.org', + }, + }, + ], + link: { + url: 'https://ams3-ib.adnxs.com/click?AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQKZS4ZZl5vVbR6p-A-MwnyTZ7QVkAAAAAOLoyQBtJAAAbSQAAAIAAAC8pM8FnPgWAAAAAABVU0QAVVNEAAEAAQBNXQAAAAABAgMCAAAAALoAURe69gAAAAA./bcr=AAAAAAAA8D8=/pp=${AUCTION_PRICE}/cnd=%21JBC72Aj8-LwKELzJvi4YnPFbIAQoADEAAAAAAAAkQDoJQU1TMzo2MTM1QNAwSQAAAAAAAPA_UQAAAAAAAAAAWQAAAAAAAAAAYQAAAAAAAAAAaQAAAAAAAAAAcQAAAAAAAAAAeACJAQAAAAAAAAAA/cca=OTMyNSNBTVMzOjYxMzU=/bn=97062/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html', + }, + eventtrackers: [ + { + event: 1, + method: 1, + url: 'https://ams3-ib.adnxs.com/it?an_audit=0&referrer=https%3A%2F%2Ftest.nexx360.io%2Fadapter%2Fnative%2Ftest.html&e=wqT_3QKJCqAJBQAAAwDWAAUBCNnbl6AGEKalhbfZzPn6WxjH1PqbsJzMzyQqNgkAAAECCCRAEQEHEAAAJEAZEQkAIREJACkRCQAxEQmoMOLRpwY47UhA7UhIAlC8yb4uWJzxW2AAaM26dXim9gWAAQGKAQNVU0SSAQEG9F4BmAEBoAEBqAEBsAEAuAECwAEDyAEC0AEJ2AEA4AEA8AEAigIpdWYoJ2EnLCAyNTI5ODg1LCAwKTt1ZigncicsIDk3NDk0MjA0LCAwKTuSAvEDIS0xRDNJQWo4LUx3S0VMekp2aTRZQUNDYzhWc3dBRGdBUUFSSTdVaFE0dEduQmxnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFIenJXcWtBQUFrUU1FQjg2MXFwQUFBSkVESkFYSUtWbWViSmZJXzJRRUFBQUFBQUFEd1AtQUJBUFVCQUFBQUFKZ0NBS0FDQUxVQ0FBQUFBTDBDQUFBQUFNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpnREFib0RDVUZOVXpNNk5qRXpOZUFEMERDSUJBQ1FCQUNZQkFIQkJBQUFBQUFBQUFBQXlRUUFBCQscQUFOZ0VBUEURlSxBQUFDSUJmY3ZxUVUBDQRBQQGoCDdFRgEKCQEMREJCUQkKAQEAeRUoAUwyKAAAWi4oALg0QVhBaEQzd0JhTEQzd0w0QmQyMG1nR0NCZ05WVTBTSUJnQ1FCZ0dZQmdDaEJnQQFONEFBQ1JBcUFZQnNnWWtDHXQARR0MAEcdDABJHQw8dUFZS5oClQEhSkJDNzJBajL1ASRuUEZiSUFRb0FEFfhUa1FEb0pRVTFUTXpvMk1UTTFRTkF3UxFRDFBBX1URDAxBQUFXHQwAWR0MAGEdDABjHQwQZUFDSkEdEMjYAvfpA-ACrZhI6gIwaHR0cHM6Ly90ZXN0Lm5leHgzNjAuaW8vYWRhcHRlci9uYXRpdmUJH_CaaHRtbIADAIgDAZADAJgDFKADAaoDAMAD4KgByAMA2AMA4AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAqAQAsgQMCAAQABgAIAAwADgAuAQAwASA2rgiyAQA0gQOOTMyNSNBTVMzOjYxMzXaBAIIAeAEAPAEvMm-LvoEEgkAAABAPG1IQBEAAACgV8oCQIgFAZgFAKAF______8BBbABqgUkM2MwMjkwYzEtNmU3NS00ZWY3LTllMzctMTdmNWViZjNiZmEzwAUAyQWJFxTwP9IFCQkJDHgAANgFAeAFAfAFmfQh-gUECAAQAJAGAZgGALgGAMEGCSUo8D_QBvUv2gYWChAJERkBAdpg4AYM8gYCCACABwGIBwCgB0HIB6b2BdIHDRVkASYI2gcGAV1oGADgBwDqBwIIAPAHAIoIAhAAlQgAAIA_mAgB&s=ccf63f2e483a37091d2475d895e7cf7c911d1a78&pp=${AUCTION_PRICE}', + }, + ], + }, + }, + }]; + expect(output).to.eql(expectOutput); }); }); - describe('getUserSyncs()', function() { + describe('getUserSyncs()', () => { const response = { body: { cookies: [] } }; - it('Verifies user sync without cookie in bid response', function () { - var syncs = spec.getUserSyncs({}, [response], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); - expect(syncs).to.have.lengthOf(0); + it('Verifies user sync without cookie in bid response', () => { + const syncs = spec.getUserSyncs({}, [response], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.eql([]); }); - it('Verifies user sync with cookies in bid response', function () { + it('Verifies user sync with cookies in bid response', () => { response.body.ext = { cookies: [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}] }; - var syncs = spec.getUserSyncs({}, [response], DEFAULT_OPTIONS.gdprConsent); - expect(syncs).to.have.lengthOf(1); - expect(syncs[0]).to.have.property('type').and.to.equal('image'); - expect(syncs[0]).to.have.property('url').and.to.equal('http://www.cookie.sync.org/'); + const syncs = spec.getUserSyncs({}, [response], DEFAULT_OPTIONS.gdprConsent); + const expectedSyncs = [{ type: 'image', url: 'http://www.cookie.sync.org/' }]; + expect(syncs).to.eql(expectedSyncs); }); - it('Verifies user sync with no bid response', function() { + it('Verifies user sync with no bid response', () => { var syncs = spec.getUserSyncs({}, null, DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); - expect(syncs).to.have.lengthOf(0); + expect(syncs).to.eql([]); }); - it('Verifies user sync with no bid body response', function() { + it('Verifies user sync with no bid body response', () => { var syncs = spec.getUserSyncs({}, [], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); - expect(syncs).to.have.lengthOf(0); + expect(syncs).to.eql([]); var syncs = spec.getUserSyncs({}, [{}], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); - expect(syncs).to.have.lengthOf(0); + expect(syncs).to.eql([]); }); }); }); diff --git a/test/spec/modules/nodalsAiRtdProvider_spec.js b/test/spec/modules/nodalsAiRtdProvider_spec.js new file mode 100644 index 00000000000..551a08064a3 --- /dev/null +++ b/test/spec/modules/nodalsAiRtdProvider_spec.js @@ -0,0 +1,858 @@ +import { expect } from 'chai'; +import { MODULE_TYPE_RTD } from 'src/activities/modules.js'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; +import { server } from 'test/mocks/xhr.js'; + +import { nodalsAiRtdSubmodule } from 'modules/nodalsAiRtdProvider.js'; + +const jsonResponseHeaders = { + 'Content-Type': 'application/json', +}; + +const overrideLocalStorageKey = '_foobarbaz_'; + +const successPubEndpointResponse = { + deps: { + '1.0.0': 'https://static.nodals.io/sdk/rule/1.0.0/engine.js', + '1.1.0': 'https://static.nodals.io/sdk/rule/1.1.0/engine.js', + }, + facts: { + 'browser.name': 'safari', + 'geo.country': 'AR', + }, + campaigns: [ + { + id: '41ffa965', + ads: [ + { + delivery_id: '1234', + property_id: 'fd32da', + weighting: 1, + kvs: [ + { + key: 'nodals', + value: '1', + }, + ], + rules: { + engine: { + version: '1.0.0', + }, + conditions: { + ANY: [ + { + fact: 'id', + op: 'allin', + val: ['1', '2', '3'], + }, + ], + NONE: [ + { + fact: 'ua.browser', + op: 'eq', + val: 'opera', + }, + ], + }, + }, + }, + ], + }, + ], +}; + +const engineGetTargetingDataReturnValue = { + adUnit1: { + adv1: 'foobarbaz', + }, +}; + +const generateGdprConsent = (consent = {}) => { + const defaults = { + gdprApplies: true, + purpose1Consent: true, + purpose7Consent: true, + nodalsConsent: true, + }; + const mergedConsent = { ...defaults, ...consent }; + return JSON.parse(JSON.stringify({ + gdpr: { + gdprApplies: mergedConsent.gdprApplies, + consentString: mergedConsent.consentString, + vendorData: { + purpose: { + consents: { + 1: mergedConsent.purpose1Consent, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: mergedConsent.purpose7Consent, + 8: true, + 9: true, + 10: true, + }, + }, + specialFeatureOptins: { + 1: true, + }, + vendor: { + consents: { + 1360: mergedConsent.nodalsConsent, + }, + }, + }, + }, + })); +}; + +const setDataInLocalStorage = (data) => { + const storageData = { ...data }; + nodalsAiRtdSubmodule.storage.setDataInLocalStorage( + nodalsAiRtdSubmodule.STORAGE_KEY, + JSON.stringify(storageData), + ); +}; + +const createTargetingEngineStub = (getTargetingDataReturnValue = {}, raiseError = false) => { + const version = '1.x.x'; + const initStub = sinon.stub(); + const getTargetingDataStub = sinon.stub(); + const getBidRequestDataStub = sinon.stub(); + const onBidResponseEventStub = sinon.stub(); + const onAuctionEndEventStub = sinon.stub(); + if (raiseError) { + getTargetingDataStub.throws(new Error('Stubbed error')); + } else { + getTargetingDataStub.returns(getTargetingDataReturnValue); + } + window.$nodals = window.$nodals || {}; + window.$nodals.adTargetingEngine = window.$nodals.adTargetingEngine || {}; + window.$nodals.adTargetingEngine[version] = { + init: initStub, + getTargetingData: getTargetingDataStub, + getBidRequestData: getBidRequestDataStub, + onBidResponseEvent: onBidResponseEventStub, + onAuctionEndEvent: onAuctionEndEventStub, + }; + return window.$nodals.adTargetingEngine[version]; +}; + + +describe('NodalsAI RTD Provider', () => { + let sandbox; + let validConfig; + const permissiveUserConsent = generateGdprConsent(); + const vendorRestrictiveUserConsent = generateGdprConsent({ nodalsConsent: false }); + const noPurpose1UserConsent = generateGdprConsent({ purpose1Consent: false }); + const noPurpose7UserConsent = generateGdprConsent({ purpose7Consent: false }); + const outsideGdprUserConsent = generateGdprConsent({ gdprApplies: false }); + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + validConfig = { params: { propertyId: '10312dd2' } }; + + server.respondWith([ + 200, + jsonResponseHeaders, + JSON.stringify(successPubEndpointResponse) + ]); + }); + + afterEach(() => { + if (window.$nodals) { + delete window.$nodals; + } + + nodalsAiRtdSubmodule.storage.removeDataFromLocalStorage( + nodalsAiRtdSubmodule.STORAGE_KEY + ); + nodalsAiRtdSubmodule.storage.removeDataFromLocalStorage( + overrideLocalStorageKey + ); + + sandbox.restore(); + }); + + describe('Module properties', () => { + it('should have the name property set correctly', function () { + expect(nodalsAiRtdSubmodule.name).equals('nodalsAi'); + }); + + it('should expose the correct TCF Global Vendor ID', function () { + expect(nodalsAiRtdSubmodule.gvlid).equals(1360); + }); + }); + + describe('init()', () => { + describe('when initialised with empty consent data', () => { + it('should return true when initialised with valid config and empty user consent', function () { + const result = nodalsAiRtdSubmodule.init(validConfig, {}); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(1); + }); + + it('should return false when initialised with invalid config', () => { + const config = { params: { invalid: true } }; + const result = nodalsAiRtdSubmodule.init(config, {}); + server.respond(); + + expect(result).to.be.false; + expect(server.requests.length).to.equal(0); + }); + }); + + describe('when initialised with valid config data', () => { + it('should return false when user is under GDPR jurisdiction and purpose1 has not been granted', () => { + const result = nodalsAiRtdSubmodule.init(validConfig, noPurpose1UserConsent); + server.respond(); + + expect(result).to.be.false; + expect(server.requests.length).to.equal(0); + }); + + it('should return false when user is under GDPR jurisdiction and purpose7 has not been granted', () => { + const result = nodalsAiRtdSubmodule.init(validConfig, noPurpose7UserConsent); + server.respond(); + + expect(result).to.be.false; + expect(server.requests.length).to.equal(0); + }); + + it('should return false when user is under GDPR jurisdiction and Nodals AI as a vendor has no consent', () => { + const result = nodalsAiRtdSubmodule.init(validConfig, vendorRestrictiveUserConsent); + server.respond(); + + expect(result).to.be.false; + expect(server.requests.length).to.equal(0); + }); + + it('should return false when user is under GDPR jurisdiction and Nodals AI is not present in the decoded consent string', () => { + const userConsent = JSON.parse(JSON.stringify(permissiveUserConsent)); + userConsent.gdpr.vendorData.vendor.consents = {}; + const result = nodalsAiRtdSubmodule.init(validConfig, userConsent); + server.respond(); + + expect(result).to.be.false; + expect(server.requests.length).to.equal(0); + }); + + it('should return true when user is under GDPR jurisdiction and all consent provided', function () { + const result = nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(1); + }); + + it('should return true when user is not under GDPR jurisdiction', () => { + const result = nodalsAiRtdSubmodule.init(validConfig, outsideGdprUserConsent); + 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', () => { + it('should return true and not make a remote request when stored data is valid', function () { + setDataInLocalStorage({ data: { foo: 'bar' }, createdAt: Date.now() }); + const result = nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(0); + }); + + it('should return true and make a remote request when stored data has no TTL defined', function () { + setDataInLocalStorage({ data: { foo: 'bar' } }); + const result = nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(1); + }); + + it('should return true and make a remote request when stored data has expired', function () { + setDataInLocalStorage({ data: { foo: 'bar' }, createdAt: 100 }); + const result = nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(1); + }); + + it('should detect stale data if override TTL is exceeded', function () { + const fiveMinutesAgoMs = Date.now() - (5 * 60 * 1000); + setDataInLocalStorage({ + data: { foo: 'bar' }, + createdAt: fiveMinutesAgoMs, + }); + const config = Object.assign({}, validConfig); + config.params.storage = { ttl: 4 * 60 }; + const result = nodalsAiRtdSubmodule.init(config, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(1); + }); + + it('should detect stale data if remote defined TTL is exceeded', function () { + const fiveMinutesAgoMs = Date.now() - (5 * 60 * 1000); + setDataInLocalStorage({ + data: { foo: 'bar', meta: { ttl: 4 * 60 } }, + createdAt: fiveMinutesAgoMs, + }); + const result = nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(1); + }); + + it('should respect pub defined TTL over remote defined TTL', function () { + const fiveMinutesAgoMs = Date.now() - (5 * 60 * 1000); + setDataInLocalStorage({ + data: { foo: 'bar', meta: { ttl: 4 * 60 } }, + createdAt: fiveMinutesAgoMs, + }); + const config = Object.assign({}, validConfig); + config.params.storage = { ttl: 6 * 60 }; + const result = nodalsAiRtdSubmodule.init(config, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(0); + }); + + it('should NOT detect stale data if override TTL is not exceeded', function () { + const fiveMinutesAgoMs = Date.now() - 5 * 60 * 1000; + setDataInLocalStorage({ + data: { foo: 'bar' }, + createdAt: fiveMinutesAgoMs, + }); + const config = Object.assign({}, validConfig); + config.params.storage = { ttl: 6 * 60 }; + const result = nodalsAiRtdSubmodule.init(config, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(0); + }); + + it('should return true and make a remote request when data stored under default key, but override key specified', () => { + setDataInLocalStorage({ data: { foo: 'bar' }, createdAt: Date.now() }); + const config = Object.assign({}, validConfig); + config.params.storage = { key: overrideLocalStorageKey }; + const result = nodalsAiRtdSubmodule.init(config, permissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(1); + }); + }); + + describe('when performing requests to the publisher endpoint', () => { + it('should construct the correct URL to the default origin', () => { + nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + let request = server.requests[0]; + server.respond(); + + expect(request.method).to.equal('GET'); + expect(request.withCredentials).to.be.false; + const requestUrl = new URL(request.url); + expect(requestUrl.origin).to.equal('https://nodals.io'); + }); + + it('should construct the URL to the overridden origin when specified in the config', () => { + const config = Object.assign({}, validConfig); + config.params.endpoint = { origin: 'http://localhost:8000' }; + nodalsAiRtdSubmodule.init(config, permissiveUserConsent); + let request = server.requests[0]; + server.respond(); + + expect(request.method).to.equal('GET'); + expect(request.withCredentials).to.be.false; + const requestUrl = new URL(request.url); + expect(requestUrl.origin).to.equal('http://localhost:8000'); + }); + + it('should construct the correct URL with the correct path', () => { + nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + let request = server.requests[0]; + server.respond(); + + const requestUrl = new URL(request.url); + expect(requestUrl.pathname).to.equal('/p/v1/10312dd2/config'); + }); + + it('should construct the correct URL with the correct GDPR query params', () => { + const consentData = { + consentString: 'foobarbaz', + }; + nodalsAiRtdSubmodule.init(validConfig, generateGdprConsent(consentData)); + let request = server.requests[0]; + server.respond(); + + const requestUrl = new URL(request.url); + expect(requestUrl.searchParams.get('gdpr')).to.equal('1'); + expect(requestUrl.searchParams.get('gdpr_consent')).to.equal( + 'foobarbaz' + ); + expect(requestUrl.searchParams.get('us_privacy')).to.equal(''); + expect(requestUrl.searchParams.get('gpp')).to.equal(''); + expect(requestUrl.searchParams.get('gpp_sid')).to.equal(''); + }); + }); + + describe('when handling responses from the publisher endpoint', () => { + it('should store successful response data in local storage', () => { + nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + let request = server.requests[0]; + server.respond(); + + const storedData = JSON.parse( + nodalsAiRtdSubmodule.storage.getDataFromLocalStorage( + nodalsAiRtdSubmodule.STORAGE_KEY + ) + ); + + expect(request.method).to.equal('GET'); + expect(storedData).to.have.property('createdAt'); + expect(storedData.data).to.deep.equal(successPubEndpointResponse); + }); + + it('should store successful response data in local storage under the override key', () => { + const config = Object.assign({}, validConfig); + config.params.storage = { key: overrideLocalStorageKey }; + nodalsAiRtdSubmodule.init(config, permissiveUserConsent); + server.respond(); + let request = server.requests[0]; + const storedData = JSON.parse( + nodalsAiRtdSubmodule.storage.getDataFromLocalStorage(overrideLocalStorageKey) + ); + + expect(request.method).to.equal('GET'); + expect(storedData).to.have.property('createdAt'); + expect(storedData.data).to.deep.equal(successPubEndpointResponse); + }); + + it('should attempt to load the referenced script libraries contained in the response payload', () => { + nodalsAiRtdSubmodule.init(validConfig, permissiveUserConsent); + server.respond(); + + expect(loadExternalScriptStub.calledTwice).to.be.true; + expect( + loadExternalScriptStub.calledWith( + successPubEndpointResponse.deps['1.0.0'], + MODULE_TYPE_RTD, + nodalsAiRtdSubmodule.name + ) + ).to.be.true; + expect( + loadExternalScriptStub.calledWith( + successPubEndpointResponse.deps['1.1.0'], + MODULE_TYPE_RTD, + nodalsAiRtdSubmodule.name + ) + ).to.be.true; + }); + }); + }); + + describe('getTargetingData()', () => { + it('should return an empty object when no data is available in local storage, but fetch data', () => { + const result = nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1'], + validConfig, + permissiveUserConsent + ); + server.respond(); + expect(result).to.deep.equal({}); + expect(server.requests.length).to.equal(1); + }); + + it('should return an empty object when getTargetingData throws error', () => { + createTargetingEngineStub({adUnit1: {someKey: 'someValue'}}, true); + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + const result = nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1'], + validConfig, + permissiveUserConsent + ); + expect(result).to.deep.equal({}); + expect(server.requests.length).to.equal(0); + }); + + it('should initialise the versioned targeting engine if fresh data is in storage and not make a HTTP request', () => { + const returnData = {}; + const engine = createTargetingEngineStub(returnData); + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1'], + validConfig, + permissiveUserConsent + ); + server.respond(); + + expect(engine.init.called).to.be.true; + const args = engine.init.getCall(0).args; + expect(args[0]).to.deep.equal(validConfig); + expect(server.requests.length).to.equal(0); + }); + + it('should initialise the versioned targeting engine with stale data if data expired and fetch fresh data', function () { + const returnData = {}; + const engine = createTargetingEngineStub(returnData); + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: 100, + }); + + nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1'], + validConfig, + permissiveUserConsent + ); + server.respond(); + + expect(engine.init.called).to.be.true; + const args = engine.init.getCall(0).args; + expect(args[0]).to.deep.equal(validConfig); + expect(server.requests.length).to.equal(1); + }); + + it('should proxy the correct data to engine.getTargetingData() when storage data is available and we have consent under GDPR jurisdiction', () => { + const engine = createTargetingEngineStub( + engineGetTargetingDataReturnValue + ); + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1', 'adUnit2'], + validConfig, + permissiveUserConsent + ); + server.respond(); + + expect(engine.getTargetingData.called).to.be.true; + const args = engine.getTargetingData.getCall(0).args; + expect(args[0]).to.deep.equal(['adUnit1', 'adUnit2']); + expect(args[1]).to.deep.equal(permissiveUserConsent); + + 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); + }); + + it('should return the data from engine.getTargetingData when storage data is available and we have consent under GDPR jurisdiction', () => { + createTargetingEngineStub(engineGetTargetingDataReturnValue); + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + const result = nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1'], + validConfig, + permissiveUserConsent + ); + server.respond(); + + expect(result).to.deep.equal(engineGetTargetingDataReturnValue); + }); + + it('should return the data from engine.getTargetingData when storage is available and we are NOT under GDPR jurisdiction', () => { + createTargetingEngineStub(engineGetTargetingDataReturnValue); + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + const result = nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1'], + validConfig, + outsideGdprUserConsent + ); + server.respond(); + + expect(result).to.deep.equal(engineGetTargetingDataReturnValue); + }); + + it('should return an empty object when data is available, but user has not provided consent to Nodals AI as a vendor', () => { + createTargetingEngineStub(engineGetTargetingDataReturnValue); + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + const result = nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1'], + validConfig, + vendorRestrictiveUserConsent + ); + server.respond(); + + expect(result).to.deep.equal({}); + }); + }); + + describe('getBidRequestData()', () => { + it('should invoke callback without attempting to initialise the engine if we do not have consent', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const customUserConsent = generateGdprConsent({ nodalsConsent: false }); + const callback = sinon.spy(); + nodalsAiRtdSubmodule.getBidRequestData( + {}, callback, validConfig, vendorRestrictiveUserConsent + ); + server.respond(); + + expect(callback.called).to.be.true; + expect(engine.init.called).to.be.false; + expect(window.$nodals.cmdQueue).to.be.undefined + expect(server.requests.length).to.equal(0); + }); + + it('should not store function arguments in a queue when no data is in localstorage and make a HTTP request for data', () => { + const callback = sinon.spy(); + const requestObj = {dummy: 'obj'} + nodalsAiRtdSubmodule.getBidRequestData( + requestObj, callback, validConfig, permissiveUserConsent + ); + server.respond(); + + expect(callback.called).to.be.true; + expect(window.$nodals).to.be.undefined; + expect(server.requests.length).to.equal(1); + }); + + it('should store function arguments in a queue when data is in localstorage and engine not loaded', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const callback = sinon.spy(); + const reqBidsConfigObj = {dummy: 'obj'} + nodalsAiRtdSubmodule.getBidRequestData( + reqBidsConfigObj, callback, validConfig, permissiveUserConsent + ); + server.respond(); + + expect(callback.called).to.be.false; + expect(window.$nodals.cmdQueue).to.be.an('array').with.length(1); + expect(window.$nodals.cmdQueue[0].cmd).to.equal('getBidRequestData'); + expect(window.$nodals.cmdQueue[0].runtimeFacts).to.have.keys(['prebid.version', 'page.url']); + expect(window.$nodals.cmdQueue[0].data).to.deep.include({config: validConfig, reqBidsConfigObj, callback, userConsent: permissiveUserConsent}); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('deps').that.deep.equals( + successPubEndpointResponse.deps); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('facts').that.deep.includes( + successPubEndpointResponse.facts); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('campaigns').that.deep.equals( + successPubEndpointResponse.campaigns); + expect(server.requests.length).to.equal(0); + }); + + it('should proxy the correct data to engine.getBidRequestData when data is in localstorage and library has loaded', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const callback = sinon.spy(); + const reqBidsConfigObj = {dummy: 'obj'} + nodalsAiRtdSubmodule.getBidRequestData( + reqBidsConfigObj, callback, validConfig, permissiveUserConsent + ); + 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(permissiveUserConsent); + 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()', () => { + it('should not proxy the call if we do not have user consent', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const bidResponse = {dummy: 'obj', 'bid': 'foo'}; + nodalsAiRtdSubmodule.onBidResponseEvent( + bidResponse, validConfig, vendorRestrictiveUserConsent + ); + server.respond(); + + expect(engine.init.called).to.be.false; + expect(engine.onBidResponseEvent.called).to.be.false; + expect(window.$nodals.cmdQueue).to.be.undefined + expect(server.requests.length).to.equal(0); + }); + + it('should not store function arguments in a queue when no data is in localstorage and make a HTTP request for data', () => { + const bidResponse = {dummy: 'obj', 'bid': 'foo'}; + nodalsAiRtdSubmodule.onBidResponseEvent( + bidResponse, validConfig, permissiveUserConsent + ); + server.respond(); + + expect(window.$nodals).to.be.undefined; + expect(server.requests.length).to.equal(1); + }); + + it('should store function arguments in a queue when data is in localstorage and engine not loaded', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const userConsent = generateGdprConsent(); + const bidResponse = {dummy: 'obj', 'bid': 'foo'}; + nodalsAiRtdSubmodule.onBidResponseEvent( + bidResponse, validConfig, permissiveUserConsent + ); + server.respond(); + + expect(window.$nodals.cmdQueue).to.be.an('array').with.length(1); + expect(window.$nodals.cmdQueue[0].cmd).to.equal('onBidResponseEvent'); + expect(window.$nodals.cmdQueue[0].runtimeFacts).to.have.keys(['prebid.version', 'page.url']); + expect(window.$nodals.cmdQueue[0].data).to.deep.include({config: validConfig, bidResponse, userConsent: permissiveUserConsent }); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('deps').that.deep.equals( + successPubEndpointResponse.deps); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('facts').that.deep.includes( + successPubEndpointResponse.facts); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('campaigns').that.deep.equals( + successPubEndpointResponse.campaigns); + expect(server.requests.length).to.equal(0); + }); + + it('should proxy the correct data to engine.onBidResponseEvent when data is in localstorage and library has loaded', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const bidResponse = {dummy: 'obj', 'bid': 'foo'}; + nodalsAiRtdSubmodule.onBidResponseEvent( + bidResponse, validConfig, permissiveUserConsent + ); + 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(permissiveUserConsent); + 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()', () => { + it('should not proxy the call if we do not have user consent', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const auctionDetails = {dummy: 'obj', auction: 'foo'}; + nodalsAiRtdSubmodule.onAuctionEndEvent( + auctionDetails, validConfig, vendorRestrictiveUserConsent + ); + server.respond(); + + expect(engine.init.called).to.be.false; + expect(engine.onAuctionEndEvent.called).to.be.false; + expect(window.$nodals.cmdQueue).to.be.undefined + expect(server.requests.length).to.equal(0); + }); + + it('should not store function arguments in a queue when no data is in localstorage and make a HTTP request for data', () => { + const auctionDetails = {dummy: 'obj', auction: 'foo'}; + nodalsAiRtdSubmodule.onAuctionEndEvent( + auctionDetails, validConfig, permissiveUserConsent + ); + server.respond(); + + expect(window.$nodals).to.be.undefined; + expect(server.requests.length).to.equal(1); + }); + + it('should store function arguments in a queue when data is in localstorage and engine not loaded', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const auctionDetails = {dummy: 'obj', auction: 'foo'}; + nodalsAiRtdSubmodule.onAuctionEndEvent( + auctionDetails, validConfig, permissiveUserConsent + ); + server.respond(); + + expect(window.$nodals.cmdQueue).to.be.an('array').with.length(1); + expect(window.$nodals.cmdQueue[0].cmd).to.equal('onAuctionEndEvent'); + expect(window.$nodals.cmdQueue[0].runtimeFacts).to.have.keys(['prebid.version', 'page.url']); + expect(window.$nodals.cmdQueue[0].data).to.deep.include({config: validConfig, auctionDetails, userConsent: permissiveUserConsent }); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('deps').that.deep.equals( + successPubEndpointResponse.deps); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('facts').that.deep.includes( + successPubEndpointResponse.facts); + expect(window.$nodals.cmdQueue[0].data.storedData).to.have.property('campaigns').that.deep.equals( + successPubEndpointResponse.campaigns); + expect(server.requests.length).to.equal(0); + }); + + it('should proxy the correct data to engine.onAuctionEndEvent when data is in localstorage and library has loaded', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const userConsent = generateGdprConsent(); + const auctionDetails = {dummy: 'obj', auction: 'foo'}; + nodalsAiRtdSubmodule.onAuctionEndEvent( + auctionDetails, validConfig, permissiveUserConsent + ); + 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(permissiveUserConsent); + 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/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index aad753571a8..369540de668 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -1,18 +1,25 @@ import { expect } from 'chai'; +import sinon from 'sinon'; import { spec } from 'modules/oguryBidAdapter'; import * as utils from 'src/utils.js'; -import {server} from '../../mocks/xhr.js'; +import { server } from '../../mocks/xhr.js'; const BID_URL = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_URL = 'https://ms-ads-monitoring-events.presage.io/bid_timeout' -describe('OguryBidAdapter', function () { - let bidRequests; - let bidderRequest; +describe('OguryBidAdapter', () => { + let bidRequests, bidderRequestBase, ortb2; + + const currentLocation = 'https://mwtt.ogury.tech/advanced'; bidRequests = [ { adUnitCode: 'adUnitCode', + ortb2Imp: { + ext: { + gpid: 'gpid' + } + }, auctionId: 'auctionId', bidId: 'bidId', bidder: 'ogury', @@ -43,20 +50,7 @@ describe('OguryBidAdapter', function () { return floorResult; }, transactionId: 'transactionId', - userId: { - pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' - }, - userIdAsEids: [ - { - source: 'pubcid.org', - uids: [ - { - id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', - atype: 1 - } - ] - } - ] + userId: { pubcid: 'f5debac9-9a8e-4c08-9820-51e96b69f858' } }, { adUnitCode: 'adUnitCode2', @@ -76,62 +70,130 @@ describe('OguryBidAdapter', function () { }, ]; - bidderRequest = { + ortb2 = { + regs: { + gpp_sid: [7], + gpp: 'DBABLA~BAAAAAAAAQA.QA', + ext: { gdpr: 1 } + }, + site: { + domain: 'mwtt.ogury.tech', + publisher: { domain: 'ogury.tech', id: 'ca06d4199b92bf6808e5ce15b28c6d30' }, + page: currentLocation, + ref: 'https://google.com' + }, + user: { + ext: { + consent: 'CQJI3tqQJI3tqFzABBENBJFsAP_gAEPgAAqIg1NX_H__bW9r8Xr3aft0eY1P99j77sQxBhfJE-4FyLvW_JwXx2EwNA26tqIKmRIEu3ZBIQFlHJHURVigaogVryHsYkGcgTNKJ6BkgFMRI2dYCF5vmYtj-QKY5_p_d3fx2D-t_dv83dzzz8VHn3e5fmckcKCdQ58tDfn9bRKb-5IO9-78v4v09l_rk2_eTVn_pcvr7B-uft87_XU-9_fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQagCzDQqIA-yJCQi0DCKBACIKwgIoEAAAAJA0QEAJAwKdgYBLrCRACBFAAMEAIAAUZAAgAAEgAQiACQAoEAAEAgEAAAAAAgEADAwADgAtBAIAAQHQMUwoAFAsIEiMiIUwIQoEggJbKBBICgQVwgCLDAigERMFAAgCQAVgAAAsVgMASAlYkECWUG0AABAAgFFKFQik6MAQwJmy1U4om0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAACAA.YAAAAAAAAAAA', + eids: [ + { + source: 'pubcid.org', + uids: [{ 'id': 'f5debac9-9a8e-4c08-9820-51e96b69f858', 'atype': 1 }] + } + ] + } + }, + device: { + w: 412, + h: 915, + dnt: 0, + ua: 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36', + language: 'en', + ext: { vpw: 412, vph: 915 }, + sua: { + source: 1, + platform: { brand: 'Android' }, + browsers: [{ brand: 'Google Chrome', version: ['131'] }], + mobile: 1 + } + } + }; + + bidderRequestBase = { + bids: bidRequests, bidderRequestId: 'mock-uuid', auctionId: bidRequests[0].auctionId, gdprConsent: {consentString: 'myConsentString', vendorData: {}, gdprApplies: true}, + gppConsent: {gppString: 'myGppString', gppData: {}, applicableSections: [7], parsedSections: {}}, + timeout: 1000, + ortb2 }; - describe('isBidRequestValid', function () { + describe('isBidRequestValid', () => { it('should validate correct bid', () => { let validBid = utils.deepClone(bidRequests[0]); let isValid = spec.isBidRequestValid(validBid); - expect(isValid).to.equal(true); + expect(isValid).to.true; }); - it('should not validate incorrect bid', () => { + it('should not validate when sizes is not defined', () => { let invalidBid = utils.deepClone(bidRequests[0]); delete invalidBid.sizes; delete invalidBid.mediaTypes; let isValid = spec.isBidRequestValid(invalidBid); - expect(isValid).to.equal(false); + expect(isValid).to.be.false; }); - it('should not validate bid if adunit is not present', () => { + it('should not validate bid when adunit is not defined', () => { let invalidBid = utils.deepClone(bidRequests[0]); delete invalidBid.params.adUnitId; let isValid = spec.isBidRequestValid(invalidBid); - expect(isValid).to.equal(false); + expect(isValid).to.to.be.false; }); - it('should not validate bid if assetKet is not present', () => { + it('should not validate bid when assetKey is not defined', () => { let invalidBid = utils.deepClone(bidRequests[0]); delete invalidBid.params.assetKey; let isValid = spec.isBidRequestValid(invalidBid); - expect(isValid).to.equal(false); + expect(isValid).to.be.false; }); - it('should validate bid if getFloor is not present', () => { - let invalidBid = utils.deepClone(bidRequests[1]); - delete invalidBid.getFloor; + it('should validate the request when only publisherId and adUnitCode is defined', () => { + const validBid = utils.deepClone(bidRequests[0]) + delete validBid.params.adUnitId + delete validBid.params.assetKey - let isValid = spec.isBidRequestValid(invalidBid); - expect(isValid).to.equal(true); + validBid.ortb2 = { site: { publisher: { id: 'publisherId' } } } + + expect(spec.isBidRequestValid(validBid)).to.be.true + }); + + it('should not validate the request when only publisherId is defined', () => { + const invalidBid = utils.deepClone(bidRequests[0]) + delete invalidBid.params.adUnitId + delete invalidBid.params.assetKey + delete invalidBid.adUnitCode + + invalidBid.ortb2 = { site: { publisher: { id: 'publisherId' } } } + + expect(spec.isBidRequestValid(invalidBid)).to.be.false + }); + + it('should not validate the request when only adUnitCode is defined', () => { + const invalidBid = utils.deepClone(bidRequests[0]) + delete invalidBid.params.adUnitId + delete invalidBid.params.assetKey + + expect(spec.isBidRequestValid(invalidBid)).to.be.false }); }); - describe('getUserSyncs', function() { - let syncOptions, gdprConsent; + describe('getUserSyncs', () => { + let syncOptions, gdprConsent, gppConsent; beforeEach(() => { gdprConsent = { gdprApplies: true, consentString: 'CPJl4C8PJl4C8OoAAAENAwCMAP_AAH_AAAAAAPgAAAAIAPgAAAAIAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g' }; + gppConsent = { + gppString: 'DBABLA~BAAAAAAAAQA.QA', + applicableSections: [7] + } }); describe('pixel', () => { @@ -140,7 +202,7 @@ describe('OguryBidAdapter', function () { }); it('should return syncs array with three elements of type image', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(3); expect(userSyncs[0].type).to.equal('image'); @@ -152,22 +214,34 @@ describe('OguryBidAdapter', function () { }); it('should set the source as query param', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs[0].url).to.contain('source=prebid'); - expect(userSyncs[1].url).to.contain('source=prebid'); - expect(userSyncs[2].url).to.contain('source=prebid'); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(new URL(userSyncs[0].url).searchParams.get('source')).to.equal('prebid') }); it('should set the tcString as query param', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs[0].url).to.contain(`iab_string=${gdprConsent.consentString}`); - expect(userSyncs[1].url).to.contain(`iab_string=${gdprConsent.consentString}`); - expect(userSyncs[2].url).to.contain(`iab_string=${gdprConsent.consentString}`); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal(gdprConsent.consentString) + expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal(gdprConsent.consentString) + expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal(gdprConsent.consentString) + }); + + it('should set the gppString as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(new URL(userSyncs[0].url).searchParams.get('gpp')).to.equal(gppConsent.gppString) + expect(new URL(userSyncs[1].url).searchParams.get('gpp')).to.equal(gppConsent.gppString) + expect(new URL(userSyncs[2].url).searchParams.get('gpp')).to.equal(gppConsent.gppString) + }); + + it('should set the gpp_sid as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(new URL(userSyncs[0].url).searchParams.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + expect(new URL(userSyncs[1].url).searchParams.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + expect(new URL(userSyncs[2].url).searchParams.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) }); it('should return an empty array when pixel is disable', () => { syncOptions.pixelEnabled = false; - expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); + expect(spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent)).to.have.lengthOf(0); }); it('should return syncs array with three elements of type image when consentString is undefined', () => { @@ -176,14 +250,14 @@ describe('OguryBidAdapter', function () { consentString: undefined }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(3); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') }); it('should return syncs array with three elements of type image when consentString is null', () => { @@ -192,40 +266,40 @@ describe('OguryBidAdapter', function () { consentString: null }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(3); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') }); it('should return syncs array with three elements of type image when gdprConsent is undefined', () => { gdprConsent = undefined; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(3); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') }); it('should return syncs array with three elements of type image when gdprConsent is null', () => { gdprConsent = null; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(3); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') }); it('should return syncs array with three elements of type image when gdprConsent is null and gdprApplies is false', () => { @@ -234,14 +308,14 @@ describe('OguryBidAdapter', function () { consentString: null }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(3); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') }); it('should return syncs array with three elements of type image when gdprConsent is empty string and gdprApplies is false', () => { @@ -250,14 +324,158 @@ describe('OguryBidAdapter', function () { consentString: '' }; + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') + expect(userSyncs[1].type).to.equal('image'); + expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') + expect(userSyncs[2].type).to.equal('image'); + expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') + }); + + it('should return syncs array with three elements of type image when gppString is undefined', () => { + gppConsent = { + applicableSections: [7], + gppString: undefined + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[2].type).to.equal('image'); + + const firstUrlSync = new URL(userSyncs[0].url).searchParams + expect(firstUrlSync.get('gpp')).to.equal('') + expect(firstUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + + const secondtUrlSync = new URL(userSyncs[1].url).searchParams + expect(secondtUrlSync.get('gpp')).to.equal('') + expect(secondtUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + + const thirdUrlSync = new URL(userSyncs[2].url).searchParams + expect(thirdUrlSync.get('gpp')).to.equal('') + expect(thirdUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + }); + + it('should return syncs array with three elements of type image when gppString is null', () => { + gppConsent = { + applicableSections: [7, 8], + gppString: null + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[2].type).to.equal('image'); + + const firstUrlSync = new URL(userSyncs[0].url).searchParams + expect(firstUrlSync.get('gpp')).to.equal('') + expect(firstUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + + const secondtUrlSync = new URL(userSyncs[1].url).searchParams + expect(secondtUrlSync.get('gpp')).to.equal('') + expect(secondtUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + + const thirdUrlSync = new URL(userSyncs[2].url).searchParams + expect(thirdUrlSync.get('gpp')).to.equal('') + expect(thirdUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + }); + + it('should return syncs array with three elements of type image when gppConsent is undefined', () => { + gppConsent = undefined; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[2].type).to.equal('image'); + + const firstUrlSync = new URL(userSyncs[0].url).searchParams + expect(firstUrlSync.get('gpp')).to.equal('') + expect(firstUrlSync.get('gpp_sid')).to.equal('') + + const secondtUrlSync = new URL(userSyncs[1].url).searchParams + expect(secondtUrlSync.get('gpp')).to.equal('') + expect(secondtUrlSync.get('gpp_sid')).to.equal('') + + const thirdUrlSync = new URL(userSyncs[2].url).searchParams + expect(thirdUrlSync.get('gpp')).to.equal('') + expect(thirdUrlSync.get('gpp_sid')).to.equal('') + }); + + it('should return syncs array with three elements of type image when gppConsent is null', () => { + gppConsent = null; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[2].type).to.equal('image'); + + const firstUrlSync = new URL(userSyncs[0].url).searchParams + expect(firstUrlSync.get('gpp')).to.equal('') + expect(firstUrlSync.get('gpp_sid')).to.equal('') + + const secondtUrlSync = new URL(userSyncs[1].url).searchParams + expect(secondtUrlSync.get('gpp')).to.equal('') + expect(secondtUrlSync.get('gpp_sid')).to.equal('') + + const thirdUrlSync = new URL(userSyncs[2].url).searchParams + expect(thirdUrlSync.get('gpp')).to.equal('') + expect(thirdUrlSync.get('gpp_sid')).to.equal('') + }); + + it('should return syncs array with three elements of type image when gppConsent is null and applicableSections is empty', () => { + gppConsent = { + applicableSections: [], + gppString: null + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[2].type).to.equal('image'); + + const firstUrlSync = new URL(userSyncs[0].url).searchParams + expect(firstUrlSync.get('gpp')).to.equal('') + expect(firstUrlSync.get('gpp_sid')).to.equal('') + + const secondtUrlSync = new URL(userSyncs[1].url).searchParams + expect(secondtUrlSync.get('gpp')).to.equal('') + expect(secondtUrlSync.get('gpp_sid')).to.equal('') + + const thirdUrlSync = new URL(userSyncs[2].url).searchParams + expect(thirdUrlSync.get('gpp')).to.equal('') + expect(thirdUrlSync.get('gpp_sid')).to.equal('') + }); + + it('should return syncs array with three elements of type image when gppString is empty string and applicableSections is empty', () => { + gppConsent = { + applicableSections: [], + gppString: '' + }; + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); expect(userSyncs).to.have.lengthOf(3); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + + const firstUrlSync = new URL(userSyncs[0].url).searchParams + expect(firstUrlSync.get('gpp')).to.equal('') + expect(firstUrlSync.get('gpp_sid')).to.equal('') + + const secondtUrlSync = new URL(userSyncs[1].url).searchParams + expect(secondtUrlSync.get('gpp')).to.equal('') + expect(secondtUrlSync.get('gpp_sid')).to.equal('') + + const thirdUrlSync = new URL(userSyncs[2].url).searchParams + expect(thirdUrlSync.get('gpp')).to.equal('') + expect(thirdUrlSync.get('gpp_sid')).to.equal('') }); }); @@ -267,7 +485,7 @@ describe('OguryBidAdapter', function () { }); it('should return syncs array with one element of type iframe', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('iframe'); @@ -275,18 +493,23 @@ describe('OguryBidAdapter', function () { }); it('should set the source as query param', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs[0].url).to.contain('source=prebid'); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(new URL(userSyncs[0].url).searchParams.get('source')).to.equal('prebid'); }); it('should set the tcString as query param', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs[0].url).to.contain(`gdpr_consent=${gdprConsent.consentString}`); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal(gdprConsent.consentString); + }); + + it('should set the gppString as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(new URL(userSyncs[0].url).searchParams.get('gpp')).to.equal(gppConsent.gppString); }); it('should return an empty array when iframe is disable', () => { syncOptions.iframeEnabled = false; - expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); + expect(spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent)).to.have.lengthOf(0); }); it('should return syncs array with one element of type iframe when consentString is undefined', () => { @@ -295,10 +518,10 @@ describe('OguryBidAdapter', function () { consentString: undefined }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('iframe'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal(''); }); it('should return syncs array with one element of type iframe when consentString is null', () => { @@ -307,28 +530,28 @@ describe('OguryBidAdapter', function () { consentString: null }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('iframe'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal(''); }); it('should return syncs array with one element of type iframe when gdprConsent is undefined', () => { gdprConsent = undefined; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('iframe'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal(''); }); it('should return syncs array with one element of type iframe when gdprConsent is null', () => { gdprConsent = null; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('iframe'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal(''); }); it('should return syncs array with one element of type iframe when gdprConsent is null and gdprApplies is false', () => { @@ -337,10 +560,10 @@ describe('OguryBidAdapter', function () { consentString: null }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('iframe'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal(''); }); it('should return syncs array with one element of type iframe when gdprConsent is empty string and gdprApplies is false', () => { @@ -349,468 +572,248 @@ describe('OguryBidAdapter', function () { consentString: '' }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('iframe'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal(''); }); - }); - }); - - describe('buildRequests', function () { - const stubbedWidth = 200 - const stubbedHeight = 600 - const stubbedCurrentTime = 1234567890 - const stubbedDevicePixelRatio = 1 - const stubbedWidthMethod = sinon.stub(window.top.document.documentElement, 'clientWidth').get(function() { - return stubbedWidth; - }); - const stubbedHeightMethod = sinon.stub(window.top.document.documentElement, 'clientHeight').get(function() { - return stubbedHeight; - }); - const stubbedCurrentTimeMethod = sinon.stub(document.timeline, 'currentTime').get(function() { - return stubbedCurrentTime; - }); - const stubbedDevicePixelMethod = sinon.stub(window, 'devicePixelRatio').get(function() { - return stubbedDevicePixelRatio; - }); - - const defaultTimeout = 1000; - const expectedRequestObject = { - id: 'mock-uuid', - at: 1, - tmax: defaultTimeout, - imp: [{ - id: bidRequests[0].bidId, - tagid: bidRequests[0].params.adUnitId, - bidfloor: 4, - banner: { - format: [{ - w: 300, - h: 250 - }] - }, - ext: { - ...bidRequests[0].params, - timeSpentOnPage: stubbedCurrentTime - } - }, { - id: bidRequests[1].bidId, - tagid: bidRequests[1].params.adUnitId, - banner: { - format: [{ - w: 600, - h: 500 - }] - }, - ext: { - ...bidRequests[1].params, - timeSpentOnPage: stubbedCurrentTime - } - }], - regs: { - ext: { - gdpr: 1 - }, - }, - site: { - id: bidRequests[0].params.assetKey, - domain: window.location.hostname, - page: window.location.href - }, - user: { - ext: { - consent: bidderRequest.gdprConsent.consentString, - uids: { - pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' - }, - eids: [ - { - source: 'pubcid.org', - uids: [ - { - id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', - atype: 1 - } - ] - } - ], - }, - }, - ext: { - prebidversion: '$prebid.version$', - adapterversion: '1.6.0' - }, - device: { - w: stubbedWidth, - h: stubbedHeight, - pxratio: stubbedDevicePixelRatio, - } - }; + it('should return syncs array with one element of type iframe when gppConsent is empty string and applicableSections is empty', () => { + gppConsent = { + applicableSections: [], + gppString: '' + }; + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); - after(function() { - stubbedWidthMethod.restore(); - stubbedHeightMethod.restore(); - stubbedCurrentTimeMethod.restore(); - stubbedDevicePixelMethod.restore(); - }); + const urlParams = new URL(userSyncs[0].url).searchParams + expect(urlParams.get('gpp')).to.equal('') + expect(urlParams.get('gpp_sid')).to.equal('') + }); - it('sends bid request to ENDPOINT via POST', function () { - const validBidRequests = utils.deepClone(bidRequests) + it('should return syncs array with one element of type iframe when gppString is undefined', () => { + gppConsent = { + applicableSections: [7], + gppString: undefined + }; - const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.url).to.equal(BID_URL); - expect(request.method).to.equal('POST'); - }); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); - it('timeSpentOnpage should be 0 if timeline is undefined', function () { - const stubbedTimelineMethod = sinon.stub(document, 'timeline').get(function() { - return undefined; + const urlParams = new URL(userSyncs[0].url).searchParams + expect(urlParams.get('gpp')).to.equal('') + expect(urlParams.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) }); - const validBidRequests = utils.deepClone(bidRequests) - - const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data.imp[0].ext.timeSpentOnPage).to.equal(0); - stubbedTimelineMethod.restore(); - }); - it('send device pixel ratio in bid request', function() { - const validBidRequests = utils.deepClone(bidRequests) - - const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data).to.deep.equal(expectedRequestObject); - expect(request.data.device.pxratio).to.be.a('number'); - }) + it('should return syncs array with one element of type iframe when gppString is null', () => { + gppConsent = { + applicableSections: [7], + gppString: null + }; - it('bid request object should be conform', function () { - const validBidRequests = utils.deepClone(bidRequests) + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); - const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data).to.deep.equal(expectedRequestObject); - expect(request.data.regs.ext.gdpr).to.be.a('number'); - }); + const urlParams = new URL(userSyncs[0].url).searchParams + expect(urlParams.get('gpp')).to.equal('') + expect(urlParams.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) + }); - describe('getClientWidth', () => { - function testGetClientWidth(testGetClientSizeParams) { - const stubbedClientWidth = sinon.stub(window.top.document.documentElement, 'clientWidth').get(function() { - return testGetClientSizeParams.docClientSize - }) + it('should return syncs array with one element of type iframe when gppConsent is undefined', () => { + gppConsent = undefined; - const stubbedInnerWidth = sinon.stub(window.top, 'innerWidth').get(function() { - return testGetClientSizeParams.innerSize - }) + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); - const stubbedOuterWidth = sinon.stub(window.top, 'outerWidth').get(function() { - return testGetClientSizeParams.outerSize - }) + const urlParams = new URL(userSyncs[0].url).searchParams + expect(urlParams.get('gpp')).to.equal('') + expect(urlParams.get('gpp_sid')).to.equal('') + }); - const stubbedWidth = sinon.stub(window.top.screen, 'width').get(function() { - return testGetClientSizeParams.screenSize - }) + it('should return syncs array with one element of type iframe when gppConsent is null', () => { + gppConsent = null; - const validBidRequests = utils.deepClone(bidRequests) + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); - const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data.device.w).to.equal(testGetClientSizeParams.expectedSize); + const urlParams = new URL(userSyncs[0].url).searchParams + expect(urlParams.get('gpp')).to.equal('') + expect(urlParams.get('gpp_sid')).to.equal('') + }); - stubbedClientWidth.restore(); - stubbedInnerWidth.restore(); - stubbedOuterWidth.restore(); - stubbedWidth.restore(); - } + it('should return syncs array with one element of type iframe when gppConsent is null and applicableSections is empty', () => { + gppConsent = { + applicableSections: [], + gppString: null + }; - it('should get documentElementClientWidth by default', () => { - testGetClientWidth({ - docClientSize: 22, - innerSize: 50, - outerSize: 45, - screenSize: 10, - expectedSize: 22, - }) - }) - - it('should get innerWidth as first fallback', () => { - testGetClientWidth({ - docClientSize: undefined, - innerSize: 700, - outerSize: 650, - screenSize: 10, - expectedSize: 700, - }) - }) - - it('should get outerWidth as second fallback', () => { - testGetClientWidth({ - docClientSize: undefined, - innerSize: undefined, - outerSize: 650, - screenSize: 10, - expectedSize: 650, - }) - }) - - it('should get screenWidth as last fallback', () => { - testGetClientWidth({ - docClientSize: undefined, - innerSize: undefined, - outerSize: undefined, - screenSize: 10, - expectedSize: 10, - }); - }); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); - it('should return 0 if all window width values are undefined', () => { - testGetClientWidth({ - docClientSize: undefined, - innerSize: undefined, - outerSize: undefined, - screenSize: undefined, - expectedSize: 0, - }); + const urlParams = new URL(userSyncs[0].url).searchParams + expect(urlParams.get('gpp')).to.equal('') + expect(urlParams.get('gpp_sid')).to.equal('') }); }); + }); - describe('getClientHeight', () => { - function testGetClientHeight(testGetClientSizeParams) { - const stubbedClientHeight = sinon.stub(window.top.document.documentElement, 'clientHeight').get(function() { - return testGetClientSizeParams.docClientSize - }) + describe('buildRequests', () => { + let windowTopStub; + const stubbedCurrentTime = 1234567890 + const stubbedDevicePixelRatio = 1 + const stubbedCurrentTimeMethod = sinon.stub(document.timeline, 'currentTime').get(function() { + return stubbedCurrentTime; + }); - const stubbedInnerHeight = sinon.stub(window.top, 'innerHeight').get(function() { - return testGetClientSizeParams.innerSize - }) + const stubbedDevicePixelMethod = sinon.stub(window, 'devicePixelRatio').get(function() { + return stubbedDevicePixelRatio; + }); - const stubbedOuterHeight = sinon.stub(window.top, 'outerHeight').get(function() { - return testGetClientSizeParams.outerSize - }) + const defaultTimeout = 1000; - const stubbedHeight = sinon.stub(window.top.screen, 'height').get(function() { - return testGetClientSizeParams.screenSize - }) + function assertImpObject(ortbBidRequest, bidRequest) { + expect(ortbBidRequest.secure).to.equal(1); + expect(ortbBidRequest.id).to.equal(bidRequest.bidId); + expect(ortbBidRequest.tagid).to.equal(bidRequest.adUnitCode); + expect(ortbBidRequest.banner).to.deep.equal({ + topframe: 0, + format: [{ + w: bidRequest.mediaTypes.banner.sizes[0][0], + h: bidRequest.mediaTypes.banner.sizes[0][1], + }] + }); - const validBidRequests = utils.deepClone(bidRequests) + expect(ortbBidRequest.ext).to.deep.equal({ + ...bidRequest.params, + gpid: bidRequest.ortb2Imp?.ext.gpid || bidRequest.adUnitCode, + timeSpentOnPage: stubbedCurrentTime + }); + } - const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data.device.h).to.equal(testGetClientSizeParams.expectedSize); + function assertRequestObject(dataRequest) { + expect(dataRequest.id).to.be.a('string'); + expect(dataRequest.tmax).to.equal(defaultTimeout); - stubbedClientHeight.restore(); - stubbedInnerHeight.restore(); - stubbedOuterHeight.restore(); - stubbedHeight.restore(); - } + assertImpObject(dataRequest.imp[0], bidRequests[0]); + assertImpObject(dataRequest.imp[1], bidRequests[1]); - it('should get documentElementClientHeight by default', () => { - testGetClientHeight({ - docClientSize: 420, - innerSize: 500, - outerSize: 480, - screenSize: 230, - expectedSize: 420, - }); + expect(dataRequest.imp[0].bidfloor).to.equal(4); + expect(dataRequest.regs).to.deep.equal(ortb2.regs); + expect(dataRequest.site).to.deep.equal({ + ...ortb2.site, + page: currentLocation, + id: bidRequests[0].params.assetKey }); - it('should get innerHeight as first fallback', () => { - testGetClientHeight({ - docClientSize: undefined, - innerSize: 500, - outerSize: 480, - screenSize: 230, - expectedSize: 500, - }); + expect(dataRequest.user).to.deep.equal({ + ext: { + ...ortb2.user.ext, + uids: bidRequests[0].userId + } }); - it('should get outerHeight as second fallback', () => { - testGetClientHeight({ - docClientSize: undefined, - innerSize: undefined, - outerSize: 480, - screenSize: 230, - expectedSize: 480, - }); + expect(dataRequest.ext).to.deep.equal({ + prebidversion: '$prebid.version$', + adapterversion: '2.0.0' }); - it('should get screenHeight as last fallback', () => { - testGetClientHeight({ - docClientSize: undefined, - innerSize: undefined, - outerSize: undefined, - screenSize: 230, - expectedSize: 230, - }); + expect(dataRequest.device).to.deep.equal({ + ...ortb2.device, + pxratio: stubbedDevicePixelRatio, }); - it('should return 0 if all window height values are undefined', () => { - testGetClientHeight({ - docClientSize: undefined, - innerSize: undefined, - outerSize: undefined, - screenSize: undefined, - expectedSize: 0, - }); - }); - }); + expect(dataRequest.regs.ext.gdpr).to.be.a('number'); + expect(dataRequest.device.pxratio).to.be.a('number'); + } - it('should not add gdpr infos if not present', () => { - const bidderRequestWithoutGdpr = { - ...bidderRequest, - gdprConsent: {}, - } - const expectedRequestObjectWithoutGdpr = { - ...expectedRequestObject, - regs: { - ext: { - gdpr: 0 - }, - }, - user: { - ext: { - consent: '', - uids: expectedRequestObject.user.ext.uids, - eids: expectedRequestObject.user.ext.eids - }, - } - }; - - const validBidRequests = bidRequests - - const request = spec.buildRequests(validBidRequests, bidderRequestWithoutGdpr); - expect(request.data).to.deep.equal(expectedRequestObjectWithoutGdpr); - expect(request.data.regs.ext.gdpr).to.be.a('number'); + beforeEach(() => { + windowTopStub = sinon.stub(utils, 'getWindowTop'); + windowTopStub.returns({ location: { href: currentLocation } }); }); - it('should not add gdpr infos if gdprConsent is undefined', () => { - const bidderRequestWithoutGdpr = { - ...bidderRequest, - gdprConsent: undefined, - } - const expectedRequestObjectWithoutGdpr = { - ...expectedRequestObject, - regs: { - ext: { - gdpr: 0 - }, - }, - user: { - ext: { - consent: '', - uids: expectedRequestObject.user.ext.uids, - eids: expectedRequestObject.user.ext.eids - }, - } - }; - - const validBidRequests = bidRequests + afterEach(() => { + windowTopStub.restore(); + }); - const request = spec.buildRequests(validBidRequests, bidderRequestWithoutGdpr); - expect(request.data).to.deep.equal(expectedRequestObjectWithoutGdpr); - expect(request.data.regs.ext.gdpr).to.be.a('number'); + after(() => { + stubbedCurrentTimeMethod.restore(); + stubbedDevicePixelMethod.restore(); }); - it('should not add tcString and turn off gdpr-applies if consentString and gdprApplies are undefined', () => { - const bidderRequestWithoutGdpr = { - ...bidderRequest, - gdprConsent: { consentString: undefined, gdprApplies: undefined }, - } - const expectedRequestObjectWithoutGdpr = { - ...expectedRequestObject, - regs: { - ext: { - gdpr: 0 - }, - }, - user: { - ext: { - consent: '', - uids: expectedRequestObject.user.ext.uids, - eids: expectedRequestObject.user.ext.eids - }, - } - }; + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequestBase); + expect(request.url).to.equal(BID_URL); + expect(request.method).to.equal('POST'); + }); - const validBidRequests = bidRequests + it('timeSpentOnpage should be 0 if timeline is undefined', function () { + const stubbedTimelineMethod = sinon.stub(document, 'timeline').get(function() { + return undefined; + }); - const request = spec.buildRequests(validBidRequests, bidderRequestWithoutGdpr); - expect(request.data).to.deep.equal(expectedRequestObjectWithoutGdpr); - expect(request.data.regs.ext.gdpr).to.be.a('number'); + const request = spec.buildRequests(bidRequests, bidderRequestBase); + expect(request.data.imp[0].ext.timeSpentOnPage).to.equal(0); + stubbedTimelineMethod.restore(); }); - it('should should not add uids infos if userId is undefined', () => { - const expectedRequestWithUndefinedUserId = { - ...expectedRequestObject, - user: { - ext: { - consent: expectedRequestObject.user.ext.consent, - eids: expectedRequestObject.user.ext.eids - } - } - }; + it('bid request object should be conform', function () { + const request = spec.buildRequests(bidRequests, bidderRequestBase); + assertRequestObject(request.data); + }); - const validBidRequests = utils.deepClone(bidRequests); - validBidRequests[0] = { - ...validBidRequests[0], - userId: undefined - }; + it('should not set site.id when assetKey is not present', () => { + const bidderRequest = utils.deepClone(bidderRequestBase); + const validBidRequests = bidderRequest.bids; + delete validBidRequests[0].params.assetKey; + delete validBidRequests[1].params.assetKey; const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserId); + expect(request.data.site.id).to.be.an('undefined'); }); - it('should should not add uids infos if userIdAsEids is undefined', () => { - const expectedRequestWithUndefinedUserIdAsEids = { - ...expectedRequestObject, - user: { - ext: { - consent: expectedRequestObject.user.ext.consent, - uids: expectedRequestObject.user.ext.uids - } - } - }; - - const validBidRequests = utils.deepClone(bidRequests); - validBidRequests[0] = { - ...validBidRequests[0], - userIdAsEids: undefined - }; + it('should not set user.ext.uids when userId is not present', () => { + const bidderRequest = utils.deepClone(bidderRequestBase); + const validBidRequests = bidderRequest.bids; + delete validBidRequests[0].userId; const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserIdAsEids); + expect(request.data.user.ext.uids).to.be.an('undefined'); }); it('should handle bidFloor undefined', () => { - const expectedRequestWithUndefinedFloor = { - ...expectedRequestObject - }; - - const validBidRequests = utils.deepClone(bidRequests); - validBidRequests[1] = { - ...validBidRequests[1], + const bidderRequest = utils.deepClone(bidderRequestBase); + const validBidRequests = bidderRequest.bids; + validBidRequests[0] = { + ...validBidRequests[0], getFloor: undefined }; const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data).to.deep.equal(expectedRequestWithUndefinedFloor); + expect(request.data.imp[0].bidfloor).to.be.an('undefined'); }); it('should handle bidFloor when is not function', () => { - const expectedRequestWithNotAFunctionFloor = { - ...expectedRequestObject - }; - - let validBidRequests = utils.deepClone(bidRequests); - validBidRequests[1] = { - ...validBidRequests[1], + const bidderRequest = utils.deepClone(bidderRequestBase); + const validBidRequests = bidderRequest.bids; + validBidRequests[0] = { + ...validBidRequests[0], getFloor: 'getFloor' }; const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data).to.deep.equal(expectedRequestWithNotAFunctionFloor); + expect(request.data.imp[0].bidfloor).to.be.an('undefined'); }); it('should handle bidFloor when currency is not USD', () => { - const expectedRequestWithUnsupportedFloorCurrency = utils.deepClone(expectedRequestObject) - delete expectedRequestWithUnsupportedFloorCurrency.imp[0].bidfloor; - let validBidRequests = utils.deepClone(bidRequests); + const bidderRequest = utils.deepClone(bidderRequestBase); + const validBidRequests = bidderRequest.bids; + validBidRequests[0] = { ...validBidRequests[0], getFloor: ({ size, currency, mediaType }) => { @@ -820,8 +823,40 @@ describe('OguryBidAdapter', function () { } } }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data.imp[0].bidfloor).to.be.an('undefined'); + }); + + it('should use adUnitCode when gpid from ortb2 is undefined', () => { + const bidderRequest = utils.deepClone(bidderRequestBase); + const validBidRequests = bidderRequest.bids; + delete validBidRequests[0].ortb2Imp.ext.gpid; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data.imp[0].ext.gpid).to.equal(bidRequests[0].adUnitCode); + }); + + it('should use adUnitCode when gpid is not present in ortb2Imp object', () => { + const bidderRequest = utils.deepClone(bidderRequestBase); + const validBidRequests = bidderRequest.bids; + validBidRequests[0] = { + ...validBidRequests[0], + ortb2Imp: { + ext: {} + } + }; + const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data).to.deep.equal(expectedRequestWithUnsupportedFloorCurrency); + expect(request.data.imp[0].ext.gpid).to.equal(bidRequests[0].adUnitCode); + }); + + it('should set the actual site location in site.page when the ORTB object contains the referrer instead of the current location', () => { + const bidderRequest = utils.deepClone(bidderRequestBase); + bidderRequest.ortb2.site.page = 'https://google.com'; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.site.page).to.equal(currentLocation); }); }); @@ -835,7 +870,7 @@ describe('OguryBidAdapter', function () { impid: 'bidId', price: 100, nurl: 'url', - adm: `test creative
cookies
`, + adm: `
cookies
`, adomain: ['renault.fr'], ext: { adcontent: 'sample_creative', @@ -854,7 +889,7 @@ describe('OguryBidAdapter', function () { impid: 'bidId2', price: 150, nurl: 'url2', - adm: `test creative
cookies
`, + adm: `
cookies
`, adomain: ['peugeot.fr'], ext: { adcontent: 'sample_creative', @@ -874,57 +909,27 @@ describe('OguryBidAdapter', function () { } }; - it('should correctly interpret bidResponse', () => { - let expectedInterpretedBidResponse = [{ - requestId: openRtbBidResponse.body.seatbid[0].bid[0].impid, - cpm: openRtbBidResponse.body.seatbid[0].bid[0].price, - currency: 'USD', - width: openRtbBidResponse.body.seatbid[0].bid[0].w, - height: openRtbBidResponse.body.seatbid[0].bid[0].h, - ad: openRtbBidResponse.body.seatbid[0].bid[0].adm, - ttl: 60, - ext: openRtbBidResponse.body.seatbid[0].bid[0].ext, - creativeId: openRtbBidResponse.body.seatbid[0].bid[0].id, - netRevenue: true, - meta: { - advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain - }, - nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, - adapterVersion: '1.6.0', - prebidVersion: '$prebid.version$' - }, { - requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, - cpm: openRtbBidResponse.body.seatbid[0].bid[1].price, - currency: 'USD', - width: openRtbBidResponse.body.seatbid[0].bid[1].w, - height: openRtbBidResponse.body.seatbid[0].bid[1].h, - ad: openRtbBidResponse.body.seatbid[0].bid[1].adm, - ttl: 60, - ext: openRtbBidResponse.body.seatbid[0].bid[1].ext, - creativeId: openRtbBidResponse.body.seatbid[0].bid[1].id, - netRevenue: true, - meta: { - advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain - }, - nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, - adapterVersion: '1.6.0', - prebidVersion: '$prebid.version$' - }] + function assertPrebidBidResponse(prebidBidResponse, ortbResponse) { + expect(prebidBidResponse.ttl).to.equal(60); + expect(prebidBidResponse.currency).to.equal('USD'); + expect(prebidBidResponse.netRevenue).to.be.true; + expect(prebidBidResponse.mediaType).to.equal('banner'); + expect(prebidBidResponse.requestId).to.equal(ortbResponse.impid); + expect(prebidBidResponse.cpm).to.equal(ortbResponse.price); + expect(prebidBidResponse.width).to.equal(ortbResponse.w); + expect(prebidBidResponse.height).to.equal(ortbResponse.h); + expect(prebidBidResponse.ad).to.contain(ortbResponse.adm); + expect(prebidBidResponse.meta.advertiserDomains).to.equal(ortbResponse.adomain); + expect(prebidBidResponse.seatBidId).to.equal(ortbResponse.id); + } - let request = spec.buildRequests(bidRequests, bidderRequest); - let result = spec.interpretResponse(openRtbBidResponse, request); + it('should correctly interpret bidResponse', () => { + const request = spec.buildRequests(bidRequests, bidderRequestBase); + const result = spec.interpretResponse(openRtbBidResponse, request); - expect(result).to.deep.equal(expectedInterpretedBidResponse) + assertPrebidBidResponse(result[0], openRtbBidResponse.body.seatbid[0].bid[0]) + assertPrebidBidResponse(result[1], openRtbBidResponse.body.seatbid[0].bid[1]) }); - - it('should return empty array if error during parsing', () => { - const wrongOpenRtbBidReponse = 'wrong data' - let request = spec.buildRequests(bidRequests, bidderRequest); - let result = spec.interpretResponse(wrongOpenRtbBidReponse, request); - - expect(result).to.be.instanceof(Array); - expect(result.length).to.equal(0) - }) }); describe('onBidWon', function() { diff --git a/test/spec/modules/omsBidAdapter_spec.js b/test/spec/modules/omsBidAdapter_spec.js index 10a9c4c946c..e8426930257 100644 --- a/test/spec/modules/omsBidAdapter_spec.js +++ b/test/spec/modules/omsBidAdapter_spec.js @@ -3,6 +3,7 @@ import * as utils from 'src/utils.js'; import {spec} from 'modules/omsBidAdapter'; import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from '../../../src/config'; +import { internal, resetWinDimensions } from '../../../src/utils'; const URL = 'https://rt.marphezis.com/hb'; @@ -36,7 +37,9 @@ describe('omsBidAdapter', function () { document: { visibilityState: 'visible' }, - + location: { + href: "http:/location" + }, innerWidth: 800, innerHeight: 600 }; @@ -107,9 +110,9 @@ describe('omsBidAdapter', function () { }); it('should return false when require params are not passed', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -130,6 +133,45 @@ describe('omsBidAdapter', function () { expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); }); + it('sets the proper video object when ad unit media type is video', function () { + const bidRequests = [ + { + 'bidder': 'oms', + 'params': { + 'publisherId': 1234567 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [640, 480] + } + }, + 'bidId': '5fb26ac22bde4', + 'bidderRequestId': '4bf93aeb730cb9', + 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + } + ] + }, + } + ] + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].video.context).to.equal('instream'); + expect(payload.imp[0].video.playerSize).to.deep.equal([640, 480]); + }); + it('accepts a single array as a size', function () { bidRequests[0].mediaTypes.banner.sizes = [300, 250]; const request = spec.buildRequests(bidRequests); @@ -289,6 +331,8 @@ describe('omsBidAdapter', function () { context('when element is partially in view', function () { it('returns percentage', function () { + const getWinDimensionsStub = sandbox.stub(utils, 'getWinDimensions') + getWinDimensionsStub.returns({ innerHeight: win.innerHeight, innerWidth: win.innerWidth }); Object.assign(element, {width: 800, height: 800}); const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); @@ -298,6 +342,8 @@ describe('omsBidAdapter', function () { context('when width or height of the element is zero', function () { it('try to use alternative values', function () { + const getWinDimensionsStub = sandbox.stub(utils, 'getWinDimensions') + getWinDimensionsStub.returns({ innerHeight: win.innerHeight, innerWidth: win.innerWidth }); Object.assign(element, {width: 0, height: 0}); bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; const request = spec.buildRequests(bidRequests); @@ -379,6 +425,45 @@ describe('omsBidAdapter', function () { expect(result[0]).to.deep.equal(expectedResponse[0]); }); + it('should get the correct bid response for video bids', function () { + let expectedResponse = [{ + 'requestId': '283a9f4cd2415d', + 'cpm': 0.35743275, + 'width': 300, + 'height': 250, + 'creativeId': '376874781', + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'video', + 'ad': `
`, + 'ttl': 300, + 'meta': { + 'advertiserDomains': ['example.com'] + } + }]; + const response = { + body: { + 'id': '37386aade21a71', + 'seatbid': [{ + 'bid': [{ + 'id': '376874781', + 'impid': '283a9f4cd2415d', + 'price': 0.35743275, + 'nurl': '', + 'adm': '', + 'w': 300, + 'h': 250, + 'adomain': ['example.com'], + 'mtype': 2 + }] + }] + } + }; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + it('crid should default to the bid id if not on the response', function () { let expectedResponse = [{ 'requestId': '283a9f4cd2415d', @@ -410,11 +495,67 @@ describe('omsBidAdapter', function () { }); describe('getUserSyncs ', () => { - let syncOptions = {iframeEnabled: true, pixelEnabled: true}; + const syncOptions = { iframeEnabled: true }; + const userSyncUrlIframe = 'https://rt.marphezis.com/sync?dpid=0'; + + it('returns empty syncs arr when syncOptions.iframeEnabled is false', () => { + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.be.empty; + }); + + it('returns syncs arr when syncOptions.iframeEnabled is true', () => { + expect(spec.getUserSyncs(syncOptions, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', + url: userSyncUrlIframe + }]); + }); + + it('should pass gdpr param when gdprConsent.gdprApplies type is boolean', () => { + expect(spec.getUserSyncs(syncOptions, {}, { gdprApplies: true }, undefined)).to.deep.equal([{ + type: 'iframe', + url: `${userSyncUrlIframe}&gdpr=1` + }]); + expect(spec.getUserSyncs(syncOptions, {}, { gdprApplies: false }, undefined)).to.deep.equal([{ + type: 'iframe', + url: `${userSyncUrlIframe}&gdpr=0` + }]); + }); + + it('should pass gdpr_consent param when gdprConsent.consentString type is string', () => { + expect(spec.getUserSyncs(syncOptions, {}, { gdprApplies: false, consentString: 'test' }, undefined)).to.deep.equal([{ + type: 'iframe', + url: `${userSyncUrlIframe}&gdpr=0&gdpr_consent=test` + }]); + }); + + it('should pass no params when gdprConsent.consentString and gdprConsent.gdprApplies types dont match', () => { + expect(spec.getUserSyncs(syncOptions, {}, { gdprApplies: 'true', consentString: 1 }, undefined)).to.deep.equal([{ + type: 'iframe', + url: `${userSyncUrlIframe}` + }]); + }); + + it('should pass us_privacy param when uspConsent is defined', function () { + expect(spec.getUserSyncs(syncOptions, {}, undefined, 'test')).to.deep.equal([{ + type: 'iframe', url: `${userSyncUrlIframe}&us_privacy=test` + }]); + }); + + it('should pass gpp and gpp_sid params when gppConsent.gppString is defined', function () { + expect(spec.getUserSyncs(syncOptions, {}, {}, undefined, { + gppString: 'test', + applicableSections: [1, 2] + })).to.deep.equal([{ + type: 'iframe', url: `${userSyncUrlIframe}&gpp=test&gpp_sid=1,2` + }]); + }); - it('should not return', () => { - let returnStatement = spec.getUserSyncs(syncOptions, []); - expect(returnStatement).to.be.empty; + it('should pass all params correctly', function () { + expect(spec.getUserSyncs(syncOptions, {}, { gdprApplies: false, consentString: 'test' }, 'test', { + gppString: 'test', + applicableSections: [] + })).to.deep.equal([{ + type: 'iframe', url: `${userSyncUrlIframe}&gdpr=0&gdpr_consent=test&us_privacy=test&gpp=test&gpp_sid=` + }]); }); }); }); diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 93db5ffc57f..a274b141fd6 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -1,9 +1,11 @@ import { spec, isValid, hasTypeVideo, isSchainValid } from 'modules/onetagBidAdapter.js'; import { expect } from 'chai'; import { find } from 'src/polyfill.js'; -import { BANNER, VIDEO } from 'src/mediaTypes.js'; +import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; import { INSTREAM, OUTSTREAM } from 'src/video.js'; +const NATIVE_SUFFIX = 'Ad'; + describe('onetag', function () { function createBid() { return { @@ -42,6 +44,55 @@ describe('onetag', function () { }; } + function createNativeBid(bidRequest) { + const bid = bidRequest || createBid(); + bid.mediaTypes = bid.mediaTypes || {}; + + bid.mediaTypes.native = { + ortb: { + ver: '1.2', + assets: [{ + id: 1, + required: 1, + title: { + len: 140 + } + }, + { + id: 2, + required: true, + img: { + type: 3, + wmin: 100, + hmin: 100, + } + }, + { + id: 3, + required: true, + data: { + type: 6 + } + }, + { + id: 4, + video: { + mimes: ['video/mp4', 'video/x-mswmv'], + minduration: 5, + maxduration: 30, + protocols: [2, 3] + } + }], + eventtrackers: [{ + event: 1, + methods: [1], + url: 'sample-url' + }] + } + }; + return bid; + } + function createBannerBid(bidRequest) { const bid = bidRequest || createBid(); bid.mediaTypes = bid.mediaTypes || {}; @@ -77,11 +128,12 @@ describe('onetag', function () { return createInstreamVideoBid(createBannerBid()); } - let bannerBid, instreamVideoBid, outstreamVideoBid; + let bannerBid, instreamVideoBid, outstreamVideoBid, nativeBid; beforeEach(() => { bannerBid = createBannerBid(); instreamVideoBid = createInstreamVideoBid(); outstreamVideoBid = createOutstreamVideoBid(); + nativeBid = createNativeBid(); }) describe('isBidRequestValid', function () { @@ -105,6 +157,107 @@ describe('onetag', function () { // expect(spec.isBidRequestValid(bannerBid)).to.be.false; }); }); + describe('native bidRequest', function () { + it('Should return true when correct native bid is passed', function () { + const nativeBid = createNativeBid(); + expect(spec.isBidRequestValid(nativeBid)).to.be.true; + }); + it('Should return false when native is not an object', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native = 30; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb is not an object', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native.ortb = 30 || 'string'; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets is not an array', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native.ortb.assets = 30; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets is an empty array', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native.ortb.assets = []; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] doesnt have \'id\'', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0], 'id'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] doesnt have any of \'title\', \'img\', \'data\' and \'video\' properties', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0], 'title'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] have title, but doesnt have \'len\' property', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0].title, 'len'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] is image but doesnt have \'wmin\' property', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[1].img, 'wmin'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] is image but doesnt have \'hmin\' property', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[1].img, 'hmin'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] is data but doesnt have \'type\' property', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[2].data, 'type'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] is video but doesnt have \'mimes\' property', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'mimes'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] is video but doesnt have \'minduration\' property', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'minduration'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] is video but doesnt have \'maxduration\' property', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'maxduration'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.assets[i] is video but doesnt have \'protocols\' property', function () { + const nativeBid = createNativeBid(); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'protocols'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.eventtrackers is not an array', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native.ortb.eventtrackers = 30; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.eventtrackers[i].event is not a number', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native.ortb.eventtrackers[0].event = 'test-string'; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.eventtrackers[i].event is not defined', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native.ortb.eventtrackers[0].event = undefined; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.eventtrackers[i].methods is not an array', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native.ortb.eventtrackers[0].methods = 30; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + it('Should return false when native.ortb.eventtrackers[i].methods is empty array', function () { + const nativeBid = createNativeBid(); + nativeBid.mediaTypes.native.ortb.eventtrackers[0].methods = []; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); + }); describe('video bidRequest', function () { it('Should return false when the context is undefined', function () { instreamVideoBid.mediaTypes.video.context = undefined; @@ -171,7 +324,7 @@ describe('onetag', function () { describe('buildRequests', function () { let serverRequest, data; before(() => { - serverRequest = spec.buildRequests([bannerBid, instreamVideoBid]); + serverRequest = spec.buildRequests([bannerBid, instreamVideoBid, nativeBid]); data = JSON.parse(serverRequest.data); }); @@ -226,6 +379,7 @@ describe('onetag', function () { 'bidId', 'bidderRequestId', 'pubId', + 'ortb2Imp', 'transactionId', 'context', 'playerSize', @@ -240,6 +394,7 @@ describe('onetag', function () { 'bidId', 'bidderRequestId', 'pubId', + 'ortb2Imp', 'transactionId', 'mediaTypeInfo', 'sizes', @@ -270,6 +425,7 @@ describe('onetag', function () { expect(payload.bids).to.exist.and.to.have.length(1); expect(payload.bids[0].auctionId).to.equal(bannerBid.ortb2.source.tid); expect(payload.bids[0].transactionId).to.equal(bannerBid.ortb2Imp.ext.tid); + expect(payload.bids[0].ortb2Imp).to.deep.equal(bannerBid.ortb2Imp); }); it('should send GDPR consent data', function () { let consentString = 'consentString'; @@ -433,13 +589,15 @@ describe('onetag', function () { 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, - 'fledgeEnabled': true + 'paapi': { + 'enabled': true + } }; let serverRequest = spec.buildRequests([bannerBid], bidderRequest); const payload = JSON.parse(serverRequest.data); expect(payload.fledgeEnabled).to.exist; - expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.fledgeEnabled); + expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.paapi.enabled); }); it('Should send FLEDGE eligibility flag when FLEDGE is not enabled', function () { let bidderRequest = { @@ -447,13 +605,15 @@ describe('onetag', function () { 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, - 'fledgeEnabled': false + paapi: { + enabled: false + } }; let serverRequest = spec.buildRequests([bannerBid], bidderRequest); const payload = JSON.parse(serverRequest.data); expect(payload.fledgeEnabled).to.exist; - expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.fledgeEnabled); + expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.paapi.enabled); }); it('Should send FLEDGE eligibility flag set to false when fledgeEnabled is not defined', function () { let bidderRequest = { @@ -471,7 +631,7 @@ describe('onetag', function () { }); describe('interpretResponse', function () { const request = getBannerVideoRequest(); - const response = getBannerVideoResponse(); + const response = getBannerVideoNativeResponse(); const fledgeResponse = getFledgeBannerResponse(); const requestData = JSON.parse(request.data); it('Returns an array of valid server responses if response object is valid', function () { @@ -482,7 +642,7 @@ describe('onetag', function () { expect(fledgeInterpretedResponse.bids).to.satisfy(function (value) { return value === null || Array.isArray(value); }); - expect(fledgeInterpretedResponse.fledgeAuctionConfigs).to.be.an('array').that.is.not.empty; + expect(fledgeInterpretedResponse.paapi).to.be.an('array').that.is.not.empty; for (let i = 0; i < interpretedResponse.length; i++) { let dataItem = interpretedResponse[i]; expect(dataItem).to.include.all.keys('requestId', 'cpm', 'width', 'height', 'ttl', 'creativeId', 'netRevenue', 'currency', 'meta', 'dealId'); @@ -501,6 +661,9 @@ describe('onetag', function () { } else if (dataItem.meta.mediaType === BANNER) { expect(dataItem).to.include.all.keys('ad'); expect(dataItem.ad).to.be.a('string'); + } else if (dataItem.meta.mediaType === NATIVE || dataItem.meta.mediaType === NATIVE + NATIVE_SUFFIX) { + expect(dataItem).to.include.all.keys('native'); + expect(dataItem.native).to.be.an('object'); } expect(dataItem.requestId).to.be.a('string'); expect(dataItem.cpm).to.be.a('number'); @@ -646,7 +809,7 @@ describe('onetag', function () { }); }); -function getBannerVideoResponse() { +function getBannerVideoNativeResponse() { return { body: { nobid: false, @@ -689,6 +852,40 @@ function getBannerVideoResponse() { rendererUrl: 'https://testRenderer', mediaType: VIDEO, adomain: [] + }, + { + requestId: 'nativeRequestId', + cpm: 10, + width: 300, + height: 600, + adomain: ['test-domain'], + creativeId: '1821', + mediaType: 'nativeAd', + native: { + ortb: { + ver: '1.2', + assets: [ + { + id: 1, + title: { + text: 'test-title', + len: 9 + } + }], + link: { + url: 'test-url', + clicktrackers: ['test-clicktracker'] + }, + eventtrackers: [ + { + event: 1, + method: 1, + url: 'test-url' + } + ] + } + }, + currency: 'EUR', } ] } @@ -696,7 +893,7 @@ function getBannerVideoResponse() { } function getFledgeBannerResponse() { - const bannerVideoResponse = getBannerVideoResponse(); + const bannerVideoResponse = getBannerVideoNativeResponse(); bannerVideoResponse.body.fledgeAuctionConfigs = [ { bidId: 'fledge', diff --git a/test/spec/modules/onomagicBidAdapter_spec.js b/test/spec/modules/onomagicBidAdapter_spec.js index 6ddc0edd477..dcef5b65419 100644 --- a/test/spec/modules/onomagicBidAdapter_spec.js +++ b/test/spec/modules/onomagicBidAdapter_spec.js @@ -92,9 +92,9 @@ describe('onomagicBidAdapter', function() { }); it('should return false when require params are not passed', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -161,6 +161,8 @@ describe('onomagicBidAdapter', function() { context('when element is partially in view', function() { it('returns percentage', function() { + const getWinDimensionsStub = sandbox.stub(utils, 'getWinDimensions') + getWinDimensionsStub.returns({ innerHeight: win.innerHeight, innerWidth: win.innerWidth }); Object.assign(element, { width: 800, height: 800 }); const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); @@ -170,6 +172,8 @@ describe('onomagicBidAdapter', function() { context('when width or height of the element is zero', function() { it('try to use alternative values', function() { + const getWinDimensionsStub = sandbox.stub(utils, 'getWinDimensions') + getWinDimensionsStub.returns({ innerHeight: win.innerHeight, innerWidth: win.innerWidth }); Object.assign(element, { width: 0, height: 0 }); bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; const request = spec.buildRequests(bidRequests); diff --git a/test/spec/modules/openPairIdSystem_spec.js b/test/spec/modules/openPairIdSystem_spec.js new file mode 100644 index 00000000000..421daebf9a2 --- /dev/null +++ b/test/spec/modules/openPairIdSystem_spec.js @@ -0,0 +1,187 @@ +import { storage, openPairIdSubmodule } from 'modules/openPairIdSystem.js'; +import * as utils from 'src/utils.js'; + +import { + attachIdSystem, + coreStorage, + getConsentHash, + init, + startAuctionHook, + setSubmoduleRegistry +} from '../../../modules/userId/index.js'; + +import {createEidsArray, getEids} from '../../../modules/userId/eids.js'; + +describe('openPairId', function () { + let sandbox; + let logInfoStub; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + logInfoStub = sandbox.stub(utils, 'logInfo'); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should read publisher id from specified clean room if configured with storageKey', function() { + let publisherIds = ['dGVzdC1wYWlyLWlkMQ==', 'test-pair-id2', 'test-pair-id3']; + sandbox.stub(storage, 'getDataFromLocalStorage').withArgs('habu_pairId_custom').returns(btoa(JSON.stringify({'envelope': publisherIds}))); + + let id = openPairIdSubmodule.getId({ + params: { + habu: { + storageKey: 'habu_pairId_custom' + } + }}) + + expect(id).to.be.deep.equal({id: publisherIds}); + }); + + it('should read publisher id from liveramp with default storageKey and additional clean room with configured storageKey', function() { + let getDataStub = sandbox.stub(storage, 'getDataFromLocalStorage'); + let liveRampPublisherIds = ['lr-test-pair-id1', 'lr-test-pair-id2', 'lr-test-pair-id3']; + getDataStub.withArgs('_lr_pairId').returns(btoa(JSON.stringify({'envelope': liveRampPublisherIds}))); + + let habuPublisherIds = ['habu-test-pair-id1', 'habu-test-pair-id2', 'habu-test-pair-id3']; + getDataStub.withArgs('habu_pairId_custom').returns(btoa(JSON.stringify({'envelope': habuPublisherIds}))); + + let id = openPairIdSubmodule.getId({ + params: { + habu: { + storageKey: 'habu_pairId_custom' + }, + liveramp: {} + }}) + + expect(id).to.be.deep.equal({id: habuPublisherIds.concat(liveRampPublisherIds)}); + }); + + it('should log an error if no ID is found when getId', function() { + openPairIdSubmodule.getId({ params: {} }); + expect(logInfoStub.calledOnce).to.be.true; + }); + + it('should read publisher id from local storage if exists', function() { + let publisherIds = ['test-pair-id1', 'test-pair-id2', 'test-pair-id3']; + sandbox.stub(storage, 'getDataFromLocalStorage').withArgs('pairId').returns(btoa(JSON.stringify(publisherIds))); + + let id = openPairIdSubmodule.getId({ params: {} }); + expect(id).to.be.deep.equal({id: publisherIds}); + }); + + it('should read publisher id from cookie if exists', function() { + let publisherIds = ['test-pair-id4', 'test-pair-id5', 'test-pair-id6']; + sandbox.stub(storage, 'getCookie').withArgs('pairId').returns(btoa(JSON.stringify(publisherIds))); + + let id = openPairIdSubmodule.getId({ params: {} }); + expect(id).to.be.deep.equal({id: publisherIds}); + }); + + it('should read publisher id from default liveramp envelope local storage key if configured', function() { + let publisherIds = ['test-pair-id1', 'test-pair-id2', 'test-pair-id3']; + sandbox.stub(storage, 'getDataFromLocalStorage').withArgs('_lr_pairId').returns(btoa(JSON.stringify({'envelope': publisherIds}))); + let id = openPairIdSubmodule.getId({ + params: { + liveramp: {} + }}) + expect(id).to.be.deep.equal({id: publisherIds}) + }); + + it('should read publisher id from default liveramp envelope cookie entry if configured', function() { + let publisherIds = ['test-pair-id4', 'test-pair-id5', 'test-pair-id6']; + sandbox.stub(storage, 'getDataFromLocalStorage').withArgs('_lr_pairId').returns(btoa(JSON.stringify({'envelope': publisherIds}))); + let id = openPairIdSubmodule.getId({ + params: { + liveramp: {} + }}) + expect(id).to.be.deep.equal({id: publisherIds}) + }); + + it('should read publisher id from specified liveramp envelope cookie entry if configured with storageKey', function() { + let publisherIds = ['test-pair-id7', 'test-pair-id8', 'test-pair-id9']; + sandbox.stub(storage, 'getDataFromLocalStorage').withArgs('lr_pairId_custom').returns(btoa(JSON.stringify({'envelope': publisherIds}))); + let id = openPairIdSubmodule.getId({ + params: { + liveramp: { + storageKey: 'lr_pairId_custom' + } + }}) + expect(id).to.be.deep.equal({id: publisherIds}) + }); + + it('should not get data from storage if local storage and cookies are disabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + sandbox.stub(storage, 'cookiesAreEnabled').returns(false); + let id = openPairIdSubmodule.getId({ + params: { + liveramp: { + storageKey: 'lr_pairId_custom' + } + } + }) + expect(id).to.equal(undefined) + }); + + it('honors inserter, matcher', () => { + const config = { + inserter: 'some-domain.com', + matcher: 'another-domain.com' + }; + + const result = openPairIdSubmodule.eids.openPairId(['some-random-id-value'], config); + + expect(result.length).to.equal(1); + + expect(result[0]).to.deep.equal( + { + source: 'pair-protocol.com', + mm: 3, + inserter: 'some-domain.com', + matcher: 'another-domain.com', + uids: [ + { + atype: 3, + id: 'some-random-id-value' + } + ] + } + ); + }); + + describe('encoding', () => { + it('encodes and decodes the original value with atob/btoa', function () { + const value = 'dGVzdC1wYWlyLWlkMQ=='; + + let publisherIds = [value]; + + const stored = btoa(JSON.stringify({'envelope': publisherIds})); + + const read = JSON.parse(atob(stored)); + + expect(value).to.eq(read.envelope[0]); + }); + }); + + describe('eid', () => { + before(() => { + attachIdSystem(openPairIdSubmodule); + }); + + it('generates the minimal eids', function() { + const userId = { + openPairId: 'some-random-id-value' + }; + + const newEids = createEidsArray(userId); + + expect(newEids.length).to.equal(1); + + expect(newEids[0]).to.deep.include({ + source: 'pair-protocol.com', + mm: 3, + uids: [{ id: 'some-random-id-value', atype: 3 }] + }); + }); + }); +}); diff --git a/test/spec/modules/openwebBidAdapter_spec.js b/test/spec/modules/openwebBidAdapter_spec.js index 4586ce78135..9ab8f608598 100644 --- a/test/spec/modules/openwebBidAdapter_spec.js +++ b/test/spec/modules/openwebBidAdapter_spec.js @@ -2,8 +2,9 @@ import { expect } from 'chai'; import { spec } from 'modules/openwebBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; import * as utils from 'src/utils.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native'; const ENDPOINT = 'https://hb.openwebmp.com/hb-multi'; const TEST_ENDPOINT = 'https://hb.openwebmp.com/hb-multi-test'; @@ -25,7 +26,8 @@ describe('openwebAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [['640', '480']], 'params': { - 'org': 'jdye8weeyirk00000001' + 'org': 'jdye8weeyirk00000001', + 'placementId': '123' } }; @@ -33,7 +35,7 @@ describe('openwebAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return false when required params are not found', function () { + it('should return false when org param is not found', function () { const newBid = Object.assign({}, bid); delete newBid.params; newBid.params = { @@ -41,6 +43,15 @@ describe('openwebAdapter', function () { }; expect(spec.isBidRequestValid(newBid)).to.equal(false); }); + + it('should return false when placementId param is not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'placementId': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); }); describe('buildRequests', function () { @@ -50,7 +61,8 @@ describe('openwebAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[640, 480]], 'params': { - 'org': 'jdye8weeyirk00000001' + 'org': 'jdye8weeyirk00000001', + 'placementId': '123' }, 'bidId': '299ffc8cca0b87', 'loop': 1, @@ -63,7 +75,6 @@ describe('openwebAdapter', function () { 'plcmt': 1 } }, - 'vastXml': '"..."' }, { 'bidder': spec.code, @@ -80,7 +91,59 @@ describe('openwebAdapter', function () { 'banner': { } }, - 'ad': '""' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ 300, 250 ] + ] + }, + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream', + 'plcmt': 1 + }, + 'native': { + 'ortb': { + 'assets': [ + { + 'id': 1, + 'required': 1, + 'img': { + 'type': 3, + 'w': 300, + 'h': 200, + } + }, + { + 'id': 2, + 'required': 1, + 'title': { + 'len': 80 + } + }, + { + 'id': 3, + 'required': 1, + 'data': { + 'type': 1 + } + } + ] + } + }, + }, } ]; @@ -103,15 +166,13 @@ describe('openwebAdapter', function () { const bidderRequest = { bidderCode: 'openweb', } - const placementId = '12345678'; const api = [1, 2]; const mimes = ['application/javascript', 'video/mp4', 'video/quicktime']; const protocols = [2, 3, 5, 6]; it('sends the placementId to ENDPOINT via POST', function () { - bidRequests[0].params.placementId = placementId; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bids[0].placementId).to.equal(placementId); + expect(request.data.bids[0].placementId).to.equal('123'); }); it('sends the plcmt to ENDPOINT via POST', function () { @@ -151,10 +212,10 @@ describe('openwebAdapter', function () { }); it('should send the correct mimes array', function () { - bidRequests[1].mediaTypes.banner.mimes = mimes; + bidRequests[0].mediaTypes.video.mimes = mimes; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bids[1].mimes).to.be.an('array'); - expect(request.data.bids[1].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); + expect(request.data.bids[0].mimes).to.be.an('array'); + expect(request.data.bids[0].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); }); it('should send the correct protocols array', function () { @@ -170,12 +231,21 @@ describe('openwebAdapter', function () { expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) expect(request.data.bids[1].sizes).to.be.an('array'); expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + expect(request.data.bids[2].sizes).to.be.an('array'); + expect(request.data.bids[2].sizes).to.eql(bidRequests[2].sizes) + }); + + it('should send nativeOrtbRequest in native bid request', function () { + decorateAdUnitsWithNativeParams(bidRequests) + const request = spec.buildRequests(bidRequests, bidderRequest); + assert.deepEqual(request.data.bids[2].nativeOrtbRequest, bidRequests[2].mediaTypes.native.ortb) }); it('should send the correct media type', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.bids[0].mediaType).to.equal(VIDEO) expect(request.data.bids[1].mediaType).to.equal(BANNER) + expect(request.data.bids[2].mediaType.split(',')).to.include.members([VIDEO, NATIVE, BANNER]) }); it('should respect syncEnabled option', function() { @@ -440,6 +510,8 @@ describe('openwebAdapter', function () { width: 640, height: 480, requestId: '21e12606d47ba7', + creativeId: 'creative-id-1', + nurl: 'http://example.com/win/1234', adomain: ['abc.com'], mediaType: VIDEO }, @@ -449,8 +521,32 @@ describe('openwebAdapter', function () { width: 300, height: 250, requestId: '21e12606d47ba7', + creativeId: 'creative-id-2', + nurl: 'http://example.com/win/1234', adomain: ['abc.com'], mediaType: BANNER + }, + { + cpm: 12.5, + width: 300, + height: 200, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + creativeId: 'creative-id-3', + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + native: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg' + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } }] }; @@ -461,7 +557,7 @@ describe('openwebAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id-1', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -476,10 +572,10 @@ describe('openwebAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id-2', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -490,10 +586,42 @@ describe('openwebAdapter', function () { ad: '""' }; + const expectedNativeResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 300, + height: 200, + ttl: TTL, + creativeId: 'creative-id-3', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + meta: { + mediaType: NATIVE, + advertiserDomains: ['abc.com'] + }, + native: { + ortb: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg', + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } + }, + }; + it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); + expect(result[2]).to.deep.equal(expectedNativeResponse); }); it('video type should have vastXml key', function () { @@ -505,6 +633,11 @@ describe('openwebAdapter', function () { const result = spec.interpretResponse({ body: response }); expect(result[1].ad).to.equal(expectedBannerResponse.ad) }); + + it('native type should have native key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[2].native).to.eql(expectedNativeResponse.native) + }); }) describe('getUserSyncs', function() { diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index 7c504bca50b..18e26b7612e 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import {spec, REQUEST_URL, SYNC_URL, DEFAULT_PH} from 'modules/openxBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from 'src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from 'src/mediaTypes.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; // load modules that register ORTB processors @@ -10,14 +10,15 @@ import 'modules/currency.js'; import 'modules/userId/index.js'; import 'modules/multibid/index.js'; import 'modules/priceFloors.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; +import 'modules/paapi.js'; + import {deepClone} from 'src/utils.js'; import {version} from 'package.json'; -import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {addFPDToBidderRequest} from '../../helpers/fpd.js'; import {hook} from '../../../src/hook.js'; - const DEFAULT_SYNC = SYNC_URL + '?ph=' + DEFAULT_PH; const BidRequestBuilder = function BidRequestBuilder(options) { @@ -187,9 +188,9 @@ describe('OpenxRtbAdapter', function () { }); it('should return false when required params are not passed', function () { - let videoBidWithMediaTypes = Object.assign({}, videoBidWithMediaTypes); - videoBidWithMediaTypes.params = {}; - expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + let invalidVideoBidWithMediaTypes = Object.assign({}, videoBidWithMediaTypes); + invalidVideoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(invalidVideoBidWithMediaTypes)).to.equal(false); }); }); describe('and request config uses both delDomain and platform', () => { @@ -216,9 +217,9 @@ describe('OpenxRtbAdapter', function () { }); it('should return false when required params are not passed', function () { - let videoBidWithMediaTypes = Object.assign({}, videoBidWithDelDomainAndPlatform); - videoBidWithMediaTypes.params = {}; - expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + let invalidVideoBidWithMediaTypes = Object.assign({}, videoBidWithDelDomainAndPlatform); + invalidVideoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(invalidVideoBidWithMediaTypes)).to.equal(false); }); }); describe('and request config uses mediaType', () => { @@ -241,10 +242,88 @@ describe('OpenxRtbAdapter', function () { }); it('should return false when required params are not passed', function () { - let videoBidWithMediaType = Object.assign({}, videoBidWithMediaType); - delete videoBidWithMediaType.params; - videoBidWithMediaType.params = {}; - expect(spec.isBidRequestValid(videoBidWithMediaType)).to.equal(false); + let invalidVideoBidWithMediaType = Object.assign({}, videoBidWithMediaType); + delete invalidVideoBidWithMediaType.params; + invalidVideoBidWithMediaType.params = {}; + expect(spec.isBidRequestValid(invalidVideoBidWithMediaType)).to.equal(false); + }); + }); + }); + + describe('when request is for a native ad', function () { + const nativeOrtbRequest = { + assets: [{ + id: 1, + required: 1, + title: { + len: 80 + } + }] + } + describe('and request config uses mediaTypes', () => { + const nativeBidWithMediaTypes = { + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + native: { + ortb: { + ...nativeOrtbRequest + } + } + }, + nativeOrtbRequest, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(nativeBidWithMediaTypes)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let invalidNativeBidWithMediaTypes = Object.assign({}, nativeBidWithMediaTypes); + invalidNativeBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(invalidNativeBidWithMediaTypes)).to.equal(false); + }); + }); + + describe('and request config uses both delDomain and platform', () => { + const nativeBidWithDelDomainAndPlatform = { + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain', + platform: '1cabba9e-cafe-3665-beef-f00f00f00f00' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + native: { + ortb: { + ...nativeOrtbRequest + } + } + }, + nativeOrtbRequest, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(nativeBidWithDelDomainAndPlatform)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let invalidNativeBidWithMediaTypes = Object.assign({}, nativeBidWithDelDomainAndPlatform); + invalidNativeBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(invalidNativeBidWithMediaTypes)).to.equal(false); }); }); }); @@ -252,8 +331,37 @@ describe('OpenxRtbAdapter', function () { describe('buildRequests()', function () { let bidRequestsWithMediaTypes; - let bidRequestsWithPlatform; let mockBidderRequest; + const nativeOrtbRequest = { + assets: [{ + id: 1, + required: 1, + title: { + len: 80 + } + }] + }; + const nativeBidRequest = { + bidder: 'openx', + params: { + unit: '33', + delDomain: 'test-del-domain', + platform: '1cabba9e-cafe-3665-beef-f00f00f00f00', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + native: { + ortb: { + ...nativeOrtbRequest + } + } + }, + nativeOrtbRequest, + bidId: 'test-bid-id-3', + bidderRequestId: 'test-bid-request-3', + auctionId: 'test-auction-3', + transactionId: 'test-transactionId-3' + }; beforeEach(function () { mockBidderRequest = {refererInfo: {}}; @@ -301,16 +409,64 @@ describe('OpenxRtbAdapter', function () { }); context('common requests checks', function() { - it('should be able to handle multiformat requests', () => { + it('should be able to handle multiformat request - banner + video', () => { const multiformat = utils.deepClone(bidRequestsWithMediaTypes[0]); multiformat.mediaTypes.video = { context: 'outstream', playerSize: [640, 480] + }; + const requests = spec.buildRequests([multiformat], mockBidderRequest); + expect(requests).to.have.length(2); + expect(requests[0].data.imp).to.have.length(1); + expect(requests[0].data.imp[0].banner).to.exist; + expect(requests[0].data.imp[0].video).to.not.exist; + expect(requests[0].data.imp[0].native).to.not.exist; + expect(requests[1].data.imp).to.have.length(1); + expect(requests[1].data.imp[0].banner).to.not.exist; + expect(requests[1].data.imp[0].native).to.not.exist; + if (FEATURES.VIDEO) { + expect(requests[1].data.imp[0].video).to.exist; + } + }) + + it('should be able to handle multiformat request - banner + native', () => { + const multiformat = utils.deepClone(nativeBidRequest); + multiformat.mediaTypes.banner = { + sizes: [[300, 250], [300, 600]] + } + const requests = spec.buildRequests([multiformat], mockBidderRequest); + expect(requests).to.have.length(1); + expect(requests[0].data.imp).to.have.length(1); + expect(requests[0].data.imp[0].banner).to.exist; + expect(requests[0].data.imp[0].video).to.not.exist; + if (FEATURES.NATIVE) { + expect(requests[0].data.imp[0].native).to.exist; + } + }) + + it('should be able to handle multiformat request - banner + video + native', () => { + const multiformat = utils.deepClone(nativeBidRequest); + multiformat.mediaTypes.video = { + context: 'outstream', + playerSize: [640, 480] + }; + multiformat.mediaTypes.banner = { + sizes: [[300, 250]] } const requests = spec.buildRequests([multiformat], mockBidderRequest); - const outgoingFormats = requests.flatMap(rq => rq.data.imp.flatMap(imp => ['banner', 'video'].filter(k => imp[k] != null))); - const expected = FEATURES.VIDEO ? ['banner', 'video'] : ['banner'] - expect(outgoingFormats).to.have.members(expected); + expect(requests).to.have.length(2); + expect(requests[0].data.imp).to.have.length(1); + expect(requests[0].data.imp[0].banner).to.exist; + expect(requests[0].data.imp[0].video).to.not.exist; + if (FEATURES.NATIVE) { + expect(requests[0].data.imp[0].native).to.exist; + } + expect(requests[1].data.imp).to.have.length(1); + expect(requests[1].data.imp[0].banner).to.not.exist; + expect(requests[1].data.imp[0].native).to.not.exist; + if (FEATURES.VIDEO) { + expect(requests[1].data.imp[0].video).to.exist; + } }) it('should send bid request to openx url via POST', function () { @@ -328,11 +484,11 @@ describe('OpenxRtbAdapter', function () { it('should send platform id, if available', function () { bidRequestsWithMediaTypes[0].params.platform = '1cabba9e-cafe-3665-beef-f00f00f00f00'; - bidRequestsWithMediaTypes[1].params.platform = '1cabba9e-cafe-3665-beef-f00f00f00f00'; + bidRequestsWithMediaTypes[1].params.platform = '51ca3159-abc2-4035-8e00-fe26eaa09397'; const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); expect(request[0].data.ext.platform).to.equal(bidRequestsWithMediaTypes[0].params.platform); - expect(request[1].data.ext.platform).to.equal(bidRequestsWithMediaTypes[0].params.platform); + expect(request[1].data.ext.platform).to.equal(bidRequestsWithMediaTypes[1].params.platform); }); it('should send openx adunit codes', function () { @@ -745,15 +901,15 @@ describe('OpenxRtbAdapter', function () { config.getConfig.restore(); }); - it('should send a signal to specify that US Privacy applies to this request', function () { - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + it('should send a signal to specify that US Privacy applies to this request', async function () { + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs.ext.us_privacy).to.equal('1YYN'); expect(request[1].data.regs.ext.us_privacy).to.equal('1YYN'); }); - it('should not send the regs object, when consent string is undefined', function () { + it('should not send the regs object, when consent string is undefined', async function () { delete bidderRequest.uspConsent; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs?.us_privacy).to.not.exist; }); }); @@ -786,49 +942,49 @@ describe('OpenxRtbAdapter', function () { config.getConfig.restore(); }); - it('should send a signal to specify that GDPR applies to this request', function () { + it('should send a signal to specify that GDPR applies to this request', async function () { bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs.ext.gdpr).to.equal(1); expect(request[1].data.regs.ext.gdpr).to.equal(1); }); - it('should send the consent string', function () { + it('should send the consent string', async function () { bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); }); - it('should send the addtlConsent string', function () { + it('should send the addtlConsent string', async function () { bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request[0].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); expect(request[1].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); }); - it('should send a signal to specify that GDPR does not apply to this request', function () { + it('should send a signal to specify that GDPR does not apply to this request', async function () { bidderRequest.gdprConsent.gdprApplies = false; bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs.ext.gdpr).to.equal(0); expect(request[1].data.regs.ext.gdpr).to.equal(0); }); it('when GDPR application is undefined, should not send a signal to specify whether GDPR applies to this request, ' + - 'but can send consent data, ', function () { + 'but can send consent data, ', async function () { delete bidderRequest.gdprConsent.gdprApplies; bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs?.ext?.gdpr).to.not.be.ok; expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); }); - it('when consent string is undefined, should not send the consent string, ', function () { + it('when consent string is undefined, should not send the consent string, ', async function () { delete bidderRequest.gdprConsent.consentString; bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request[0].data.imp[0].ext.consent).to.equal(undefined); expect(request[1].data.imp[0].ext.consent).to.equal(undefined); }); @@ -836,12 +992,12 @@ describe('OpenxRtbAdapter', function () { }); context('coppa', function() { - it('when there are no coppa param settings, should not send a coppa flag', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + it('when there are no coppa param settings, should not send a coppa flag', async function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, await addFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.regs?.coppa).to.be.not.ok; }); - it('should send a coppa flag there is when there is coppa param settings in the bid requests', function () { + it('should send a coppa flag there is when there is coppa param settings in the bid requests', async function () { let mockConfig = { coppa: true }; @@ -850,12 +1006,12 @@ describe('OpenxRtbAdapter', function () { return utils.deepAccess(mockConfig, key); }); - const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + const request = spec.buildRequests(bidRequestsWithMediaTypes, await addFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.regs.coppa).to.equal(1); }); - it('should send a coppa flag there is when there is coppa param settings in the bid params', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + it('should send a coppa flag there is when there is coppa param settings in the bid params', async function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, await addFPDToBidderRequest(mockBidderRequest)); request.params = {coppa: true}; expect(request[0].data.regs.coppa).to.equal(1); }); @@ -875,24 +1031,24 @@ describe('OpenxRtbAdapter', function () { doNotTrackStub.restore(); }); - it('when there is a do not track, should send a dnt', function () { + it('when there is a do not track, should send a dnt', async function () { doNotTrackStub.returns(1); - const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + const request = spec.buildRequests(bidRequestsWithMediaTypes, await addFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.device.dnt).to.equal(1); }); - it('when there is not do not track, don\'t send dnt', function () { + it('when there is not do not track, don\'t send dnt', async function () { doNotTrackStub.returns(0); - const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + const request = spec.buildRequests(bidRequestsWithMediaTypes, await addFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.device.dnt).to.equal(0); }); - it('when there is no defined do not track, don\'t send dnt', function () { + it('when there is no defined do not track, don\'t send dnt', async function () { doNotTrackStub.returns(null); - const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + const request = spec.buildRequests(bidRequestsWithMediaTypes, await addFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.device.dnt).to.equal(0); }); }); @@ -971,7 +1127,7 @@ describe('OpenxRtbAdapter', function () { }); context('when there are userid providers', function () { - const userIdAsEids = [ + const eids = [ { source: 'adserver.org', uids: [{ @@ -1002,14 +1158,12 @@ describe('OpenxRtbAdapter', function () { ]; it(`should send the user id under the extended ids`, function () { - const bidRequestsWithUserId = [{ + const bidRequests = [{ bidder: 'openx', params: { unit: '11', delDomain: 'test-del-domain' }, - userId: { - }, adUnitCode: 'adunit-code', mediaTypes: { banner: { @@ -1019,12 +1173,12 @@ describe('OpenxRtbAdapter', function () { bidId: 'test-bid-id-1', bidderRequestId: 'test-bid-request-1', auctionId: 'test-auction-1', - userIdAsEids: userIdAsEids }]; // enrich bid request with userId key/value - const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); - expect(request[0].data.user.ext.eids).to.equal(userIdAsEids); + mockBidderRequest.ortb2 = {user: {ext: {eids}}} + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.user.ext.eids).to.eql(eids); }); it(`when no user ids are available, it should not send any extended ids`, function () { @@ -1037,7 +1191,9 @@ describe('OpenxRtbAdapter', function () { it('when FLEDGE is enabled, should send whatever is set in ortb2imp.ext.ae in all bid requests', function () { const request = spec.buildRequests(bidRequestsWithMediaTypes, { ...mockBidderRequest, - fledgeEnabled: true + paapi: { + enabled: true + } }); expect(request[0].data.imp[0].ext.ae).to.equal(2); }); @@ -1087,7 +1243,6 @@ describe('OpenxRtbAdapter', function () { skipafter: 4, minduration: 10, maxduration: 30, - placement: 4, protocols: [8], w: 300, h: 250 @@ -1107,7 +1262,7 @@ describe('OpenxRtbAdapter', function () { let bid; context('when there is an nbr response', function () { - let bids; + let response; beforeEach(function () { bidRequestConfigs = [{ bidder: 'openx', @@ -1129,16 +1284,16 @@ describe('OpenxRtbAdapter', function () { bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; bidResponse = {nbr: 0}; // Unknown error - bids = spec.interpretResponse({body: bidResponse}, bidRequest); + response = spec.interpretResponse({body: bidResponse}, bidRequest); }); it('should not return any bids', function () { - expect(bids.length).to.equal(0); + expect(response.bids.length).to.equal(0); }); }); context('when no seatbid in response', function () { - let bids; + let response; beforeEach(function () { bidRequestConfigs = [{ bidder: 'openx', @@ -1160,16 +1315,16 @@ describe('OpenxRtbAdapter', function () { bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; bidResponse = {ext: {}, id: 'test-bid-id'}; - bids = spec.interpretResponse({body: bidResponse}, bidRequest); + response = spec.interpretResponse({body: bidResponse}, bidRequest); }); it('should not return any bids', function () { - expect(bids.length).to.equal(0); + expect(response.bids.length).to.equal(0); }); }); context('when there is no response', function () { - let bids; + let response; beforeEach(function () { bidRequestConfigs = [{ bidder: 'openx', @@ -1191,11 +1346,11 @@ describe('OpenxRtbAdapter', function () { bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; bidResponse = ''; // Unknown error - bids = spec.interpretResponse({body: bidResponse}, bidRequest); + response = spec.interpretResponse({body: bidResponse}, bidRequest); }); it('should not return any bids', function () { - expect(bids.length).to.equal(0); + expect(response.bids.length).to.equal(0); }); }); @@ -1226,23 +1381,16 @@ describe('OpenxRtbAdapter', function () { crid: 'test-creative-id', dealid: 'test-deal-id', adm: 'test-ad-markup', + mtype: 1, adomain: ['brand.com'], ext: { dsp_id: '123', buyer_id: '456', - brand_id: '789', - paf: { - content_id: 'paf_content_id' - } + brand_id: '789' } }] }], - cur: 'AUS', - ext: { - paf: { - transmission: {version: '12'} - } - } + cur: 'AUS' }; context('when there is a response, the common response properties', function () { @@ -1251,7 +1399,7 @@ describe('OpenxRtbAdapter', function () { bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; bidResponse = deepClone(SAMPLE_BID_RESPONSE); - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; }); it('should return a price', function () { @@ -1306,34 +1454,9 @@ describe('OpenxRtbAdapter', function () { it('should return adomain', function () { expect(bid.meta.advertiserDomains).to.equal(bidResponse.seatbid[0].bid[0].adomain); }); - - it('should return paf fields', function () { - const paf = { - transmission: {version: '12'}, - content_id: 'paf_content_id' - } - expect(bid.meta.paf).to.deep.equal(paf); - }); }); - context('when there is more than one response', () => { - let bids; - beforeEach(function () { - bidRequestConfigs = deepClone(SAMPLE_BID_REQUESTS); - bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; - bidResponse = deepClone(SAMPLE_BID_RESPONSE); - bidResponse.seatbid[0].bid.push(deepClone(bidResponse.seatbid[0].bid[0])); - bidResponse.seatbid[0].bid[1].ext.paf.content_id = 'second_paf' - - bids = spec.interpretResponse({body: bidResponse}, bidRequest); - }); - - it('should not confuse paf content_id', () => { - expect(bids.map(b => b.meta.paf.content_id)).to.eql(['paf_content_id', 'second_paf']); - }); - }) - - context('when the response is a banner', function() { + context('when banner request and the response is a banner', function() { beforeEach(function () { bidRequestConfigs = [{ bidder: 'openx', @@ -1363,24 +1486,23 @@ describe('OpenxRtbAdapter', function () { h: 250, crid: 'test-creative-id', dealid: 'test-deal-id', - adm: 'test-ad-markup' + adm: 'test-ad-markup', + mtype: 1, }] }], cur: 'AUS' }; - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; }); it('should return the proper mediaType', function () { - it('should return a creativeId', function () { - expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); - }); + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); }); }); if (FEATURES.VIDEO) { - context('when the response is a video', function() { + context('when video request and the response is a video', function() { beforeEach(function () { bidRequestConfigs = [{ bidder: 'openx', @@ -1410,7 +1532,7 @@ describe('OpenxRtbAdapter', function () { h: 480, crid: 'test-creative-id', dealid: 'test-deal-id', - adm: 'test-ad-markup', + adm: '', }] }], cur: 'AUS' @@ -1418,18 +1540,310 @@ describe('OpenxRtbAdapter', function () { }); it('should return the proper mediaType', function () { - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); }); - it('should return the proper mediaType', function () { + it('should return the proper vastUrl', function () { const winUrl = 'https//my.win.url'; bidResponse.seatbid[0].bid[0].nurl = winUrl - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; expect(bid.vastUrl).to.equal(winUrl); }); }); + + context('when multi-format request (banner + video) and the response is a video', function() { + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + size: [[300, 600]] + }, + video: { + playerSize: [[640, 360], [854, 480]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 5, + adm: '', + mtype: 2 + }] + }], + cur: 'USD' + }; + }); + + it('should return video mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; + expect(bid.mediaType).to.equal(VIDEO); + }); + }); + + context('when multiple bid requests (banner + video) and the response is a banner', function() { + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [[640, 360], [854, 480]], + }, + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bidder-request-id-1', + auctionId: 'test-auction-id-1' + }, + { + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bidder-request-id-2', + auctionId: 'test-auction-id-2' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id-2', + price: 2, + adm: '', + mtype: 1 + }] + }], + cur: 'USD' + }; + }); + + it('should return banner mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; + expect(bid.mediaType).to.equal(BANNER); + }); + }); + } + + if (FEATURES.NATIVE) { + context('when native request and the response is a native', function() { + beforeEach(function () { + const nativeOrtbRequest = { + ver: '1.2', + assets: [{ + id: 1, + required: 1, + title: { + len: 80 + } + }] + }; + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + native: { + ...nativeOrtbRequest + }, + }, + nativeOrtbRequest, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + adm: '{"ver": "1.2", "assets": [{"id": 1, "required": 1,"title": {"text": "OpenX (Title)"}}], "link": {"url": "https://www.openx.com/"}, "eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', + mtype: 4 + }] + }], + cur: 'AUS' + }; + }); + + it('should return the proper mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); + }); + + it('should return parsed adm JSON in native.ortb response field', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; + + expect(bid.native.ortb).to.deep.equal({ + ver: '1.2', + assets: [{ + id: 1, + required: 1, + title: {text: 'OpenX (Title)'} + }], + link: {url: 'https://www.openx.com/'}, + eventtrackers: [{ + event: 1, + method: 1, + url: 'http://example.com/impression' + }] + }); + }); + }); + + context('when multi-format request (banner + native) and the response is a banner', function() { + beforeEach(function () { + const nativeOrtbRequest = { + ver: '1.2', + assets: [{ + id: 1, + required: 1, + title: { + len: 80 + } + }] + }; + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + native: { + ...nativeOrtbRequest + }, + }, + nativeOrtbRequest, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + adm: '', + mtype: 1 + }] + }], + cur: 'AUS' + }; + }); + + it('should return banner mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; + expect(bid.mediaType).to.equal(BANNER); + }); + }); + + context('when multiple bid requests (banner + native) and the response is a native', function() { + beforeEach(function () { + const nativeOrtbRequest = { + ver: '1.2', + assets: [{ + id: 1, + required: 1, + title: { + len: 80 + } + }] + }; + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + native: { + ...nativeOrtbRequest + }, + }, + nativeOrtbRequest, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bidder-request-id-1', + auctionId: 'test-auction-id-1' + }, + { + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bidder-request-id-2', + auctionId: 'test-auction-id-2' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id-1', + price: 2, + adm: '{"ver": "1.2", "assets": [{"id": 1, "required": 1,"title": {"text": "OpenX (Title)"}}], "link": {"url": "https://www.openx.com/"}, "eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', + mtype: 4 + }] + }], + cur: 'USD' + }; + }); + + it('should return native mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; + expect(bid.mediaType).to.equal(NATIVE); + }); + }); } context('when the response contains FLEDGE interest groups config', function() { @@ -1504,13 +1918,13 @@ describe('OpenxRtbAdapter', function () { it('should return FLEDGE auction_configs alongside bids', function () { expect(response).to.have.property('bids'); - expect(response).to.have.property('fledgeAuctionConfigs'); - expect(response.fledgeAuctionConfigs.length).to.equal(1); - expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test-bid-id'); + expect(response).to.have.property('paapi'); + expect(response.paapi.length).to.equal(1); + expect(response.paapi[0].bidId).to.equal('test-bid-id'); }); it('should inject ortb2Imp in auctionSignals', function () { - const auctionConfig = response.fledgeAuctionConfigs[0].config; + const auctionConfig = response.paapi[0].config; expect(auctionConfig).to.deep.include({ auctionSignals: { ortb2Imp: { @@ -1522,7 +1936,8 @@ describe('OpenxRtbAdapter', function () { }, ext: { divid: 'adunit-code', - } + }, + secure: 1 } } }); diff --git a/test/spec/modules/operaadsIdSystem_spec.js b/test/spec/modules/operaadsIdSystem_spec.js index d81f643d62f..b6acb942331 100644 --- a/test/spec/modules/operaadsIdSystem_spec.js +++ b/test/spec/modules/operaadsIdSystem_spec.js @@ -1,53 +1,76 @@ import { operaIdSubmodule } from 'modules/operaadsIdSystem' import * as ajaxLib from 'src/ajax.js' +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; const TEST_ID = 'opera-test-id'; const operaIdRemoteResponse = { uid: TEST_ID }; - -describe('operaId submodule properties', () => { - it('should expose a "name" property equal to "operaId"', () => { - expect(operaIdSubmodule.name).to.equal('operaId'); +describe('operaads ID', () => { + describe('operaId submodule properties', () => { + it('should expose a "name" property equal to "operaId"', () => { + expect(operaIdSubmodule.name).to.equal('operaId'); + }); }); -}); -function fakeRequest(fn) { - const ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => { - return (url, cbObj) => { - cbObj.success(JSON.stringify(operaIdRemoteResponse)); - } - }); - fn(); - ajaxBuilderStub.restore(); -} + function fakeRequest(fn) { + const ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => { + return (url, cbObj) => { + cbObj.success(JSON.stringify(operaIdRemoteResponse)); + } + }); + fn(); + ajaxBuilderStub.restore(); + } -describe('operaId submodule getId', function() { - it('request to the fake server to correctly extract test ID', function() { - fakeRequest(() => { - const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: { pid: 'pub123' } }); - moduleIdCallbackResponse.callback((id) => { - expect(id).to.equal(operaIdRemoteResponse.operaId); + describe('operaId submodule getId', function() { + it('request to the fake server to correctly extract test ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: { pid: 'pub123' } }); + moduleIdCallbackResponse.callback((id) => { + expect(id).to.equal(operaIdRemoteResponse.operaId); + }); }); }); - }); - it('request to the fake server without publiser ID', function() { - fakeRequest(() => { - const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: {} }); - expect(moduleIdCallbackResponse).to.equal(undefined); + it('request to the fake server without publiser ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: {} }); + expect(moduleIdCallbackResponse).to.equal(undefined); + }); }); }); -}); -describe('operaId submodule decode', function() { - it('should respond with an object containing "operaId" as key with the value', () => { - expect(operaIdSubmodule.decode(TEST_ID)).to.deep.equal({ - operaId: TEST_ID + describe('operaId submodule decode', function() { + it('should respond with an object containing "operaId" as key with the value', () => { + expect(operaIdSubmodule.decode(TEST_ID)).to.deep.equal({ + operaId: TEST_ID + }); }); - }); - it('should respond with undefined if the value is not a string or an empty string', () => { - [1, 2.0, null, undefined, NaN, [], {}].forEach((value) => { - expect(operaIdSubmodule.decode(value)).to.equal(undefined); + it('should respond with undefined if the value is not a string or an empty string', () => { + [1, 2.0, null, undefined, NaN, [], {}].forEach((value) => { + expect(operaIdSubmodule.decode(value)).to.equal(undefined); + }); }); }); -}); + describe('eid', () => { + before(() => { + attachIdSystem(operaIdSubmodule); + }); + it('operaId', function() { + const userId = { + operaId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 't.adx.opera.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + }) +}) diff --git a/test/spec/modules/optableBidAdapter_spec.js b/test/spec/modules/optableBidAdapter_spec.js index d7f2230328e..b7cf2e3b44d 100644 --- a/test/spec/modules/optableBidAdapter_spec.js +++ b/test/spec/modules/optableBidAdapter_spec.js @@ -47,6 +47,33 @@ describe('optableBidAdapter', function() { }); }); + describe('buildPAAPIConfigs', () => { + function makeRequest({bidId, site = 'mockSite', ae = 1}) { + return { + bidId, + params: { + site + }, + ortb2Imp: { + ext: {ae} + } + } + } + it('should generate auction configs for ae requests', () => { + const configs = spec.buildPAAPIConfigs([ + makeRequest({bidId: 'bid1', ae: 1}), + makeRequest({bidId: 'bid2', ae: 0}), + makeRequest({bidId: 'bid3', ae: 1}), + ]); + expect(configs.map(cfg => cfg.bidId)).to.eql(['bid1', 'bid3']); + configs.forEach(cfg => sinon.assert.match(cfg.config, { + seller: 'https://ads.optable.co', + decisionLogicURL: `https://ads.optable.co/ca/paapi/v1/ssp/decision-logic.js?origin=mockSite`, + interestGroupBuyers: ['https://ads.optable.co'] + })) + }) + }) + describe('interpretResponse', function() { const validBid = { bidder: 'optable', @@ -78,10 +105,10 @@ describe('optableBidAdapter', function() { } }; - it('maps fledgeAuctionConfigs from ext.optable.fledge.auctionconfigs', function() { + it('maps paapi from ext.optable.fledge.auctionconfigs', function() { const request = spec.buildRequests([validBid], bidderRequest); const result = spec.interpretResponse(response, request); - expect(result.fledgeAuctionConfigs).to.deep.equal([ + expect(result.paapi).to.deep.equal([ { bidId: 'bid123', config: { seller: 'https://ads.optable.co' } } ]); }); diff --git a/test/spec/modules/optableRtdProvider_spec.js b/test/spec/modules/optableRtdProvider_spec.js new file mode 100644 index 00000000000..7aa4be3c8b2 --- /dev/null +++ b/test/spec/modules/optableRtdProvider_spec.js @@ -0,0 +1,262 @@ +import { + parseConfig, + defaultHandleRtd, + mergeOptableData, + getBidRequestData, + getTargetingData, + optableSubmodule, +} from 'modules/optableRtdProvider'; + +describe('Optable RTD Submodule', function () { + describe('parseConfig', function () { + it('parses valid config correctly', function () { + const config = { + params: { + bundleUrl: 'https://cdn.optable.co/bundle.js', + adserverTargeting: true, + handleRtd: () => {} + } + }; + expect(parseConfig(config)).to.deep.equal({ + bundleUrl: 'https://cdn.optable.co/bundle.js', + adserverTargeting: true, + handleRtd: config.params.handleRtd, + }); + }); + + it('trims bundleUrl if it contains extra spaces', function () { + const config = {params: {bundleUrl: ' https://cdn.optable.co/bundle.js '}}; + expect(parseConfig(config).bundleUrl).to.equal('https://cdn.optable.co/bundle.js'); + }); + + it('throws an error for invalid bundleUrl format', function () { + expect(() => parseConfig({params: {bundleUrl: 'invalidURL'}})).to.throw(); + expect(() => parseConfig({params: {bundleUrl: 'www.invalid.com'}})).to.throw(); + }); + + it('throws an error for non-HTTPS bundleUrl', function () { + expect(() => parseConfig({params: {bundleUrl: 'http://cdn.optable.co/bundle.js'}})).to.throw(); + expect(() => parseConfig({params: {bundleUrl: '//cdn.optable.co/bundle.js'}})).to.throw(); + expect(() => parseConfig({params: {bundleUrl: '/bundle.js'}})).to.throw(); + }); + + it('defaults adserverTargeting to true if missing', function () { + expect(parseConfig( + {params: {bundleUrl: 'https://cdn.optable.co/bundle.js'}} + ).adserverTargeting).to.be.true; + }); + + it('throws an error if handleRtd is not a function', function () { + expect(() => parseConfig({params: {handleRtd: 'notAFunction'}})).to.throw(); + }); + }); + + describe('defaultHandleRtd', function () { + let sandbox, reqBidsConfigObj, mergeFn; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + reqBidsConfigObj = {ortb2Fragments: {global: {}}}; + mergeFn = sinon.spy(); + window.optable = { + instance: { + targeting: sandbox.stub(), + targetingFromCache: sandbox.stub(), + }, + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('merges valid targeting data into the global ORTB2 object', async function () { + const targetingData = {ortb2: {user: {ext: {optable: 'testData'}}}}; + window.optable.instance.targetingFromCache.returns(targetingData); + window.optable.instance.targeting.resolves(targetingData); + + await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn); + expect(mergeFn.calledWith(reqBidsConfigObj.ortb2Fragments.global, targetingData.ortb2)).to.be.true; + }); + + it('does nothing if targeting data is missing the ortb2 property', async function () { + window.optable.instance.targetingFromCache.returns({}); + window.optable.instance.targeting.resolves({}); + + await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn); + expect(mergeFn.called).to.be.false; + }); + + it('uses targeting data from cache if available', async function () { + const targetingData = {ortb2: {user: {ext: {optable: 'testData'}}}}; + window.optable.instance.targetingFromCache.returns(targetingData); + + await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn); + expect(mergeFn.calledWith(reqBidsConfigObj.ortb2Fragments.global, targetingData.ortb2)).to.be.true; + }); + + it('calls targeting function if no data is found in cache', async function () { + const targetingData = {ortb2: {user: {ext: {optable: 'testData'}}}}; + window.optable.instance.targetingFromCache.returns(null); + window.optable.instance.targeting.resolves(targetingData); + + await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn); + expect(mergeFn.calledWith(reqBidsConfigObj.ortb2Fragments.global, targetingData.ortb2)).to.be.true; + }); + }); + + describe('mergeOptableData', function () { + let sandbox, mergeFn, handleRtdFn, reqBidsConfigObj; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + mergeFn = sinon.spy(); + reqBidsConfigObj = {ortb2Fragments: {global: {}}}; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('calls handleRtdFn synchronously if it is a regular function', async function () { + handleRtdFn = sinon.spy(); + await mergeOptableData(handleRtdFn, reqBidsConfigObj, {}, mergeFn); + expect(handleRtdFn.calledOnceWith(reqBidsConfigObj, {}, mergeFn)).to.be.true; + }); + + it('calls handleRtdFn asynchronously if it is an async function', async function () { + handleRtdFn = sinon.stub().resolves(); + await mergeOptableData(handleRtdFn, reqBidsConfigObj, {}, mergeFn); + expect(handleRtdFn.calledOnceWith(reqBidsConfigObj, {}, mergeFn)).to.be.true; + }); + }); + + describe('getBidRequestData', function () { + let sandbox, reqBidsConfigObj, callback, moduleConfig; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + reqBidsConfigObj = {ortb2Fragments: {global: {}}}; + callback = sinon.spy(); + moduleConfig = {params: {bundleUrl: 'https://cdn.optable.co/bundle.js'}}; + + sandbox.stub(window, 'optable').value({cmd: []}); + sandbox.stub(window.document, 'createElement'); + sandbox.stub(window.document, 'head'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('loads Optable JS bundle if bundleUrl is provided', function () { + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + expect(window.document.createElement.called).to.be.true; + }); + + it('uses existing Optable instance if no bundleUrl is provided', function () { + moduleConfig.params.bundleUrl = null; + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + expect(window.optable.cmd.length).to.equal(1); + }); + + it('calls callback when assuming the bundle is present', function (done) { + moduleConfig.params.bundleUrl = null; + + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + + // Check that the function is queued + expect(window.optable.cmd.length).to.equal(1); + // Manually trigger the queued function + window.optable.cmd[0](); + + setTimeout(() => { + expect(callback.calledOnce).to.be.true; + done(); + }, 50); + }); + + it('mergeOptableData catches error and executes callback when something goes wrong', function (done) { + moduleConfig.params.bundleUrl = null; + moduleConfig.params.handleRtd = () => { throw new Error('Test error'); }; + + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + + expect(window.optable.cmd.length).to.equal(1); + window.optable.cmd[0](); + + setTimeout(() => { + expect(callback.calledOnce).to.be.true; + done(); + }, 50); + }); + + it('getBidRequestData catches error and executes callback when something goes wrong', function (done) { + moduleConfig.params.bundleUrl = null; + moduleConfig.params.handleRtd = 'not a function'; + + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + + expect(window.optable.cmd.length).to.equal(0); + + setTimeout(() => { + expect(callback.calledOnce).to.be.true; + done(); + }, 50); + }); + + it("doesn't fail when optable is not available", function (done) { + delete window.optable; + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); + expect(window?.optable?.cmd?.length).to.be.undefined; + + setTimeout(() => { + expect(callback.calledOnce).to.be.true; + done(); + }, 50); + }); + }); + + describe('getTargetingData', function () { + let sandbox, moduleConfig; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + moduleConfig = {params: {adserverTargeting: true}}; + window.optable = {instance: {targetingKeyValuesFromCache: sandbox.stub().returns({key1: 'value1'})}}; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('returns correct targeting data when Optable data is available', function () { + const result = getTargetingData(['adUnit1'], moduleConfig, {}, {}); + expect(result).to.deep.equal({adUnit1: {key1: 'value1'}}); + }); + + it('returns empty object when no Optable data is found', function () { + window.optable.instance.targetingKeyValuesFromCache.returns({}); + expect(getTargetingData(['adUnit1'], moduleConfig, {}, {})).to.deep.equal({}); + }); + + it('returns empty object when adserverTargeting is disabled', function () { + moduleConfig.params.adserverTargeting = false; + expect(getTargetingData(['adUnit1'], moduleConfig, {}, {})).to.deep.equal({}); + }); + + it('returns empty object when provided keys contain no data', function () { + window.optable.instance.targetingKeyValuesFromCache.returns({key1: []}); + expect(getTargetingData(['adUnit1'], moduleConfig, {}, {})).to.deep.equal({}); + + window.optable.instance.targetingKeyValuesFromCache.returns({key1: [], key2: [], key3: []}); + expect(getTargetingData(['adUnit1'], moduleConfig, {}, {})).to.deep.equal({}); + }); + }); + + describe('init', function () { + it('initializes Optable RTD module', function () { + expect(optableSubmodule.init()).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index 8a9f000bbb9..e1d962d306c 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -37,7 +37,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { optimeraRTD.init(conf.dataProviders[0]); optimeraRTD.setScores(); expect(optimeraRTD.apiVersion).to.equal('v0'); - expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); }); it('should properly set the score file URL without apiVersion set', () => { @@ -54,7 +54,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { optimeraRTD.init(conf.dataProviders[0]); optimeraRTD.setScores(); expect(optimeraRTD.apiVersion).to.equal('v0'); - expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); }); it('should properly set the score file URL with an api version other than v0 or v1', () => { @@ -71,7 +71,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { }; optimeraRTD.init(conf.dataProviders[0]); optimeraRTD.setScores(); - expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); }); }); diff --git a/test/spec/modules/orakiBidAdapter_spec.js b/test/spec/modules/orakiBidAdapter_spec.js new file mode 100644 index 00000000000..1a00100cf61 --- /dev/null +++ b/test/spec/modules/orakiBidAdapter_spec.js @@ -0,0 +1,515 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/orakiBidAdapter'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes'; +import { getUniqueIdentifierStr } from '../../../src/utils'; + +const bidder = 'oraki'; + +describe('OrakiBidAdapter', 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 () { + let 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 + } + ]; + + let 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); + let 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); + let 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] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + + 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 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let 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 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let 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' + }] + }; + + let 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' + }] + }; + let 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', + }] + }; + let 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' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.oraki.io/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.oraki.io/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.oraki.io/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index e6abb5e9caa..c38f8e44ab5 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -58,12 +58,30 @@ describe('Outbrain Adapter', function () { minduration: 3, maxduration: 10, startdelay: 2, - placement: 4, + plcmt: 4, + placement: 5, linearity: 1 } } } + const ortb2WithDeviceData = { + ortb2: { + device: { + 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' + } + } + }; + describe('isBidRequestValid', function () { before(() => { config.setConfig({ @@ -389,7 +407,8 @@ describe('Outbrain Adapter', function () { minduration: 3, maxduration: 10, startdelay: 2, - placement: 4, + placement: 5, + plcmt: 4, linearity: 1 } } @@ -619,6 +638,19 @@ describe('Outbrain Adapter', function () { const resData = JSON.parse(res.data) expect(resData.imp[0].native.request).to.equal(JSON.stringify(expectedNativeAssets)); }); + + it('should pass ortb2 device data', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + }; + + const res = spec.buildRequests( + [bidRequest], + {...commonBidderRequest, ...ortb2WithDeviceData}, + ); + expect(JSON.parse(res.data).device).to.deep.equal(ortb2WithDeviceData.ortb2.device); + }); }) describe('interpretResponse', function () { @@ -671,7 +703,7 @@ describe('Outbrain Adapter', function () { cpm: 1.1, creativeId: '28023739', ttl: 360, - netRevenue: false, + netRevenue: true, currency: 'USD', mediaType: 'native', nurl: 'http://example.com/win/${AUCTION_PRICE}', @@ -748,7 +780,7 @@ describe('Outbrain Adapter', function () { cpm: 1.1, creativeId: '29998660', ttl: 360, - netRevenue: false, + netRevenue: true, currency: 'USD', mediaType: 'banner', nurl: 'http://example.com/win/${AUCTION_PRICE}', @@ -811,7 +843,7 @@ describe('Outbrain Adapter', function () { cpm: 1.1, creativeId: '29998660', ttl: 360, - netRevenue: false, + netRevenue: true, currency: 'USD', mediaType: 'video', nurl: 'http://example.com/win/${AUCTION_PRICE}', diff --git a/test/spec/modules/overtoneRtdProvider_spec.mjs b/test/spec/modules/overtoneRtdProvider_spec.mjs new file mode 100644 index 00000000000..a58c21fd95c --- /dev/null +++ b/test/spec/modules/overtoneRtdProvider_spec.mjs @@ -0,0 +1,84 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { overtoneModule, overtoneRtdProvider } from '../../../modules/overtoneRtdProvider.js'; +import { logMessage } from '../../../src/utils.js'; + +const TEST_URLS = { + success: 'https://www.theguardian.com/film/2024/nov/15/duncan-cowles-silent-men-interview', + fail: 'https://www.nytimes.com', + ignore: 'https://wsj.com', +}; + +describe('Overtone RTD Submodule with Test URLs', function () { + let fetchContextDataStub; + let getBidRequestDataStub; + + beforeEach(function () { + fetchContextDataStub = sinon.stub(overtoneModule, 'fetchContextData').callsFake(async (url) => { + if (url === TEST_URLS.success) { + return { categories: ['ovtn_004', 'ovtn_104', 'ovtn_309', 'ovtn_202'], status: 1 }; + } + if (url === TEST_URLS.fail) { + return { categories: [], status: 3 }; + } + if (url === TEST_URLS.ignore) { + return { categories: [], status: 4 }; + } + throw new Error('Unexpected URL in test'); + }); + + getBidRequestDataStub = sinon.stub(overtoneRtdProvider, 'getBidRequestData').callsFake((config, callback) => { + if (config.shouldFail) { + return; + } + callback(); + }); + }); + + afterEach(function () { + fetchContextDataStub.restore(); + getBidRequestDataStub.restore(); + }); + + it('should fetch and return categories for the success URL', async function () { + const data = await overtoneModule.fetchContextData(TEST_URLS.success); + logMessage(data); + expect(data).to.deep.equal({ + categories: ['ovtn_004', 'ovtn_104', 'ovtn_309', 'ovtn_202'], + status: 1, + }); + }); + + it('should return the expected structure for the fail URL', async function () { + const data = await overtoneModule.fetchContextData(TEST_URLS.fail); + expect(data).to.deep.equal({ + categories: [], + status: 3, + }); + }); + + it('should return the expected structure for the ignore URL', async function () { + const data = await overtoneModule.fetchContextData(TEST_URLS.ignore); + expect(data).to.deep.equal({ + categories: [], + status: 4, + }); + }); + + describe('getBidRequestData', function () { + it('should call callback function after execution', function (done) { + const bidReqConfig = { ortb2Fragments: { global: { site: { ext: {} } } } }; + overtoneRtdProvider.getBidRequestData(bidReqConfig, () => { + expect(true).to.be.true; + done(); + }); + }); + + it('should not call callback if config has shouldFail set to true', function () { + const bidReqConfig = { shouldFail: true, ortb2Fragments: { global: { site: { ext: {} } } } }; + const callbackSpy = sinon.spy(); + overtoneRtdProvider.getBidRequestData(bidReqConfig, callbackSpy); + sinon.assert.notCalled(callbackSpy); + }); + }); +}); diff --git a/test/spec/modules/ownadxBidAdapter_spec.js b/test/spec/modules/ownadxBidAdapter_spec.js new file mode 100644 index 00000000000..13d69b3a261 --- /dev/null +++ b/test/spec/modules/ownadxBidAdapter_spec.js @@ -0,0 +1,103 @@ +import { expect } from 'chai'; +import { spec } from 'modules/ownadxBidAdapter.js'; + +describe('ownadx', function () { + const METHOD = 'POST'; + const URL = 'https://pbs-js.prebid-ownadx.com/publisher/prebid/9/1231?token=3f2941af4f7e446f9a19ca6045f8cff4'; + + const bidRequest = { + bidder: 'ownadx', + params: { + tokenId: '3f2941af4f7e446f9a19ca6045f8cff4', + sspId: '1231', + seatId: '9' + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + sizes: [ + [300, 250], + [300, 600] + ], + bidId: 'bid-id-123456', + adUnitCode: 'ad-unit-code-1', + bidderRequestId: 'bidder-request-id-123456', + auctionId: 'auction-id-123456', + transactionId: 'transaction-id-123456' + }; + + describe('isBidRequestValid', function () { + it('should return true where required params found', function () { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + let bidderRequest = { + refererInfo: { + page: 'https://www.test.com', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: [ + 'https://www.test.com' + ], + canonicalUrl: null + } + }; + + it('should build correct POST request for banner bid', function () { + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + expect(request).to.be.an('object'); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://pbs-js.prebid-ownadx.com/publisher/prebid/9/1231?token=3f2941af4f7e446f9a19ca6045f8cff4'); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload.sizes).to.be.an('array'); + expect(payload.slotBidId).to.be.a('string'); + expect(payload.PageUrl).to.be.a('string'); + expect(payload.mediaChannel).to.be.a('number'); + }); + }); + + describe('interpretResponse', function () { + let serverResponse = { + body: { + tokenId: '3f2941af4f7e446f9a19ca6045f8cff4', + bid: 'BID-XXXX-XXXX', + width: '300', + height: '250', + cpm: '0.7', + adm: '

Ad from OwnAdX

', + slotBidId: 'bid-id-123456', + adType: '1', + statusText: 'Success' + } + }; + + let expectedResponse = [{ + token: '3f2941af4f7e446f9a19ca6045f8cff4', + requestId: 'bid-id-123456', + cpm: '0.7', + currency: 'USD', + aType: '1', + netRevenue: false, + width: '300', + height: '250', + creativeId: 0, + ttl: 300, + ad: '

Ad from OwnAdX

', + meta: { + mediaType: 'banner', + advertiserDomains: [] + } + }]; + + it('should correctly interpret valid banner response', function () { + let result = spec.interpretResponse(serverResponse); + expect(result).to.deep.equal(expectedResponse); + }); + }); +}); diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index 73df2fba8fd..b2b494b04c6 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -4,8 +4,15 @@ import { config } from 'src/config.js'; import {Renderer} from '../../../src/Renderer.js'; import {getGranularityKeyName, getGranularityObject} from '../../../modules/ozoneBidAdapter.js'; import * as utils from '../../../src/utils.js'; +import {deepSetValue} from '../../../src/utils.js'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; const BIDDER_CODE = 'ozone'; +spec.getGetParametersAsObject = function() { + return { + page: 'https://www.ardm.io/sometestPage/?qsParam1=123', + location: 'https://www.ardm.io/sometestPage/?qsParam1=123' + }; +} var validBidRequests = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', @@ -62,6 +69,699 @@ var validBidRequestsMulti = [ transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } ]; +var validBidRequestsWithAuctionIdTransactionId = [{ + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONENUK0001', + 'siteId': '4204204201', + 'placementId': '8000000330', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ] + }, + 'ortb2Imp': { + 'ext': { + 'gpid': 'mpu_pbadslot_from_adunit', + 'data': { + 'pbadslot': 'mpu_pbadslot_from_adunit', + 'adserver': { + 'name': 'gam', + 'adslot': '/22037345/projectozone' + } + }, + 'tid': 'f0dac8b5-09df-4da7-9d83-c99786d4517a' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu', + 'transactionId': 'f0dac8b5-09df-4da7-9d83-c99786d4517a', + 'adUnitId': '715b4bdc-515f-488b-8633-333654e72f3f', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '3da18cc31f1dda', + 'bidderRequestId': '263c3b0d970326', + 'auctionId': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'source': { + 'tid': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0' + }, + 'regs': { + 'ext': { + 'gdpr': 1, + 'us_privacy': '1Y--' + } + }, + 'user': { + 'ext': { + 'consent': 'CQAaAwAQAaAwAAKA1AENA5EsAP_gAEPgACiQKRNV_G__bWlr8X73aftkeY1P9_h77sQxBhfJE-4FzLuW_JwXx2ExNA36tqIKmRIEu3bBIQNlHJDUTVCgaogVryDMakWcoTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr993dx2B-t_dv83dzyz4VHn3a5_2e0WJCdA58tDfv9bROb-9IPd_58v4v8_F_rE2_eT1l_tevp7D9-cts7_XW-9_fff79Ll_-mBwUcALMNCogDLIkJCDQMIIEAKgrCAigQAAAAkDRAQAmDAp2BgEusJEAIAUAAwQAgABRkACAAASABCIAIACgQAAQCBQAAgAACAQAMDAAGACwEAgABAdAhTAggUCwASMyIhTAgCgSCAlsqEEgCBBXCEIs8CCAREwUAAAJABWAAICwWAxJICViQQJcQbQAAEACAQQAVCKTswBBAGbLVXiibRlaQFo-ACjgAAAAA.YAAAAAAAAAAA' + } + }, + 'site': { + 'domain': 'ardm.io', + 'publisher': { + 'domain': 'ardm.io' + }, + 'page': 'https://www.ardm.io/ozone/2.9.4/20240715-test-singlereq-optin.html?pbjs_debug=true' + }, + 'device': { + 'w': 1609, + 'h': 279, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'language': 'en' + } + } +}]; +var valid6BidRequestsWithAuctionIdTransactionId = [{ + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONENUK0001', + 'siteId': '4204204201', + 'placementId': '8000000330', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ] + }, + 'ortb2Imp': { + 'ext': { + 'gpid': 'mpu_pbadslot_from_adunit', + 'data': { + 'pbadslot': 'mpu_pbadslot_from_adunit', + 'adserver': { + 'name': 'gam', + 'adslot': '/22037345/projectozone' + } + }, + 'tid': 'f0dac8b5-09df-4da7-9d83-c99786d4517a' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu', + 'transactionId': 'f0dac8b5-09df-4da7-9d83-c99786d4517a', + 'adUnitId': '715b4bdc-515f-488b-8633-333654e72f3f', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '3da18cc31f1dda', + 'bidderRequestId': '263c3b0d970326', + 'auctionId': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'source': { + 'tid': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0' + }, + 'regs': { + 'ext': { + 'gdpr': 1, + 'us_privacy': '1Y--' + } + }, + 'user': { + 'ext': { + 'consent': 'CQAaAwAQAaAwAAKA1AENA5EsAP_gAEPgACiQKRNV_G__bWlr8X73aftkeY1P9_h77sQxBhfJE-4FzLuW_JwXx2ExNA36tqIKmRIEu3bBIQNlHJDUTVCgaogVryDMakWcoTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr993dx2B-t_dv83dzyz4VHn3a5_2e0WJCdA58tDfv9bROb-9IPd_58v4v8_F_rE2_eT1l_tevp7D9-cts7_XW-9_fff79Ll_-mBwUcALMNCogDLIkJCDQMIIEAKgrCAigQAAAAkDRAQAmDAp2BgEusJEAIAUAAwQAgABRkACAAASABCIAIACgQAAQCBQAAgAACAQAMDAAGACwEAgABAdAhTAggUCwASMyIhTAgCgSCAlsqEEgCBBXCEIs8CCAREwUAAAJABWAAICwWAxJICViQQJcQbQAAEACAQQAVCKTswBBAGbLVXiibRlaQFo-ACjgAAAAA.YAAAAAAAAAAA' + } + }, + 'site': { + 'domain': 'ardm.io', + 'publisher': { + 'domain': 'ardm.io' + }, + 'page': 'https://www.ardm.io/ozone/2.9.4/20240715-test-singlereq-optin.html?pbjs_debug=true' + }, + 'device': { + 'w': 1609, + 'h': 279, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'language': 'en' + } + } +}, +{ + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONENUK0001', + 'siteId': '4204204201', + 'placementId': '8000000330', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ] + }, + 'ortb2Imp': { + 'ext': { + 'gpid': 'mpu_pbadslot_from_adunit', + 'data': { + 'pbadslot': 'mpu_pbadslot_from_adunit', + 'adserver': { + 'name': 'gam', + 'adslot': '/22037345/projectozone' + } + }, + 'tid': 'f0dac8b5-09df-4da7-9d83-c99786d4517a' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu2', + 'transactionId': 'f0dac8b5-09df-4da7-9d83-c99786d4517a', + 'adUnitId': '715b4bdc-515f-488b-8633-333654e72f3f', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '3da18cc31f1ddb', + 'bidderRequestId': '263c3b0d970326', + 'auctionId': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'source': { + 'tid': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0' + }, + 'regs': { + 'ext': { + 'gdpr': 1, + 'us_privacy': '1Y--' + } + }, + 'user': { + 'ext': { + 'consent': 'CQAaAwAQAaAwAAKA1AENA5EsAP_gAEPgACiQKRNV_G__bWlr8X73aftkeY1P9_h77sQxBhfJE-4FzLuW_JwXx2ExNA36tqIKmRIEu3bBIQNlHJDUTVCgaogVryDMakWcoTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr993dx2B-t_dv83dzyz4VHn3a5_2e0WJCdA58tDfv9bROb-9IPd_58v4v8_F_rE2_eT1l_tevp7D9-cts7_XW-9_fff79Ll_-mBwUcALMNCogDLIkJCDQMIIEAKgrCAigQAAAAkDRAQAmDAp2BgEusJEAIAUAAwQAgABRkACAAASABCIAIACgQAAQCBQAAgAACAQAMDAAGACwEAgABAdAhTAggUCwASMyIhTAgCgSCAlsqEEgCBBXCEIs8CCAREwUAAAJABWAAICwWAxJICViQQJcQbQAAEACAQQAVCKTswBBAGbLVXiibRlaQFo-ACjgAAAAA.YAAAAAAAAAAA' + } + }, + 'site': { + 'domain': 'ardm.io', + 'publisher': { + 'domain': 'ardm.io' + }, + 'page': 'https://www.ardm.io/ozone/2.9.4/20240715-test-singlereq-optin.html?pbjs_debug=true' + }, + 'device': { + 'w': 1609, + 'h': 279, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'language': 'en' + } + } +}, +{ + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONENUK0001', + 'siteId': '4204204201', + 'placementId': '8000000330', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ] + }, + 'ortb2Imp': { + 'ext': { + 'gpid': 'mpu_pbadslot_from_adunit', + 'data': { + 'pbadslot': 'mpu_pbadslot_from_adunit', + 'adserver': { + 'name': 'gam', + 'adslot': '/22037345/projectozone' + } + }, + 'tid': 'f0dac8b5-09df-4da7-9d83-c99786d4517a' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu3', + 'transactionId': 'f0dac8b5-09df-4da7-9d83-c99786d4517a', + 'adUnitId': '715b4bdc-515f-488b-8633-333654e72f3f', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '3da18cc31f1ddc', + 'bidderRequestId': '263c3b0d970326', + 'auctionId': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'source': { + 'tid': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0' + }, + 'regs': { + 'ext': { + 'gdpr': 1, + 'us_privacy': '1Y--' + } + }, + 'user': { + 'ext': { + 'consent': 'CQAaAwAQAaAwAAKA1AENA5EsAP_gAEPgACiQKRNV_G__bWlr8X73aftkeY1P9_h77sQxBhfJE-4FzLuW_JwXx2ExNA36tqIKmRIEu3bBIQNlHJDUTVCgaogVryDMakWcoTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr993dx2B-t_dv83dzyz4VHn3a5_2e0WJCdA58tDfv9bROb-9IPd_58v4v8_F_rE2_eT1l_tevp7D9-cts7_XW-9_fff79Ll_-mBwUcALMNCogDLIkJCDQMIIEAKgrCAigQAAAAkDRAQAmDAp2BgEusJEAIAUAAwQAgABRkACAAASABCIAIACgQAAQCBQAAgAACAQAMDAAGACwEAgABAdAhTAggUCwASMyIhTAgCgSCAlsqEEgCBBXCEIs8CCAREwUAAAJABWAAICwWAxJICViQQJcQbQAAEACAQQAVCKTswBBAGbLVXiibRlaQFo-ACjgAAAAA.YAAAAAAAAAAA' + } + }, + 'site': { + 'domain': 'ardm.io', + 'publisher': { + 'domain': 'ardm.io' + }, + 'page': 'https://www.ardm.io/ozone/2.9.4/20240715-test-singlereq-optin.html?pbjs_debug=true' + }, + 'device': { + 'w': 1609, + 'h': 279, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'language': 'en' + } + } +}, +{ + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONENUK0001', + 'siteId': '4204204201', + 'placementId': '8000000330', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ] + }, + 'ortb2Imp': { + 'ext': { + 'gpid': 'mpu_pbadslot_from_adunit', + 'data': { + 'pbadslot': 'mpu_pbadslot_from_adunit', + 'adserver': { + 'name': 'gam', + 'adslot': '/22037345/projectozone' + } + }, + 'tid': 'f0dac8b5-09df-4da7-9d83-c99786d4517a' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu4', + 'transactionId': 'f0dac8b5-09df-4da7-9d83-c99786d4517a', + 'adUnitId': '715b4bdc-515f-488b-8633-333654e72f3f', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '3da18cc31f1ddd', + 'bidderRequestId': '263c3b0d970326', + 'auctionId': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'source': { + 'tid': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0' + }, + 'regs': { + 'ext': { + 'gdpr': 1, + 'us_privacy': '1Y--' + } + }, + 'user': { + 'ext': { + 'consent': 'CQAaAwAQAaAwAAKA1AENA5EsAP_gAEPgACiQKRNV_G__bWlr8X73aftkeY1P9_h77sQxBhfJE-4FzLuW_JwXx2ExNA36tqIKmRIEu3bBIQNlHJDUTVCgaogVryDMakWcoTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr993dx2B-t_dv83dzyz4VHn3a5_2e0WJCdA58tDfv9bROb-9IPd_58v4v8_F_rE2_eT1l_tevp7D9-cts7_XW-9_fff79Ll_-mBwUcALMNCogDLIkJCDQMIIEAKgrCAigQAAAAkDRAQAmDAp2BgEusJEAIAUAAwQAgABRkACAAASABCIAIACgQAAQCBQAAgAACAQAMDAAGACwEAgABAdAhTAggUCwASMyIhTAgCgSCAlsqEEgCBBXCEIs8CCAREwUAAAJABWAAICwWAxJICViQQJcQbQAAEACAQQAVCKTswBBAGbLVXiibRlaQFo-ACjgAAAAA.YAAAAAAAAAAA' + } + }, + 'site': { + 'domain': 'ardm.io', + 'publisher': { + 'domain': 'ardm.io' + }, + 'page': 'https://www.ardm.io/ozone/2.9.4/20240715-test-singlereq-optin.html?pbjs_debug=true' + }, + 'device': { + 'w': 1609, + 'h': 279, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'language': 'en' + } + } +}, +{ + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONENUK0001', + 'siteId': '4204204201', + 'placementId': '8000000330', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ] + }, + 'ortb2Imp': { + 'ext': { + 'gpid': 'mpu_pbadslot_from_adunit', + 'data': { + 'pbadslot': 'mpu_pbadslot_from_adunit', + 'adserver': { + 'name': 'gam', + 'adslot': '/22037345/projectozone' + } + }, + 'tid': 'f0dac8b5-09df-4da7-9d83-c99786d4517a' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu5', + 'transactionId': 'f0dac8b5-09df-4da7-9d83-c99786d4517a', + 'adUnitId': '715b4bdc-515f-488b-8633-333654e72f3f', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '3da18cc31f1dde', + 'bidderRequestId': '263c3b0d970326', + 'auctionId': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'source': { + 'tid': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0' + }, + 'regs': { + 'ext': { + 'gdpr': 1, + 'us_privacy': '1Y--' + } + }, + 'user': { + 'ext': { + 'consent': 'CQAaAwAQAaAwAAKA1AENA5EsAP_gAEPgACiQKRNV_G__bWlr8X73aftkeY1P9_h77sQxBhfJE-4FzLuW_JwXx2ExNA36tqIKmRIEu3bBIQNlHJDUTVCgaogVryDMakWcoTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr993dx2B-t_dv83dzyz4VHn3a5_2e0WJCdA58tDfv9bROb-9IPd_58v4v8_F_rE2_eT1l_tevp7D9-cts7_XW-9_fff79Ll_-mBwUcALMNCogDLIkJCDQMIIEAKgrCAigQAAAAkDRAQAmDAp2BgEusJEAIAUAAwQAgABRkACAAASABCIAIACgQAAQCBQAAgAACAQAMDAAGACwEAgABAdAhTAggUCwASMyIhTAgCgSCAlsqEEgCBBXCEIs8CCAREwUAAAJABWAAICwWAxJICViQQJcQbQAAEACAQQAVCKTswBBAGbLVXiibRlaQFo-ACjgAAAAA.YAAAAAAAAAAA' + } + }, + 'site': { + 'domain': 'ardm.io', + 'publisher': { + 'domain': 'ardm.io' + }, + 'page': 'https://www.ardm.io/ozone/2.9.4/20240715-test-singlereq-optin.html?pbjs_debug=true' + }, + 'device': { + 'w': 1609, + 'h': 279, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'language': 'en' + } + } +}, +{ + 'bidder': 'ozone', + 'params': { + 'publisherId': 'OZONENUK0001', + 'siteId': '4204204201', + 'placementId': '8000000330', + 'customData': [ + { + 'settings': {}, + 'targeting': { + 'sens': 'f', + 'pt1': '/uk', + 'pt5': [ + 'uk' + ], + 'pt7': 'desktop', + 'pt9': '|k0xw2vqzp33kklb3j5w4|||' + } + } + ] + }, + 'ortb2Imp': { + 'ext': { + 'gpid': 'mpu_pbadslot_from_adunit', + 'data': { + 'pbadslot': 'mpu_pbadslot_from_adunit', + 'adserver': { + 'name': 'gam', + 'adslot': '/22037345/projectozone' + } + }, + 'tid': 'f0dac8b5-09df-4da7-9d83-c99786d4517a' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'mpu6', + 'transactionId': 'f0dac8b5-09df-4da7-9d83-c99786d4517a', + 'adUnitId': '715b4bdc-515f-488b-8633-333654e72f3f', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '3da18cc31f1ddf', + 'bidderRequestId': '263c3b0d970326', + 'auctionId': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'source': { + 'tid': 'a9c479d0-d9cc-4505-a0a6-5982ce8fb8f0' + }, + 'regs': { + 'ext': { + 'gdpr': 1, + 'us_privacy': '1Y--' + } + }, + 'user': { + 'ext': { + 'consent': 'CQAaAwAQAaAwAAKA1AENA5EsAP_gAEPgACiQKRNV_G__bWlr8X73aftkeY1P9_h77sQxBhfJE-4FzLuW_JwXx2ExNA36tqIKmRIEu3bBIQNlHJDUTVCgaogVryDMakWcoTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr993dx2B-t_dv83dzyz4VHn3a5_2e0WJCdA58tDfv9bROb-9IPd_58v4v8_F_rE2_eT1l_tevp7D9-cts7_XW-9_fff79Ll_-mBwUcALMNCogDLIkJCDQMIIEAKgrCAigQAAAAkDRAQAmDAp2BgEusJEAIAUAAwQAgABRkACAAASABCIAIACgQAAQCBQAAgAACAQAMDAAGACwEAgABAdAhTAggUCwASMyIhTAgCgSCAlsqEEgCBBXCEIs8CCAREwUAAAJABWAAICwWAxJICViQQJcQbQAAEACAQQAVCKTswBBAGbLVXiibRlaQFo-ACjgAAAAA.YAAAAAAAAAAA' + } + }, + 'site': { + 'domain': 'ardm.io', + 'publisher': { + 'domain': 'ardm.io' + }, + 'page': 'https://www.ardm.io/ozone/2.9.4/20240715-test-singlereq-optin.html?pbjs_debug=true' + }, + 'device': { + 'w': 1609, + 'h': 279, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'language': 'en' + } + } +}]; var validBidRequestsWithUserIdData = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', @@ -401,6 +1101,66 @@ var validBidderRequest = { start: 1536838908987, timeout: 3000 }; +var validBidderRequestWithCookieDeprecation = { + auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', + auctionStart: 1536838908986, + bidderCode: 'ozone', + bidderRequestId: '1c1586b27a1b5c8', + bids: [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', + bidId: '2899ec066a91ff8', + bidRequestsCount: 1, + bidder: 'ozone', + bidderRequestId: '1c1586b27a1b5c8', + crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { banner: { topframe: 1, w: 300, h: 250, format: [{ w: 300, h: 250 }, { w: 300, h: 600 }] }, id: '2899ec066a91ff8', secure: 1, tagid: 'undefined' } ] }, + sizes: [[300, 250], [300, 600]], + transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' + }], + doneCbCallCount: 1, + start: 1536838908987, + timeout: 3000, + ortb2: { + 'device': { + 'w': 1617, + 'h': 317, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', + 'language': 'en', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Google Chrome', + 'version': [ + '125' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '125' + ] + }, + { + 'brand': 'Not.A/Brand', + 'version': [ + '24' + ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'fake_control_2' + } + } + } +}; var bidderRequestWithFullGdpr = { auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', auctionStart: 1536838908986, @@ -1814,7 +2574,7 @@ describe('ozone Adapter', function () { }); it('should add gdpr consent information to the request when ozone is true', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; + let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); bidderRequest.gdprConsent = { consentString: consentString, gdprApplies: true, @@ -1832,7 +2592,7 @@ describe('ozone Adapter', function () { }); it('should add gdpr consent information to the request when vendorData is missing vendorConsents (Mirror)', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; + let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); bidderRequest.gdprConsent = { consentString: consentString, gdprApplies: true, @@ -1848,7 +2608,7 @@ describe('ozone Adapter', function () { }); it('should set regs.ext.gdpr flag to 0 when gdprApplies is false', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; + let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); bidderRequest.gdprConsent = { consentString: consentString, gdprApplies: false, @@ -1863,9 +2623,24 @@ describe('ozone Adapter', function () { const payload = JSON.parse(request.data); expect(payload.regs.ext.gdpr).to.equal(0); }); + it('should set gpp and gpp_sid when available', function() { + let gppString = 'gppConsentString'; + let gppSections = [7, 8, 9]; + let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); + bidderRequest.ortb2 = {regs: {gpp: gppString, gpp_sid: gppSections}}; + const request = spec.buildRequests(validBidRequestsNoSizes, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.regs.gpp).to.equal(gppString); + expect(payload.regs.gpp_sid).to.have.same.members(gppSections); + }); + it('should not set gpp and gpp_sid keys when not available', function() { + const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); + const payload = JSON.parse(request.data); + expect(payload).to.not.contain.keys(['gpp', 'gpp_sid', 'ext', 'regs']); + }); it('should not have imp[N].ext.ozone.userId', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; - let bidderRequest = validBidderRequest; + let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); bidderRequest.gdprConsent = { consentString: consentString, gdprApplies: false, @@ -1876,7 +2651,7 @@ describe('ozone Adapter', function () { purposeConsents: {1: true, 2: true, 3: true, 4: true, 5: true} } }; - let bidRequests = validBidRequests; + let bidRequests = JSON.parse(JSON.stringify(validBidRequests)); bidRequests[0]['userId'] = { 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, 'id5id': { uid: '1111', ext: { linkType: 2, abTestingControlGroup: false } }, @@ -1891,23 +2666,12 @@ describe('ozone Adapter', function () { const payload = JSON.parse(request.data); let firstBid = payload.imp[0].ext.ozone; expect(firstBid).to.not.have.property('userId'); - delete validBidRequests[0].userId; // tidy up now, else it will screw with other tests }); it('should pick up the value of pubcid when built using the pubCommonId module (not userId)', function () { let bidRequests = validBidRequests; - bidRequests[0]['userId'] = { - 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - 'id5id': { uid: '1111', ext: { linkType: 2, abTestingControlGroup: false } }, - 'idl_env': '3333', - 'parrableid': 'eidVersion.encryptionKeyReference.encryptedValue', - 'tdid': '6666', - 'sharedid': {'id': '01EAJWWNEPN3CYMM5N8M5VXY22', 'third': '01EAJWWNEPN3CYMM5N8M5VXY22'} - }; - bidRequests[0]['userIdAsEids'] = validBidRequestsWithUserIdData[0]['userIdAsEids']; const request = spec.buildRequests(bidRequests, validBidderRequest); const payload = JSON.parse(request.data); expect(payload.ext.ozone.pubcid).to.equal(bidRequests[0]['crumbs']['pubcid']); - delete validBidRequests[0].userId; // tidy up now, else it will screw with other tests }); it('should add a user.ext.eids object to contain user ID data in the new location (Nov 2019) Updated Aug 2020', function() { const request = spec.buildRequests(validBidRequestsWithUserIdData, validBidderRequest); @@ -1990,18 +2754,18 @@ describe('ozone Adapter', function () { const request = spec.buildRequests(validBidRequests, validBidderRequest); const data = JSON.parse(request.data); expect(data.ext.ozone).to.haveOwnProperty('test_rw'); - config.setConfig({'ozone': {'kvpPrefix': null}}); + config.resetConfig(); spec.propertyBag.whitelabel = null; }); it('handles an alias ', function () { spec.propertyBag.whitelabel = null; - config.setConfig({'lmc': {'kvpPrefix': 'test'}}); + config.setConfig({'venatus': {'kvpPrefix': 've'}}); let br = JSON.parse(JSON.stringify(validBidRequests)); - br[0]['bidder'] = 'lmc'; + br[0]['bidder'] = 'venatus'; const request = spec.buildRequests(br, validBidderRequest); const data = JSON.parse(request.data); - expect(data.ext.lmc).to.haveOwnProperty('test_rw'); - config.setConfig({'lmc': {'kvpPrefix': null}}); // I cant remove the key so set the value to null + expect(data.ext.venatus).to.haveOwnProperty('ve_rw'); + config.resetConfig(); spec.propertyBag.whitelabel = null; }); it('should use oztestmode GET value if set', function() { @@ -2014,29 +2778,14 @@ describe('ozone Adapter', function () { expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.customData[0].targeting.oztestmode).to.equal('mytestvalue_123'); }); - it('should pass through GET params if present: ozf, ozpf, ozrp, ozip', function() { + it('should ignore these GET params if present (removed 202410): ozf, ozpf, ozrp, ozip', function() { var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { - return {ozf: '1', ozpf: '0', ozrp: '2', ozip: '123'}; + return {ozf: '1', ozpf: '10', ozrp: '2', ozip: '123'}; }; const request = specMock.buildRequests(validBidRequests, validBidderRequest); const data = JSON.parse(request.data); - expect(data.ext.ozone.ozf).to.equal(1); - expect(data.ext.ozone.ozpf).to.equal(0); - expect(data.ext.ozone.ozrp).to.equal(2); - expect(data.ext.ozone.ozip).to.equal(123); - }); - it('should pass through GET params if present: ozf, ozpf, ozrp, ozip with alternative values', function() { - var specMock = utils.deepClone(spec); - specMock.getGetParametersAsObject = function() { - return {ozf: 'false', ozpf: 'true', ozrp: 'xyz', ozip: 'hello'}; - }; - const request = specMock.buildRequests(validBidRequests, validBidderRequest); - const data = JSON.parse(request.data); - expect(data.ext.ozone.ozf).to.equal(0); - expect(data.ext.ozone.ozpf).to.equal(1); - expect(data.ext.ozone).to.not.haveOwnProperty('ozrp'); - expect(data.ext.ozone).to.not.haveOwnProperty('ozip'); + expect(data.ext.ozone).to.not.have.any.keys('zf', 'ozpf', 'ozrp', 'ozip'); }); it('should use oztestmode GET value if set, even if there is no customdata in config', function() { var specMock = utils.deepClone(spec); @@ -2056,7 +2805,7 @@ describe('ozone Adapter', function () { const data = JSON.parse(request.data); expect(data.imp[0].ext.gpid).to.equal('/22037345/projectozone'); }); - it('should batch into 10s if config is set', function () { + it('should batch into 10s if config is set to true', function () { config.setConfig({ozone: {'batchRequests': true}}); var specMock = utils.deepClone(spec); let arrReq = []; @@ -2069,7 +2818,20 @@ describe('ozone Adapter', function () { expect(request.length).to.equal(3); config.resetConfig(); }); - it('should not batch into 10s if config is set to false and singleRequest is true', function () { + it('should batch into 7 if config is set to 7', function () { + config.setConfig({ozone: {'batchRequests': 7}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 25; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.length).to.equal(4); + config.resetConfig(); + }); + it('should not batch if config is set to false and singleRequest is true', function () { config.setConfig({ozone: {'batchRequests': false, 'singleRequest': true}}); var specMock = utils.deepClone(spec); let arrReq = []; @@ -2082,6 +2844,57 @@ describe('ozone Adapter', function () { expect(request.method).to.equal('POST'); config.resetConfig(); }); + it('should not batch if config is set to invalid value -10 and singleRequest is true', function () { + config.setConfig({ozone: {'batchRequests': -10, 'singleRequest': true}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 15; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.method).to.equal('POST'); + config.resetConfig(); + }); + it('should use GET values for batchRequests if found', function() { + var specMock = utils.deepClone(spec); + specMock.getGetParametersAsObject = function() { + return {'batchRequests': '5'}; + }; + let arrReq = []; + for (let i = 0; i < 25; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + let request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.length).to.equal(5); // 5 x 5 = 25 + specMock = utils.deepClone(spec); + specMock.getGetParametersAsObject = function() { + return {'batchRequests': '10'}; // the built in function will return '10' (string) + }; + request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.length).to.equal(3); // 10, 10, 5 + specMock = utils.deepClone(spec); + specMock.getGetParametersAsObject = function() { + return {'batchRequests': true}; + }; + request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.method).to.equal('POST'); // no batching - GET param must be numeric + specMock = utils.deepClone(spec); + specMock.getGetParametersAsObject = function() { + return {'batchRequests': 'true'}; + }; + request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.method).to.equal('POST'); // no batching - GET param must be numeric + specMock = utils.deepClone(spec); + specMock.getGetParametersAsObject = function() { + return {'batchRequests': -5}; + }; + request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.method).to.equal('POST'); // no batching + }); it('should use GET values auction=dev & cookiesync=dev if set', function() { var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { @@ -2128,24 +2941,49 @@ describe('ozone Adapter', function () { const payload = JSON.parse(request.data); expect(payload.regs).to.include.keys('coppa'); expect(payload.regs.coppa).to.equal(1); + config.resetConfig(); }); it('should pick up the config value of coppa & only set it in the request if its true', function () { config.setConfig({'coppa': false}); const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); const payload = JSON.parse(request.data); expect(utils.deepAccess(payload, 'regs.coppa')).to.be.undefined; + config.resetConfig(); }); it('should handle oz_omp_floor correctly', function () { config.setConfig({'ozone': {'oz_omp_floor': 1.56}}); const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); const payload = JSON.parse(request.data); expect(utils.deepAccess(payload, 'ext.ozone.oz_omp_floor')).to.equal(1.56); + config.resetConfig(); }); it('should ignore invalid oz_omp_floor values', function () { config.setConfig({'ozone': {'oz_omp_floor': '1.56'}}); const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); const payload = JSON.parse(request.data); expect(utils.deepAccess(payload, 'ext.ozone.oz_omp_floor')).to.be.undefined; + config.resetConfig(); + }); + it('should handle a valid ozFloor string value in the adunit correctly', function () { + let cloneBidRequests = JSON.parse(JSON.stringify(validBidRequests)); + cloneBidRequests[0].params.ozFloor = '0.1234'; // string or float - doesnt matter + const request = spec.buildRequests(cloneBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + expect(utils.deepAccess(payload, 'imp.0.ext.ozone.ozFloor')).to.equal(0.1234); + }); + it('should handle a valid ozFloor float value in the adunit correctly', function () { + let cloneBidRequests = JSON.parse(JSON.stringify(validBidRequests)); + cloneBidRequests[0].params.ozFloor = 0.1234; // string or float - doesnt matter + const request = spec.buildRequests(cloneBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + expect(utils.deepAccess(payload, 'imp.0.ext.ozone.ozFloor')).to.equal(0.1234); + }); + it('should ignore an invalid ozFloor string value in the adunit correctly', function () { + let cloneBidRequests = JSON.parse(JSON.stringify(validBidRequests)); + cloneBidRequests[0].params.ozFloor = 'this is no good!'; // string or float - doesnt matter + const request = spec.buildRequests(cloneBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + expect(utils.deepAccess(payload, 'imp.0.ext.ozone.ozFloor', null)).to.be.null; }); it('should should contain a unique page view id in the auction request which persists across calls', function () { let request = spec.buildRequests(validBidRequests, validBidderRequest); @@ -2276,6 +3114,30 @@ describe('ozone Adapter', function () { expect(utils.deepAccess(payload, 'imp.0.floor.banner.currency')).to.equal('USD'); expect(utils.deepAccess(payload, 'imp.0.floor.banner.floor')).to.equal(0.8); }); + it(' (getFloorObjectForAuction) should handle advanced/custom floor config function correctly (note you cant fully test floor functionality because it relies on the floor module - only our code that interacts with it; we must extract the first w/h pair)', function () { + let testBidObject = { + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + playerSize: [[640, 360]] + }, + native: { + image: { + sizes: [[300, 250], [640, 480]] + } + } + }, + getFloor: function(obj) { + return obj.size; // we just want to look at the size that was sent + } + }; + let floorObject = spec.getFloorObjectForAuction(testBidObject); + expect(floorObject.banner).to.deep.equal([300, 250]); + expect(floorObject.video).to.deep.equal([640, 360]); + expect(floorObject.native).to.deep.equal([300, 250]); + }); it('handles schain object in each bidrequest (will be the same in each br)', function () { let br = JSON.parse(JSON.stringify(validBidRequests)); let schainConfigObject = { @@ -2295,6 +3157,88 @@ describe('ozone Adapter', function () { expect(data.source.ext).to.haveOwnProperty('schain'); expect(data.source.ext.schain).to.deep.equal(schainConfigObject); // .deep.equal() : Target object deeply (but not strictly) equals `{a: 1}` }); + it('should find ortb2 cookieDeprecation values', function () { + let bidderRequest = JSON.parse(JSON.stringify(validBidderRequestWithCookieDeprecation)); + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.ext.ozone.cookieDeprecationLabel).to.equal('fake_control_2'); + }); + it('should set ortb2 cookieDeprecation to "none" if there is none', function () { + let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.ext.ozone.cookieDeprecationLabel).to.equal('none'); + }); + it('should handle fledge requests', function () { + let bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); + let bidRequests = JSON.parse(JSON.stringify(validBidRequests)); + deepSetValue(bidRequests[0], 'ortb2Imp.ext.ae', 1); + bidderRequest.fledgeEnabled = true; + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.imp[0].ext.ae).to.equal(1); + }); + it('Single request: should use ortb auction ID & transaction ID values if set (this will be the case when publisher opts in with config)', function() { + var specMock = utils.deepClone(spec); + specMock.propertyBag.whitelabel = null; + config.setConfig({'ozone': {'singleRequest': true}}); + specMock.loadWhitelabelData(validBidRequestsWithAuctionIdTransactionId[0]); + const request = specMock.buildRequests(validBidRequestsWithAuctionIdTransactionId, validBidderRequest); // I don't look in the bidderRequest for this - there's no point + expect(request).to.be.an('Object'); + const payload = JSON.parse(request.data); + expect(payload.source.tid).to.equal(validBidRequestsWithAuctionIdTransactionId[0].ortb2.source.tid); + expect(payload.imp[0].ext.ozone.auctionId).to.equal(validBidRequestsWithAuctionIdTransactionId[0].ortb2.source.tid); + expect(payload.imp[0].ext.ozone.transactionId).to.equal(validBidRequestsWithAuctionIdTransactionId[0].ortb2Imp.ext.tid); + config.resetConfig(); + }); + it('non-Single request: should use ortb auction ID & transaction ID values if set (this will be the case when publisher opts in with config)', function() { + var specMock = utils.deepClone(spec); + specMock.propertyBag.whitelabel = null; + config.setConfig({'ozone': {'singleRequest': false}}); + specMock.loadWhitelabelData(validBidRequestsWithAuctionIdTransactionId[0]); + const request = specMock.buildRequests(validBidRequestsWithAuctionIdTransactionId, validBidderRequest); // I don't look in the bidderRequest for this - there's no point + expect(request).to.be.an('Array'); + const payload = JSON.parse(request[0].data); + expect(payload.source.tid).to.equal(validBidRequestsWithAuctionIdTransactionId[0].ortb2.source.tid); + expect(payload.imp[0].ext.ozone.auctionId).to.equal(validBidRequestsWithAuctionIdTransactionId[0].ortb2.source.tid); + expect(payload.imp[0].ext.ozone.transactionId).to.equal(validBidRequestsWithAuctionIdTransactionId[0].ortb2Imp.ext.tid); + config.resetConfig(); + }); + it('Batch request (flat array of single requests): should use ortb auction ID & transaction ID values if set (this will be the case when publisher opts in with config)', function() { + var specMock = utils.deepClone(spec); + specMock.propertyBag.whitelabel = null; + config.setConfig({'ozone': {'batchRequests': 3}}); + specMock.loadWhitelabelData(valid6BidRequestsWithAuctionIdTransactionId[0]); + const request = specMock.buildRequests(valid6BidRequestsWithAuctionIdTransactionId, validBidderRequest); // I don't look in the bidderRequest for this - there's no point + expect(request).to.be.an('Array'); + expect(request).to.have.lengthOf(2); + const payload = JSON.parse(request[0].data); + expect(payload.source.tid).to.equal(valid6BidRequestsWithAuctionIdTransactionId[0].ortb2.source.tid); + expect(payload.imp[0].ext.ozone.auctionId).to.equal(valid6BidRequestsWithAuctionIdTransactionId[0].ortb2.source.tid); + expect(payload.imp[0].ext.ozone.transactionId).to.equal(valid6BidRequestsWithAuctionIdTransactionId[0].ortb2Imp.ext.tid); + config.resetConfig(); + }); + it('should handle ortb2 device data', function () { + const bidderRequest = JSON.parse(JSON.stringify(validBidderRequest)); + bidderRequest.ortb2 = { + device: { + 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'}, + }, + }; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.device).to.deep.equal(bidderRequest.ortb2.device); + }); }); describe('interpretResponse', function () { beforeEach(function () { @@ -2396,6 +3340,14 @@ describe('ozone Adapter', function () { expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_flr')).to.equal(1); expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_rid')).to.equal('ZjbsYE1q'); }); + it('Alias venatus: should handle ext.bidder.venatus.floor correctly, setting flr & rid as necessary', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest); + let vres = JSON.parse(JSON.stringify(validResponse)); + vres.body.seatbid[0].bid[0].ext.bidder.ozone = {floor: 1, ruleId: 'ZjbsYE1q'}; + const result = spec.interpretResponse(vres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_flr')).to.equal(1); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_rid')).to.equal('ZjbsYE1q'); + }); it('should handle ext.bidder.ozone.floor correctly, inserting 0 as necessary', function () { const request = spec.buildRequests(validBidRequests, validBidderRequest); let vres = JSON.parse(JSON.stringify(validResponse)); @@ -2509,6 +3461,64 @@ describe('ozone Adapter', function () { const bid = result[0]; expect(bid.mediaType).to.equal('video'); }); + it('should handle fledge response', function () { + const req = spec.buildRequests(validBidRequests, validBidderRequest); + let objResp = JSON.parse(JSON.stringify(validResponse)); + objResp.body.ext = {igi: [{ + 'impid': '1', + 'igb': [{ + 'origin': 'https://paapi.dsp.com', + 'pbs': '{"key": "value"}' + }] + }]}; + const result = spec.interpretResponse(objResp, req); + expect(result).to.be.an('object'); + expect(result.fledgeAuctionConfigs[0]['impid']).to.equal('1'); + }); + it('should add labels in the adserver request if they are present in the auction response', function () { + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest); + let validres = JSON.parse(JSON.stringify(validResponse2Bids)); + validres.body.seatbid.push(JSON.parse(JSON.stringify(validres.body.seatbid[0]))); // add another bidder + validres.body.seatbid[1].seat = 'marktest'; + validres.body.seatbid[1].bid[0].ext.prebid.labels = ['b1', 'b2', 'b3']; + validres.body.seatbid[1].bid[0].price = 10; // will win + validres.body.seatbid[1].bid[1].price = 0; // will lose + validres.body.seatbid[0].bid[0].ext.prebid.labels = ['bid1label1', 'bid1label2', 'bid1label3']; + validres.body.seatbid[0].bid[1].ext.prebid.labels = ['bid2label']; + const result = spec.interpretResponse(validres, request); + expect(result.length).to.equal(4); // 4 bids will be returned; 2 from each bidder. All will have the winning keys attached. + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_winner')).to.equal('marktest'); // the first bid + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_labels')).to.equal('b1,b2,b3'); // the winner + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_labels')).to.equal('bid1label1,bid1label2,bid1label3'); + expect(utils.deepAccess(result[1].adserverTargeting, 'oz_winner')).to.equal('appnexus'); // the second bid + expect(utils.deepAccess(result[1].adserverTargeting, 'oz_appnexus_labels')).to.equal('bid2label'); + expect(utils.deepAccess(result[1].adserverTargeting, 'oz_labels')).to.equal('bid2label'); // the second adslot winning label + expect(utils.deepAccess(result[2].adserverTargeting, 'oz_labels')).to.equal('b1,b2,b3'); // we're back to the first of the 2 bids again + expect(utils.deepAccess(result[3].adserverTargeting, 'oz_labels')).to.equal('bid2label'); // the second adslot winning label + }); + it('should not add labels in the adserver request if they are present in the auction response when config contains ozone.enhancedAdserverTargeting', function () { + config.setConfig({'ozone': {'enhancedAdserverTargeting': false}}); + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest); + let validres = JSON.parse(JSON.stringify(validResponse2Bids)); + validres.body.seatbid.push(JSON.parse(JSON.stringify(validres.body.seatbid[0]))); // add another bidder + validres.body.seatbid[1].seat = 'marktest'; + validres.body.seatbid[1].bid[0].ext.prebid.labels = ['b1', 'b2', 'b3']; + validres.body.seatbid[1].bid[0].price = 10; // will win + validres.body.seatbid[1].bid[1].price = 0; // will lose + validres.body.seatbid[0].bid[0].ext.prebid.labels = ['bid1label1', 'bid1label2', 'bid1label3']; + validres.body.seatbid[0].bid[1].ext.prebid.labels = ['bid2label']; + const result = spec.interpretResponse(validres, request); + expect(result.length).to.equal(4); // 4 bids will be returned; 2 from each bidder. All will have the winning keys attached. + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_winner')).to.equal('marktest'); // the first bid + expect(result[0].adserverTargeting).to.not.have.property('oz_labels'); + expect(result[0].adserverTargeting).to.not.have.property('oz_appnexus_labels'); + expect(utils.deepAccess(result[1].adserverTargeting, 'oz_winner')).to.equal('appnexus'); // the second bid + expect(result[1].adserverTargeting).to.not.have.property('oz_appnexus_labels'); + expect(result[1].adserverTargeting).to.not.have.property('oz_labels'); // the second adslot winning label + expect(result[2].adserverTargeting).to.not.have.property('oz_labels'); // we're back to the first of the 2 bids again + expect(result[3].adserverTargeting).to.not.have.property('oz_labels'); // the second adslot winning label + config.resetConfig(); + }); }); describe('userSyncs', function () { it('should fail gracefully if no server response', function () { @@ -2540,6 +3550,10 @@ describe('ozone Adapter', function () { expect(result).to.be.an('array'); expect(result[0].url).to.include('usp_consent=&'); }); + it('should add gpp if its present', function () { + const result = spec.getUserSyncs({iframeEnabled: true}, 'good server response', gdpr1, '1---', { gppString: 'gppStringHere', applicableSections: [7, 8, 9] }); + expect(result[0].url).to.include('gpp=gppStringHere&gpp_sid=7,8,9'); + }); }); describe('video object utils', function () { it('should find width & height from video object', function () { @@ -2698,7 +3712,7 @@ describe('ozone Adapter', function () { }); }); describe('addVideoDefaults', function() { - it('should correctly add video defaults', function () { + it('should not add video defaults if there is no videoParams config', function () { let mediaTypes = { playerSize: [640, 480], mimes: ['video/mp4'], @@ -2715,12 +3729,14 @@ describe('ozone Adapter', function () { testKey: 'child value' }; let result = spec.addVideoDefaults({}, mediaTypes, mediaTypes); - expect(result.placement).to.equal(3); + expect(result.placement).to.be.undefined; expect(result.skip).to.equal(0); result = spec.addVideoDefaults({}, mediaTypes, bid_params_video); expect(result.skip).to.equal(1); }); - it('should correctly add video defaults including skippable in parent', function () { + it('should correctly add video defaults if page config videoParams is defined, also check skip in the parent', function () { + var specMock = utils.deepClone(spec); + specMock.propertyBag.whitelabel.videoParams = {outstream: 3, instream: 1}; let mediaTypes = { playerSize: [640, 480], mimes: ['video/mp4'], @@ -2736,7 +3752,7 @@ describe('ozone Adapter', function () { skipafter: 5, testKey: 'child value' }; - let result = spec.addVideoDefaults({}, mediaTypes, bid_params_video); + let result = specMock.addVideoDefaults({}, mediaTypes, bid_params_video); expect(result.placement).to.equal(3); expect(result.skip).to.equal(1); }); @@ -2772,6 +3788,7 @@ describe('ozone Adapter', function () { let testKey2 = 'ozone.singleRequest'; let markbidder_config2 = specMock.getWhitelabelConfigItem(testKey2); expect(markbidder_config2).to.equal('markbidder-singlerequest-value'); + config.resetConfig(); }); }); describe('setBidMediaTypeIfNotExist', function() { diff --git a/test/spec/modules/paapiForGpt_spec.js b/test/spec/modules/paapiForGpt_spec.js new file mode 100644 index 00000000000..9a6637f82aa --- /dev/null +++ b/test/spec/modules/paapiForGpt_spec.js @@ -0,0 +1,216 @@ +import { + getPAAPISizeHook, + onAuctionConfigFactory, + setPAAPIConfigFactory, setTargetingHookFactory, + slotConfigurator +} from 'modules/paapiForGpt.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; +import 'modules/appnexusBidAdapter.js'; +import 'modules/rubiconBidAdapter.js'; +import {deepSetValue} from '../../../src/utils.js'; +import {config} from 'src/config.js'; + +describe('paapiForGpt module', () => { + let sandbox, fledgeAuctionConfig; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + fledgeAuctionConfig = { + seller: 'bidder', + mock: 'config' + }; + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('slotConfigurator', () => { + let setGptConfig; + function mockGptSlot(auPath) { + return { + setConfig: sinon.stub(), + getAdUnitPath: () => auPath + } + } + beforeEach(() => { + setGptConfig = slotConfigurator(); + }); + + Object.entries({ + 'single slot': [mockGptSlot('mock/gpt/au')], + 'multiple slots': [mockGptSlot('mock/gpt/au'), mockGptSlot('mock/gpt/au2')] + }).forEach(([t, gptSlots]) => { + describe(`when ad unit code matches ${t}`, () => { + it('should set GPT slot config', () => { + setGptConfig('au', gptSlots, [fledgeAuctionConfig]); + gptSlots.forEach(slot => { + sinon.assert.calledWith(slot.setConfig, { + componentAuction: [{ + configKey: 'bidder', + auctionConfig: fledgeAuctionConfig, + }] + }); + }) + }); + describe('when reset = true', () => { + it('should reset GPT slot config', () => { + setGptConfig('au', gptSlots, [fledgeAuctionConfig]); + gptSlots.forEach(slot => slot.setConfig.resetHistory()); + setGptConfig('au', gptSlots, [], true); + gptSlots.forEach(slot => { + sinon.assert.calledWith(slot.setConfig, { + componentAuction: [{ + configKey: 'bidder', + auctionConfig: null + }] + }); + }) + }); + + it('should reset only sellers with no fresh config', () => { + setGptConfig('au', gptSlots, [{seller: 's1'}, {seller: 's2'}]); + gptSlots.forEach(slot => slot.setConfig.resetHistory()); + setGptConfig('au', gptSlots, [{seller: 's1'}], true); + gptSlots.forEach(slot => { + sinon.assert.calledWith(slot.setConfig, { + componentAuction: [{ + configKey: 's1', + auctionConfig: {seller: 's1'} + }, { + configKey: 's2', + auctionConfig: null + }] + }) + }) + }); + + it('should not reset sellers that were already reset', () => { + setGptConfig('au', gptSlots, [{seller: 's1'}]); + setGptConfig('au', gptSlots, [], true); + gptSlots.forEach(slot => slot.setConfig.resetHistory()); + setGptConfig('au', gptSlots, [], true); + gptSlots.forEach(slot => sinon.assert.notCalled(slot.setConfig)); + }) + + it('should keep track of configuration history by ad unit', () => { + setGptConfig('au1', gptSlots, [{seller: 's1'}]); + setGptConfig('au1', gptSlots, [{seller: 's2'}], false); + setGptConfig('au2', gptSlots, [{seller: 's3'}]); + gptSlots.forEach(slot => slot.setConfig.resetHistory()); + setGptConfig('au1', gptSlots, [], true); + gptSlots.forEach(slot => { + sinon.assert.calledWith(slot.setConfig, { + componentAuction: [{ + configKey: 's1', + auctionConfig: null + }, { + configKey: 's2', + auctionConfig: null + }] + }); + }) + }) + }); + }) + }) + }); + describe('setTargeting hook', () => { + let setPaapiConfig, setTargetingHook, next; + beforeEach(() => { + setPaapiConfig = sinon.stub() + setTargetingHook = setTargetingHookFactory(setPaapiConfig); + next = sinon.stub(); + }); + function expectFilters(...filters) { + expect(setPaapiConfig.args.length).to.eql(filters.length) + filters.forEach(filter => { + sinon.assert.calledWith(setPaapiConfig, filter, 'mock-matcher') + }) + } + function runHook(adUnit) { + setTargetingHook(next, adUnit, 'mock-matcher'); + sinon.assert.calledWith(next, adUnit, 'mock-matcher'); + } + it('should invoke with no filters when adUnit is undef', () => { + runHook(); + expectFilters(undefined); + }); + it('should invoke once when adUnit is a string', () => { + runHook('mock-au'); + expectFilters({adUnitCode: 'mock-au'}) + }); + it('should invoke once per ad unit when an array', () => { + runHook(['au1', 'au2']); + expectFilters({adUnitCode: 'au1'}, {adUnitCode: 'au2'}); + }) + }) + describe('setPAAPIConfigForGpt', () => { + let getPAAPIConfig, setGptConfig, getSlots, setPAAPIConfigForGPT; + beforeEach(() => { + getPAAPIConfig = sinon.stub(); + setGptConfig = sinon.stub(); + getSlots = sinon.stub().callsFake((codes) => Object.fromEntries(codes.map(code => [code, ['mock-slot']]))) + setPAAPIConfigForGPT = setPAAPIConfigFactory(getPAAPIConfig, setGptConfig, getSlots); + }); + + Object.entries({ + missing: null, + empty: {} + }).forEach(([t, configs]) => { + it(`does not set GPT slot config when config is ${t}`, () => { + getPAAPIConfig.returns(configs); + setPAAPIConfigForGPT('mock-filters'); + sinon.assert.calledWith(getPAAPIConfig, 'mock-filters'); + sinon.assert.notCalled(setGptConfig); + }) + }); + + it('passes customSlotMatching to getSlots', () => { + getPAAPIConfig.returns({au1: {}}); + setPAAPIConfigForGPT('mock-filters', 'mock-custom-matching'); + sinon.assert.calledWith(getSlots, ['au1'], 'mock-custom-matching'); + }) + + it('sets GPT slot config for each ad unit that has PAAPI config, and resets the rest', () => { + const cfg = { + au1: { + componentAuctions: [{seller: 's1'}, {seller: 's2'}] + }, + au2: { + componentAuctions: [{seller: 's3'}] + }, + au3: null + } + getPAAPIConfig.returns(cfg); + setPAAPIConfigForGPT('mock-filters'); + sinon.assert.calledWith(getPAAPIConfig, 'mock-filters'); + Object.entries(cfg).forEach(([au, config]) => { + sinon.assert.calledWith(setGptConfig, au, ['mock-slot'], config?.componentAuctions ?? [], true); + }) + }); + }); + + describe('getPAAPISizeHook', () => { + let next; + beforeEach(() => { + next = sinon.stub(); + next.bail = sinon.stub(); + }); + + it('should pick largest supported size over larger unsupported size', () => { + getPAAPISizeHook(next, [[999, 999], [300, 250], [300, 600], [1234, 4321]]); + sinon.assert.calledWith(next.bail, [300, 600]); + }); + + Object.entries({ + 'present': [], + 'supported': [[123, 4], [321, 5]], + 'defined': undefined, + }).forEach(([t, sizes]) => { + it(`should defer to next when no size is ${t}`, () => { + getPAAPISizeHook(next, sizes); + sinon.assert.calledWith(next, sizes); + }) + }) + }) +}); diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js index c7d6d88bd12..56e5449a524 100644 --- a/test/spec/modules/paapi_spec.js +++ b/test/spec/modules/paapi_spec.js @@ -2,24 +2,38 @@ import {expect} from 'chai'; import {config} from '../../../src/config.js'; import adapterManager from '../../../src/adapterManager.js'; import * as utils from '../../../src/utils.js'; +import {deepAccess, deepClone} from '../../../src/utils.js'; import {hook} from '../../../src/hook.js'; import 'modules/appnexusBidAdapter.js'; import 'modules/rubiconBidAdapter.js'; import { - addComponentAuctionHook, + adAuctionHeadersHook, + addPaapiConfigHook, + addPaapiData, + ASYNC_SIGNALS, AsyncPAAPIParam, buildPAAPIParams, + buyersToAuctionConfigs, getPAAPIConfig, + getPAAPISize, + IGB_TO_CONFIG, + mergeBuyers, NAVIGATOR_APIS, + onAuctionInit, + parallelPaapiProcessing, + parseExtIgi, parseExtPrebidFledge, + partitionBuyers, + partitionBuyersByBidder, registerSubmodule, + reset, setImpExtAe, - setResponseFledgeConfigs, - reset + setResponsePaapiConfigs } from 'modules/paapi.js'; import * as events from 'src/events.js'; -import { EVENTS } from 'src/constants.js'; +import {EVENTS} from 'src/constants.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {AuctionIndex} from '../../../src/auctionIndex.js'; +import {buildActivityParams} from '../../../src/activities/params.js'; describe('paapi module', () => { let sandbox; @@ -32,299 +46,425 @@ describe('paapi module', () => { reset(); }); - [ - 'fledgeForGpt', - 'paapi' - ].forEach(configNS => { - describe(`using ${configNS} for configuration`, () => { - describe('getPAAPIConfig', function () { - let nextFnSpy, fledgeAuctionConfig; - before(() => { - config.setConfig({[configNS]: {enabled: true}}); + describe(`using paapi configuration`, () => { + let getPAAPISizeStub; + + function getPAAPISizeHook(next, sizes) { + next.bail(getPAAPISizeStub(sizes)); + } + + before(() => { + getPAAPISize.before(getPAAPISizeHook, 100); + }); + + after(() => { + getPAAPISize.getHooks({hook: getPAAPISizeHook}).remove(); + }); + + beforeEach(() => { + getPAAPISizeStub = sinon.stub(); + }); + + describe('adAuctionHeadersHook', () => { + let bidderRequest, ajax; + beforeEach(() => { + ajax = sinon.stub(); + bidderRequest = {paapi: {}} + }) + function getWrappedAjax() { + let wrappedAjax; + let next = sinon.stub().callsFake((spec, bids, br, ajax) => { + wrappedAjax = ajax; }); + adAuctionHeadersHook(next, {}, [], bidderRequest, ajax); + return wrappedAjax; + } + describe('when PAAPI is enabled', () => { beforeEach(() => { - fledgeAuctionConfig = { - seller: 'bidder', - mock: 'config' - }; - nextFnSpy = sinon.spy(); + bidderRequest.paapi.enabled = true; + }); + [ + undefined, + {}, + {adAuctionHeaders: true} + ].forEach(options => + it(`should set adAuctionHeaders = true (when options are ${JSON.stringify(options)})`, () => { + getWrappedAjax()('url', {}, 'data', options); + sinon.assert.calledWith(ajax, 'url', {}, 'data', sinon.match({adAuctionHeaders: true})); + })); + + it('should respect adAuctionHeaders: false', () => { + getWrappedAjax()('url', {}, 'data', {adAuctionHeaders: false}); + sinon.assert.calledWith(ajax, 'url', {}, 'data', sinon.match({adAuctionHeaders: false})); + }) + }); + it('should not alter ajax when paapi is not enabled', () => { + expect(getWrappedAjax()).to.equal(ajax); + }) + }) + + describe('getPAAPIConfig', function () { + let nextFnSpy, auctionConfig, paapiConfig; + before(() => { + config.setConfig({paapi: {enabled: true}}); + }); + beforeEach(() => { + auctionConfig = { + seller: 'bidder', + mock: 'config' + }; + paapiConfig = { + config: auctionConfig + }; + nextFnSpy = sinon.spy(); + }); + + describe('on a single auction', function () { + const auctionId = 'aid'; + beforeEach(function () { + sandbox.stub(auctionManager, 'index').value(stubAuctionIndex({auctionId})); }); - describe('on a single auction', function () { - const auctionId = 'aid'; - beforeEach(function () { - sandbox.stub(auctionManager, 'index').value(stubAuctionIndex({auctionId})); + it('should call next()', function () { + const request = {auctionId, adUnitCode: 'auc'}; + addPaapiConfigHook(nextFnSpy, request, paapiConfig); + sinon.assert.calledWith(nextFnSpy, request, paapiConfig); + }); + + describe('igb', () => { + let igb1, igb2, buyerAuctionConfig; + beforeEach(() => { + igb1 = { + origin: 'buyer.1' + }; + igb2 = { + origin: 'buyer.2' + }; + buyerAuctionConfig = { + seller: 'seller', + decisionLogicURL: 'seller-decision-logic' + }; + config.mergeConfig({ + paapi: { + componentSeller: { + auctionConfig: buyerAuctionConfig + } + } + }); }); - it('should call next()', function () { - const request = {auctionId, adUnitCode: 'auc'}; - addComponentAuctionHook(nextFnSpy, request, fledgeAuctionConfig); - sinon.assert.calledWith(nextFnSpy, request, fledgeAuctionConfig); + function addIgb(request, igb) { + addPaapiConfigHook(nextFnSpy, Object.assign({auctionId}, request), {igb}); + } + + it('should be collected into an auction config', () => { + addIgb({adUnitCode: 'au1'}, igb1); + addIgb({adUnitCode: 'au1'}, igb2); + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']}); + const buyerConfig = getPAAPIConfig({auctionId}).au1.componentAuctions[0]; + sinon.assert.match(buyerConfig, { + interestGroupBuyers: [igb1.origin, igb2.origin], + ...buyerAuctionConfig + }); }); - describe('should collect auction configs', () => { - let cf1, cf2; + describe('FPD', () => { + let ortb2, ortb2Imp; beforeEach(() => { - cf1 = {...fledgeAuctionConfig, id: 1, seller: 'b1'}; - cf2 = {...fledgeAuctionConfig, id: 2, seller: 'b2'}; - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, cf1); - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au2'}, cf2); - events.emit(EVENTS.AUCTION_END, { auctionId, adUnitCodes: ['au1', 'au2', 'au3'] }); + ortb2 = {'fpd': 1}; + ortb2Imp = {'fpd': 2}; }); - it('and make them available at end of auction', () => { - sinon.assert.match(getPAAPIConfig({auctionId}), { - au1: { - componentAuctions: [cf1] - }, - au2: { - componentAuctions: [cf2] + function getBuyerAuctionConfig() { + addIgb({adUnitCode: 'au1', ortb2, ortb2Imp}, igb1); + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']}); + return getPAAPIConfig({auctionId}).au1.componentAuctions[0]; + } + + it('should be added to auction config', () => { + sinon.assert.match(getBuyerAuctionConfig().perBuyerSignals[igb1.origin], { + prebid: { + ortb2, + ortb2Imp } }); }); - it('and filter them by ad unit', () => { - const cfg = getPAAPIConfig({auctionId, adUnitCode: 'au1'}); - expect(Object.keys(cfg)).to.have.members(['au1']); - sinon.assert.match(cfg.au1, { - componentAuctions: [cf1] + it('should not override existing perBuyerSignals', () => { + const original = { + ortb2: { + fpd: 'original' + } + }; + igb1.pbs = { + prebid: deepClone(original) + }; + sinon.assert.match(getBuyerAuctionConfig().perBuyerSignals[igb1.origin], { + prebid: original }); }); + }); + }); + + describe('should collect auction configs', () => { + let cf1, cf2; + beforeEach(() => { + cf1 = {...auctionConfig, id: 1, seller: 'b1'}; + cf2 = {...auctionConfig, id: 2, seller: 'b2'}; + addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, {config: cf1}); + addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au2'}, {config: cf2}); + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1', 'au2', 'au3']}); + }); - it('and not return them again', () => { - getPAAPIConfig(); - const cfg = getPAAPIConfig(); - expect(cfg).to.eql({}); + it('and make them available at end of auction', () => { + sinon.assert.match(getPAAPIConfig({auctionId}), { + au1: { + componentAuctions: [cf1] + }, + au2: { + componentAuctions: [cf2] + } }); + }); - describe('includeBlanks = true', () => { - it('includes all ad units', () => { - const cfg = getPAAPIConfig({}, true); - expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']); - expect(cfg.au3).to.eql(null); - }) - it('includes the targeted adUnit', () => { - expect(getPAAPIConfig({adUnitCode: 'au3'}, true)).to.eql({ - au3: null - }) - }); - it('includes the targeted auction', () => { - const cfg = getPAAPIConfig({auctionId}, true); - expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']); - expect(cfg.au3).to.eql(null); - }); - it('does not include non-existing ad units', () => { - expect(getPAAPIConfig({adUnitCode: 'other'})).to.eql({}); - }); - it('does not include non-existing auctions', () => { - expect(getPAAPIConfig({auctionId: 'other'})).to.eql({}); - }) + it('and filter them by ad unit', () => { + const cfg = getPAAPIConfig({auctionId, adUnitCode: 'au1'}); + expect(Object.keys(cfg)).to.have.members(['au1']); + sinon.assert.match(cfg.au1, { + componentAuctions: [cf1] }); }); - it('should drop auction configs after end of auction', () => { - events.emit(EVENTS.AUCTION_END, { auctionId }); - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, fledgeAuctionConfig); - events.emit(EVENTS.AUCTION_END, { auctionId }); - expect(getPAAPIConfig({auctionId})).to.eql({}); + it('and not return them again', () => { + getPAAPIConfig(); + const cfg = getPAAPIConfig(); + expect(cfg).to.eql({}); }); - it('should use first size as requestedSize', () => { - addComponentAuctionHook(nextFnSpy, { - auctionId, - adUnitCode: 'au1', - }, fledgeAuctionConfig); - events.emit(EVENTS.AUCTION_END, { - auctionId, - adUnits: [ - { - code: 'au1', - mediaTypes: { - banner: { - sizes: [[200, 100], [300, 200]] - } - } - } - ] + describe('includeBlanks = true', () => { + it('includes all ad units', () => { + const cfg = getPAAPIConfig({}, true); + expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']); + expect(cfg.au3).to.eql(null); }); - expect(getPAAPIConfig({auctionId}).au1.requestedSize).to.eql({ - width: '200', - height: '100' - }) - }) + it('includes the targeted adUnit', () => { + expect(getPAAPIConfig({adUnitCode: 'au3'}, true)).to.eql({ + au3: null + }); + }); + it('includes the targeted auction', () => { + const cfg = getPAAPIConfig({auctionId}, true); + expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']); + expect(cfg.au3).to.eql(null); + }); + it('does not include non-existing ad units', () => { + expect(getPAAPIConfig({adUnitCode: 'other'})).to.eql({}); + }); + it('does not include non-existing auctions', () => { + expect(getPAAPIConfig({auctionId: 'other'})).to.eql({}); + }); + }); + }); + + it('should drop auction configs after end of auction', () => { + events.emit(EVENTS.AUCTION_END, {auctionId}); + addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, paapiConfig); + expect(getPAAPIConfig({auctionId})).to.eql({}); + }); + + describe('FPD', () => { + let ortb2, ortb2Imp; + beforeEach(() => { + ortb2 = {fpd: 1}; + ortb2Imp = {fpd: 2}; + }); - it('should augment auctionSignals with FPD', () => { - addComponentAuctionHook(nextFnSpy, { + function getComponentAuctionConfig() { + addPaapiConfigHook(nextFnSpy, { auctionId, adUnitCode: 'au1', ortb2: {fpd: 1}, ortb2Imp: {fpd: 2} - }, fledgeAuctionConfig); - events.emit(EVENTS.AUCTION_END, { auctionId }); - sinon.assert.match(getPAAPIConfig({auctionId}), { - au1: { - componentAuctions: [{ - ...fledgeAuctionConfig, - auctionSignals: { - prebid: { - ortb2: {fpd: 1}, - ortb2Imp: {fpd: 2} - } - } - }] + }, paapiConfig); + events.emit(EVENTS.AUCTION_END, {auctionId}); + return getPAAPIConfig({auctionId}).au1.componentAuctions[0]; + } + + it('should be added to auctionSignals', () => { + sinon.assert.match(getComponentAuctionConfig().auctionSignals, { + prebid: {ortb2, ortb2Imp} + }); + }); + it('should not override existing auctionSignals', () => { + auctionConfig.auctionSignals = {prebid: {ortb2: {fpd: 'original'}}}; + sinon.assert.match(getComponentAuctionConfig().auctionSignals, { + prebid: { + ortb2: {fpd: 'original'}, + ortb2Imp } }); }); - describe('submodules', () => { - let submods; - beforeEach(() => { - submods = [1, 2].map(i => ({ - name: `test${i}`, - onAuctionConfig: sinon.stub() - })); - submods.forEach(registerSubmodule); - }); - - describe('onAuctionConfig', () => { - const auctionId = 'aid'; - it('is invoked with null configs when there\'s no config', () => { - events.emit(EVENTS.AUCTION_END, { auctionId, adUnitCodes: ['au'] }); - submods.forEach(submod => sinon.assert.calledWith(submod.onAuctionConfig, auctionId, {au: null})); - }); - it('is invoked with relevant configs', () => { - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au2'}, fledgeAuctionConfig); - events.emit(EVENTS.AUCTION_END, { auctionId, adUnitCodes: ['au1', 'au2', 'au3'] }); - submods.forEach(submod => { - sinon.assert.calledWith(submod.onAuctionConfig, auctionId, { - au1: {componentAuctions: [fledgeAuctionConfig]}, - au2: {componentAuctions: [fledgeAuctionConfig]}, - au3: null - }) - }); - }); - it('removes configs from getPAAPIConfig if the module calls markAsUsed', () => { - submods[0].onAuctionConfig.callsFake((auctionId, configs, markAsUsed) => { - markAsUsed('au1'); - }); - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); - events.emit(EVENTS.AUCTION_END, { auctionId, adUnitCodes: ['au1'] }); - expect(getPAAPIConfig()).to.eql({}); - }); - it('keeps them available if they do not', () => { - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); - events.emit(EVENTS.AUCTION_END, { auctionId, adUnitCodes: ['au1'] }); - expect(getPAAPIConfig()).to.not.be.empty; - }) + it('should be added to perBuyerSignals', () => { + auctionConfig.interestGroupBuyers = ['buyer.1', 'buyer.2']; + const pbs = getComponentAuctionConfig().perBuyerSignals; + sinon.assert.match(pbs, { + 'buyer.1': {prebid: {ortb2, ortb2Imp}}, + 'buyer.2': {prebid: {ortb2, ortb2Imp}} }); }); - describe('floor signal', () => { - before(() => { - if (!getGlobal().convertCurrency) { - getGlobal().convertCurrency = () => null; - getGlobal().convertCurrency.mock = true; + it('should not override existing perBuyerSignals', () => { + auctionConfig.interestGroupBuyers = ['buyer']; + const original = { + prebid: { + ortb2: { + fpd: 'original' + } } + }; + auctionConfig.perBuyerSignals = { + buyer: deepClone(original) + }; + sinon.assert.match(getComponentAuctionConfig().perBuyerSignals.buyer, original); + }); + }); + + describe('submodules', () => { + let submods; + beforeEach(() => { + submods = [1, 2].map(i => ({ + name: `test${i}`, + onAuctionConfig: sinon.stub() + })); + submods.forEach(registerSubmodule); + }); + + describe('onAuctionConfig', () => { + const auctionId = 'aid'; + it('is invoked with null configs when there\'s no config', () => { + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au']}); + submods.forEach(submod => sinon.assert.calledWith(submod.onAuctionConfig, auctionId, {au: null})); }); - after(() => { - if (getGlobal().convertCurrency.mock) { - delete getGlobal().convertCurrency; - } + it('is invoked with relevant configs', () => { + addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, paapiConfig); + addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au2'}, paapiConfig); + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1', 'au2', 'au3']}); + submods.forEach(submod => { + sinon.assert.calledWith(submod.onAuctionConfig, auctionId, { + au1: {componentAuctions: [auctionConfig]}, + au2: {componentAuctions: [auctionConfig]}, + au3: null + }); + }); }); + }); + }); - beforeEach(() => { - sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amount, from, to) => { - if (from === to) return amount; - if (from === 'USD' && to === 'JPY') return amount * 100; - if (from === 'JPY' && to === 'USD') return amount / 100; - throw new Error('unexpected currency conversion'); - }); + describe('floor signal', () => { + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }); + + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amount, from, to) => { + if (from === to) return amount; + if (from === 'USD' && to === 'JPY') return amount * 100; + if (from === 'JPY' && to === 'USD') return amount / 100; + throw new Error('unexpected currency conversion'); }); + }); - Object.entries({ - 'bids': (payload, values) => { - payload.bidsReceived = values - .map((val) => ({adUnitCode: 'au', cpm: val.amount, currency: val.cur})) - .concat([{adUnitCode: 'other', cpm: 10000, currency: 'EUR'}]); - }, - 'no bids': (payload, values) => { - payload.bidderRequests = values - .map((val) => ({ - bids: [{ - adUnitCode: 'au', - getFloor: () => ({floor: val.amount, currency: val.cur}) - }] - })) - .concat([{bids: {adUnitCode: 'other', getFloor: () => ({floor: -10000, currency: 'EUR'})}}]); - } - }).forEach(([tcase, setup]) => { - describe(`when auction has ${tcase}`, () => { - Object.entries({ - 'no currencies': { - values: [{amount: 1}, {amount: 100}, {amount: 10}, {amount: 100}], - 'bids': { - bidfloor: 100, - bidfloorcur: undefined - }, - 'no bids': { - bidfloor: 1, - bidfloorcur: undefined, - } + Object.entries({ + 'bids': (payload, values) => { + payload.bidsReceived = values + .map((val) => ({adUnitCode: 'au', cpm: val.amount, currency: val.cur})) + .concat([{adUnitCode: 'other', cpm: 10000, currency: 'EUR'}]); + }, + 'no bids': (payload, values) => { + payload.bidderRequests = values + .map((val) => ({ + bids: [{ + adUnitCode: 'au', + getFloor: () => ({floor: val.amount, currency: val.cur}) + }] + })) + .concat([{bids: {adUnitCode: 'other', getFloor: () => ({floor: -10000, currency: 'EUR'})}}]); + } + }).forEach(([tcase, setup]) => { + describe(`when auction has ${tcase}`, () => { + Object.entries({ + 'no currencies': { + values: [{amount: 1}, {amount: 100}, {amount: 10}, {amount: 100}], + 'bids': { + bidfloor: 100, + bidfloorcur: undefined }, - 'only zero values': { - values: [{amount: 0, cur: 'USD'}, {amount: 0, cur: 'JPY'}], - 'bids': { - bidfloor: undefined, - bidfloorcur: undefined, - }, - 'no bids': { - bidfloor: undefined, - bidfloorcur: undefined, - } + 'no bids': { + bidfloor: 1, + bidfloorcur: undefined, + } + }, + 'only zero values': { + values: [{amount: 0, cur: 'USD'}, {amount: 0, cur: 'JPY'}], + 'bids': { + bidfloor: undefined, + bidfloorcur: undefined, }, - 'matching currencies': { - values: [{amount: 10, cur: 'JPY'}, {amount: 100, cur: 'JPY'}], - 'bids': { - bidfloor: 100, - bidfloorcur: 'JPY', - }, - 'no bids': { - bidfloor: 10, - bidfloorcur: 'JPY', - } + 'no bids': { + bidfloor: undefined, + bidfloorcur: undefined, + } + }, + 'matching currencies': { + values: [{amount: 10, cur: 'JPY'}, {amount: 100, cur: 'JPY'}], + 'bids': { + bidfloor: 100, + bidfloorcur: 'JPY', }, - 'mixed currencies': { - values: [{amount: 10, cur: 'USD'}, {amount: 10, cur: 'JPY'}], - 'bids': { - bidfloor: 10, - bidfloorcur: 'USD' - }, - 'no bids': { - bidfloor: 10, - bidfloorcur: 'JPY', - } + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + }, + 'mixed currencies': { + values: [{amount: 10, cur: 'USD'}, {amount: 10, cur: 'JPY'}], + 'bids': { + bidfloor: 10, + bidfloorcur: 'USD' + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', } - }).forEach(([t, testConfig]) => { - const values = testConfig.values; - const {bidfloor, bidfloorcur} = testConfig[tcase]; - - describe(`with ${t}`, () => { - let payload; - beforeEach(() => { - payload = {auctionId}; - setup(payload, values); - }); - - it('should populate bidfloor/bidfloorcur', () => { - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, fledgeAuctionConfig); - events.emit(EVENTS.AUCTION_END, payload); - const cfg = getPAAPIConfig({auctionId}).au; - const signals = cfg.auctionSignals; - sinon.assert.match(cfg.componentAuctions[0].auctionSignals, signals || {}); - expect(signals?.prebid?.bidfloor).to.eql(bidfloor); - expect(signals?.prebid?.bidfloorcur).to.eql(bidfloorcur); - }); + } + }).forEach(([t, testConfig]) => { + const values = testConfig.values; + const {bidfloor, bidfloorcur} = testConfig[tcase]; + + describe(`with ${t}`, () => { + let payload; + beforeEach(() => { + payload = {auctionId}; + setup(payload, values); + }); + + it('should populate bidfloor/bidfloorcur', () => { + addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, paapiConfig); + events.emit(EVENTS.AUCTION_END, payload); + const cfg = getPAAPIConfig({auctionId}).au; + const signals = cfg.auctionSignals; + sinon.assert.match(cfg.componentAuctions[0].auctionSignals, signals || {}); + expect(signals?.prebid?.bidfloor).to.eql(bidfloor); + expect(signals?.prebid?.bidfloorcur).to.eql(bidfloorcur); }); }); }); @@ -332,136 +472,195 @@ describe('paapi module', () => { }); }); - describe('with multiple auctions', () => { - const AUCTION1 = 'auction1'; - const AUCTION2 = 'auction2'; - - function mockAuction(auctionId) { - return { - getAuctionId() { - return auctionId; - } + describe('requestedSize', () => { + let adUnit; + beforeEach(() => { + adUnit = { + code: 'au', }; - } + }); - function expectAdUnitsFromAuctions(actualConfig, auToAuctionMap) { - expect(Object.keys(actualConfig)).to.have.members(Object.keys(auToAuctionMap)); - Object.entries(actualConfig).forEach(([au, cfg]) => { - cfg.componentAuctions.forEach(cmp => expect(cmp.auctionId).to.eql(auToAuctionMap[au])); - }); + function getConfig() { + addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: adUnit.code}, paapiConfig); + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: [adUnit.code], adUnits: [adUnit]}); + return getPAAPIConfig()[adUnit.code]; } - let configs; - beforeEach(() => { - const mockAuctions = [mockAuction(AUCTION1), mockAuction(AUCTION2)]; - sandbox.stub(auctionManager, 'index').value(new AuctionIndex(() => mockAuctions)); - configs = {[AUCTION1]: {}, [AUCTION2]: {}}; - Object.entries({ - [AUCTION1]: [['au1', 'au2'], ['missing-1']], - [AUCTION2]: [['au2', 'au3'], []], - }).forEach(([auctionId, [adUnitCodes, noConfigAdUnitCodes]]) => { - adUnitCodes.forEach(adUnitCode => { - const cfg = {...fledgeAuctionConfig, auctionId, adUnitCode}; - configs[auctionId][adUnitCode] = cfg; - addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode}, cfg); + Object.entries({ + 'adUnit.ortb2Imp.ext.paapi.requestedSize'() { + adUnit.ortb2Imp = { + ext: { + paapi: { + requestedSize: { + width: 123, + height: 321 + } + } + } + }; + }, + 'largest size'() { + getPAAPISizeStub.returns([123, 321]); + } + }).forEach(([t, setup]) => { + describe(`should be set from ${t}`, () => { + beforeEach(setup); + + it('without overriding component auctions, if set', () => { + auctionConfig.requestedSize = {width: '1px', height: '2px'}; + expect(getConfig().componentAuctions[0].requestedSize).to.eql({ + width: '1px', + height: '2px' + }); + }); + + it('on component auction, if missing', () => { + expect(getConfig().componentAuctions[0].requestedSize).to.eql({ + width: 123, + height: 321 + }); + }); + + it('on top level auction', () => { + expect(getConfig().requestedSize).to.eql({ + width: 123, + height: 321, + }); }); - events.emit(EVENTS.AUCTION_END, { auctionId, adUnitCodes: adUnitCodes.concat(noConfigAdUnitCodes) }); }); }); + }); + }); - it('should filter by auction', () => { - expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1}), {au1: AUCTION1, au2: AUCTION1}); - expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION2}), {au2: AUCTION2, au3: AUCTION2}); - }); + describe('with multiple auctions', () => { + const AUCTION1 = 'auction1'; + const AUCTION2 = 'auction2'; - it('should filter by auction and ad unit', () => { - expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1, adUnitCode: 'au2'}), {au2: AUCTION1}); - expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION2, adUnitCode: 'au2'}), {au2: AUCTION2}); - }); + function mockAuction(auctionId) { + return { + getAuctionId() { + return auctionId; + } + }; + } - it('should use last auction for each ad unit', () => { - expectAdUnitsFromAuctions(getPAAPIConfig(), {au1: AUCTION1, au2: AUCTION2, au3: AUCTION2}); + function expectAdUnitsFromAuctions(actualConfig, auToAuctionMap) { + expect(Object.keys(actualConfig)).to.have.members(Object.keys(auToAuctionMap)); + Object.entries(actualConfig).forEach(([au, cfg]) => { + cfg.componentAuctions.forEach(cmp => expect(cmp.auctionId).to.eql(auToAuctionMap[au])); }); + } - it('should filter by ad unit and use latest auction', () => { - expectAdUnitsFromAuctions(getPAAPIConfig({adUnitCode: 'au2'}), {au2: AUCTION2}); + let configs; + beforeEach(() => { + const mockAuctions = [mockAuction(AUCTION1), mockAuction(AUCTION2)]; + sandbox.stub(auctionManager, 'index').value(new AuctionIndex(() => mockAuctions)); + configs = {[AUCTION1]: {}, [AUCTION2]: {}}; + Object.entries({ + [AUCTION1]: [['au1', 'au2'], ['missing-1']], + [AUCTION2]: [['au2', 'au3'], []], + }).forEach(([auctionId, [adUnitCodes, noConfigAdUnitCodes]]) => { + adUnitCodes.forEach(adUnitCode => { + const cfg = {...auctionConfig, auctionId, adUnitCode}; + configs[auctionId][adUnitCode] = cfg; + addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode}, {config: cfg}); + }); + events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: adUnitCodes.concat(noConfigAdUnitCodes)}); }); + }); - it('should keep track of which configs were returned', () => { - expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1}), {au1: AUCTION1, au2: AUCTION1}); - expect(getPAAPIConfig({auctionId: AUCTION1})).to.eql({}); - expectAdUnitsFromAuctions(getPAAPIConfig(), {au2: AUCTION2, au3: AUCTION2}); - }); + it('should filter by auction', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1}), {au1: AUCTION1, au2: AUCTION1}); + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION2}), {au2: AUCTION2, au3: AUCTION2}); + }); - describe('includeBlanks = true', () => { - Object.entries({ - 'auction with blanks': { - filters: {auctionId: AUCTION1}, - expected: {au1: true, au2: true, 'missing-1': false} - }, - 'blank adUnit in an auction': { - filters: {auctionId: AUCTION1, adUnitCode: 'missing-1'}, - expected: {'missing-1': false} - }, - 'non-existing auction': { - filters: {auctionId: 'other'}, - expected: {} - }, - 'non-existing adUnit in an auction': { - filters: {auctionId: AUCTION2, adUnitCode: 'other'}, - expected: {} - }, - 'non-existing ad unit': { - filters: {adUnitCode: 'other'}, - expected: {}, - }, - 'non existing ad unit in a non-existing auction': { - filters: {adUnitCode: 'other', auctionId: 'other'}, - expected: {} - }, - 'all ad units': { - filters: {}, - expected: {'au1': true, 'au2': true, 'missing-1': false, 'au3': true} - } - }).forEach(([t, {filters, expected}]) => { - it(t, () => { - const cfg = getPAAPIConfig(filters, true); - expect(Object.keys(cfg)).to.have.members(Object.keys(expected)); - Object.entries(expected).forEach(([au, shouldBeFilled]) => { - if (shouldBeFilled) { - expect(cfg[au]).to.not.be.null; - } else { - expect(cfg[au]).to.be.null; - } - }) - }) - }) - }); + it('should filter by auction and ad unit', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1, adUnitCode: 'au2'}), {au2: AUCTION1}); + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION2, adUnitCode: 'au2'}), {au2: AUCTION2}); }); - }); - describe('markForFledge', function () { - const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); + it('should use last auction for each ad unit', () => { + expectAdUnitsFromAuctions(getPAAPIConfig(), {au1: AUCTION1, au2: AUCTION2, au3: AUCTION2}); + }); - before(function () { - // navigator.runAdAuction & co may not exist, so we can't stub it normally with - // sinon.stub(navigator, 'runAdAuction') or something - Object.keys(navProps).forEach(p => { - navigator[p] = sinon.stub(); - }); - hook.ready(); - config.resetConfig(); + it('should filter by ad unit and use latest auction', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({adUnitCode: 'au2'}), {au2: AUCTION2}); + }); + + it('should keep track of which configs were returned', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1}), {au1: AUCTION1, au2: AUCTION1}); + expect(getPAAPIConfig({auctionId: AUCTION1})).to.eql({}); + expectAdUnitsFromAuctions(getPAAPIConfig(), {au2: AUCTION2, au3: AUCTION2}); }); - after(function () { - Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); + describe('includeBlanks = true', () => { + Object.entries({ + 'auction with blanks': { + filters: {auctionId: AUCTION1}, + expected: {au1: true, au2: true, 'missing-1': false} + }, + 'blank adUnit in an auction': { + filters: {auctionId: AUCTION1, adUnitCode: 'missing-1'}, + expected: {'missing-1': false} + }, + 'non-existing auction': { + filters: {auctionId: 'other'}, + expected: {} + }, + 'non-existing adUnit in an auction': { + filters: {auctionId: AUCTION2, adUnitCode: 'other'}, + expected: {} + }, + 'non-existing ad unit': { + filters: {adUnitCode: 'other'}, + expected: {}, + }, + 'non existing ad unit in a non-existing auction': { + filters: {adUnitCode: 'other', auctionId: 'other'}, + expected: {} + }, + 'all ad units': { + filters: {}, + expected: {'au1': true, 'au2': true, 'missing-1': false, 'au3': true} + } + }).forEach(([t, {filters, expected}]) => { + it(t, () => { + const cfg = getPAAPIConfig(filters, true); + expect(Object.keys(cfg)).to.have.members(Object.keys(expected)); + Object.entries(expected).forEach(([au, shouldBeFilled]) => { + if (shouldBeFilled) { + expect(cfg[au]).to.not.be.null; + } else { + expect(cfg[au]).to.be.null; + } + }); + }); + }); }); + }); + }); + + describe('markForFledge', function () { + const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); + let adUnits; - afterEach(function () { - config.resetConfig(); + before(function () { + // navigator.runAdAuction & co may not exist, so we can't stub it normally with + // sinon.stub(navigator, 'runAdAuction') or something + Object.keys(navProps).forEach(p => { + navigator[p] = sinon.stub(); }); + hook.ready(); + config.resetConfig(); + }); + + after(function () { + Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); + }); - const adUnits = [{ + beforeEach(() => { + getPAAPISizeStub = sinon.stub(); + adUnits = [{ 'code': '/19968336/header-bid-tag1', 'mediaTypes': { 'banner': { @@ -477,9 +676,31 @@ describe('paapi module', () => { }, ] }]; + }); + + afterEach(function () { + config.resetConfig(); + }); + + describe('makeBidRequests', () => { + before(() => { + NAVIGATOR_APIS.forEach(method => { + if (navigator[method] == null) { + navigator[method] = () => null; + after(() => { + delete navigator[method]; + }) + } + }) + }); + beforeEach(() => { + NAVIGATOR_APIS.forEach(method => { + sandbox.stub(navigator, method) + }) + }); - function expectFledgeFlags(...enableFlags) { - const bidRequests = Object.fromEntries( + function mark() { + return Object.fromEntries( adapterManager.makeBidRequests( adUnits, Date.now(), @@ -489,160 +710,1284 @@ describe('paapi module', () => { [] ).map(b => [b.bidderCode, b]) ); + } + + async function testAsyncParams(bidderRequest) { + for (const method of NAVIGATOR_APIS) { + navigator[method].returns('result'); + expect(await bidderRequest.paapi[method]('arg').resolve()).to.eql('result'); + sinon.assert.calledWith(navigator[method], 'arg'); + } + } - expect(bidRequests.appnexus.fledgeEnabled).to.eql(enableFlags[0].enabled); + async function expectFledgeFlags(...enableFlags) { + const bidRequests = mark(); + expect(bidRequests.appnexus.paapi?.enabled).to.eql(enableFlags[0].enabled); + if (bidRequests.appnexus.paapi?.enabled) { + await testAsyncParams(bidRequests.appnexus) + } bidRequests.appnexus.bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)); - expect(bidRequests.rubicon.fledgeEnabled).to.eql(enableFlags[1].enabled); + expect(bidRequests.rubicon.paapi?.enabled).to.eql(enableFlags[1].enabled); + if (bidRequests.rubicon.paapi?.enabled) { + testAsyncParams(bidRequests.rubicon); + } + bidRequests.rubicon.bids.forEach(bid => expect(bid.ortb2Imp?.ext?.ae).to.eql(enableFlags[1].ae)); - } - describe('with setBidderConfig()', () => { - it('should set fledgeEnabled correctly per bidder', function () { - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - defaultForSlots: 1, - fledgeEnabled: true - } - }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); + Object.values(bidRequests).flatMap(req => req.bids).forEach(bid => { + if (bid.ortb2Imp?.ext?.ae) { + sinon.assert.match(bid.ortb2Imp.ext.igs, { + ae: bid.ortb2Imp.ext.ae, + biddable: 1 + }); + } }); - }); + } describe('with setConfig()', () => { - it('should set fledgeEnabled correctly per bidder', function () { + it('should set paapi.enabled correctly per bidder', async function () { config.setConfig({ bidderSequence: 'fixed', - [configNS]: { + paapi: { enabled: true, bidders: ['appnexus'], defaultForSlots: 1, } }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: false, ae: undefined}); + await expectFledgeFlags({enabled: true, ae: 1}, {enabled: false, ae: 0}); }); - it('should set fledgeEnabled correctly for all bidders', function () { + it('should set paapi.enabled correctly for all bidders', async function () { config.setConfig({ bidderSequence: 'fixed', - [configNS]: { + paapi: { enabled: true, defaultForSlots: 1, } }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); + await expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); }); - it('should not override pub-defined ext.ae', () => { - config.setConfig({ - bidderSequence: 'fixed', - [configNS]: { - enabled: true, - defaultForSlots: 1, - } + Object.entries({ + 'not set': { + cfg: {}, + componentSeller: false + }, + 'set': { + cfg: { + componentSeller: { + auctionConfig: { + decisionLogicURL: 'publisher.example' + } + } + }, + componentSeller: true + } + }).forEach(([t, {cfg, componentSeller}]) => { + it(`should set request paapi.componentSeller = ${componentSeller} when config componentSeller is ${t}`, () => { + config.setConfig({ + paapi: { + enabled: true, + defaultForSlots: 1, + ...cfg + } + }); + Object.values(mark()).forEach(br => expect(br.paapi?.componentSeller).to.eql(componentSeller)); }); - Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 0}}}); - expectFledgeFlags({enabled: true, ae: 0}, {enabled: true, ae: 0}); }); }); }); - }); - }); + describe('addPaapiData', () => { + function getEnrichedAdUnits() { + const next = sinon.stub(); + addPaapiData(next, adUnits); + sinon.assert.calledWith(next, adUnits); + return adUnits; + } - describe('ortb processors for fledge', () => { - it('imp.ext.ae should be removed if fledge is not enabled', () => { - const imp = {ext: {ae: 1}}; - setImpExtAe(imp, {}, {bidderRequest: {}}); - expect(imp.ext.ae).to.not.exist; - }); - it('imp.ext.ae should be left intact if fledge is enabled', () => { - const imp = {ext: {ae: 2}}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); - expect(imp.ext.ae).to.equal(2); + function getImpExt() { + const next = sinon.stub(); + addPaapiData(next, adUnits); + sinon.assert.calledWith(next, adUnits); + return { + global: adUnits[0].ortb2Imp?.ext, + ...Object.fromEntries(adUnits[0].bids.map(bid => [bid.bidder, bid.ortb2Imp?.ext])) + } + } + + it('should not override pub-defined ext.ae', () => { + config.setConfig({ + paapi: { + enabled: true, + defaultForSlots: 1, + } + }); + Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 0}}}); + sinon.assert.match(getImpExt(), { + global: { + ae: 0, + }, + rubicon: undefined, + appnexus: undefined + }); + }); + + it('should override per-bidder when excluded via paapi.bidders', () => { + config.setConfig({ + paapi: { + enabled: true, + defaultForSlots: 1, + bidders: ['rubicon'] + } + }) + sinon.assert.match(getImpExt(), { + global: { + ae: 1, + igs: { + ae: 1, + biddable: 1 + } + }, + rubicon: undefined, + appnexus: { + ae: 0, + igs: { + ae: 0, + biddable: 0 + } + } + }) + }) + + it('should populate ext.igs when request has ext.ae', () => { + config.setConfig({ + paapi: { + enabled: true + } + }); + Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 3}}}); + sinon.assert.match(getImpExt(), { + global: { + ae: 3, + igs: { + ae: 3, + biddable: 1 + } + }, + rubicon: undefined, + appnexus: undefined, + }); + }); + + it('should not override pub-defined ext.igs', () => { + config.setConfig({ + paapi: { + enabled: true + } + }); + Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 1, igs: {biddable: 0}}}}); + sinon.assert.match(getImpExt(), { + global: { + ae: 1, + igs: { + ae: 1, + biddable: 0 + } + }, + rubicon: undefined, + appnexus: undefined + }) + }); + + it('should fill ext.ae from ext.igs, if defined', () => { + config.setConfig({ + paapi: { + enabled: true + } + }); + Object.assign(adUnits[0], {ortb2Imp: {ext: {igs: {}}}}); + sinon.assert.match(getImpExt(), { + global: { + ae: 1, + igs: { + ae: 1, + biddable: 1 + } + }, + appnexus: undefined, + rubicon: undefined + }) + }); + + describe('ortb2Imp.ext.paapi.requestedSize', () => { + beforeEach(() => { + config.setConfig({ + paapi: { + enabled: true, + defaultForSlots: 1, + } + }); + }); + + it('should default to value returned by getPAAPISize', () => { + getPAAPISizeStub.returns([123, 321]); + expect(getImpExt().global.paapi).to.eql({ + requestedSize: { + width: 123, + height: 321 + } + }); + }); + + it('should not be overridden, if provided by the pub', () => { + adUnits[0].ortb2Imp = { + ext: { + paapi: { + requestedSize: { + width: '123px', + height: '321px' + } + } + } + }; + expect(getImpExt().global.paapi).to.eql({ + requestedSize: { + width: '123px', + height: '321px' + } + }) + sinon.assert.notCalled(getPAAPISizeStub); + }); + + it('should not be set if adUnit has no banner sizes', () => { + adUnits[0].mediaTypes = { + video: {} + }; + expect(getImpExt().global?.paapi?.requestedSize).to.not.exist; + }); + }); + }); + }); + }); + + describe('igb', () => { + let igb1, igb2; + const buyer1 = 'https://buyer1.example'; + const buyer2 = 'https://buyer2.example'; + beforeEach(() => { + igb1 = { + origin: buyer1, + cur: 'EUR', + maxbid: 1, + pbs: { + signal: 1 + }, + ps: { + priority: 1 + } + }; + igb2 = { + origin: buyer2, + cur: 'USD', + maxbid: 2, + pbs: { + signal: 2 + }, + ps: { + priority: 2 + } + }; }); - describe('parseExtPrebidFledge', () => { - function packageConfigs(configs) { - return { - ext: { + + describe('mergeBuyers', () => { + it('should merge multiple igb into a partial auction config', () => { + sinon.assert.match(mergeBuyers([igb1, igb2]), { + interestGroupBuyers: [buyer1, buyer2], + perBuyerCurrencies: { + [buyer1]: 'EUR', + [buyer2]: 'USD' + }, + perBuyerSignals: { + [buyer1]: { + signal: 1 + }, + [buyer2]: { + signal: 2 + } + }, + perBuyerPrioritySignals: { + [buyer1]: { + priority: 1 + }, + [buyer2]: { + priority: 2 + } + }, + auctionSignals: { prebid: { - fledge: { - auctionconfigs: configs + perBuyerMaxbid: { + [buyer1]: 1, + [buyer2]: 2 } } } + }); + }); + + Object.entries(IGB_TO_CONFIG).forEach(([igbField, configField]) => { + it(`should not set ${configField} if ${igbField} is undefined`, () => { + delete igb1[igbField]; + expect(deepAccess(mergeBuyers([igb1, igb2]), configField)[buyer1]).to.not.exist; + }); + }); + + it('ignores igbs that have no origin', () => { + delete igb1.origin; + expect(mergeBuyers([igb1, igb2])).to.eql(mergeBuyers([igb2])); + }); + + it('ignores igbs with duplicate origin', () => { + igb2.origin = igb1.origin; + expect(mergeBuyers([igb1, igb2])).to.eql(mergeBuyers([igb1])); + }); + }); + + describe('partitionBuyers', () => { + it('should return a single partition when there are no duplicates', () => { + expect(partitionBuyers([igb1, igb2])).to.eql([[igb1, igb2]]); + }); + it('should ignore igbs that have no origin', () => { + delete igb1.origin; + expect(partitionBuyers([igb1, igb2])).to.eql([[igb2]]); + }); + it('should return a single partition when duplicates exist, but do not conflict', () => { + expect(partitionBuyers([igb1, igb2, deepClone(igb1)])).to.eql([[igb1, igb2]]); + }); + it('should return multiple partitions when there are conflicts', () => { + const igb3 = deepClone(igb1); + const igb4 = deepClone(igb1); + igb3.pbs.signal = 'conflict'; + igb4.ps.signal = 'conflict'; + expect(partitionBuyers([igb1, igb2, igb3, igb4])).to.eql([ + [igb1, igb2], + [igb3], + [igb4] + ]); + }); + }); + + describe('partitionBuyersByBidder', () => { + it('should split requests by bidder', () => { + expect(partitionBuyersByBidder([[{bidder: 'a'}, igb1], [{bidder: 'b'}, igb2]])).to.eql([ + [{bidder: 'a'}, [igb1]], + [{bidder: 'b'}, [igb2]] + ]); + }); + + it('accepts repeated buyers, if from different bidders', () => { + expect(partitionBuyersByBidder([ + [{bidder: 'a', extra: 'data'}, igb1], + [{bidder: 'b', more: 'data'}, igb1], + [{bidder: 'a'}, igb2], + [{bidder: 'b'}, igb2] + ])).to.eql([ + [{bidder: 'a', extra: 'data'}, [igb1, igb2]], + [{bidder: 'b', more: 'data'}, [igb1, igb2]] + ]); + }); + describe('buyersToAuctionConfig', () => { + let config, partitioners, merge, igbRequests; + beforeEach(() => { + config = { + auctionConfig: { + decisionLogicURL: 'mock-decision-logic' + } + }; + partitioners = { + compact: sinon.stub(), + expand: sinon.stub(), + }; + let i = 0; + merge = sinon.stub().callsFake(() => ({config: i++})); + igbRequests = [ + [{}, igb1], + [{}, igb2] + ]; + }); + + function toAuctionConfig(reqs = igbRequests) { + return buyersToAuctionConfigs(reqs, merge, config, partitioners); + } + + it('uses compact partitions by default, and returns an auction config for each one', () => { + partitioners.compact.returns([[{}, 1], [{}, 2]]); + const [cf1, cf2] = toAuctionConfig(); + sinon.assert.match(cf1[1], { + ...config.auctionConfig, + config: 0 + }); + sinon.assert.match(cf2[1], { + ...config.auctionConfig, + config: 1 + }); + sinon.assert.calledWith(partitioners.compact, igbRequests); + [1, 2].forEach(mockPart => sinon.assert.calledWith(merge, mockPart)); + }); + + it('uses per-bidder partition when config has separateAuctions', () => { + config.separateAuctions = true; + partitioners.expand.returns([]); + toAuctionConfig(); + sinon.assert.called(partitioners.expand); + }); + + it('does not return any auction config when configuration does not specify auctionConfig', () => { + delete config.auctionConfig; + expect(toAuctionConfig()).to.eql([]); + Object.values(partitioners).forEach(part => sinon.assert.notCalled(part)); + }); + + it('sets FPD in auction signals when partitioner returns it', () => { + const fpd = { + ortb2: {fpd: 1}, + ortb2Imp: {fpd: 2} + }; + partitioners.compact.returns([[{}], [fpd]]); + const [cf1, cf2] = toAuctionConfig(); + expect(cf1[1].auctionSignals?.prebid).to.not.exist; + expect(cf2[1].auctionSignals.prebid).to.eql(fpd); + }); + }); + }); + }); + + describe('getPAAPISize', () => { + before(() => { + getPAAPISize.getHooks().remove(); + }); + + Object.entries({ + 'ignores placeholders': { + in: [[1, 1], [0, 0], [3, 4]], + out: [3, 4] + }, + 'picks largest size by area': { + in: [[200, 100], [150, 150]], + out: [150, 150] + }, + 'can handle no sizes': { + in: [], + out: undefined + }, + 'can handle no input': { + in: undefined, + out: undefined + }, + 'can handle placeholder sizes': { + in: [[1, 1]], + out: undefined + } + }).forEach(([t, {in: input, out}]) => { + it(t, () => { + expect(getPAAPISize(input)).to.eql(out); + }); + }); + }); + + describe('buildPaapiParameters', () => { + let next, bidderRequest, spec, bids; + beforeEach(() => { + next = sinon.stub(); + spec = {}; + bidderRequest = {paapi: {enabled: true}}; + bids = []; + }); + + function runParamHook() { + return Promise.resolve(buildPAAPIParams(next, spec, bids, bidderRequest)); + } + + Object.entries({ + 'has no paapiParameters': () => null, + 'returns empty parameter map'() { + spec.paapiParameters = () => ({}) + }, + 'returns null parameter map'() { + spec.paapiParameters = () => null + }, + 'returns params, but PAAPI is disabled'() { + bidderRequest.paapi.enabled = false; + spec.paapiParameters = () => ({param: new AsyncPAAPIParam()}) + } + }).forEach(([t, setup]) => { + it(`should do nothing if spec ${t}`, async () => { + setup(); + await runParamHook(); + sinon.assert.calledWith(next, spec, bids, bidderRequest); + }) + }) + + describe('when paapiParameters returns a map', () => { + let params; + beforeEach(() => { + spec.paapiParameters = sinon.stub().callsFake(() => params); + }); + it('should be invoked with bids & bidderRequest', async () => { + await runParamHook(); + sinon.assert.calledWith(spec.paapiParameters, bids, bidderRequest); + }); + it('should leave most things (including promises) untouched', async () => { + params = { + 'p1': 'scalar', + 'p2': Promise.resolve() + } + await runParamHook(); + expect(bidderRequest.paapi.params).to.eql(params); + }); + it('should resolve async PAAPI parameeters', async () => { + params = { + 'resolved': new AsyncPAAPIParam(() => Promise.resolve('value')), + } + await runParamHook(); + expect(bidderRequest.paapi.params).to.eql({ + 'resolved': 'value' + }) + }) + + it('should still call next if the resolution fails', async () => { + params = { + error: new AsyncPAAPIParam(() => Promise.reject(new Error())) + } + await runParamHook(); + sinon.assert.called(next); + expect(bidderRequest.paapi.params).to.not.exist; + }) + }) + }) + + describe('parallel PAAPI auctions', () => { + describe('parallellPaapiProcessing', () => { + let next, spec, bids, bidderRequest, restOfTheArgs, mockConfig, mockAuction, bidsReceived, bidderRequests, adUnitCodes, adUnits; + + beforeEach(() => { + next = sinon.stub(); + spec = { + code: 'mockBidder', }; + bids = [{ + bidder: 'mockBidder', + bidId: 'bidId', + adUnitCode: 'au', + auctionId: 'aid', + mediaTypes: { + banner: { + sizes: [[123, 321]] + } + } + }]; + bidderRequest = {auctionId: 'aid', bidderCode: 'mockBidder', paapi: {enabled: true}, bids}; + restOfTheArgs = [{more: 'args'}]; + mockConfig = { + seller: 'mock.seller', + decisionLogicURL: 'mock.seller/decisionLogic', + interestGroupBuyers: ['mock.buyer'] + } + mockAuction = {}; + bidsReceived = [{adUnitCode: 'au', cpm: 1}]; + adUnits = [{code: 'au'}] + adUnitCodes = ['au']; + bidderRequests = [bidderRequest]; + sandbox.stub(auctionManager.index, 'getAuction').callsFake(() => mockAuction); + sandbox.stub(auctionManager.index, 'getAdUnit').callsFake((req) => bids.find(bid => bid.adUnitCode === req.adUnitCode)) + config.setConfig({paapi: {enabled: true}}); + }); + + afterEach(() => { + sinon.assert.calledWith(next, spec, bids, bidderRequest, ...restOfTheArgs); + config.resetConfig(); + }); + + function startParallel() { + parallelPaapiProcessing(next, spec, bids, bidderRequest, ...restOfTheArgs); + onAuctionInit({auctionId: 'aid'}) } - function generateImpCtx(fledgeFlags) { - return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); + function endAuction() { + events.emit(EVENTS.AUCTION_END, {auctionId: 'aid', bidsReceived, bidderRequests, adUnitCodes, adUnits}) } - function generateCfg(impid, ...ids) { - return ids.map((id) => ({impid, config: {id}})); + describe('should have no effect when', () => { + afterEach(() => { + expect(getPAAPIConfig({}, true)).to.eql({au: null}); + }) + it('spec has no buildPAAPIConfigs', () => { + startParallel(); + }); + Object.entries({ + 'returns no configs': () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); }, + 'throws': () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => { throw new Error() }) }, + 'returns too little config': () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => [ {bidId: 'bidId', config: {seller: 'mock.seller'}} ]) }, + 'bidder is not paapi enabled': () => { + bidderRequest.paapi.enabled = false; + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => [{config: mockConfig, bidId: 'bidId'}]) + }, + 'paapi module is not enabled': () => { + delete bidderRequest.paapi; + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => [{config: mockConfig, bidId: 'bidId'}]) + }, + 'bidId points to missing bid': () => { spec.buildPAAPIConfigs = sinon.stub().callsFake(() => [{config: mockConfig, bidId: 'missing'}]) } + }).forEach(([t, setup]) => { + it(`buildPAAPIConfigs ${t}`, () => { + setup(); + startParallel(); + }); + }); + }); + + function resolveConfig(auctionConfig) { + return Promise.all( + Object.entries(auctionConfig) + .map(([key, value]) => Promise.resolve(value).then(value => [key, value])) + ).then(result => Object.fromEntries(result)) } - function extractResult(ctx) { + describe('when buildPAAPIConfigs returns valid config', () => { + let builtCfg; + beforeEach(() => { + builtCfg = [{bidId: 'bidId', config: mockConfig}]; + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => builtCfg); + }); + + it('should make async config available from getPAAPIConfig', () => { + startParallel(); + const actual = getPAAPIConfig(); + const promises = Object.fromEntries(ASYNC_SIGNALS.map(signal => [signal, sinon.match((arg) => arg instanceof Promise)])) + sinon.assert.match(actual, { + au: sinon.match({ + ...promises, + requestedSize: { + width: 123, + height: 321 + }, + componentAuctions: [ + sinon.match({ + ...mockConfig, + ...promises, + requestedSize: { + width: 123, + height: 321 + } + }) + ] + }) + }); + }); + + it('should work when called multiple times for the same auction', () => { + startParallel(); + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); + startParallel(); + expect(getPAAPIConfig().au.componentAuctions.length).to.eql(1); + }); + + it('should hide TIDs from buildPAAPIConfigs', () => { + config.setConfig({enableTIDs: false}); + startParallel(); + sinon.assert.calledWith( + spec.buildPAAPIConfigs, + sinon.match(bidRequests => bidRequests.every(req => req.auctionId == null)), + sinon.match(bidderRequest => bidderRequest.auctionId == null) + ); + }); + + it('should show TIDs when enabled', () => { + config.setConfig({enableTIDs: true}); + startParallel(); + sinon.assert.calledWith( + spec.buildPAAPIConfigs, + sinon.match(bidRequests => bidRequests.every(req => req.auctionId === 'aid')), + sinon.match(bidderRequest => bidderRequest.auctionId === 'aid') + ) + }) + + it('should respect requestedSize from adapter', () => { + mockConfig.requestedSize = {width: 1, height: 2}; + startParallel(); + sinon.assert.match(getPAAPIConfig().au, { + requestedSize: { + width: 123, + height: 321 + }, + componentAuctions: [sinon.match({ + requestedSize: { + width: 1, + height: 2 + } + })] + }) + }) + + it('should not accept multiple partial configs for the same bid/seller', () => { + builtCfg.push(builtCfg[0]) + startParallel(); + expect(getPAAPIConfig().au.componentAuctions.length).to.eql(1); + }); + it('should resolve top level config with auction signals', async () => { + startParallel(); + let config = getPAAPIConfig().au; + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, { + auctionSignals: { + prebid: {bidfloor: 1} + } + }) + }); + + describe('when adapter returns the rest of auction config', () => { + let configRemainder; + beforeEach(() => { + configRemainder = { + ...Object.fromEntries(ASYNC_SIGNALS.map(signal => [signal, {type: signal}])), + seller: 'mock.seller' + }; + }) + function returnRemainder() { + addPaapiConfigHook(sinon.stub(), bids[0], {config: configRemainder}); + } + it('should resolve component configs with values returned by adapters', async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, configRemainder); + }); + + it('should pick first config that matches bidId/seller', async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + returnRemainder(); + const expectedSignals = {...configRemainder}; + configRemainder = { + ...configRemainder, + auctionSignals: { + this: 'should be ignored' + } + } + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, expectedSignals); + }); + + describe('should default to values returned from buildPAAPIConfigs when interpretResponse does not return', () => { + beforeEach(() => { + ASYNC_SIGNALS.forEach(signal => mockConfig[signal] = {default: signal}) + }); + Object.entries({ + 'returns no matching config'() { + }, + 'does not include values in response'() { + configRemainder = {}; + returnRemainder(); + } + }).forEach(([t, postResponse]) => { + it(t, async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + postResponse(); + endAuction(); + config = await resolveConfig(config); + sinon.assert.match(config, mockConfig); + }); + }); + }); + + it('should resolve to undefined when no value is available', async () => { + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + delete configRemainder.sellerSignals; + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + expect(config.sellerSignals).to.be.undefined; + }); + + [ + { + start: {t: 'scalar', value: 'str'}, + end: {t: 'array', value: ['abc']}, + should: {t: 'array', value: ['abc']} + }, + { + start: {t: 'object', value: {a: 'b'}}, + end: {t: 'scalar', value: 'abc'}, + should: {t: 'scalar', value: 'abc'} + }, + { + start: {t: 'object', value: {outer: {inner: 'val'}}}, + end: {t: 'object', value: {outer: {other: 'val'}}}, + should: {t: 'merge', value: {outer: {inner: 'val', other: 'val'}}} + } + ].forEach(({start, end, should}) => { + it(`when buildPAAPIConfigs returns ${start.t}, interpretResponse return ${end.t}, promise should resolve to ${should.t}`, async () => { + mockConfig.sellerSignals = start.value + startParallel(); + let config = getPAAPIConfig().au.componentAuctions[0]; + configRemainder.sellerSignals = end.value; + returnRemainder(); + endAuction(); + config = await resolveConfig(config); + expect(config.sellerSignals).to.eql(should.value); + }) + }) + + it('should make extra configs available', async () => { + startParallel(); + returnRemainder(); + configRemainder = {...configRemainder, seller: 'other.seller'}; + returnRemainder(); + endAuction(); + let configs = getPAAPIConfig().au.componentAuctions; + configs = [await resolveConfig(configs[0]), configs[1]]; + expect(configs.map(cfg => cfg.seller)).to.eql(['mock.seller', 'other.seller']); + }); + + describe('submodule\'s onAuctionConfig', () => { + let onAuctionConfig; + beforeEach(() => { + onAuctionConfig = sinon.stub(); + registerSubmodule({onAuctionConfig}) + }); + + Object.entries({ + 'parallel=true, some configs deferred': { + setup() { + config.mergeConfig({paapi: {parallel: true}}) + }, + delayed: false, + }, + 'parallel=true, no deferred configs': { + setup() { + config.mergeConfig({paapi: {parallel: true}}); + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => []); + }, + delayed: true + }, + 'parallel=false, some configs deferred': { + setup() { + config.mergeConfig({paapi: {parallel: false}}) + }, + delayed: true + } + }).forEach(([t, {setup, delayed}]) => { + describe(`when ${t}`, () => { + beforeEach(() => { + mockAuction.requestsDone = Promise.resolve(); + setup(); + }); + + function expectInvoked(shouldBeInvoked) { + if (shouldBeInvoked) { + sinon.assert.calledWith(onAuctionConfig, 'aid', sinon.match(arg => arg.au.componentAuctions[0].seller === 'mock.seller')); + } else { + sinon.assert.notCalled(onAuctionConfig); + } + } + + it(`should invoke onAuctionConfig when ${delayed ? 'auction ends' : 'auction requests have started'}`, async () => { + startParallel(); + await mockAuction.requestsDone; + expectInvoked(!delayed); + onAuctionConfig.reset(); + returnRemainder(); + endAuction(); + expectInvoked(delayed); + }) + }) + }) + }) + }); + }); + describe('when buildPAAPIConfigs returns igb', () => { + let builtCfg, igb, auctionConfig; + beforeEach(() => { + igb = {origin: 'mock.buyer'} + builtCfg = [{bidId: 'bidId', igb}]; + spec.buildPAAPIConfigs = sinon.stub().callsFake(() => builtCfg); + auctionConfig = { + seller: 'mock.seller', + decisionLogicUrl: 'mock.seller/decisionLogic' + } + config.mergeConfig({ + paapi: { + componentSeller: { + auctionConfig + } + } + }) + bidderRequest.paapi.componentSeller = true; + }); + Object.entries({ + 'componentSeller not configured'() { + bidderRequest.paapi.componentSeller = false; + }, + 'buildPAAPIconfig returns nothing'() { + builtCfg = [] + }, + 'returned igb is not valid'() { + builtCfg = [{bidId: 'bidId', igb: {}}]; + } + }).forEach(([t, setup]) => { + it(`should have no effect when ${t}`, () => { + setup(); + startParallel(); + expect(getPAAPIConfig()).to.eql({}); + }) + }) + + describe('when component seller is set up', () => { + it('should generate a deferred auctionConfig', () => { + startParallel(); + sinon.assert.match(getPAAPIConfig().au.componentAuctions[0], { + ...auctionConfig, + interestGroupBuyers: ['mock.buyer'], + }) + }); + + it('should use signal values from componentSeller.auctionConfig', async () => { + auctionConfig.auctionSignals = {test: 'signal'}; + config.mergeConfig({ + paapi: {componentSeller: {auctionConfig}} + }) + startParallel(); + endAuction(); + const cfg = await resolveConfig(getPAAPIConfig().au.componentAuctions[0]); + sinon.assert.match(cfg.auctionSignals, auctionConfig.auctionSignals); + }) + + it('should collate buyers', () => { + startParallel(); + startParallel(); + sinon.assert.match(getPAAPIConfig().au.componentAuctions[0], { + interestGroupBuyers: ['mock.buyer'] + }); + }); + + function returnIgb(igb) { + addPaapiConfigHook(sinon.stub(), bids[0], {igb}); + } + + it('should resolve to values from interpretResponse as well as buildPAAPIConfigs', async () => { + igb.cur = 'cur'; + igb.pbs = {over: 'ridden'} + startParallel(); + let cfg = getPAAPIConfig().au.componentAuctions[0]; + returnIgb({ + origin: 'mock.buyer', + pbs: {some: 'signal'} + }); + endAuction(); + cfg = await resolveConfig(cfg); + sinon.assert.match(cfg, { + perBuyerSignals: { + [igb.origin]: {some: 'signal'}, + }, + perBuyerCurrencies: { + [igb.origin]: 'cur' + } + }) + }); + + it('should not overwrite config once resolved', () => { + startParallel(); + returnIgb({ + origin: 'mock.buyer', + }); + endAuction(); + const cfg = getPAAPIConfig().au; + sinon.assert.match(cfg, Object.fromEntries(ASYNC_SIGNALS.map(signal => [signal, sinon.match(arg => arg instanceof Promise)]))) + }) + + it('can resolve multiple igbs', async () => { + igb.cur = 'cur1'; + startParallel(); + spec.code = 'other'; + igb.origin = 'other.buyer' + igb.cur = 'cur2' + startParallel(); + let cfg = getPAAPIConfig().au.componentAuctions[0]; + returnIgb({ + origin: 'mock.buyer', + pbs: {signal: 1} + }); + returnIgb({ + origin: 'other.buyer', + pbs: {signal: 2} + }); + endAuction(); + cfg = await resolveConfig(cfg); + sinon.assert.match(cfg, { + perBuyerSignals: { + 'mock.buyer': {signal: 1}, + 'other.buyer': {signal: 2} + }, + perBuyerCurrencies: { + 'mock.buyer': 'cur1', + 'other.buyer': 'cur2' + } + }) + }) + + function startMultiple() { + startParallel(); + spec.code = 'other'; + igb.origin = 'other.buyer' + startParallel(); + } + + describe('when using separateAuctions=false', () => { + beforeEach(() => { + config.mergeConfig({ + paapi: { + componentSeller: { + separateAuctions: false + } + } + }) + }); + + it('should merge igb from different specs into a single auction config', () => { + startMultiple(); + sinon.assert.match(getPAAPIConfig().au.componentAuctions[0], { + interestGroupBuyers: ['mock.buyer', 'other.buyer'] + }); + }); + }) + + describe('when using separateAuctions=true', () => { + beforeEach(() => { + config.mergeConfig({ + paapi: { + componentSeller: { + separateAuctions: true + } + } + }) + }); + it('should generate an auction config for each bidder', () => { + startMultiple(); + const components = getPAAPIConfig().au.componentAuctions; + sinon.assert.match(components[0], { + interestGroupBuyers: ['mock.buyer'] + }) + sinon.assert.match(components[1], { + interestGroupBuyers: ['other.buyer'] + }) + }) + }) + }) + }) + }); + }); + + describe('ortb processors for fledge', () => { + it('imp.ext.ae should be removed if fledge is not enabled', () => { + const imp = {ext: {ae: 1, igs: {}}}; + setImpExtAe(imp, {}, {bidderRequest: {}}); + expect(imp.ext.ae).to.not.exist; + expect(imp.ext.igs).to.not.exist; + }); + it('imp.ext.ae should be left intact if fledge is enabled', () => { + const imp = {ext: {ae: 2, igs: {biddable: 0}}}; + setImpExtAe(imp, {}, {bidderRequest: {paapi: {enabled: true}}}); + expect(imp.ext).to.eql({ + ae: 2, + igs: { + biddable: 0 + } + }); + }); + + describe('response parsing', () => { + function generateImpCtx(fledgeFlags) { + return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); + } + + function extractResult(type, ctx) { return Object.fromEntries( Object.entries(ctx) - .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) + .map(([impid, ctx]) => [impid, ctx.paapiConfigs?.map(cfg => cfg[type].id)]) .filter(([_, val]) => val != null) ); } - it('should collect fledge configs by imp', () => { - const ctx = { - impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) - }; - const resp = packageConfigs( - generateCfg('e1', 1, 2, 3) - .concat(generateCfg('e2', 4) - .concat(generateCfg('d1', 5, 6))) - ); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({ - e1: [1, 2, 3], - e2: [4], + Object.entries({ + 'parseExtPrebidFledge': { + parser: parseExtPrebidFledge, + responses: { + 'ext.prebid.fledge'(configs) { + return { + ext: { + prebid: { + fledge: { + auctionconfigs: configs + } + } + } + }; + }, + } + }, + 'parseExtIgi': { + parser: parseExtIgi, + responses: { + 'ext.igi.igs'(configs) { + return { + ext: { + igi: [{ + igs: configs + }] + } + }; + }, + 'ext.igi.igs with impid on igi'(configs) { + return { + ext: { + igi: configs.map(cfg => { + const impid = cfg.impid; + delete cfg.impid; + return { + impid, + igs: [cfg] + }; + }) + } + }; + }, + 'ext.igi.igs with conflicting impid'(configs) { + return { + ext: { + igi: [{ + impid: 'conflict', + igs: configs + }] + } + }; + } + } + } + }).forEach(([t, {parser, responses}]) => { + describe(t, () => { + Object.entries(responses).forEach(([t, packageConfigs]) => { + describe(`when response uses ${t}`, () => { + function generateCfg(impid, ...ids) { + return ids.map((id) => ({impid, config: {id}})); + } + + it('should collect auction configs by imp', () => { + const ctx = { + impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) + }; + const resp = packageConfigs( + generateCfg('e1', 1, 2, 3) + .concat(generateCfg('e2', 4) + .concat(generateCfg('d1', 5, 6))) + ); + parser({}, resp, ctx); + expect(extractResult('config', ctx.impContext)).to.eql({ + e1: [1, 2, 3], + e2: [4], + }); + }); + it('should not choke if fledge config references unknown imp', () => { + const ctx = {impContext: generateImpCtx({i: 1})}; + const resp = packageConfigs(generateCfg('unknown', 1)); + parser({}, resp, ctx); + expect(extractResult('config', ctx.impContext)).to.eql({}); + }); + }); + }); }); }); - it('should not choke if fledge config references unknown imp', () => { - const ctx = {impContext: generateImpCtx({i: 1})}; - const resp = packageConfigs(generateCfg('unknown', 1)); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({}); + + describe('response ext.igi.igb', () => { + it('should collect igb by imp', () => { + const ctx = { + impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) + }; + const resp = { + ext: { + igi: [ + { + impid: 'e1', + igb: [ + {id: 1}, + {id: 2} + ] + }, + { + impid: 'e2', + igb: [ + {id: 3} + ] + }, + { + impid: 'd1', + igb: [ + {id: 4} + ] + } + ] + } + }; + parseExtIgi({}, resp, ctx); + expect(extractResult('igb', ctx.impContext)).to.eql({ + e1: [1, 2], + e2: [3], + }); + }); }); }); - describe('setResponseFledgeConfigs', () => { - it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { + + describe('setResponsePaapiConfigs', () => { + it('should set paapi configs/igb paired with their corresponding bid id', () => { const ctx = { impContext: { 1: { bidRequest: {bidId: 'bid1'}, - fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] + paapiConfigs: [{config: {id: 1}}, {config: {id: 2}}] }, 2: { bidRequest: {bidId: 'bid2'}, - fledgeConfigs: [{config: {id: 3}}] + paapiConfigs: [{config: {id: 3}}] }, 3: { bidRequest: {bidId: 'bid3'} + }, + 4: { + bidRequest: {bidId: 'bid1'}, + paapiConfigs: [{igb: {id: 4}}] } } }; const resp = {}; - setResponseFledgeConfigs(resp, {}, ctx); - expect(resp.fledgeAuctionConfigs).to.eql([ + setResponsePaapiConfigs(resp, {}, ctx); + expect(resp.paapi).to.eql([ {bidId: 'bid1', config: {id: 1}}, {bidId: 'bid1', config: {id: 2}}, {bidId: 'bid2', config: {id: 3}}, + {bidId: 'bid1', igb: {id: 4}} ]); }); - it('should not set fledgeAuctionConfigs if none exist', () => { + it('should not set paapi if no config or igb exists', () => { const resp = {}; - setResponseFledgeConfigs(resp, {}, { + setResponsePaapiConfigs(resp, {}, { impContext: { 1: { - fledgeConfigs: [] + paapiConfigs: [] }, 2: {} } diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js deleted file mode 100644 index 55287e0bfec..00000000000 --- a/test/spec/modules/parrableIdSystem_spec.js +++ /dev/null @@ -1,762 +0,0 @@ -import { expect } from 'chai'; -import {find} from 'src/polyfill.js'; -import { config } from 'src/config.js'; -import * as utils from 'src/utils.js'; -import { newStorageManager } from 'src/storageManager.js'; -import { getRefererInfo } from 'src/refererDetection.js'; -import { uspDataHandler } from 'src/adapterManager.js'; -import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; -import { parrableIdSubmodule } from 'modules/parrableIdSystem.js'; -import { server } from 'test/mocks/xhr.js'; -import {mockGdprConsent} from '../../helpers/consentData.js'; - -const storage = newStorageManager(); - -const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; -const EXPIRE_COOKIE_TIME = 864000000; -const P_COOKIE_NAME = '_parrable_id'; -const P_COOKIE_EID = '01.1563917337.test-eid'; -const P_XHR_EID = '01.1588030911.test-new-eid' -const P_CONFIG_MOCK = { - name: 'parrableId', - params: { - partners: 'parrable_test_partner_123,parrable_test_partner_456' - } -}; -const RESPONSE_HEADERS = { 'Content-Type': 'application/json' }; - -function getConfigMock() { - return { - userSync: { - syncDelay: 0, - userIds: [P_CONFIG_MOCK] - } - } -} - -function getAdUnitMock(code = 'adUnit-code') { - return { - code, - mediaTypes: {banner: {}, native: {}}, - sizes: [ - [300, 200], - [300, 600] - ], - bids: [{ - bidder: 'sampleBidder', - params: { placementId: 'banner-only-bidder' } - }] - }; -} - -function serializeParrableId(parrableId) { - let str = ''; - if (parrableId.eid) { - str += 'eid:' + parrableId.eid; - } - if (parrableId.ibaOptout) { - str += ',ibaOptout:1'; - } - if (parrableId.ccpaOptout) { - str += ',ccpaOptout:1'; - } - if (parrableId.tpc !== undefined) { - const tpcSupportComponent = parrableId.tpc === true ? 'tpc:1' : 'tpc:0'; - str += `,${tpcSupportComponent}`; - str += `,tpcUntil:${parrableId.tpcUntil}`; - } - if (parrableId.filteredUntil) { - str += `,filteredUntil:${parrableId.filteredUntil}`; - str += `,filterHits:${parrableId.filterHits}`; - } - return str; -} - -function writeParrableCookie(parrableId) { - let cookieValue = encodeURIComponent(serializeParrableId(parrableId)); - storage.setCookie( - P_COOKIE_NAME, - cookieValue, - (new Date(Date.now() + EXPIRE_COOKIE_TIME).toUTCString()), - 'lax' - ); -} - -function removeParrableCookie() { - storage.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE); -} - -function decodeBase64UrlSafe(encBase64) { - const DEC = { - '-': '+', - '_': '/', - '.': '=' - }; - return encBase64.replace(/[-_.]/g, (m) => DEC[m]); -} - -describe('Parrable ID System', function() { - after(() => { - // reset ID system to avoid delayed callbacks in other tests - config.resetConfig(); - init(config); - }); - - describe('parrableIdSystem.getId()', function() { - describe('response callback function', function() { - let logErrorStub; - let callbackSpy = sinon.spy(); - - beforeEach(function() { - logErrorStub = sinon.stub(utils, 'logError'); - callbackSpy.resetHistory(); - writeParrableCookie({ eid: P_COOKIE_EID }); - }); - - afterEach(function() { - removeParrableCookie(); - logErrorStub.restore(); - }) - - it('creates xhr to Parrable that synchronizes the ID', function() { - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); - - getIdResult.callback(callbackSpy); - - let request = server.requests[0]; - let queryParams = utils.parseQS(request.url.split('?')[1]); - let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data))); - - expect(getIdResult.callback).to.be.a('function'); - expect(request.url).to.contain('h.parrable.com'); - - expect(queryParams).to.not.have.property('us_privacy'); - expect(data).to.deep.equal({ - eid: P_COOKIE_EID, - trackers: P_CONFIG_MOCK.params.partners.split(','), - url: getRefererInfo().page, - prebidVersion: '$prebid.version$', - isIframe: true - }); - - server.requests[0].respond(200, - { 'Content-Type': 'text/plain' }, - JSON.stringify({ eid: P_XHR_EID }) - ); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({ - eid: P_XHR_EID - }); - - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent('eid:' + P_XHR_EID) - ); - }); - - it('xhr passes the uspString to Parrable', function() { - let uspString = '1YNN'; - uspDataHandler.setConsentData(uspString); - parrableIdSubmodule.getId( - P_CONFIG_MOCK, - null, - null - ).callback(callbackSpy); - uspDataHandler.setConsentData(null); - expect(server.requests[0].url).to.contain('us_privacy=' + uspString); - }); - - it('xhr base64 safely encodes url data object', function() { - const urlSafeBase64EncodedData = '-_.'; - const btoaStub = sinon.stub(window, 'btoa').returns('+/='); - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); - - getIdResult.callback(callbackSpy); - - let request = server.requests[0]; - let queryParams = utils.parseQS(request.url.split('?')[1]); - expect(queryParams.data).to.equal(urlSafeBase64EncodedData); - btoaStub.restore(); - }); - - it('should log an error and continue to callback if ajax request errors', function () { - let callBackSpy = sinon.spy(); - let submoduleCallback = parrableIdSubmodule.getId({ params: {partners: 'prebid'} }).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.contain('h.parrable.com'); - request.respond( - 503, - null, - 'Unavailable' - ); - expect(logErrorStub.calledOnce).to.be.true; - expect(callBackSpy.calledOnce).to.be.true; - }); - }); - - describe('response id', function() { - it('provides the stored Parrable values if a cookie exists', function() { - writeParrableCookie({ eid: P_COOKIE_EID }); - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); - removeParrableCookie(); - - expect(getIdResult.id).to.deep.equal({ - eid: P_COOKIE_EID - }); - }); - - it('provides the stored legacy Parrable ID values if cookies exist', function() { - let oldEid = '01.111.old-eid'; - let oldEidCookieName = '_parrable_eid'; - let oldOptoutCookieName = '_parrable_optout'; - - storage.setCookie(oldEidCookieName, oldEid); - storage.setCookie(oldOptoutCookieName, 'true'); - - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); - expect(getIdResult.id).to.deep.equal({ - eid: oldEid, - ibaOptout: true - }); - - // The ID system is expected to migrate old cookies to the new format - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent('eid:' + oldEid + ',ibaOptout:1') - ); - expect(storage.getCookie(oldEidCookieName)).to.equal(null); - expect(storage.getCookie(oldOptoutCookieName)).to.equal(null); - removeParrableCookie(); - }); - }); - - describe('GDPR consent', () => { - let callbackSpy = sinon.spy(); - - const config = { - params: { - partner: 'partner' - } - }; - - const gdprConsentTestCases = [ - { consentData: { gdprApplies: true, consentString: 'expectedConsentString' }, expected: { gdpr: 1, gdpr_consent: 'expectedConsentString' } }, - { consentData: { gdprApplies: false, consentString: 'expectedConsentString' }, expected: { gdpr: 0 } }, - { consentData: { gdprApplies: true, consentString: undefined }, expected: { gdpr: 1, gdpr_consent: '' } }, - { consentData: { gdprApplies: 'yes', consentString: 'expectedConsentString' }, expected: { gdpr: 0 } }, - { consentData: undefined, expected: { gdpr: 0 } } - ]; - - gdprConsentTestCases.forEach((testCase, index) => { - it(`should call user sync url with the gdprConsent - case ${index}`, () => { - parrableIdSubmodule.getId(config, testCase.consentData).callback(callbackSpy); - - if (testCase.expected.gdpr === 1) { - expect(server.requests[0].url).to.contain('gdpr=' + testCase.expected.gdpr); - expect(server.requests[0].url).to.contain('gdpr_consent=' + testCase.expected.gdpr_consent); - } else { - expect(server.requests[0].url).to.contain('gdpr=' + testCase.expected.gdpr); - expect(server.requests[0].url).to.not.contain('gdpr_consent'); - } - }) - }); - }); - - describe('third party cookie support', function () { - let logErrorStub; - let callbackSpy = sinon.spy(); - - beforeEach(function() { - logErrorStub = sinon.stub(utils, 'logError'); - }); - - afterEach(function () { - callbackSpy.resetHistory(); - removeParrableCookie(); - }); - - afterEach(function() { - logErrorStub.restore(); - }); - - describe('when getting tpcSupport from XHR response', function () { - let request; - let dateNowStub; - const dateNowMock = Date.now(); - const tpcSupportTtl = 1; - - before(() => { - dateNowStub = sinon.stub(Date, 'now').returns(dateNowMock); - }); - - after(() => { - dateNowStub.restore(); - }); - - it('should set tpcSupport: true and tpcUntil in the cookie', function () { - let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK); - callback(callbackSpy); - request = server.requests[0]; - - request.respond( - 200, - RESPONSE_HEADERS, - JSON.stringify({ eid: P_XHR_EID, tpcSupport: true, tpcSupportTtl }) - ); - - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent('eid:' + P_XHR_EID + ',tpc:1,tpcUntil:' + Math.floor((dateNowMock / 1000) + tpcSupportTtl)) - ); - }); - - it('should set tpcSupport: false and tpcUntil in the cookie', function () { - let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK); - callback(callbackSpy); - request = server.requests[0]; - request.respond( - 200, - RESPONSE_HEADERS, - JSON.stringify({ eid: P_XHR_EID, tpcSupport: false, tpcSupportTtl }) - ); - - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent('eid:' + P_XHR_EID + ',tpc:0,tpcUntil:' + Math.floor((dateNowMock / 1000) + tpcSupportTtl)) - ); - }); - - it('should not set tpcSupport in the cookie', function () { - let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK); - callback(callbackSpy); - request = server.requests[0]; - - request.respond( - 200, - RESPONSE_HEADERS, - JSON.stringify({ eid: P_XHR_EID }) - ); - - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent('eid:' + P_XHR_EID) - ); - }); - }); - }); - - describe('request-filter status', function () { - let logErrorStub; - let callbackSpy = sinon.spy(); - - beforeEach(function() { - logErrorStub = sinon.stub(utils, 'logError'); - }); - - afterEach(function () { - callbackSpy.resetHistory(); - removeParrableCookie(); - }); - - afterEach(function() { - logErrorStub.restore(); - }); - - describe('when getting filterTtl from XHR response', function () { - let request; - let dateNowStub; - const dateNowMock = Date.now(); - const filterTtl = 1000; - - before(() => { - dateNowStub = sinon.stub(Date, 'now').returns(dateNowMock); - }); - - after(() => { - dateNowStub.restore(); - }); - - it('should set filteredUntil in the cookie', function () { - let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK); - callback(callbackSpy); - request = server.requests[0]; - - request.respond( - 200, - RESPONSE_HEADERS, - JSON.stringify({ eid: P_XHR_EID, filterTtl }) - ); - - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent( - 'eid:' + P_XHR_EID + - ',filteredUntil:' + Math.floor((dateNowMock / 1000) + filterTtl) + - ',filterHits:0') - ); - }); - - it('should increment filterHits in the cookie', function () { - writeParrableCookie({ - eid: P_XHR_EID, - filteredUntil: Math.floor((dateNowMock / 1000) + filterTtl), - filterHits: 0 - }); - let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK); - callback(callbackSpy); - - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent( - 'eid:' + P_XHR_EID + - ',filteredUntil:' + Math.floor((dateNowMock / 1000) + filterTtl) + - ',filterHits:1') - ); - }); - - it('should send filterHits in the XHR', function () { - const filterHits = 1; - writeParrableCookie({ - eid: P_XHR_EID, - filteredUntil: Math.floor(dateNowMock / 1000), - filterHits - }); - let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK); - callback(callbackSpy); - request = server.requests[0]; - - let queryParams = utils.parseQS(request.url.split('?')[1]); - let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data))); - - expect(data.filterHits).to.equal(filterHits); - }); - }); - }); - }); - - describe('parrableIdSystem.decode()', function() { - it('provides the Parrable ID (EID) from a stored object', function() { - let eid = '01.123.4567890'; - let parrableId = { - eid, - ibaOptout: true - }; - - expect(parrableIdSubmodule.decode(parrableId)).to.deep.equal({ - parrableId - }); - }); - }); - - describe('timezone filtering', function() { - before(function() { - sinon.stub(Intl, 'DateTimeFormat'); - }); - - after(function() { - Intl.DateTimeFormat.restore(); - }); - - it('permits an impression when no timezoneFilter is configured', function() { - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - } })).to.have.property('callback'); - }); - - it('permits an impression from a blocked timezone when a cookie exists', function() { - const blockedZone = 'Antarctica/South_Pole'; - const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone }); - Intl.DateTimeFormat.returns({ resolvedOptions }); - - writeParrableCookie({ eid: P_COOKIE_EID }); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - blockedZones: [ blockedZone ] - } - } })).to.have.property('callback'); - expect(resolvedOptions.called).to.equal(false); - - removeParrableCookie(); - }) - - it('permits an impression from an allowed timezone', function() { - const allowedZone = 'America/New_York'; - const resolvedOptions = sinon.stub().returns({ timeZone: allowedZone }); - Intl.DateTimeFormat.returns({ resolvedOptions }); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - allowedZones: [ allowedZone ] - } - } })).to.have.property('callback'); - expect(resolvedOptions.called).to.equal(true); - }); - - it('permits an impression from a lower cased allowed timezone', function() { - const allowedZone = 'America/New_York'; - const resolvedOptions = sinon.stub().returns({ timeZone: allowedZone }); - Intl.DateTimeFormat.returns({ resolvedOptions }); - - expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', - timezoneFilter: { - allowedZones: [ allowedZone.toLowerCase() ] - } - } })).to.have.property('callback'); - expect(resolvedOptions.called).to.equal(true); - }); - - it('permits an impression from a timezone that is not blocked', function() { - const blockedZone = 'America/New_York'; - const resolvedOptions = sinon.stub().returns({ timeZone: 'Iceland' }); - Intl.DateTimeFormat.returns({ resolvedOptions }); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - blockedZones: [ blockedZone ] - } - } })).to.have.property('callback'); - expect(resolvedOptions.called).to.equal(true); - }); - - it('does not permit an impression from a blocked timezone', function() { - const blockedZone = 'America/New_York'; - const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone }); - Intl.DateTimeFormat.returns({ resolvedOptions }); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - blockedZones: [ blockedZone ] - } - } })).to.equal(null); - expect(resolvedOptions.called).to.equal(true); - }); - - it('does not permit an impression from a lower cased blocked timezone', function() { - const blockedZone = 'America/New_York'; - const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone }); - Intl.DateTimeFormat.returns({ resolvedOptions }); - - expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', - timezoneFilter: { - blockedZones: [ blockedZone.toLowerCase() ] - } - } })).to.equal(null); - expect(resolvedOptions.called).to.equal(true); - }); - - it('does not permit an impression from a blocked timezone even when also allowed', function() { - const timezone = 'America/New_York'; - const resolvedOptions = sinon.stub().returns({ timeZone: timezone }); - Intl.DateTimeFormat.returns({ resolvedOptions }); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - allowedZones: [ timezone ], - blockedZones: [ timezone ] - } - } })).to.equal(null); - expect(resolvedOptions.called).to.equal(true); - }); - }); - - describe('timezone offset filtering', function() { - before(function() { - sinon.stub(Date.prototype, 'getTimezoneOffset'); - }); - - afterEach(function() { - Date.prototype.getTimezoneOffset.reset(); - }) - - after(function() { - Date.prototype.getTimezoneOffset.restore(); - }); - - it('permits an impression from a blocked offset when a cookie exists', function() { - const blockedOffset = -4; - Date.prototype.getTimezoneOffset.returns(blockedOffset * 60); - - writeParrableCookie({ eid: P_COOKIE_EID }); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - blockedOffsets: [ blockedOffset ] - } - } })).to.have.property('callback'); - - removeParrableCookie(); - }); - - it('permits an impression from an allowed offset', function() { - const allowedOffset = -5; - Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - allowedOffsets: [ allowedOffset ] - } - } })).to.have.property('callback'); - expect(Date.prototype.getTimezoneOffset.called).to.equal(true); - }); - - it('permits an impression from an offset that is not blocked', function() { - const allowedOffset = -5; - const blockedOffset = 5; - Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - blockedOffsets: [ blockedOffset ] - } - }})).to.have.property('callback'); - expect(Date.prototype.getTimezoneOffset.called).to.equal(true); - }); - - it('does not permit an impression from a blocked offset', function() { - const blockedOffset = -5; - Date.prototype.getTimezoneOffset.returns(blockedOffset * 60); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - blockedOffsets: [ blockedOffset ] - } - } })).to.equal(null); - expect(Date.prototype.getTimezoneOffset.called).to.equal(true); - }); - - it('does not permit an impression from a blocked offset even when also allowed', function() { - const offset = -5; - Date.prototype.getTimezoneOffset.returns(offset * 60); - - expect(parrableIdSubmodule.getId({ params: { - partners: 'prebid-test', - timezoneFilter: { - allowedOffset: [ offset ], - blockedOffsets: [ offset ] - } - } })).to.equal(null); - expect(Date.prototype.getTimezoneOffset.called).to.equal(true); - }); - }); - - describe('userId requestBids hook', function() { - let adUnits; - let sandbox; - - beforeEach(function() { - sandbox = sinon.sandbox.create(); - mockGdprConsent(sandbox); - adUnits = [getAdUnitMock()]; - writeParrableCookie({ eid: P_COOKIE_EID, ibaOptout: true }); - init(config); - setSubmoduleRegistry([parrableIdSubmodule]); - }); - - afterEach(function() { - removeParrableCookie(); - storage.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE); - sandbox.restore(); - }); - - it('when a stored Parrable ID exists it is added to bids', function(done) { - config.setConfig(getConfigMock()); - requestBidsHook(function() { - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property('userId.parrableId'); - expect(bid.userId.parrableId.eid).to.equal(P_COOKIE_EID); - expect(bid.userId.parrableId.ibaOptout).to.equal(true); - const parrableIdAsEid = find(bid.userIdAsEids, e => e.source == 'parrable.com'); - expect(parrableIdAsEid).to.deep.equal({ - source: 'parrable.com', - uids: [{ - id: P_COOKIE_EID, - atype: 1, - ext: { - ibaOptout: true - } - }] - }); - }); - }); - done(); - }, { adUnits }); - }); - - it('supplies an optout reason when the EID is missing due to CCPA non-consent', function(done) { - // the ID system itself will not write a cookie with an EID when CCPA=true - writeParrableCookie({ ccpaOptout: true }); - config.setConfig(getConfigMock()); - - requestBidsHook(function() { - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property('userId.parrableId'); - expect(bid.userId.parrableId).to.not.have.property('eid'); - expect(bid.userId.parrableId.ccpaOptout).to.equal(true); - const parrableIdAsEid = find(bid.userIdAsEids, e => e.source == 'parrable.com'); - expect(parrableIdAsEid).to.deep.equal({ - source: 'parrable.com', - uids: [{ - id: '', - atype: 1, - ext: { - ccpaOptout: true - } - }] - }); - }); - }); - done(); - }, { adUnits }); - }); - }); - - describe('partners parsing', function () { - let callbackSpy = sinon.spy(); - - const partnersTestCase = [ - { - name: '"partners" as an array', - config: { params: { partners: ['parrable_test_partner_123', 'parrable_test_partner_456'] } }, - expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] - }, - { - name: '"partners" as a string list', - config: { params: { partners: 'parrable_test_partner_123,parrable_test_partner_456' } }, - expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] - }, - { - name: '"partners" as a string', - config: { params: { partners: 'parrable_test_partner_123' } }, - expected: ['parrable_test_partner_123'] - }, - { - name: '"partner" as a string list', - config: { params: { partner: 'parrable_test_partner_123,parrable_test_partner_456' } }, - expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] - }, - { - name: '"partner" as string', - config: { params: { partner: 'parrable_test_partner_123' } }, - expected: ['parrable_test_partner_123'] - }, - ]; - partnersTestCase.forEach(testCase => { - it(`accepts config property ${testCase.name}`, () => { - parrableIdSubmodule.getId(testCase.config).callback(callbackSpy); - - let request = server.requests[0]; - let queryParams = utils.parseQS(request.url.split('?')[1]); - let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data))); - - expect(data.trackers).to.deep.equal(testCase.expected); - }); - }); - }); -}); diff --git a/test/spec/modules/performaxBidAdapter_spec.js b/test/spec/modules/performaxBidAdapter_spec.js new file mode 100644 index 00000000000..49a6a83e29d --- /dev/null +++ b/test/spec/modules/performaxBidAdapter_spec.js @@ -0,0 +1,175 @@ +import { expect } from 'chai'; +import { spec, converter } from 'modules/performaxBidAdapter.js'; + +describe('Performax adapter', function () { + let bids = [{ + bidder: 'performax', + params: { + tagid: 'sample' + }, + ortb2Imp: { + ext: {} + }, + mediaTypes: { + banner: { + sizes: [ + [300, 300], + ]}}, + adUnitCode: 'postbid_iframe', + transactionId: '84deda92-e9ba-4b0d-a797-43be5e522430', + adUnitId: '4ee4643b-931f-4a17-a571-ccba57886dc8', + sizes: [ + [300, 300], + ], + bidId: '2bc545c347dbbe', + bidderRequestId: '1534dec005b9a', + auctionId: 'acd97e55-01e1-45ad-813c-67fa27fc5c1b', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + ortb2: { + source: {}, + site: {}, + device: {} + }, + }, + + { + bidder: 'performax', + params: { + tagid: '1545' + }, + ortb2Imp: { + ext: {} + }, + mediaTypes: { + banner: { + sizes: [ + [300, 600], + ]}}, + adUnitCode: 'postbid_halfpage_iframe', + transactionId: '84deda92-e9ba-4b0d-a797-43be5e522430', + adUnitId: '4ee4643b-931f-4a17-a571-ccba57886dc8', + sizes: [ + [300, 600], + ], + bidId: '3dd53d30c691fe', + bidderRequestId: '1534dec005b9a', + auctionId: 'acd97e55-01e1-45ad-813c-67fa27fc5c1b', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + ortb2: { + source: {}, + site: {}, + device: {} + }}]; + + let bidderRequest = { + bidderCode: 'performax2', + auctionId: 'acd97e55-01e1-45ad-813c-67fa27fc5c1b', + id: 'acd97e55-01e1-45ad-813c-67fa27fc5c1b', + bidderRequestId: '1534dec005b9a', + bids: bids, + ortb2: { + regs: { + ext: { + gdpr: 1 + }}, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }}; + + let serverResponse = { + body: { + cur: 'CZK', + seatbid: [ + { + seat: 'performax', + bid: [ + { + id: 'sample', + price: 20, + w: 300, + h: 300, + adm: 'My ad' + } + ]}]}, + } + + describe('isBidRequestValid', function () { + let bid = {}; + it('should return false when missing "tagid" param', function() { + bid.params = {slotId: 'param'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when tagid is correct', function() { + bid.params = {tagid: 'sample'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }) + + describe('buildRequests', function () { + it('should set correct request method and url', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + expect(requests).to.be.an('array').that.has.lengthOf(1); + let request = requests[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://dale.performax.cz/ortb'); + expect(request.data).to.be.an('object'); + }); + + it('should pass correct imp', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(imp[0]).to.be.an('object'); + let bid = imp[0]; + expect(bid.id).to.equal('2bc545c347dbbe'); + expect(bid.banner).to.deep.equal({topframe: 0, format: [{w: 300, h: 300}]}); + }); + + it('should process multiple bids', function () { + let requests = spec.buildRequests(bids, bidderRequest); + expect(requests).to.be.an('array').that.has.lengthOf(1); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(bids.length); + let bid1 = imp[0]; + expect(bid1.banner).to.deep.equal({topframe: 0, format: [{w: 300, h: 300}]}); + let bid2 = imp[1]; + expect(bid2.banner).to.deep.equal({topframe: 0, format: [{w: 300, h: 600}]}); + }); + }); + + describe('interpretResponse', function () { + it('should map params correctly', function () { + let ortbRequest = {data: converter.toORTB({bidderRequest, bids})}; + serverResponse.body.id = ortbRequest.data.id; + serverResponse.body.seatbid[0].bid[0].imp_id = ortbRequest.data.imp[0].id; + + let result = spec.interpretResponse(serverResponse, ortbRequest); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + + expect(bid.cpm).to.equal(20); + expect(bid.ad).to.equal('My ad'); + expect(bid.currency).to.equal('CZK'); + expect(bid.mediaType).to.equal('banner'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(360); + expect(bid.creativeId).to.equal('sample'); + }); + }); +}); diff --git a/test/spec/modules/permutiveIdentityManagerIdSystem_spec.js b/test/spec/modules/permutiveIdentityManagerIdSystem_spec.js new file mode 100644 index 00000000000..96c581844c1 --- /dev/null +++ b/test/spec/modules/permutiveIdentityManagerIdSystem_spec.js @@ -0,0 +1,126 @@ +import { permutiveIdentityManagerIdSubmodule, storage } from 'modules/permutiveIdentityManagerIdSystem' +import { deepSetValue } from 'src/utils.js' + +const STORAGE_KEY = 'permutive-prebid-id' + +describe('permutiveIdentityManagerIdSystem', () => { + afterEach(() => { + storage.removeDataFromLocalStorage(STORAGE_KEY) + }) + + describe('decode', () => { + it('returns the input unchanged', () => { + const input = { + id5id: { + uid: '0', + ext: { + abTestingControlGroup: false, + linkType: 2, + pba: 'somepba' + } + } + } + const result = permutiveIdentityManagerIdSubmodule.decode(input) + expect(result).to.be.equal(input) + }) + }) + + describe('getId', () => { + it('returns relevant IDs from localStorage and does not return unexpected IDs', () => { + const data = getUserIdData() + storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify(data)) + const result = permutiveIdentityManagerIdSubmodule.getId({}) + const expected = { + 'id': { + 'id5id': { + 'uid': '0', + 'linkType': 0, + 'ext': { + 'abTestingControlGroup': false, + 'linkType': 0, + 'pba': 'EVqgf9vY0fSrsrqJZMOm+Q==' + } + } + } + } + expect(result).to.deep.equal(expected) + }) + + it('returns undefined if no relevant IDs are found in localStorage', () => { + storage.setDataInLocalStorage(STORAGE_KEY, '{}') + const result = permutiveIdentityManagerIdSubmodule.getId({}) + expect(result).to.be.undefined + }) + + it('will optionally wait for Permutive SDK if no identities are in local storage already', async () => { + const cleanup = setWindowPermutive() + const result = permutiveIdentityManagerIdSubmodule.getId({params: {ajaxTimeout: 50}}) + expect(result).not.to.be.undefined + expect(result.id).to.be.undefined + expect(result.callback).not.to.be.undefined + const expected = { + 'id5id': { + 'uid': '0', + 'linkType': 0, + 'ext': { + 'abTestingControlGroup': false, + 'linkType': 0, + 'pba': 'EVqgf9vY0fSrsrqJZMOm+Q==' + } + } + } + const r = await new Promise(result.callback) + expect(r).to.deep.equal(expected) + cleanup() + }) + }) +}) + +const setWindowPermutive = () => { + // Read from Permutive + const backup = window.permutive + + deepSetValue(window, 'permutive.ready', (f) => { + setTimeout(() => f(), 5) + }) + + deepSetValue(window, 'permutive.addons.identity_manager.prebid.onReady', (f) => { + setTimeout(() => f(sdkUserIdData()), 5) + }) + + // Cleanup + return () => window.permutive = backup +} + +const sdkUserIdData = () => ({ + 'id5id': { + 'uid': '0', + 'linkType': 0, + 'ext': { + 'abTestingControlGroup': false, + 'linkType': 0, + 'pba': 'EVqgf9vY0fSrsrqJZMOm+Q==' + } + }, +}) + +const getUserIdData = () => ({ + 'providers': { + 'id5id': { + 'userId': { + 'uid': '0', + 'linkType': 0, + 'ext': { + 'abTestingControlGroup': false, + 'linkType': 0, + 'pba': 'EVqgf9vY0fSrsrqJZMOm+Q==' + } + } + }, + 'fooid': { + 'userId': { + 'id': '1' + } + } + } +}) diff --git a/test/spec/modules/permutiveRtdProvider_spec.js b/test/spec/modules/permutiveRtdProvider_spec.js index 73218fee7b9..51fbba7e936 100644 --- a/test/spec/modules/permutiveRtdProvider_spec.js +++ b/test/spec/modules/permutiveRtdProvider_spec.js @@ -484,7 +484,9 @@ describe('permutiveRtdProvider', function () { _psegs: [], _ppam: [], _pcrprs: [], - _pssps: { ssps: [], cohorts: [] } + _pindexs: [], + _pssps: { ssps: [], cohorts: [] }, + _ppsts: {}, }) setBidderRtb(bidderConfig, moduleConfig, segmentsData) @@ -568,6 +570,7 @@ describe('permutiveRtdProvider', function () { const data = transformedTargeting() expect(getSegments(250)).to.deep.equal(data) }) + it('should enforce max segments', function () { const max = 1 const segments = getSegments(max) @@ -584,6 +587,26 @@ describe('permutiveRtdProvider', function () { } } }) + + it('should coerce numbers to strings', function () { + setLocalStorage({ _prubicons: [1, 2, 3], _pssps: { ssps: ['foo', 'bar'], cohorts: [4, 5, 6] } }) + + const segments = getSegments(200) + + expect(segments.rubicon).to.deep.equal(['1', '2', '3']) + expect(segments.ssp.ssps).to.deep.equal(['foo', 'bar']) + expect(segments.ssp.cohorts).to.deep.equal(['4', '5', '6']) + }) + + it('should return empty values on unexpected format', function () { + setLocalStorage({ _prubicons: 'a string instead?', _pssps: 123 }) + + const segments = getSegments(200) + + expect(segments.rubicon).to.deep.equal([]) + expect(segments.ssp.ssps).to.deep.equal([]) + expect(segments.ssp.cohorts).to.deep.equal([]) + }) }) describe('Existing key-value targeting', function () { @@ -703,14 +726,25 @@ function getConfig () { } function transformedTargeting (data = getTargetingData()) { + const topics = (() => { + const topics = {} + for (const topic in data._ppsts) { + topics[topic] = data._ppsts[topic].map(String) + } + return topics + })() + return { - ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)], - appnexus: data._papns, - ix: data._pindexs, - rubicon: data._prubicons, - gam: data._pdfps, - ssp: data._pssps, - topics: data._ppsts, + ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)].map(String), + appnexus: data._papns.map(String), + ix: data._pindexs.map(String), + rubicon: data._prubicons.map(String), + gam: data._pdfps.map(String), + ssp: { + ssps: data._pssps.ssps.map(String), + cohorts: data._pssps.cohorts.map(String) + }, + topics, } } diff --git a/test/spec/modules/pgamsspBidAdapter_spec.js b/test/spec/modules/pgamsspBidAdapter_spec.js index 281a012fb7a..ace20539459 100644 --- a/test/spec/modules/pgamsspBidAdapter_spec.js +++ b/test/spec/modules/pgamsspBidAdapter_spec.js @@ -3,9 +3,19 @@ import { spec } from '../../../modules/pgamsspBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'pgamssp' +const bidder = 'pgamssp'; describe('PGAMBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('PGAMBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('PGAMBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('PGAMBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -74,10 +87,19 @@ describe('PGAMBidAdapter', function () { const bidderRequest = { uspConsent: '1---', gdprConsent: { - consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw' + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -114,6 +136,7 @@ describe('PGAMBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -122,7 +145,11 @@ describe('PGAMBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -147,7 +174,56 @@ describe('PGAMBidAdapter', function () { 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.an('array'); + 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 + } + ]; + + let 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'); @@ -174,6 +250,8 @@ describe('PGAMBidAdapter', function () { let 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; @@ -189,12 +267,6 @@ describe('PGAMBidAdapter', function () { expect(data.ccpa).to.equal(bidderRequest.uspConsent); expect(data.gdpr).to.not.exist; }); - - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); - let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); }); describe('gpp consent', function () { diff --git a/test/spec/modules/pirIdSystem_spec.js b/test/spec/modules/pirIdSystem_spec.js deleted file mode 100644 index 5acc5a5eb9c..00000000000 --- a/test/spec/modules/pirIdSystem_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import { pirIdSubmodule, storage, readId } from 'modules/pirIdSystem.js'; -import sinon from 'sinon'; - -describe('pirIdSystem', () => { - let sandbox; - let getCookieStub; - let getDataFromLocalStorageStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - getCookieStub = sandbox.stub(storage, 'getCookie'); - getDataFromLocalStorageStub = sandbox.stub(storage, 'getDataFromLocalStorage'); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('getId', () => { - it('should return an object with id when pirIdToken is found', () => { - getDataFromLocalStorageStub.returns('testToken'); - getCookieStub.returns('testToken'); - - const result = pirIdSubmodule.getId(); - - expect(result).to.deep.equal({ id: 'testToken' }); - }); - - it('should return undefined when pirIdToken is not found', () => { - const result = pirIdSubmodule.getId(); - - expect(result).to.be.undefined; - }); - }); - - describe('decode', () => { - it('should return an object with pirId when value is a string', () => { - const result = pirIdSubmodule.decode('testId'); - - expect(result).to.deep.equal({ pirId: 'testId' }); - }); - - it('should return undefined when value is not a string', () => { - const result = pirIdSubmodule.decode({}); - - expect(result).to.be.undefined; - }); - }); - - describe('readId', () => { - it('should return data from local storage when it exists', () => { - getDataFromLocalStorageStub.returns('local_storage_data'); - - const result = readId(); - - expect(result).to.equal('local_storage_data'); - }); - - it('should return data from cookie when local storage data does not exist', () => { - getDataFromLocalStorageStub.returns(null); - getCookieStub.returns('cookie_data'); - - const result = readId(); - - expect(result).to.equal('cookie_data'); - }); - - it('should return null when neither local storage data nor cookie data exists', () => { - getDataFromLocalStorageStub.returns(null); - getCookieStub.returns(null); - - const result = readId(); - - expect(result).to.be.null; - }); - }); -}); diff --git a/test/spec/modules/pixfutureBidAdapter_spec.js b/test/spec/modules/pixfutureBidAdapter_spec.js index a236478c9b4..bdf40fbb06b 100644 --- a/test/spec/modules/pixfutureBidAdapter_spec.js +++ b/test/spec/modules/pixfutureBidAdapter_spec.js @@ -43,12 +43,12 @@ describe('PixFutureAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'pix_id': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/playdigoBidAdapter_spec.js b/test/spec/modules/playdigoBidAdapter_spec.js new file mode 100644 index 00000000000..107e0ebc7aa --- /dev/null +++ b/test/spec/modules/playdigoBidAdapter_spec.js @@ -0,0 +1,480 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/playdigoBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'playdigo' + +describe('PlaydigoBidAdapter', 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 valid URL', function () { + expect(serverRequest.url).to.equal('https://server.playdigo.com/pbjs'); + }); + + it('Returns general data valid', function () { + let 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 + } + ]; + + let 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); + let 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); + let 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] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + + 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 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let 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 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let 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' + }] + }; + + let 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' + }] + }; + let 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', + }] + }; + let 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' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 9c2ac8a23a9..33cc676368f 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1,9 +1,9 @@ -/* eslint-disable no-trailing-spaces */ + import {expect} from 'chai'; import { PrebidServer as Adapter, resetSyncedStatus, - resetWurlMap, + validateConfig, s2sDefaultConfig } from 'modules/prebidServerBidAdapter/index.js'; import adapterManager, {PBS_ADAPTER_NAME} from 'src/adapterManager.js'; @@ -21,28 +21,34 @@ import 'modules/currency.js'; // adServerCurrency test import 'modules/userId/index.js'; import 'modules/multibid/index.js'; import 'modules/priceFloors.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'modules/consentManagementUsp.js'; +import 'modules/consentManagementGpp.js'; import 'modules/schain.js'; -import 'modules/fledgeForGpt.js'; +import 'modules/paapi.js'; import * as redactor from 'src/activities/redactor.js'; import * as activityRules from 'src/activities/rules.js'; import {hook} from '../../../src/hook.js'; import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; -import {addComponentAuction, registerBidder} from 'src/adapters/bidderFactory.js'; +import {addPaapiConfig, registerBidder} from 'src/adapters/bidderFactory.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; -import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {addFPDToBidderRequest} from '../../helpers/fpd.js'; import {deepSetValue} from '../../../src/utils.js'; import {ACTIVITY_TRANSMIT_UFPD} from '../../../src/activities/activities.js'; import {MODULE_TYPE_PREBID} from '../../../src/activities/modules.js'; +import { + consolidateEids, + extractEids, + getPBSBidderConfig +} from '../../../modules/prebidServerBidAdapter/bidderConfig.js'; +import {markWinningBid} from '../../../src/adRendering.js'; let CONFIG = { accountId: '1', enabled: true, bidders: ['appnexus'], - timeout: 1000, cacheMarkup: 2, endpoint: { p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', @@ -550,16 +556,48 @@ const RESPONSE_OPENRTB_NATIVE = { ] }; -function addFpdEnrichmentsToS2SRequest(s2sReq, bidderRequests) { +async function addFpdEnrichmentsToS2SRequest(s2sReq, bidderRequests) { return { ...s2sReq, ortb2Fragments: { ...(s2sReq.ortb2Fragments || {}), - global: syncAddFPDToBidderRequest({...(bidderRequests?.[0] || {}), ortb2: s2sReq.ortb2Fragments?.global || {}}).ortb2 + global: (await addFPDToBidderRequest({ + ...(bidderRequests?.[0] || {}), + ortb2: s2sReq.ortb2Fragments?.global || {} + })).ortb2 } } } +describe('s2s configuration', () => { + let cfg1, cfg2; + beforeEach(() => { + cfg1 = { + enabled: true, + bidders: ['bidderB'], + accountId: '123456', + endpoint: { + p1Consent: 'first.endpoint' + } + }; + cfg2 = { + enabled: true, + bidders: ['bidderA'], + accountId: '123456', + endpoint: { + p1Consent: 'second.endpoint', + } + }; + }) + it('sets prebid server adapter by default', () => { + expect(validateConfig(cfg1)[0].adapter).to.eql('prebidServer'); + }); + it('filters out disabled configs', () => { + cfg1.enabled = false; + expect(validateConfig([cfg1, cfg2])).to.eql([cfg2]); + }) +}); + describe('S2S Adapter', function () { let adapter, addBidResponse = sinon.spy(), @@ -763,12 +801,82 @@ describe('S2S Adapter', function () { }); }) - it('should set tmax to s2sConfig.timeout', () => { - const cfg = {...CONFIG, timeout: 123}; - config.setConfig({s2sConfig: cfg}); - adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); + it('should set tmaxmax correctly when publisher has specified it', () => { + const cfg = {...CONFIG}; + config.setConfig({s2sConfig: cfg}) + + // publisher has specified a tmaxmax in their setup + const ortb2Fragments = { + global: { + ext: { + tmaxmax: 4242 + } + } + }; + const s2sCfg = {...REQUEST, cfg} + const payloadWithFragments = { ...s2sCfg, ortb2Fragments }; + + adapter.callBids(payloadWithFragments, BID_REQUESTS, addBidResponse, done, ajax); + const req = JSON.parse(server.requests[0].requestBody); + + expect(req.ext.tmaxmax).to.eql(4242); + }); + + it('should set tmaxmax correctly when publisher has not specified it', () => { + const cfg = {...CONFIG}; + config.setConfig({s2sConfig: cfg}) + + // publisher has not specified a tmaxmax in their setup - so we should be + // falling back to requestBidsTimeout + const ortb2Fragments = {}; + const s2sCfg = {...REQUEST, cfg}; + const requestBidsTimeout = 808; + const payloadWithFragments = { ...s2sCfg, ortb2Fragments, requestBidsTimeout }; + + adapter.callBids(payloadWithFragments, BID_REQUESTS, addBidResponse, done, ajax); const req = JSON.parse(server.requests[0].requestBody); - expect(req.tmax).to.eql(123); + + expect(req.ext.tmaxmax).to.eql(808); + }); + + describe('default tmax', () => { + [null, 3000].forEach(maxTimeout => { + describe(`when maxTimeout is ${maxTimeout}`, () => { + let cfg; + + beforeEach(() => { + cfg = {accountId: '1', endpoint: 'mock-endpoint', maxTimeout}; + config.setConfig({s2sConfig: cfg}); + maxTimeout = maxTimeout ?? s2sDefaultConfig.maxTimeout + }); + + it('should cap tmax to maxTimeout', () => { + adapter.callBids({...REQUEST, requestBidsTimeout: maxTimeout * 2, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); + const req = JSON.parse(server.requests[0].requestBody); + expect(req.tmax).to.eql(maxTimeout); + }); + + it('should be set to 0.75 * requestTimeout, if lower than maxTimeout', () => { + adapter.callBids({...REQUEST, requestBidsTimeout: maxTimeout / 2}, BID_REQUESTS, addBidResponse, done, ajax); + const req = JSON.parse(server.requests[0].requestBody); + expect(req.tmax).to.eql(maxTimeout / 2 * 0.75); + }) + }) + }) + }) + + it('should set customHeaders correctly when publisher has provided it', () => { + let configWithCustomHeaders = utils.deepClone(CONFIG); + configWithCustomHeaders.customHeaders = { customHeader1: 'customHeader1Value' }; + config.setConfig({ s2sConfig: configWithCustomHeaders }); + + let reqWithNewConfig = utils.deepClone(REQUEST); + reqWithNewConfig.s2sConfig = configWithCustomHeaders; + + adapter.callBids(reqWithNewConfig, BID_REQUESTS, addBidResponse, done, ajax); + const reqHeaders = server.requests[0].requestHeaders + expect(reqHeaders.customHeader1).to.exist; + expect(reqHeaders.customHeader1).to.equal('customHeader1Value'); }); it('should block request if config did not define p1Consent URL in endpoint object config', function () { @@ -836,22 +944,6 @@ describe('S2S Adapter', function () { expect(requestBid.imp[0].video).to.exist; }); - it('should default video placement if not defined and instream', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint.p1Consent = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - - config.setConfig({ s2sConfig: ortb2Config }); - - let videoBid = utils.deepClone(VIDEO_REQUEST); - videoBid.ad_units[0].mediaTypes.video.context = 'instream'; - adapter.callBids(videoBid, BID_REQUESTS, addBidResponse, done, ajax); - - const requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.imp[0].banner).to.not.exist; - expect(requestBid.imp[0].video).to.exist; - expect(requestBid.imp[0].video.placement).to.equal(1); - }); - it('converts video mediaType properties into openRTB format', function () { let ortb2Config = utils.deepClone(CONFIG); ortb2Config.endpoint.p1Consent = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; @@ -865,7 +957,6 @@ describe('S2S Adapter', function () { const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.imp[0].banner).to.not.exist; expect(requestBid.imp[0].video).to.exist; - expect(requestBid.imp[0].video.placement).to.equal(1); expect(requestBid.imp[0].video.w).to.equal(640); expect(requestBid.imp[0].video.h).to.equal(480); expect(requestBid.imp[0].video.playerSize).to.be.undefined; @@ -890,31 +981,31 @@ describe('S2S Adapter', function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); }); - it('adds gdpr consent information to ortb2 request depending on presence of module', function () { - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: CONFIG }; + it('adds gdpr consent information to ortb2 request depending on presence of module', async function () { + let consentConfig = {consentManagement: {cmpApi: 'iab'}, s2sConfig: CONFIG}; config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); gdprBidRequest[0].gdprConsent = mockTCF(); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); + adapter.callBids(await addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.gdpr).is.equal(1); expect(requestBid.user.ext.consent).is.equal('mockConsent'); config.resetConfig(); - config.setConfig({ s2sConfig: CONFIG }); + config.setConfig({s2sConfig: CONFIG}); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(await addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); requestBid = JSON.parse(server.requests[1].requestBody); - expect(requestBid.regs).to.not.exist; - expect(requestBid.user).to.not.exist; + expect(requestBid.regs?.ext?.gdpr).to.not.exist; + expect(requestBid.user?.ext?.consent).to.not.exist; }); - it('adds additional consent information to ortb2 request depending on presence of module', function () { - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: CONFIG }; + it('adds additional consent information to ortb2 request depending on presence of module', async function () { + let consentConfig = {consentManagement: {cmpApi: 'iab'}, s2sConfig: CONFIG}; config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); @@ -922,7 +1013,7 @@ describe('S2S Adapter', function () { addtlConsent: 'superduperconsent', }); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); + adapter.callBids(await addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.gdpr).is.equal(1); @@ -930,7 +1021,7 @@ describe('S2S Adapter', function () { expect(requestBid.user.ext.ConsentedProvidersSettings.consented_providers).is.equal('superduperconsent'); config.resetConfig(); - config.setConfig({ s2sConfig: CONFIG }); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); requestBid = JSON.parse(server.requests[1].requestBody); @@ -945,24 +1036,24 @@ describe('S2S Adapter', function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); }); - it('is added to ortb2 request when in FPD', function () { - config.setConfig({ s2sConfig: CONFIG }); + it('is added to ortb2 request when in FPD', async function () { + config.setConfig({s2sConfig: CONFIG}); let uspBidRequest = utils.deepClone(BID_REQUESTS); uspBidRequest[0].uspConsent = '1NYN'; - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, uspBidRequest), uspBidRequest, addBidResponse, done, ajax); + adapter.callBids(await addFpdEnrichmentsToS2SRequest(REQUEST, uspBidRequest), uspBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.us_privacy).is.equal('1NYN'); config.resetConfig(); - config.setConfig({ s2sConfig: CONFIG }); + config.setConfig({s2sConfig: CONFIG}); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(await addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); requestBid = JSON.parse(server.requests[1].requestBody); - expect(requestBid.regs).to.not.exist; + expect(requestBid.regs?.ext?.us_privacy).to.not.exist; }); }); @@ -971,14 +1062,14 @@ describe('S2S Adapter', function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); }); - it('is added to ortb2 request when in bidRequest', function () { - config.setConfig({ s2sConfig: CONFIG }); + it('is added to ortb2 request when in bidRequest', async function () { + config.setConfig({s2sConfig: CONFIG}); let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1NYN'; consentBidRequest[0].gdprConsent = mockTCF(); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, consentBidRequest), consentBidRequest, addBidResponse, done, ajax); + adapter.callBids(await addFpdEnrichmentsToS2SRequest(REQUEST, consentBidRequest), consentBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.us_privacy).is.equal('1NYN'); @@ -986,7 +1077,7 @@ describe('S2S Adapter', function () { expect(requestBid.user.ext.consent).is.equal('mockConsent'); config.resetConfig(); - config.setConfig({ s2sConfig: CONFIG }); + config.setConfig({s2sConfig: CONFIG}); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); requestBid = JSON.parse(server.requests[1].requestBody); @@ -1018,51 +1109,62 @@ describe('S2S Adapter', function () { }); }); - it('adds device and app objects to request', function () { + it('adds device and app objects to request', async function () { const _config = { s2sConfig: CONFIG, - device: { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC' }, - app: { bundle: 'com.test.app' }, }; - config.setConfig(_config); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + const s2sreq = await addFpdEnrichmentsToS2SRequest({ + ...REQUEST, + ortb2Fragments: { + global: { + device: {ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC'}, + app: {bundle: 'com.test.app'}, + } + } + }, BID_REQUESTS) + adapter.callBids(s2sreq, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC', - w: window.innerWidth, - h: window.innerHeight + w: window.screen.width, + h: window.screen.height, }) sinon.assert.match(requestBid.app, { bundle: 'com.test.app', - publisher: { 'id': '1' } + publisher: {'id': '1'} }); }); - it('adds device and app objects to request for OpenRTB', function () { + it('adds device and app objects to request for OpenRTB', async function () { const s2sConfig = Object.assign({}, CONFIG, { endpoint: { p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' } }); - const _config = { s2sConfig: s2sConfig, - device: { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC' }, - app: { bundle: 'com.test.app' }, }; - config.setConfig(_config); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + const s2sReq = await addFpdEnrichmentsToS2SRequest({ + ...REQUEST, + ortb2Fragments: { + global: { + device: {ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC'}, + app: {bundle: 'com.test.app'}, + } + } + }, BID_REQUESTS) + adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC', - w: window.innerWidth, - h: window.innerHeight + w: window.screen.width, + h: window.screen.height, }) sinon.assert.match(requestBid.app, { bundle: 'com.test.app', - publisher: { 'id': '1' } + publisher: {'id': '1'} }); }); @@ -1304,65 +1406,97 @@ describe('S2S Adapter', function () { updateBid(BID_REQUESTS[1].bids[0]); adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); const pbsReq = JSON.parse(server.requests[server.requests.length - 1].requestBody); - expect(pbsReq.imp[0].bidfloor).to.be.undefined; - expect(pbsReq.imp[0].bidfloorcur).to.be.undefined; + [pbsReq.imp[0], pbsReq.imp[0].banner, pbsReq.imp[0].banner.format[0]].forEach(obj => { + expect(obj.bidfloor).to.be.undefined; + expect(obj.bidfloorcur).to.be.undefined; + }) }); }) Object.entries({ - 'is available': { - expectDesc: 'minimum after conversion', - expectedFloor: 10, - expectedCur: '0.1', - conversionFn: (amount, from, to) => { - from = parseFloat(from); - to = parseFloat(to); - return amount * from / to; - }, + 'imp level floors': { + target: 'imp.0' }, - 'is not available': { - expectDesc: 'absolute minimum', - expectedFloor: 1, - expectedCur: '10', - conversionFn: null + 'mediaType level floors': { + target: 'imp.0.banner.ext', + floorFilter: ({mediaType, size}) => size === '*' && mediaType !== '*' }, - 'is not working': { - expectDesc: 'absolute minimum', - expectedFloor: 1, - expectedCur: '10', - conversionFn: () => { - throw new Error(); - } + 'format level floors': { + target: 'imp.0.banner.format.0.ext', + floorFilter: ({size}) => size !== '*' } - }).forEach(([t, {expectDesc, expectedFloor, expectedCur, conversionFn}]) => { - describe(`and currency conversion ${t}`, () => { - let mockConvertCurrency; - const origConvertCurrency = getGlobal().convertCurrency; + }).forEach(([t, {target, floorFilter}]) => { + describe(t, () => { beforeEach(() => { - if (conversionFn) { - getGlobal().convertCurrency = mockConvertCurrency = sinon.stub().callsFake(conversionFn) - } else { - mockConvertCurrency = null; - delete getGlobal().convertCurrency; + if (floorFilter != null) { + BID_REQUESTS + .flatMap(req => req.bids) + .forEach(req => { + req.getFloor = ((orig) => (params) => { + if (floorFilter(params)) { + return orig(params); + } + })(req.getFloor); + }) } - }); + }) - afterEach(() => { - if (origConvertCurrency != null) { - getGlobal().convertCurrency = origConvertCurrency; - } else { - delete getGlobal().convertCurrency; + Object.entries({ + 'is available': { + expectDesc: 'minimum after conversion', + expectedFloor: 10, + expectedCur: '0.1', + conversionFn: (amount, from, to) => { + from = parseFloat(from); + to = parseFloat(to); + return amount * from / to; + }, + }, + 'is not available': { + expectDesc: 'absolute minimum', + expectedFloor: 1, + expectedCur: '10', + conversionFn: null + }, + 'is not working': { + expectDesc: 'absolute minimum', + expectedFloor: 1, + expectedCur: '10', + conversionFn: () => { + throw new Error(); + } } - }); + }).forEach(([t, {expectDesc, expectedFloor, expectedCur, conversionFn}]) => { + describe(`and currency conversion ${t}`, () => { + let mockConvertCurrency; + const origConvertCurrency = getGlobal().convertCurrency; + beforeEach(() => { + if (conversionFn) { + getGlobal().convertCurrency = mockConvertCurrency = sinon.stub().callsFake(conversionFn) + } else { + mockConvertCurrency = null; + delete getGlobal().convertCurrency; + } + }); - it(`should pick the ${expectDesc}`, () => { - adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); - const pbsReq = JSON.parse(server.requests[server.requests.length - 1].requestBody); - expect(pbsReq.imp[0].bidfloor).to.eql(expectedFloor); - expect(pbsReq.imp[0].bidfloorcur).to.eql(expectedCur); + afterEach(() => { + if (origConvertCurrency != null) { + getGlobal().convertCurrency = origConvertCurrency; + } else { + delete getGlobal().convertCurrency; + } + }); + + it(`should pick the ${expectDesc}`, () => { + adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); + const pbsReq = JSON.parse(server.requests[server.requests.length - 1].requestBody); + expect(deepAccess(pbsReq, `${target}.bidfloor`)).to.eql(expectedFloor); + expect(deepAccess(pbsReq, `${target}.bidfloorcur`)).to.eql(expectedCur) + }); + }); }); - }); - }); + }) + }) }); }); @@ -1409,23 +1543,17 @@ describe('S2S Adapter', function () { ] }; - it('adds device.w and device.h even if the config lacks a device object', function () { + it('adds device.w and device.h even if the config lacks a device object', async function () { const _config = { s2sConfig: CONFIG, - app: { bundle: 'com.test.app' }, }; - config.setConfig(_config); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + 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.innerWidth, - h: window.innerHeight + w: window.screen.width, + h: window.screen.height, }) - sinon.assert.match(requestBid.app, { - bundle: 'com.test.app', - publisher: { 'id': '1' } - }); expect(requestBid.imp[0].native.ver).to.equal('1.2'); }); @@ -1506,22 +1634,29 @@ describe('S2S Adapter', function () { }); } - it('adds site if app is not present', function () { + it('adds site if app is not present', async function () { const _config = { s2sConfig: CONFIG, - site: { - publisher: { - id: '1234', - domain: 'test.com' - }, - content: { - language: 'en' - } - } }; config.setConfig(_config); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + const s2sReq = await addFpdEnrichmentsToS2SRequest({ + ...REQUEST, + ortb2Fragments: { + global: { + site: { + publisher: { + id: '1234', + domain: 'test.com' + }, + content: { + language: 'en' + } + } + } + } + }, BID_REQUESTS); + adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); expect(requestBid.site.publisher).to.exist.and.to.be.a('object'); @@ -1543,26 +1678,34 @@ describe('S2S Adapter', function () { }); }); - it('site should not be present when app is present', function () { + it('site should not be present when app is present', async function () { const _config = { s2sConfig: CONFIG, - app: { bundle: 'com.test.app' }, - site: { - publisher: { - id: '1234', - domain: 'test.com' - }, - content: { - language: 'en' - } - } }; config.setConfig(_config); - adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + + const s2sReq = await addFpdEnrichmentsToS2SRequest({ + ...REQUEST, + ortb2Fragments: { + global: { + app: {bundle: 'com.test.app'}, + site: { + publisher: { + id: '1234', + domain: 'test.com' + }, + content: { + language: 'en' + } + } + } + } + }, BID_REQUESTS) + adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.not.exist; - expect(requestBid.app).to.exist.and.to.be.a('object'); + expect(requestBid.app.bundle).to.eql('com.test.app'); }); it('adds appnexus aliases to request', function () { @@ -1726,39 +1869,6 @@ describe('S2S Adapter', function () { }); }); - it('converts appnexus params to expected format for PBS', function () { - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: { - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - } - }); - config.setConfig({ s2sConfig: s2sConfig }); - - Object.assign(BID_REQUESTS[0].bids[0].params, { - usePaymentRule: true, - keywords: { - foo: ['bar', 'baz'], - fizz: ['buzz'] - } - }) - - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - const requestBid = JSON.parse(server.requests[0].requestBody); - - const requestParams = requestBid.imp[0].ext.prebid.bidder; - expect(requestParams.appnexus).to.exist; - expect(requestParams.appnexus.placement_id).to.exist.and.to.equal(10433394); - expect(requestParams.appnexus.use_pmt_rule).to.exist.and.to.be.true; - expect(requestParams.appnexus.member).to.exist; - expect(requestParams.appnexus.keywords).to.exist.and.to.deep.equal([{ - key: 'foo', - value: ['bar', 'baz'] - }, { - key: 'fizz', - value: ['buzz'] - }]); - }); - describe('cookie sync', () => { let s2sConfig, bidderReqs; @@ -1973,18 +2083,24 @@ describe('S2S Adapter', function () { const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = s2sConfig; - it('and overrides publisher and page', function () { + it('and overrides publisher and page', async function () { config.setConfig({ s2sConfig: s2sConfig, - site: { - domain: 'nytimes.com', - page: 'http://www.nytimes.com', - publisher: { id: '2' } - }, - device: device }); - - adapter.callBids(addFpdEnrichmentsToS2SRequest(s2sBidRequest, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + const s2sReq = await addFpdEnrichmentsToS2SRequest({ + ...s2sBidRequest, + ortb2Fragments: { + global: { + site: { + domain: 'nytimes.com', + page: 'http://www.nytimes.com', + publisher: {id: '2'} + }, + device, + } + } + }, BID_REQUESTS); + adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); @@ -1994,16 +2110,22 @@ describe('S2S Adapter', function () { expect(requestBid.site.publisher.id).to.equal('2'); }); - it('and merges domain and page with the config site value', function () { + it('and merges domain and page with the config site value', async function () { config.setConfig({ s2sConfig: s2sConfig, - site: { - foo: 'bar' - }, - device: device }); - - adapter.callBids(addFpdEnrichmentsToS2SRequest(s2sBidRequest, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + const s2sReq = await addFpdEnrichmentsToS2SRequest({ + ...s2sBidRequest, + ortb2Fragments: { + global: { + site: { + foo: 'bar' + }, + device: device + } + } + }, BID_REQUESTS); + adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); @@ -2014,35 +2136,112 @@ describe('S2S Adapter', function () { }); }); - it('when userId is defined on bids, it\'s properties should be copied to user.ext.tpid properties', function () { - let consentConfig = { s2sConfig: CONFIG }; - config.setConfig(consentConfig); + describe('user.ext.eids', () => { + let req; + beforeEach(() => { + const s2sConfig = { + ...CONFIG, + bidders: ['appnexus', 'rubicon'] + } + config.setConfig({s2sConfig}); + req = { + ...REQUEST, + s2sConfig, + ortb2Fragments: { + global: { + user: { + ext: { + eids: [{source: 'idA', id: 1}, {source: 'idB', id: 2}] + } + } + }, + bidder: { + appnexus: { + user: { + ext: { + eids: [{source: 'idC', id: 3}] + } + } + } + } + } + } + }) + it('should get picked up from from FPD', function () { + adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); + const payload = JSON.parse(server.requests[0].requestBody); + expect(payload.user.ext.eids).to.eql([ + {source: 'idA', id: 1}, + {source: 'idB', id: 2}, + {source: 'idC', id: 3} + ]); + expect(payload.ext.prebid.data.eidpermissions).to.eql([{ + bidders: ['appnexus'], + source: 'idC' + }]); + }); - let userIdBidRequest = utils.deepClone(BID_REQUESTS); - userIdBidRequest[0].bids[0].userId = { - criteoId: '44VmRDeUE3ZGJ5MzRkRVJHU3BIUlJ6TlFPQUFU', - tdid: 'abc123', - pubcid: '1234', - parrableId: { eid: '01.1563917337.test-eid' }, - lipb: { - lipbid: 'li-xyz', - segments: ['segA', 'segB'] - }, - idl_env: '0000-1111-2222-3333', - id5id: { - uid: '11111', - ext: { - linkType: 'some-link-type' + it('should not set eidpermissions for unrequested bidders', () => { + req.ortb2Fragments.bidder.unknown = { + user: { + eids: [{source: 'idC', id: 3}, {source: 'idD', id: 4}] } } - }; - userIdBidRequest[0].bids[0].userIdAsEids = [{id: 1}, {id: 2}]; + adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); + const payload = JSON.parse(server.requests[0].requestBody); + expect(payload.ext.prebid.data.eidpermissions).to.eql([{ + bidders: ['appnexus'], + source: 'idC' + }, { + bidders: [], + source: 'idD' + }]); + }); - adapter.callBids(REQUEST, userIdBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - expect(typeof requestBid.user.ext.eids).is.equal('object'); - expect(requestBid.user.ext.eids).to.eql([{id: 1}, {id: 2}]); - }); + it('should repeat global EIDs when bidder-specific EIDs conflict', () => { + BID_REQUESTS.push({ + ...BID_REQUESTS[0], + bidderCode: 'rubicon', + bids: [{ + bidder: 'rubicon', + params: {} + }] + }) + req.ortb2Fragments.bidder.rubicon = { + user: { + ext: { + eids: [{source: 'idC', id: 4}] + } + } + } + adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); + const payload = JSON.parse(server.requests[0].requestBody); + const globalEids = [ + {source: 'idA', id: 1}, + {source: 'idB', id: 2}, + ] + expect(payload.user.ext.eids).to.eql(globalEids); + expect(payload.ext.prebid?.data?.eidpermissions).to.not.exist; + expect(payload.ext.prebid.bidderconfig).to.have.deep.members([ + { + bidders: ['appnexus'], + config: { + ortb2: { + user: {ext: {eids: globalEids.concat([{source: 'idC', id: 3}])}} + } + } + }, + { + bidders: ['rubicon'], + config: { + ortb2: { + user: {ext: {eids: globalEids.concat([{source: 'idC', id: 4}])}} + } + } + } + ]) + }) + }) it('when config \'currency.adServerCurrency\' value is a string: ORTB has property \'cur\' value set to a single item array', function () { config.setConfig({ @@ -2377,23 +2576,28 @@ describe('S2S Adapter', function () { ]); }); - it('should "promote" the most reused bidder schain to source.ext.schain', () => { - const bidderReqs = [ - {...deepClone(BID_REQUESTS[0]), bidderCode: 'A'}, - {...deepClone(BID_REQUESTS[0]), bidderCode: 'B'}, - {...deepClone(BID_REQUESTS[0]), bidderCode: 'C'} - ]; - const chain1 = {chain: 1}; - const chain2 = {chain: 2}; + Object.entries({ + 'set': {}, + 'override': {source: {ext: {schain: 'pub-provided'}}} + }).forEach(([t, fpd]) => { + it(`should not ${t} source.ext.schain`, () => { + const bidderReqs = [ + {...deepClone(BID_REQUESTS[0]), bidderCode: 'A'}, + {...deepClone(BID_REQUESTS[0]), bidderCode: 'B'}, + {...deepClone(BID_REQUESTS[0]), bidderCode: 'C'} + ]; + const chain1 = {chain: 1}; + const chain2 = {chain: 2}; - bidderReqs[0].bids[0].schain = chain1; - bidderReqs[1].bids[0].schain = chain2; - bidderReqs[2].bids[0].schain = chain2; + bidderReqs[0].bids[0].schain = chain1; + bidderReqs[1].bids[0].schain = chain2; + bidderReqs[2].bids[0].schain = chain2; - adapter.callBids(REQUEST, bidderReqs, addBidResponse, done, ajax); - const req = JSON.parse(server.requests[0].requestBody); - expect(req.source.ext.schain).to.eql(chain2); - }); + adapter.callBids({...REQUEST, ortb2Fragments: {global: fpd}}, bidderReqs, addBidResponse, done, ajax); + const req = JSON.parse(server.requests[0].requestBody); + expect(req.source?.ext?.schain).to.eql(fpd?.source?.ext?.schain); + }) + }) it('passes multibid array in request', function () { const bidRequests = utils.deepClone(BID_REQUESTS); @@ -2447,7 +2651,7 @@ describe('S2S Adapter', function () { }); }); - it('passes first party data in request', () => { + it('passes first party data in request', async () => { const s2sBidRequest = utils.deepClone(REQUEST); const bidRequests = utils.deepClone(BID_REQUESTS); @@ -2471,7 +2675,7 @@ describe('S2S Adapter', function () { }; const user = { yob: '1984', - geo: { country: 'ca' }, + geo: {country: 'ca'}, ext: { data: { registered: true, @@ -2488,7 +2692,7 @@ describe('S2S Adapter', function () { config: { ortb2: { site: { - content: { userrating: 4 }, + content: {userrating: 4}, ext: { data: { pageType: 'article', @@ -2498,7 +2702,7 @@ describe('S2S Adapter', function () { }, user: { yob: '1984', - geo: { country: 'ca' }, + geo: {country: 'ca'}, ext: { data: { registered: true, @@ -2525,7 +2729,10 @@ describe('S2S Adapter', function () { bidder: Object.fromEntries(allowedBidders.map(bidder => [bidder, {site, user, bcat, badv}])) }; - adapter.callBids(addFpdEnrichmentsToS2SRequest({...s2sBidRequest, ortb2Fragments}, bidRequests), bidRequests, addBidResponse, done, ajax); + adapter.callBids(await addFpdEnrichmentsToS2SRequest({ + ...s2sBidRequest, + ortb2Fragments + }, bidRequests), bidRequests, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); expect(parsedRequestBody.ext.prebid.bidderconfig).to.deep.equal(expected); expect(parsedRequestBody.site).to.deep.equal(commonContextExpected); @@ -2534,9 +2741,9 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.bcat).to.deep.equal(bcat); }); - it('passes first party data in request for unknown when allowUnknownBidderCodes is true', () => { - const cfg = { ...CONFIG, allowUnknownBidderCodes: true }; - config.setConfig({ s2sConfig: cfg }); + it('passes first party data in request for unknown when allowUnknownBidderCodes is true', async () => { + const cfg = {...CONFIG, allowUnknownBidderCodes: true}; + config.setConfig({s2sConfig: cfg}); const clonedReq = {...REQUEST, s2sConfig: cfg} const s2sBidRequest = utils.deepClone(clonedReq); @@ -2562,7 +2769,7 @@ describe('S2S Adapter', function () { }; const user = { yob: '1984', - geo: { country: 'ca' }, + geo: {country: 'ca'}, ext: { data: { registered: true, @@ -2579,7 +2786,7 @@ describe('S2S Adapter', function () { config: { ortb2: { site: { - content: { userrating: 4 }, + content: {userrating: 4}, ext: { data: { pageType: 'article', @@ -2589,7 +2796,7 @@ describe('S2S Adapter', function () { }, user: { yob: '1984', - geo: { country: 'ca' }, + geo: {country: 'ca'}, ext: { data: { registered: true, @@ -2618,7 +2825,10 @@ describe('S2S Adapter', function () { // adapter.callBids({ ...REQUEST, s2sConfig: cfg }, BID_REQUESTS, addBidResponse, done, ajax); - adapter.callBids(addFpdEnrichmentsToS2SRequest({...s2sBidRequest, ortb2Fragments}, bidRequests, cfg), bidRequests, addBidResponse, done, ajax); + adapter.callBids(await addFpdEnrichmentsToS2SRequest({ + ...s2sBidRequest, + ortb2Fragments + }, bidRequests, cfg), bidRequests, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); // eslint-disable-next-line no-console console.log(parsedRequestBody); @@ -3060,8 +3270,8 @@ describe('S2S Adapter', function () { adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); - sinon.assert.calledOnce(events.emit); - const event = events.emit.firstCall.args; + sinon.assert.calledTwice(events.emit); + const event = events.emit.secondCall.args; expect(event[0]).to.equal(EVENTS.BIDDER_DONE); expect(event[1].bids[0]).to.have.property('serverResponseTimeMs', 8); @@ -3092,13 +3302,60 @@ describe('S2S Adapter', function () { const responding = deepClone(nonbidResponse); Object.assign(responding.ext.seatnonbid, [{auctionId: 2}]) server.requests[0].respond(200, {}, JSON.stringify(responding)); - const event = events.emit.secondCall.args; + const event = events.emit.thirdCall.args; expect(event[0]).to.equal(EVENTS.SEAT_NON_BID); expect(event[1].seatnonbid[0]).to.have.property('auctionId', 2); expect(event[1].requestedBidders).to.deep.equal(['appnexus']); expect(event[1].response).to.deep.equal(responding); }); + it('emits the PBS_ANALYTICS event and captures seatnonbid responses', function () { + const original = CONFIG; + CONFIG.extPrebid = { returnallbidstatus: true }; + const nonbidResponse = {...RESPONSE_OPENRTB, ext: {seatnonbid: [{}]}}; + config.setConfig({ CONFIG }); + CONFIG = original; + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const responding = deepClone(nonbidResponse); + Object.assign(responding.ext.seatnonbid, [{auctionId: 2}]) + server.requests[0].respond(200, {}, JSON.stringify(responding)); + const event = events.emit.getCall(3).args; + expect(event[0]).to.equal(EVENTS.PBS_ANALYTICS); + expect(event[1].seatnonbid[0]).to.have.property('auctionId', 2); + expect(event[1].requestedBidders).to.deep.equal(['appnexus']); + expect(event[1].response).to.deep.equal(responding); + }); + + it('emits the PBS_ANALYTICS event and captures atag responses', function () { + const original = CONFIG; + CONFIG.extPrebid = { returnallbidstatus: true }; + const atagResponse = {...RESPONSE_OPENRTB, ext: {prebid: {analytics: {tags: ['data']}}}}; + config.setConfig({ CONFIG }); + CONFIG = original; + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const responding = deepClone(atagResponse); + Object.assign(responding.ext.prebid.analytics.tags, ['stuff']) + server.requests[0].respond(200, {}, JSON.stringify(responding)); + const event = events.emit.thirdCall.args; + expect(event[0]).to.equal(EVENTS.PBS_ANALYTICS); + expect(event[1].atag[0]).to.deep.equal('stuff'); + expect(event[1].response).to.deep.equal(responding); + }); + + it('emits the BEFORE_PBS_HTTP event and captures responses', function () { + config.setConfig({ CONFIG }); + + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); + + sinon.assert.calledTwice(events.emit); + const event = events.emit.firstCall.args; + expect(event[0]).to.equal(EVENTS.BEFORE_PBS_HTTP); + expect(event[1]).to.have.property('requestJson', server.requests[0].requestBody); + expect(event[1]).to.have.property('endpointUrl', CONFIG.endpoint.p1Consent); + expect(event[1].customHeaders).to.deep.equal({}); + }); + it('respects defaultTtl', function () { const s2sConfig = Object.assign({}, CONFIG, { defaultTtl: 30 @@ -3110,7 +3367,7 @@ describe('S2S Adapter', function () { adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); - sinon.assert.calledOnce(events.emit); + sinon.assert.calledTwice(events.emit); const event = events.emit.firstCall.args; sinon.assert.calledOnce(addBidResponse); const response = addBidResponse.firstCall.args[1]; @@ -3419,6 +3676,18 @@ describe('S2S Adapter', function () { expect(response).to.have.property('adapterCode', 'appnexus2'); }); + it('should set deferBilling and deferRendering to true when request has deferBilling = true', () => { + config.setConfig({ CONFIG }); + const req = deepClone(REQUEST); + req.ad_units.forEach(au => au.deferBilling = true); + adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); + sinon.assert.match(addBidResponse.firstCall.args[1], { + deferBilling: true, + deferRendering: true + }); + }); + describe('on sync requested with no cookie', () => { let cfg, req, csRes; @@ -3482,21 +3751,24 @@ describe('S2S Adapter', function () { } before(() => { - addComponentAuction.before(fledgeHook); + addPaapiConfig.before(fledgeHook); }); after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); + addPaapiConfig.getHooks({hook: fledgeHook}).remove(); }) beforeEach(function () { fledgeStub = sinon.stub(); - config.setConfig({CONFIG}); + config.setConfig({ + s2sConfig: CONFIG, + }); bidderRequests = deepClone(BID_REQUESTS); - AU bidderRequests.forEach(req => { Object.assign(req, { - fledgeEnabled: true, + paapi: { + enabled: true + }, ortb2: { fpd: 1 } @@ -3504,7 +3776,7 @@ describe('S2S Adapter', function () { req.bids.forEach(bid => { Object.assign(bid, { ortb2Imp: { - fpd: 2 + fpd: 2, } }) }) @@ -3515,27 +3787,41 @@ describe('S2S Adapter', function () { function expectFledgeCalls() { const auctionId = bidderRequests[0].auctionId; - sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: bidderRequests[0].ortb2, ortb2Imp: bidderRequests[0].bids[0].ortb2Imp}), {id: 1}) - sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: undefined, ortb2Imp: undefined}), {id: 2}) + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: bidderRequests[0].ortb2, ortb2Imp: bidderRequests[0].bids[0].ortb2Imp}), sinon.match({config: {id: 1}})) + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: undefined, ortb2Imp: undefined}), sinon.match({config: {id: 2}})) } - it('calls addComponentAuction alongside addBidResponse', function () { + it('calls addPaapiConfig alongside addBidResponse', function () { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(mergeDeep({}, RESPONSE_OPENRTB, FLEDGE_RESP))); expect(addBidResponse.called).to.be.true; expectFledgeCalls(); }); - it('calls addComponentAuction when there is no bid in the response', () => { + it('calls addPaapiConfig when there is no bid in the response', () => { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); expect(addBidResponse.called).to.be.false; expectFledgeCalls(); - }) - }); - }); + }); - describe('bid won events', function () { + it('wraps call in runWithBidder', () => { + let fail = false; + fledgeStub.callsFake(({bidder}) => { + try { + expect(bidder).to.exist.and.to.eql(config.getCurrentBidder()); + } catch (e) { + fail = true; + } + }); + adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); + expect(fail).to.be.false; + }) + }); + }); + + describe('bid won events', function () { let uniqueIdCount = 0; let triggerPixelStub; const staticUniqueIds = ['1000', '1001', '1002', '1003']; @@ -3545,7 +3831,6 @@ describe('S2S Adapter', function () { }); beforeEach(function () { - resetWurlMap(); sinon.stub(utils, 'insertUserSyncIframe'); sinon.stub(utils, 'logError'); sinon.stub(utils, 'getUniqueIdentifierStr').callsFake(() => { @@ -3575,26 +3860,28 @@ describe('S2S Adapter', function () { triggerPixelStub.restore(); }); - it('should call triggerPixel if wurl is defined', function () { - const clonedResponse = utils.deepClone(RESPONSE_OPENRTB); - clonedResponse.seatbid[0].bid[0].ext.prebid.events = { - win: 'https://wurl.org' - }; + it('should translate wurl and burl into eventtrackers', () => { + const burlEvent = {event: 1, method: 1, url: 'burl'}; + const winEvent = {event: 500, method: 1, url: 'events.win'}; + const trackerEvent = {event: 500, method: 1, url: 'eventtracker'}; + const resp = utils.deepClone(RESPONSE_OPENRTB); + resp.seatbid[0].bid[0].ext.eventtrackers = [ + trackerEvent, + burlEvent + ] + resp.seatbid[0].bid[0].ext.prebid.events = { + win: winEvent.url + }; + resp.seatbid[0].bid[0].burl = burlEvent.url; adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - server.requests[0].respond(200, {}, JSON.stringify(clonedResponse)); - - events.emit(EVENTS.BID_WON, { - auctionId: '173afb6d132ba3', - adId: '1000' - }); - - sinon.assert.calledOnce(addBidResponse); - expect(utils.triggerPixel.called).to.be.true; - expect(utils.triggerPixel.getCall(0).args[0]).to.include('https://wurl.org'); - }); + server.requests[0].respond(200, {}, JSON.stringify(resp)); + expect(addBidResponse.getCall(0).args[1].eventtrackers).to.have.deep.members([ + burlEvent, trackerEvent, winEvent + ]); + }) - it('should not call triggerPixel if the wurl cache does not contain the winning bid', function () { + it('should call triggerPixel if wurl is defined', function () { const clonedResponse = utils.deepClone(RESPONSE_OPENRTB); clonedResponse.seatbid[0].bid[0].ext.prebid.events = { win: 'https://wurl.org' @@ -3603,13 +3890,11 @@ describe('S2S Adapter', function () { adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(clonedResponse)); - events.emit(EVENTS.BID_WON, { - auctionId: '173afb6d132ba3', - adId: 'missingAdId' - }); + sinon.assert.calledOnce(addBidResponse); + markWinningBid(addBidResponse.getCall(0).args[1]); - sinon.assert.calledOnce(addBidResponse) - expect(utils.triggerPixel.called).to.be.false; + expect(utils.triggerPixel.called).to.be.true; + expect(utils.triggerPixel.getCall(0).args[0]).to.include('https://wurl.org'); }); it('should not call triggerPixel if wurl is undefined', function () { @@ -3619,12 +3904,8 @@ describe('S2S Adapter', function () { adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(clonedResponse)); - events.emit(EVENTS.BID_WON, { - auctionId: '173afb6d132ba3', - adId: '1060' - }); - - sinon.assert.calledOnce(addBidResponse) + sinon.assert.calledOnce(addBidResponse); + markWinningBid(addBidResponse.getCall(0).args[1]); expect(utils.triggerPixel.called).to.be.false; }); }) @@ -3679,181 +3960,135 @@ describe('S2S Adapter', function () { config.setConfig({ s2sConfig: options }); sinon.assert.calledOnce(logErrorSpy); }); + describe('vendor: appnexuspsp', () => { + it('should configure the s2sConfig object with appnexuspsp vendor defaults unless specified by user', function () { + const options = { + accountId: '123', + bidders: ['appnexus'], + defaultVendor: 'appnexuspsp', + timeout: 750 + }; - it('should configure the s2sConfig object with appnexuspsp vendor defaults unless specified by user', function () { - const options = { - accountId: '123', - bidders: ['appnexus'], - defaultVendor: 'appnexuspsp', - timeout: 750 - }; - - config.setConfig({ s2sConfig: options }); - sinon.assert.notCalled(logErrorSpy); - - let vendorConfig = config.getConfig('s2sConfig'); - expect(vendorConfig).to.have.property('accountId', '123'); - expect(vendorConfig).to.have.property('adapter', 'prebidServer'); - expect(vendorConfig.bidders).to.deep.equal(['appnexus']); - expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({ - p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', - noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' - }); - expect(vendorConfig.syncEndpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' + config.setConfig({ s2sConfig: options }); + sinon.assert.notCalled(logErrorSpy); + + let vendorConfig = config.getConfig('s2sConfig'); + expect(vendorConfig).to.have.property('accountId', '123'); + expect(vendorConfig).to.have.property('adapter', 'prebidServer'); + expect(vendorConfig.bidders).to.deep.equal(['appnexus']); + expect(vendorConfig.enabled).to.be.true; + expect(vendorConfig.endpoint).to.deep.equal({ + p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', + noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' + }); + expect(vendorConfig.syncEndpoint).to.deep.equal({ + p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' + }); + expect(vendorConfig).to.have.property('timeout', 750); }); - expect(vendorConfig).to.have.property('timeout', 750); - }); - - it('should configure the s2sConfig object with rubicon vendor defaults unless specified by user', function () { - const options = { - accountId: 'abc', - bidders: ['rubicon'], - defaultVendor: 'rubicon', - timeout: 750 - }; + }) - config.setConfig({ s2sConfig: options }); - sinon.assert.notCalled(logErrorSpy); - - let vendorConfig = config.getConfig('s2sConfig'); - expect(vendorConfig).to.have.property('accountId', 'abc'); - expect(vendorConfig).to.have.property('adapter', 'prebidServer'); - expect(vendorConfig.bidders).to.deep.equal(['rubicon']); - expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({ - p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', - noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction' - }); - expect(vendorConfig.syncEndpoint).to.deep.equal({ - p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', - noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync' - }); - expect(vendorConfig).to.have.property('timeout', 750); - }); + describe('vendor: rubicon', () => { + it('should configure the s2sConfig object with rubicon vendor defaults unless specified by user', function () { + const options = { + accountId: 'abc', + bidders: ['rubicon'], + defaultVendor: 'rubicon', + timeout: 750 + }; - it('should return proper defaults', function () { - const options = { - accountId: 'abc', - bidders: ['rubicon'], - defaultVendor: 'rubicon', - timeout: 750 - }; + config.setConfig({ s2sConfig: options }); + sinon.assert.notCalled(logErrorSpy); - config.setConfig({ s2sConfig: options }); - expect(config.getConfig('s2sConfig')).to.deep.equal({ - 'accountId': 'abc', - 'adapter': 'prebidServer', - 'bidders': ['rubicon'], - 'defaultVendor': 'rubicon', - 'enabled': true, - 'endpoint': { + let vendorConfig = config.getConfig('s2sConfig'); + expect(vendorConfig).to.have.property('accountId', 'abc'); + expect(vendorConfig).to.have.property('adapter', 'prebidServer'); + expect(vendorConfig.bidders).to.deep.equal(['rubicon']); + expect(vendorConfig.enabled).to.be.true; + expect(vendorConfig.endpoint).to.deep.equal({ p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction' - }, - 'syncEndpoint': { + }); + expect(vendorConfig.syncEndpoint).to.deep.equal({ p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync' - }, - 'timeout': 750 - }) - }); - - it('should return default adapterOptions if not set', function () { - config.setConfig({ - s2sConfig: { + }); + expect(vendorConfig).to.have.property('timeout', 750); + }); + it('should return proper defaults', function () { + const options = { accountId: 'abc', bidders: ['rubicon'], defaultVendor: 'rubicon', timeout: 750 - } - }); - expect(config.getConfig('s2sConfig')).to.deep.equal({ - enabled: true, - timeout: 750, - adapter: 'prebidServer', - accountId: 'abc', - bidders: ['rubicon'], - defaultVendor: 'rubicon', - endpoint: { - p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', - noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction' - }, - syncEndpoint: { - p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', - noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync' - }, - }) - }); - - it('should configure the s2sConfig object with openwrap vendor defaults unless specified by user', function () { - const options = { - accountId: '1234', - bidders: ['pubmatic'], - defaultVendor: 'openwrap' - }; + }; - config.setConfig({ s2sConfig: options }); - sinon.assert.notCalled(logErrorSpy); - - let vendorConfig = config.getConfig('s2sConfig'); - expect(vendorConfig).to.have.property('accountId', '1234'); - expect(vendorConfig).to.have.property('adapter', 'prebidServer'); - expect(vendorConfig.bidders).to.deep.equal(['pubmatic']); - expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({ - p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', - noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + config.setConfig({ s2sConfig: options }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + 'accountId': 'abc', + 'adapter': 'prebidServer', + 'bidders': ['rubicon'], + 'defaultVendor': 'rubicon', + 'enabled': true, + 'endpoint': { + p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', + noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction' + }, + 'syncEndpoint': { + p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', + noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync' + }, + 'timeout': 750, + maxTimeout: 500, + }) }); - expect(vendorConfig).to.have.property('timeout', 500); - }); + }) - it('should return proper defaults', function () { - const options = { - accountId: '1234', - bidders: ['pubmatic'], - defaultVendor: 'openwrap', - timeout: 500 - }; + describe('vendor: openwrap', () => { + it('should configure the s2sConfig object with openwrap vendor defaults unless specified by user', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap' + }; - config.setConfig({ s2sConfig: options }); - expect(config.getConfig('s2sConfig')).to.deep.equal({ - 'accountId': '1234', - 'adapter': 'prebidServer', - 'bidders': ['pubmatic'], - 'defaultVendor': 'openwrap', - 'enabled': true, - 'endpoint': { + config.setConfig({ s2sConfig: options }); + sinon.assert.notCalled(logErrorSpy); + + let vendorConfig = config.getConfig('s2sConfig'); + expect(vendorConfig).to.have.property('accountId', '1234'); + expect(vendorConfig).to.have.property('adapter', 'prebidServer'); + expect(vendorConfig.bidders).to.deep.equal(['pubmatic']); + expect(vendorConfig.enabled).to.be.true; + expect(vendorConfig.endpoint).to.deep.equal({ p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' - }, - 'timeout': 500 - }) - }); - - it('should return default adapterOptions if not set', function () { - config.setConfig({ - s2sConfig: { + }); + }); + it('should return proper defaults', function () { + const options = { accountId: '1234', bidders: ['pubmatic'], defaultVendor: 'openwrap', timeout: 500 - } + }; + + config.setConfig({ s2sConfig: options }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + 'accountId': '1234', + 'adapter': 'prebidServer', + 'bidders': ['pubmatic'], + 'defaultVendor': 'openwrap', + 'enabled': true, + 'endpoint': { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + 'timeout': 500, + maxTimeout: 500, + }) }); - expect(config.getConfig('s2sConfig')).to.deep.equal({ - enabled: true, - timeout: 500, - adapter: 'prebidServer', - accountId: '1234', - bidders: ['pubmatic'], - defaultVendor: 'openwrap', - endpoint: { - p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', - noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' - }, - }) }); it('should set adapterOptions', function () { @@ -3896,77 +4131,94 @@ describe('S2S Adapter', function () { expect(typeof config.getConfig('s2sConfig').syncUrlModifier.appnexus).to.equal('function') }); - it('should set correct bidder names to bidders property when using an alias for that bidder', function () { - const s2sConfig = utils.deepClone(CONFIG); - - // Add syncEndpoint so that the request goes to the User Sync endpoint - // Modify the bidders property to include an alias for Rubicon adapter - s2sConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - s2sConfig.bidders = ['appnexus', 'rubicon-alias']; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = s2sConfig; - - // Add another bidder, `rubicon-alias` - s2sBidRequest.ad_units[0].bids.push({ - bidder: 'rubicon-alias', - params: { - accoundId: 14062, - siteId: 70608, - zoneId: 498816 + Object.entries({ + 'an alias'() { + adapterManager.aliasBidAdapter('rubicon', 'rubicon-alias'); + }, + 'a server side alias'(s2sConfig) { + s2sConfig.extPrebid = { + aliases: { + 'rubicon-alias': 'rubicon' + } } - }); - - // create an alias for the Rubicon Bid Adapter - adapterManager.aliasBidAdapter('rubicon', 'rubicon-alias'); + } + }).forEach(([t, setupAlias]) => { + describe(`when using ${t}`, () => { + afterEach(() => { + delete adapterManager.aliasRegistry['rubicon-alias']; + }); + it(`should set correct bidder names to bidders property`, function () { + const s2sConfig = utils.deepClone(CONFIG); + + // Add syncEndpoint so that the request goes to the User Sync endpoint + // Modify the bidders property to include an alias for Rubicon adapter + s2sConfig.syncEndpoint = {p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync'}; + s2sConfig.bidders = ['appnexus', 'rubicon-alias']; + + setupAlias(s2sConfig); + + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + // Add another bidder, `rubicon-alias` + s2sBidRequest.ad_units[0].bids.push({ + bidder: 'rubicon-alias', + params: { + accoundId: 14062, + siteId: 70608, + zoneId: 498816 + } + }); - const bidRequest = utils.deepClone(BID_REQUESTS); - bidRequest.push({ - 'bidderCode': 'rubicon-alias', - 'auctionId': '4146ab2b-9422-4040-9b1c-966fffbfe2d4', - 'bidderRequestId': '4b1a4f9c3e4546', - 'tid': 'd7fa8342-ae22-4ca1-b237-331169350f84', - 'bids': [ - { - 'bidder': 'rubicon-alias', - 'params': { - 'accountId': 14062, - 'siteId': 70608, - 'zoneId': 498816 - }, - 'bid_id': '2a9523915411c3', - 'mediaTypes': { - 'banner': { + const bidRequest = utils.deepClone(BID_REQUESTS); + bidRequest.push({ + 'bidderCode': 'rubicon-alias', + 'auctionId': '4146ab2b-9422-4040-9b1c-966fffbfe2d4', + 'bidderRequestId': '4b1a4f9c3e4546', + 'tid': 'd7fa8342-ae22-4ca1-b237-331169350f84', + 'bids': [ + { + 'bidder': 'rubicon-alias', + 'params': { + 'accountId': 14062, + 'siteId': 70608, + 'zoneId': 498816 + }, + 'bid_id': '2a9523915411c3', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '78ddc106-b7d8-45d1-bd29-86993098e53d', 'sizes': [ [ 300, 250 ] - ] + ], + 'bidId': '2a9523915411c3', + 'bidderRequestId': '4b1a4f9c3e4546', + 'auctionId': '4146ab2b-9422-4040-9b1c-966fffbfe2d4' } - }, - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'transactionId': '78ddc106-b7d8-45d1-bd29-86993098e53d', - 'sizes': [ - [ - 300, - 250 - ] ], - 'bidId': '2a9523915411c3', - 'bidderRequestId': '4b1a4f9c3e4546', - 'auctionId': '4146ab2b-9422-4040-9b1c-966fffbfe2d4' - } - ], - 'auctionStart': 1569234122602, - 'timeout': 1000, - 'src': 's2s' - }); + 'auctionStart': 1569234122602, + 'timeout': 1000, + 'src': 's2s' + }); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - const requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.bidders).to.deep.equal(['appnexus', 'rubicon']); + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.bidders).to.deep.equal(['appnexus', 'rubicon']); + }); + }); }); it('should add cooperative sync flag to cookie_sync request if property is present', function () { @@ -4214,4 +4466,365 @@ describe('S2S Adapter', function () { expect(requestBid.ext.prebid.floors).to.deep.equal({ enabled: true, floorMin: 1, floorMinCur: 'CUR' }); }); }); + + describe('getPBSBidderConfig', () => { + [ + { + t: 'does not alter config when there are no conflicts', + global: { + k1: 'val' + }, + bidder: { + bidderA: { + k2: 'val' + } + }, + expected: { + bidderA: { + k2: 'val' + } + } + }, + { + t: 'uses bidder config on type mismatch (scalar/object)', + global: { + k1: 'val', + k2: 'val' + }, + bidder: { + bidderA: { + k1: {k3: 'val'} + } + }, + expected: { + bidderA: { + k1: {k3: 'val'} + } + } + }, + { + t: 'uses bidder config on type mismatch (array/object)', + global: { + k: [1, 2] + }, + bidder: { + bidderA: { + k: {inner: 'val'} + } + }, + expected: { + bidderA: { + k: {inner: 'val'} + } + } + }, + { + t: 'uses bidder config on type mismatch (object/array)', + global: { + k: {inner: 'val'} + }, + bidder: { + bidderA: { + k: [1, 2] + } + }, + expected: { + bidderA: { + k: [1, 2] + } + } + }, + { + t: 'uses bidder config on type mismatch (array/null)', + global: { + k: [1, 2] + }, + bidder: { + bidderA: { + k: null + } + }, + expected: { + bidderA: { + k: null + } + } + }, + { + t: 'uses bidder config on type mismatch (null/array)', + global: {}, + bidder: { + bidderA: { + k: [1, 2] + } + }, + expected: { + bidderA: { + k: [1, 2] + } + } + }, + { + t: 'concatenates arrays', + global: { + key: 'value', + array: [1] + }, + bidder: { + bidderA: { + array: [2] + } + }, + expected: { + bidderA: { + array: [1, 2] + } + } + }, + { + t: 'concatenates nested arrays', + global: { + nested: { + array: [1] + } + }, + bidder: { + bidderA: { + key: 'value', + nested: { + array: [2] + } + } + }, + expected: { + bidderA: { + key: 'value', + nested: { + array: [1, 2] + } + } + } + }, + { + t: 'does not repeat equal elements', + global: { + array: [{id: 1}] + }, + bidder: { + bidderA: { + array: [{id: 1}, {id: 2}] + } + }, + expected: { + bidderA: { + array: [{id: 1}, {id: 2}] + } + } + } + ].forEach(({t, global, bidder, expected}) => { + it(t, () => { + expect(getPBSBidderConfig({global, bidder})).to.eql(expected); + }) + }) + }); + describe('EID handling', () => { + function mkEid(source, value = source) { + return {source, value}; + } + + function eidEntry(source, value = source, bidders = false) { + return {eid: {source, value}, bidders}; + } + + describe('extractEids', () => { + [ + { + t: 'no bidder-specific eids', + global: { + user: { + ext: { + eids: [ + mkEid('idA', 'id1'), + mkEid('idA', 'id2') + ] + }, + eids: [mkEid('idB')] + } + }, + expected: { + eids: [ + eidEntry('idA', 'id1'), + eidEntry('idA', 'id2'), + eidEntry('idB') + ], + conflicts: ['idA'] + } + }, + { + t: 'bidder-specific eids', + global: { + user: { + eids: [ + mkEid('idA') + ] + }, + }, + bidder: { + bidderA: { + user: { + ext: { + eids: [ + mkEid('idB') + ] + } + } + } + }, + expected: { + eids: [ + eidEntry('idA'), + eidEntry('idB', 'idB', ['bidderA']) + ] + } + }, + { + t: 'conflicting bidder-specific eids', + global: { + user: { + eids: [mkEid('idA', 'idA1')] + }, + }, + bidder: { + bidderA: { + user: { + eids: [mkEid('idA', 'idA2'), mkEid('idB', 'idB1'), mkEid('idD')] + }, + }, + bidderB: { + user: { + ext: { + eids: [mkEid('idB', 'idB2'), mkEid('idC'), mkEid('idD')] + } + } + }, + }, + expected: { + eids: [ + eidEntry('idA', 'idA1'), + eidEntry('idA', 'idA2', ['bidderA']), + eidEntry('idB', 'idB1', ['bidderA']), + eidEntry('idB', 'idB2', ['bidderB']), + eidEntry('idC', 'idC', ['bidderB']), + eidEntry('idD', 'idD', ['bidderA', 'bidderB']) + ], + conflicts: ['idA', 'idB'] + } + }, + { + t: 'duplicated bidder-specific eids', + bidder: { + bidderA: { + user: { + eids: [mkEid('id'), mkEid('id')] + } + } + }, + expected: { + eids: [ + eidEntry('id', 'id', ['bidderA']) + ] + } + } + ].forEach(({t, global = {}, bidder = {}, expected}) => { + it(t, () => { + const {eids, conflicts} = extractEids({global, bidder}); + expect(eids).to.have.deep.members(expected.eids); + expect(Array.from(conflicts)).to.have.members(expected.conflicts || []); + }) + }); + }); + describe('consolidateEids', () => { + it('returns global EIDs without permissions', () => { + expect(consolidateEids({ + eids: [eidEntry('idA'), eidEntry('idB')] + })).to.eql({ + global: [mkEid('idA'), mkEid('idB')], + permissions: [], + bidder: {} + }) + }); + + it('returns conflicting, but global EIDs', () => { + expect(consolidateEids({ + eids: [eidEntry('idA', 'idA1'), eidEntry('idA', 'idA2')], + conflicts: new Set(['idA']) + })).to.eql({ + global: [mkEid('idA', 'idA1'), mkEid('idA', 'idA2')], + permissions: [], + bidder: {} + }) + }) + + it('sets permissions for bidder-speficic EIDS', () => { + expect(consolidateEids({ + eids: [ + eidEntry('idA'), + eidEntry('idB', 'idB', ['bidderB']) + ] + })).to.eql({ + global: [mkEid('idA'), mkEid('idB')], + permissions: [{source: 'idB', bidders: ['bidderB']}], + bidder: {} + }) + }) + + it('does not consolidate conflicting bidder-specific EIDs', () => { + expect(consolidateEids({ + eids: [ + eidEntry('global'), + eidEntry('idA', 'idA1', ['bidderA']), + eidEntry('idA', 'idA2', ['bidderB']) + ], + conflicts: new Set(['idA']) + })).to.eql({ + global: [mkEid('global')], + permissions: [], + bidder: { + bidderA: [mkEid('idA', 'idA1')], + bidderB: [mkEid('idA', 'idA2')] + } + }) + }) + + it('does not set permissions for conflicting bidder-specific eids', () => { + expect(consolidateEids({ + eids: [eidEntry('idA', 'idA1'), eidEntry('idA', 'idA2', ['bidderA'])], + conflicts: new Set(['idA']) + })).to.eql({ + global: [mkEid('idA', 'idA1')], + permissions: [], + bidder: { + bidderA: [mkEid('idA', 'idA2')] + } + }) + }); + + it('can do partial consolidation when only some IDs are conflicting', () => { + expect(consolidateEids({ + eids: [ + eidEntry('idA', 'idA1'), + eidEntry('idB', 'idB', ['bidderB']), + eidEntry('idA', 'idA2', ['bidderA']) + ], + conflicts: new Set(['idA']) + })).to.eql({ + global: [mkEid('idA', 'idA1'), mkEid('idB')], + permissions: [{source: 'idB', bidders: ['bidderB']}], + bidder: { + bidderA: [mkEid('idA', 'idA2')] + } + }); + }); + }) + }); }); diff --git a/test/spec/modules/precisoBidAdapter_spec.js b/test/spec/modules/precisoBidAdapter_spec.js index 78a1615a02e..83dea6951e5 100644 --- a/test/spec/modules/precisoBidAdapter_spec.js +++ b/test/spec/modules/precisoBidAdapter_spec.js @@ -1,6 +1,9 @@ import { expect } from 'chai'; import { spec } from '../../../modules/precisoBidAdapter.js'; -import { config } from '../../../src/config.js'; +import { NATIVE } from '../../../src/mediaTypes.js'; +import { OPENRTB } from '../../../libraries/precisoUtils/bidNativeUtils.js'; + +// simport { config } from '../../../src/config.js'; const DEFAULT_PRICE = 1 const DEFAULT_CURRENCY = 'USD' @@ -10,8 +13,10 @@ const BIDDER_CODE = 'preciso'; describe('PrecisoAdapter', function () { let bid = { + precisoBid: true, bidId: '23fhj33i987f', bidder: 'preciso', + buyerUid: 'testuid', mediaTypes: { banner: { sizes: [[DEFAULT_BANNER_WIDTH, DEFAULT_BANNER_HEIGHT]] @@ -22,15 +27,101 @@ describe('PrecisoAdapter', function () { sourceid: '0', publisherId: '0', mediaType: 'banner', - region: 'prebid-eu' + region: 'IND' + + }, + userId: { + pubcid: '12355454test' + + }, + user: { + geo: { + region: 'IND', + } + }, + ortb2: { + device: { + w: 1920, + h: 166, + dnt: 0, + }, + site: { + domain: 'localHost' + }, + source: { + tid: 'eaff09b-a1ab-4ec6-a81e-c5a75bc1dde3' + } + + } + + }; + + let nativeBid = { + + precisoBid: true, + bidId: '23fhj33i987f', + bidder: 'precisonat', + buyerUid: 'testuid', + params: { + host: 'prebid', + sourceid: '0', + publisherId: '0', + mediaType: 'native', + region: 'IND' }, userId: { pubcid: '12355454test' }, - geo: 'NA', - city: 'Asia,delhi' + user: { + geo: { + region: 'IND', + } + }, + ortb2: { + device: { + w: 1920, + h: 166, + dnt: 0, + }, + site: { + domain: 'localHost' + }, + source: { + tid: 'eaff09b-a1ab-4ec6-a81e-c5a75bc1dde3' + } + + }, + mediaType: 'native', + nativeOrtbRequest: { + assets: [ + { + id: 2, + required: 1, + img: { + type: 3, + w: 300, + h: 250 + } + } + ] + }, + nativeParams: { + ortb: { + assets: [ + { + id: 2, + required: 1, + img: { + type: 3, + w: 300, + h: 250 + } + } + ] + } + } }; describe('isBidRequestValid', function () { @@ -41,6 +132,14 @@ describe('PrecisoAdapter', function () { delete bid.params.publisherId; expect(spec.isBidRequestValid(bid)).to.be.false; }); + it('Should return true if there are bidId, params and sourceid parameters present native Bid', function () { + expect(spec.isBidRequestValid(nativeBid)).to.be.true; + }); + + it('Should return false if at least one of parameters is not present in native bid', function () { + delete nativeBid.params.publisherId; + expect(spec.isBidRequestValid(nativeBid)).to.be.false; + }); }); describe('buildRequests', function () { @@ -55,47 +154,46 @@ describe('PrecisoAdapter', function () { expect(serverRequest.method).to.equal('POST'); }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://ssp-bidder.mndtrk.com/bid_request/openrtb'); + expect(serverRequest.url).to.equal('https://ssp-bidder.2trk.info/bid_request/openrtb'); }); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; - // expect(data).to.be.an('object'); - - // expect(data).to.have.all.keys('bidId', 'imp', 'site', 'deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa'); - - expect(data.deviceWidth).to.be.a('number'); - expect(data.deviceHeight).to.be.a('number'); - expect(data.coppa).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.city).to.be.a('string'); - expect(data.geo).to.be.a('object'); - // expect(data.userId).to.be.a('string'); - // expect(data.imp).to.be.a('object'); + expect(data).to.be.an('object'); + expect(data.device).to.be.a('object'); + expect(data.user).to.be.a('object'); + expect(data.source).to.be.a('object'); + expect(data.site).to.be.a('object'); }); - // it('Returns empty data if no valid requests are passed', function () { - /// serverRequest = spec.buildRequests([]); - // let data = serverRequest.data; - // expect(data.imp).to.be.an('array').that.is.empty; - // }); - }); - - describe('with COPPA', function () { - beforeEach(function () { - sinon.stub(config, 'getConfig') - .withArgs('coppa') - .returns(true); + it('Returns empty data if no valid requests are passed', function () { + delete bid.ortb2.device; + serverRequest = spec.buildRequests([bid]); + let data = serverRequest.data; + expect(data.device).to.be.undefined; }); - afterEach(function () { - config.getConfig.restore(); + + let ServeNativeRequest = spec.buildRequests([nativeBid]); + it('Creates a valid nativeServerRequest object ', function () { + expect(ServeNativeRequest).to.exist; + expect(ServeNativeRequest.method).to.exist; + expect(ServeNativeRequest.url).to.exist; + expect(ServeNativeRequest.data).to.exist; + expect(ServeNativeRequest.method).to.equal('POST'); + expect(ServeNativeRequest.url).to.equal('https://ssp-bidder.2trk.info/bid_request/openrtb'); }); - it('should send the Coppa "required" flag set to "1" in the request', function () { - let serverRequest = spec.buildRequests([bid]); - expect(serverRequest.data.coppa).to.equal(1); + it('should extract the native params', function () { + let nativeData = ServeNativeRequest.data; + const asset = JSON.parse(nativeData.imp[0].native.request).assets[0] + expect(asset).to.deep.equal({ + id: OPENRTB.NATIVE.ASSET_ID.IMAGE, + required: 1, + img: { + w: 300, + h: 250, + type: OPENRTB.NATIVE.IMAGE_TYPE.MAIN, + } + } + ) }); }); @@ -143,11 +241,92 @@ describe('PrecisoAdapter', function () { expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])) }) + + it('should get correct native bid response', function () { + const adm = { + native: { + ver: 1.2, + link: { + url: 'https://example.com', + clicktrackers: 'https://example.com/clktracker' + }, + eventtrackers: [ + { + url: 'https://example.com/imptracker' + } + ], + imptrackers: [ + 'https://example.com/imptracker' + ], + assets: [{ + id: OPENRTB.NATIVE.ASSET_ID.IMAGE, + required: 1, + img: { + url: 'https://example.com/image.jpg', + w: 150, + h: 50 + } + }], + } + } + let nativeResponse = { + bidderRequestId: 'f6adb85f-4e19-45a0-b41e-2a5b9a48f23a', + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + price: DEFAULT_PRICE, + adm: JSON.stringify(adm), + cid: 'test_cid', + crid: 'test_banner_crid', + w: DEFAULT_BANNER_WIDTH, + h: DEFAULT_BANNER_HEIGHT, + adomain: [], + } + ], + seat: BIDDER_CODE + } + ], + } + + let expectedNativeResponse = [ + { + requestId: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + mediaType: NATIVE, + cpm: DEFAULT_PRICE, + creativeId: 'test_banner_crid', + width: 1, + height: 1, + ttl: 300, + meta: { + advertiserDomains: [] + }, + netRevenue: true, + currency: 'USD', + // meta: { advertiserDomains: [] }, + native: { + clickUrl: encodeURI('https://example.com'), + impressionTrackers: ['https://example.com/imptracker'], + image: { + url: encodeURI('https://example.com/image.jpg'), + width: 150, + height: 50 + }, + } + } + ] + let result = spec.interpretResponse({ body: nativeResponse }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedNativeResponse[0])); + }) }) + describe('getUserSyncs', function () { const syncUrl = 'https://ck.2trk.info/rtb/user/usersync.aspx?id=NA&gdpr=0&gdpr_consent=&us_privacy=&t=4'; const syncOptions = { - iframeEnabled: true + iframeEnabled: true, + spec: true }; let userSync = spec.getUserSyncs(syncOptions); it('Returns valid URL and type', function () { diff --git a/test/spec/modules/previousAuctionInfo_spec.js b/test/spec/modules/previousAuctionInfo_spec.js new file mode 100644 index 00000000000..d1003c0082a --- /dev/null +++ b/test/spec/modules/previousAuctionInfo_spec.js @@ -0,0 +1,258 @@ +import * as previousAuctionInfo from '../../../modules/previousAuctionInfo'; +import sinon from 'sinon'; +import { expect } from 'chai'; +import { config } from 'src/config.js'; +import * as events from 'src/events.js'; +import {CONFIG_NS, resetPreviousAuctionInfo, startAuctionHook} from '../../../modules/previousAuctionInfo'; +import { REJECTION_REASON } from '../../../src/constants.js'; + +describe('previous auction info', () => { + let sandbox; + + const auctionDetails = { + auctionId: 'auction123', + bidsReceived: [ + { requestId: 'bid123', bidderCode: 'testBidder1', cpm: 1, adUnitCode: 'adUnit1', currency: 'USD', originalCpm: 1.1, originalCurrency: 'USD' }, + { requestId: 'bidabc', bidderCode: 'testBidder2', cpm: 2, adUnitCode: 'adUnit1', currency: 'EUR', originalCpm: 2.1, originalCurrency: 'EUR' }, + { requestId: 'bidxyz', bidderCode: 'testBidder3', cpm: 3, adUnitCode: 'adUnit2', currency: 'USD', originalCpm: 3.2, originalCurrency: 'USD' } + ], + bidsRejected: [ + { requestId: 'bid456', rejectionReason: 1 }, + { requestId: 'bid789', rejectionReason: 2 } + ], + bidderRequests: [ + { + bidderCode: 'testBidder1', + bidderRequestId: 'req1', + bids: [ + { bidId: 'bid123', ortb2: { cur: ['USD'] }, ortb2Imp: { ext: { tid: 'trans123' } }, adUnitCode: 'adUnit1' } + ] + }, + { + bidderCode: 'testBidder2', + bidderRequestId: 'req2', + bids: [ + { bidId: 'bidabc', ortb2: { cur: ['EUR'] }, ortb2Imp: { ext: { tid: 'trans456' } }, adUnitCode: 'adUnit1' } + ] + }, + { + bidderCode: 'testBidder3', + bidderRequestId: 'req3', + bids: [ + { bidId: 'bidxyz', ortb2: { cur: ['USD'] }, ortb2Imp: { ext: { tid: 'trans789' } }, adUnitCode: 'adUnit2' } + ] + } + ], + timestamp: Date.now(), + }; + + before(() => { + config.resetConfig(); + }) + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + config.resetConfig(); + resetPreviousAuctionInfo(); + sandbox.restore(); + }); + + describe('config', () => { + it('should initialize the module if publisher enabled', () => { + sandbox.spy(events, 'on'); + config.setConfig({ [CONFIG_NS]: { enabled: true, bidders: ['testBidder1', 'testBidder2'] } }); + expect(previousAuctionInfo.previousAuctionInfoEnabled).to.be.true; + sinon.assert.called(events.on); + }); + + it('should not enable previous auction info if config.previousAuctionInfo is not set', () => { + config.setConfig({}); + expect(previousAuctionInfo.previousAuctionInfoEnabled).to.be.false; + }); + }); + + describe('onAuctionEndHandler', () => { + it('should store auction data for enabled bidders in auctionState', () => { + config.setConfig({ [CONFIG_NS]: { enabled: true, bidders: ['testBidder2'] } }); + previousAuctionInfo.onAuctionEndHandler(auctionDetails); + + expect(previousAuctionInfo.auctionState).to.have.property('testBidder2'); + expect(previousAuctionInfo.auctionState['testBidder2']).to.be.an('array').with.lengthOf(1); + + const storedData = previousAuctionInfo.auctionState['testBidder2'][0]; + + expect(storedData).to.include({ + bidderRequestId: 'req2', + bidId: 'bidabc', + rendered: 0, + source: 'pbjs', + adUnitCode: 'adUnit1', + highestBidCpm: 2, + highestBidCurrency: 'EUR', + bidderCpm: 2, + bidderOriginalCpm: 2.1, + bidderCurrency: 'EUR', + bidderOriginalCurrency: 'EUR', + rejectionReason: null, + timestamp: auctionDetails.timestamp + }); + }); + + it('should store auction data for multiple bidders correctly', () => { + config.setConfig({ [CONFIG_NS]: { enabled: true, bidders: ['testBidder1', 'testBidder3'] } }); + previousAuctionInfo.onAuctionEndHandler(auctionDetails); + + expect(previousAuctionInfo.auctionState).to.have.property('testBidder1'); + expect(previousAuctionInfo.auctionState).to.have.property('testBidder3'); + + expect(previousAuctionInfo.auctionState['testBidder1'][0]).to.include({ + bidId: 'bid123', + highestBidCpm: 2, + highestBidCurrency: 'EUR', + adUnitCode: 'adUnit1', + bidderCpm: 1, + bidderCurrency: 'USD', + rejectionReason: null, + }); + + expect(previousAuctionInfo.auctionState['testBidder3'][0]).to.include({ + bidId: 'bidxyz', + highestBidCpm: 3, + highestBidCurrency: 'USD', + adUnitCode: 'adUnit2', + bidderCpm: 3, + bidderCurrency: 'USD', + rejectionReason: null, + }); + }); + + it('should not store auction data for disabled bidders', () => { + config.setConfig({ [CONFIG_NS]: { enabled: true, bidders: ['testBidder1'] } }); + previousAuctionInfo.onAuctionEndHandler(auctionDetails); + + expect(previousAuctionInfo.auctionState).to.have.property('testBidder1'); + expect(previousAuctionInfo.auctionState).to.not.have.property('testBidder2'); + }); + + it('should include rejectionReason string if the bid was rejected', () => { + const auctionDetailsWithRejectedBid = { + auctionId: 'auctionXYZ', + bidsReceived: [], + bidsRejected: [ + { requestId: 'bid456', rejectionReason: REJECTION_REASON.FLOOR_NOT_MET } // string from REJECTION_REASON + ], + bidderRequests: [ + { + bidderCode: 'testBidder1', + bidderRequestId: 'req1', + bids: [ + { bidId: 'bid456', adUnitCode: 'adUnit1' } + ] + } + ], + timestamp: Date.now(), + }; + + config.setConfig({ [CONFIG_NS]: { enabled: true, bidders: ['testBidder1'] } }); + previousAuctionInfo.onAuctionEndHandler(auctionDetailsWithRejectedBid); + + const stored = previousAuctionInfo.auctionState['testBidder1'][0]; + expect(stored).to.include({ + bidId: 'bid456', + rejectionReason: REJECTION_REASON.FLOOR_NOT_MET, + bidderCpm: null, + highestBidCpm: null + }); + }); + }); + + describe('startAuctionHook', () => { + let global, bidder, next; + beforeEach(() => { + global = {}; + bidder = {}; + next = sinon.spy(); + }); + function runHook() { + startAuctionHook(next, {ortb2Fragments: {global, bidder}}); + } + it('should not add info when none is available', () => { + runHook(); + expect(global).to.eql({}); + expect(bidder).to.eql({}); + }) + it('should call next', () => { + runHook(); + sinon.assert.called(next); + }) + describe('when info is available', () => { + beforeEach(() => { + Object.assign(previousAuctionInfo.auctionState, { + bidder1: [{transactionId: 'tid1', auction: '1'}], + bidder2: [{transactionId: 'tid2', auction: '2'}] + }) + }) + + function extractInfo() { + return Object.fromEntries( + Object.entries(bidder) + .map(([bidder, ortb2]) => [bidder, ortb2.ext?.prebid?.previousauctioninfo]) + ) + } + + it('should set info for enabled bidders, when only some are enabled', () => { + config.setConfig({[CONFIG_NS]: {enabled: true, bidders: ['bidder1']}}); + runHook(); + expect(extractInfo()).to.eql({ + bidder1: [{auction: '1'}] + }) + }); + + it('should set info for all bidders, when none is specified', () => { + config.setConfig({[CONFIG_NS]: {enabled: true}}); + runHook(); + expect(extractInfo()).to.eql({ + bidder1: [{auction: '1'}], + bidder2: [{auction: '2'}] + }) + }) + }) + }) + + describe('onBidWonHandler', () => { + it('should update the rendered field in auctionState when a pbjs bid wins', () => { + config.setConfig({ previousAuctionInfo: { enabled: true, bidders: ['testBidder3'] } }); + + previousAuctionInfo.auctionState['testBidder3'] = [ + { transactionId: 'trans789', rendered: 0 } + ]; + + const winningBid = { + transactionId: 'trans789' + }; + + previousAuctionInfo.onBidWonHandler(winningBid); + + expect(previousAuctionInfo.auctionState['testBidder3'][0]).to.include({ rendered: 1 }); + }); + + it('should not update the rendered field if no matching transactionId is found', () => { + config.setConfig({ previousAuctionInfo: { enabled: true, bidders: ['testBidder3'] } }); + + previousAuctionInfo.auctionState['testBidder3'] = [ + { transactionId: 'someOtherTid', rendered: 0 } + ]; + + const winningBid = { + transactionId: 'trans789' + }; + + previousAuctionInfo.onBidWonHandler(winningBid); + + expect(previousAuctionInfo.auctionState['testBidder3'][0]).to.include({ rendered: 0 }); + }); + }); +}); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 7223940bc45..8cfa6aabb12 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -1751,7 +1751,7 @@ describe('the price floors module', function () { const req = utils.deepClone(bidRequest); _floorDataForAuction[req.auctionId] = utils.deepClone(basicFloorConfig); - expect(guardTids('mock-bidder').bidRequest(req).getFloor({})).to.deep.equal({ + expect(guardTids({bidderCode: 'mock-bidder'}).bidRequest(req).getFloor({})).to.deep.equal({ currency: 'USD', floor: 1.0 }); @@ -1865,159 +1865,194 @@ describe('the price floors module', function () { }); }); - it('should use inverseFloorAdjustment function before bidder cpm adjustment', function () { - let functionUsed; - getGlobal().bidderSettings = { - rubicon: { - bidCpmAdjustment: function (bidCpm, bidResponse) { - functionUsed = 'Rubicon Adjustment'; - bidCpm *= 0.5; - if (bidResponse.mediaType === 'video') bidCpm -= 0.18; - return bidCpm; - }, - inverseBidAdjustment: function (bidCpm, bidRequest) { - functionUsed = 'Rubicon Inverse'; - // if video is the only mediaType on Bid Request => add 0.18 - if (bidRequest.mediaTypes.video && Object.keys(bidRequest.mediaTypes).length === 1) bidCpm += 0.18; - return bidCpm / 0.5; - }, - }, - appnexus: { - bidCpmAdjustment: function (bidCpm, bidResponse) { - functionUsed = 'Appnexus Adjustment'; - bidCpm *= 0.75; - if (bidResponse.mediaType === 'video') bidCpm -= 0.18; - return bidCpm; - }, - inverseBidAdjustment: function (bidCpm, bidRequest) { - functionUsed = 'Appnexus Inverse'; - // if video is the only mediaType on Bid Request => add 0.18 - if (bidRequest.mediaTypes.video && Object.keys(bidRequest.mediaTypes).length === 1) bidCpm += 0.18; - return bidCpm / 0.75; - }, - } - }; + describe('inverse adjustment', () => { + beforeEach(() => { + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + }); - _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + it('should use inverseFloorAdjustment function before bidder cpm adjustment', function () { + let functionUsed; + getGlobal().bidderSettings = { + rubicon: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + functionUsed = 'Rubicon Adjustment'; + bidCpm *= 0.5; + if (bidResponse.mediaType === 'video') bidCpm -= 0.18; + return bidCpm; + }, + inverseBidAdjustment: function (bidCpm, bidRequest) { + functionUsed = 'Rubicon Inverse'; + // if video is the only mediaType on Bid Request => add 0.18 + if (bidRequest.mediaTypes.video && Object.keys(bidRequest.mediaTypes).length === 1) bidCpm += 0.18; + return bidCpm / 0.5; + }, + }, + appnexus: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + functionUsed = 'Appnexus Adjustment'; + bidCpm *= 0.75; + if (bidResponse.mediaType === 'video') bidCpm -= 0.18; + return bidCpm; + }, + inverseBidAdjustment: function (bidCpm, bidRequest) { + functionUsed = 'Appnexus Inverse'; + // if video is the only mediaType on Bid Request => add 0.18 + if (bidRequest.mediaTypes.video && Object.keys(bidRequest.mediaTypes).length === 1) bidCpm += 0.18; + return bidCpm / 0.75; + }, + } + }; - _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + // start with banner as only mediaType + bidRequest.mediaTypes = { banner: { sizes: [[300, 250]] } }; + let appnexusBid = { + ...bidRequest, + bidder: 'appnexus', + }; - // start with banner as only mediaType - bidRequest.mediaTypes = { banner: { sizes: [[300, 250]] } }; - let appnexusBid = { - ...bidRequest, - bidder: 'appnexus', - }; + // should be same as the adjusted calculated inverses above test (banner) + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 + }); - // should be same as the adjusted calculated inverses above test (banner) - expect(bidRequest.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 2.0 - }); + // should use rubicon inverse + expect(functionUsed).to.equal('Rubicon Inverse'); - // should use rubicon inverse - expect(functionUsed).to.equal('Rubicon Inverse'); + // appnexus just using banner should be same + expect(appnexusBid.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.3334 + }); - // appnexus just using banner should be same - expect(appnexusBid.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 1.3334 - }); + expect(functionUsed).to.equal('Appnexus Inverse'); - expect(functionUsed).to.equal('Appnexus Inverse'); + // now since asking for 'video' only mediaType inverse function should include the .18 + bidRequest.mediaTypes = { video: { context: 'instream' } }; + expect(bidRequest.getFloor({ mediaType: 'video' })).to.deep.equal({ + currency: 'USD', + floor: 2.36 + }); - // now since asking for 'video' only mediaType inverse function should include the .18 - bidRequest.mediaTypes = { video: { context: 'instream' } }; - expect(bidRequest.getFloor({ mediaType: 'video' })).to.deep.equal({ - currency: 'USD', - floor: 2.36 - }); + expect(functionUsed).to.equal('Rubicon Inverse'); - expect(functionUsed).to.equal('Rubicon Inverse'); + // now since asking for 'video' inverse function should include the .18 + appnexusBid.mediaTypes = { video: { context: 'instream' } }; + expect(appnexusBid.getFloor({ mediaType: 'video' })).to.deep.equal({ + currency: 'USD', + floor: 1.5734 + }); - // now since asking for 'video' inverse function should include the .18 - appnexusBid.mediaTypes = { video: { context: 'instream' } }; - expect(appnexusBid.getFloor({ mediaType: 'video' })).to.deep.equal({ - currency: 'USD', - floor: 1.5734 + expect(functionUsed).to.equal('Appnexus Inverse'); }); - expect(functionUsed).to.equal('Appnexus Inverse'); - }); - - it('should pass inverseFloorAdjustment the bidRequest object so it can be used', function () { - // Adjustment factors based on Bid Media Type - const mediaTypeFactors = { - banner: 0.5, - native: 0.7, - video: 0.9 - } - getGlobal().bidderSettings = { - rubicon: { - bidCpmAdjustment: function (bidCpm, bidResponse) { - return bidCpm * mediaTypeFactors[bidResponse.mediaType]; - }, - inverseBidAdjustment: function (bidCpm, bidRequest) { - // For the inverse we add up each mediaType in the request and divide by number of Mt's to get the inverse number - let factor = Object.keys(bidRequest.mediaTypes).reduce((sum, mediaType) => sum += mediaTypeFactors[mediaType], 0); - factor = factor / Object.keys(bidRequest.mediaTypes).length; - return bidCpm / factor; - }, + it('should pass inverseFloorAdjustment the bidRequest object so it can be used', function () { + // Adjustment factors based on Bid Media Type + const mediaTypeFactors = { + banner: 0.5, + native: 0.7, + video: 0.9 } - }; - - _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + getGlobal().bidderSettings = { + rubicon: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + return bidCpm * mediaTypeFactors[bidResponse.mediaType]; + }, + inverseBidAdjustment: function (bidCpm, bidRequest) { + // For the inverse we add up each mediaType in the request and divide by number of Mt's to get the inverse number + let factor = Object.keys(bidRequest.mediaTypes).reduce((sum, mediaType) => sum += mediaTypeFactors[mediaType], 0); + factor = factor / Object.keys(bidRequest.mediaTypes).length; + return bidCpm / factor; + }, + } + }; - _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + // banner only should be 2 + bidRequest.mediaTypes = { banner: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 + }); - // banner only should be 2 - bidRequest.mediaTypes = { banner: {} }; - expect(bidRequest.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 2.0 - }); + // native only should be 1.4286 + bidRequest.mediaTypes = { native: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.4286 + }); - // native only should be 1.4286 - bidRequest.mediaTypes = { native: {} }; - expect(bidRequest.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 1.4286 - }); + // video only should be 1.1112 + bidRequest.mediaTypes = { video: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.1112 + }); - // video only should be 1.1112 - bidRequest.mediaTypes = { video: {} }; - expect(bidRequest.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 1.1112 - }); + // video and banner should even out to 0.7 factor so 1.4286 + bidRequest.mediaTypes = { video: {}, banner: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.4286 + }); - // video and banner should even out to 0.7 factor so 1.4286 - bidRequest.mediaTypes = { video: {}, banner: {} }; - expect(bidRequest.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 1.4286 - }); + // video and native should even out to 0.8 factor so -- 1.25 + bidRequest.mediaTypes = { video: {}, native: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.25 + }); - // video and native should even out to 0.8 factor so -- 1.25 - bidRequest.mediaTypes = { video: {}, native: {} }; - expect(bidRequest.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 1.25 - }); + // banner and native should even out to 0.6 factor so -- 1.6667 + bidRequest.mediaTypes = { banner: {}, native: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.6667 + }); - // banner and native should even out to 0.6 factor so -- 1.6667 - bidRequest.mediaTypes = { banner: {}, native: {} }; - expect(bidRequest.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 1.6667 + // all 3 banner video and native should even out to 0.7 factor so -- 1.4286 + bidRequest.mediaTypes = { banner: {}, native: {}, video: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.4286 + }); }); - // all 3 banner video and native should even out to 0.7 factor so -- 1.4286 - bidRequest.mediaTypes = { banner: {}, native: {}, video: {} }; - expect(bidRequest.getFloor()).to.deep.equal({ - currency: 'USD', - floor: 1.4286 - }); + Object.entries({ + 'both unspecified': { + getFloorParams: undefined, + inverseParams: {} + }, + 'only mediaType': { + getFloorParams: {mediaType: 'video'}, + inverseParams: {mediaType: 'video'} + }, + 'only size': { + getFloorParams: {mediaType: '*', size: [1, 2]}, + inverseParams: {size: [1, 2]} + }, + 'both': { + getFloorParams: {mediaType: 'banner', size: [1, 2]}, + inverseParams: {mediaType: 'banner', size: [1, 2]} + } + }).forEach(([t, {getFloorParams, inverseParams}]) => { + it(`should pass inverseFloorAdjustment mediatype and size (${t})`, () => { + getGlobal().bidderSettings = { + standard: { + inverseBidAdjustment: sinon.stub() + } + } + bidRequest.mediaTypes = { + video: {}, + native: {}, + banner: { + sizes: [[100, 200], [200, 300]] + } + } + bidRequest.getFloor(getFloorParams); + sinon.assert.calledWith(getGlobal().bidderSettings.standard.inverseBidAdjustment, 1, bidRequest, inverseParams); + }); + }) }); it('should use standard cpmAdjustment if no bidder cpmAdjustment', function () { @@ -2401,3 +2436,87 @@ describe('the price floors module', function () { }) }); }); + +describe('setting null as rule value', () => { + const nullFloorData = { + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType', 'size'] + }, + values: { + 'banner|600x300': null, + } + }; + + const basicBidRequest = { + bidder: 'rubicon', + adUnitCode: 'test_div_1', + auctionId: '1234-56-789', + transactionId: 'tr_test_div_1', + adUnitId: 'tr_test_div_1', + }; + + it('should validate for null values', function () { + let data = utils.deepClone(nullFloorData); + data.floorsSchemaVersion = 1; + expect(isFloorsDataValid(data)).to.to.equal(true); + }); + + it('getFloor should not return numeric value if null set as value', function () { + const bidRequest = { ...basicBidRequest, getFloor }; + const basicFloorConfig = { + enabled: true, + auctionDelay: 0, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: nullFloorData + } + _floorDataForAuction[bidRequest.auctionId] = basicFloorConfig; + + let inputParams = {mediaType: 'banner', size: [600, 300]}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal(null); + }) + + it('getFloor should not return numeric value if null set as value - external floor provider', function () { + const basicFloorConfig = { + enabled: true, + auctionDelay: 0, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: nullFloorData + } + server.respondWith(JSON.stringify(nullFloorData)); + let exposedAdUnits; + + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + + const adUnits = [{ + cod: 'test_div_1', + mediaTypes: {banner: { sizes: [[600, 300]] }, native: {}}, + bids: [{bidder: 'someBidder', adUnitCode: 'test_div_1'}, {bidder: 'someOtherBidder', adUnitCode: 'test_div_1'}] + }]; + + requestBidsHook(config => exposedAdUnits = config.adUnits, { + auctionId: basicBidRequest.auctionId, + adUnits + }); + + let inputParams = {mediaType: 'banner', size: [600, 300]}; + + expect(exposedAdUnits[0].bids[0].getFloor(inputParams)).to.deep.equal(null); + }); +}) diff --git a/test/spec/modules/prismaBidAdapter_spec.js b/test/spec/modules/prismaBidAdapter_spec.js index be1c16c9059..b0d068e5614 100644 --- a/test/spec/modules/prismaBidAdapter_spec.js +++ b/test/spec/modules/prismaBidAdapter_spec.js @@ -3,7 +3,7 @@ import {spec} from 'modules/prismaBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import { requestBidsHook } from 'modules/consentManagement.js'; +import { requestBidsHook } from 'modules/consentManagementTcf.js'; describe('Prisma bid adapter tests', function () { const DISPLAY_BID_REQUEST = [{ diff --git a/test/spec/modules/proxistoreBidAdapter_spec.js b/test/spec/modules/proxistoreBidAdapter_spec.js index bcddb9e8b04..92e68f60b85 100644 --- a/test/spec/modules/proxistoreBidAdapter_spec.js +++ b/test/spec/modules/proxistoreBidAdapter_spec.js @@ -81,7 +81,7 @@ describe('ProxistoreBidAdapter', function () { it('should contain a valid url', function () { // has gdpr consent expect(request.url).equal(url.cookieBase); - // doens't have gpdr consent + // doens't have gdpr consent bidderRequest.gdprConsent.vendorData = null; request = spec.buildRequests([bid], bidderRequest); diff --git a/test/spec/modules/pstudioBidAdapter_spec.js b/test/spec/modules/pstudioBidAdapter_spec.js index 52ecb820ed3..2ec38e8dcda 100644 --- a/test/spec/modules/pstudioBidAdapter_spec.js +++ b/test/spec/modules/pstudioBidAdapter_spec.js @@ -18,7 +18,7 @@ describe('PStudioAdapter', function () { bidder: 'pstudio', params: { pubid: '258c2a8d-d2ad-4c31-a2a5-e63001186456', - floorPrice: 1.15, + adtagid: 'aae1aabb-6699-4b5a-9c3f-9ed034b1932c', }, adUnitCode: 'test-div-1', mediaTypes: { @@ -38,7 +38,7 @@ describe('PStudioAdapter', function () { bidder: 'pstudio', params: { pubid: '258c2a8d-d2ad-4c31-a2a5-e63001186456', - floorPrice: 1.15, + adtagid: '34833639-f17c-40bc-9c4b-222b1b7459c7', }, adUnitCode: 'test-div-1', mediaTypes: { @@ -197,7 +197,7 @@ describe('PStudioAdapter', function () { it('should return false when publisher id not found', function () { const localBid = deepClone(bannerBid); delete localBid.params.pubid; - delete localBid.params.floorPrice; + delete localBid.params.adtagid; expect(spec.isBidRequestValid(localBid)).to.equal(false); }); @@ -232,7 +232,7 @@ describe('PStudioAdapter', function () { it('should properly map ids in request payload', function () { expect(bannerPayload.id).to.equal(bannerBid.bidId); - expect(bannerPayload.adtagid).to.equal(bannerBid.adUnitCode); + expect(bannerPayload.adtagid).to.equal(bannerBid.params.adtagid); }); it('should properly map banner mediaType in request payload', function () { @@ -266,7 +266,7 @@ describe('PStudioAdapter', function () { it('should properly set required bidder params in request payload', function () { expect(bannerPayload.pubid).to.equal(bannerBid.params.pubid); - expect(bannerPayload.floor_price).to.equal(bannerBid.params.floorPrice); + expect(bannerPayload.adtagid).to.equal(bannerBid.params.adtagid); }); it('should omit optional bidder params or first-party data from bid request if they are not provided', function () { diff --git a/test/spec/modules/pubCircleBidAdapter_spec.js b/test/spec/modules/pubCircleBidAdapter_spec.js index 8aaa023ee1c..f02aab9d4d6 100644 --- a/test/spec/modules/pubCircleBidAdapter_spec.js +++ b/test/spec/modules/pubCircleBidAdapter_spec.js @@ -1,11 +1,21 @@ import { expect } from 'chai'; -import { spec } from '../../../modules/pubCircleBidAdapter'; +import { spec } from '../../../modules/pubCircleBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'pubcircle' +const bidder = 'pubcircle'; describe('PubCircleBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -16,8 +26,9 @@ describe('PubCircleBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -30,8 +41,9 @@ describe('PubCircleBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -53,8 +65,9 @@ describe('PubCircleBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -73,9 +86,20 @@ describe('PubCircleBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -112,6 +136,7 @@ describe('PubCircleBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -120,7 +145,11 @@ describe('PubCircleBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -129,7 +158,7 @@ describe('PubCircleBidAdapter', function () { 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('string'); + 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); @@ -145,6 +174,7 @@ describe('PubCircleBidAdapter', function () { 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'); @@ -170,8 +200,10 @@ describe('PubCircleBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -186,12 +218,38 @@ describe('PubCircleBidAdapter', function () { expect(data.ccpa).to.equal(bidderRequest.uspConsent); expect(data.gdpr).to.not.exist; }); + }); - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -395,5 +453,17 @@ describe('PubCircleBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal('https://cs.pubcircle.ai/image?pbjs=1&ccpa_consent=1---&coppa=0') }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.pubcircle.ai/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); }); }); diff --git a/test/spec/modules/pubgeniusBidAdapter_spec.js b/test/spec/modules/pubgeniusBidAdapter_spec.js index 86c8794dc4c..e1d579aaa4a 100644 --- a/test/spec/modules/pubgeniusBidAdapter_spec.js +++ b/test/spec/modules/pubgeniusBidAdapter_spec.js @@ -383,7 +383,6 @@ describe('pubGENIUS adapter', () => { w: 200, h: 100, startdelay: -1, - placement: 1, skip: 1, skipafter: 1, playbackmethod: [3, 4], diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index 5ad58ea1a37..65f4f312676 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -2,7 +2,6 @@ import {publinkIdSubmodule} from 'modules/publinkIdSystem.js'; import {getCoreStorageManager, getStorageManager} from '../../../src/storageManager'; import {server} from 'test/mocks/xhr.js'; import sinon from 'sinon'; -import {uspDataHandler} from '../../../src/adapterManager'; import {parseUrl} from '../../../src/utils'; const storage = getCoreStorageManager(); @@ -117,9 +116,9 @@ describe('PublinkIdSystem', () => { expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); }); - it('Fetch with consent data', () => { + it('Fetch with GDPR consent data', () => { const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; - const consentData = {gdprApplies: 1, consentString: 'myconsentstring'}; + const consentData = {gdpr: {gdprApplies: 1, consentString: 'myconsentstring'}}; let submoduleCallback = publinkIdSubmodule.getId(config, consentData).callback; submoduleCallback(callbackSpy); @@ -170,18 +169,10 @@ describe('PublinkIdSystem', () => { describe('usPrivacy', () => { let callbackSpy = sinon.spy(); - const oldPrivacy = uspDataHandler.getConsentData(); - before(() => { - uspDataHandler.setConsentData('1YNN'); - }); - after(() => { - uspDataHandler.setConsentData(oldPrivacy); - callbackSpy.resetHistory(); - }); it('Fetch with usprivacy data', () => { const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', api_key: 'abcdefg'}}; - let submoduleCallback = publinkIdSubmodule.getId(config).callback; + let submoduleCallback = publinkIdSubmodule.getId(config, {usp: '1YNN'}).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index 002b7fb3063..7e333bc2d78 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -29,6 +29,8 @@ const { SET_TARGETING } = EVENTS; +const DISPLAY_MANAGER = 'Prebid.js'; + const BID = { 'bidder': 'pubmatic', 'width': 640, @@ -75,6 +77,9 @@ const BID = { 'floorRuleValue': 1.1, 'floorValue': 1.1 }, + 'meta': { + 'demandSource': 1208, + }, getStatusCode() { return 1; } @@ -103,7 +108,8 @@ const BID2 = Object.assign({}, BID, { 'hb_source': 'server' }, meta: { - advertiserDomains: ['example.com'] + advertiserDomains: ['example.com'], + 'demandSource': 1208, } }); @@ -568,16 +574,18 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); - expect(data.pbv).to.equal(getGlobal()?.version || '-1'); + expect(data.dm).to.equal(DISPLAY_MANAGER); + expect(data.dmv).to.equal('$prebid.version$' || '-1'); + expect(data.ctr).not.to.be.null; expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sid).not.to.be.undefined; - expect(data.s[0].ffs).to.equal(1); - expect(data.s[0].fsrc).to.equal(2); - expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); @@ -609,9 +617,6 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); expect(data.s[1].sid).not.to.be.undefined; - expect(data.s[1].ffs).to.equal(1); - expect(data.s[1].fsrc).to.equal(2); - expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -667,6 +672,10 @@ describe('pubmatic analytics adapter', function () { expect(data.ss).to.equal('1'); expect(data.fskp).to.equal('0'); expect(data.af).to.equal('video'); + expect(data.ffs).to.equal('1'); + expect(data.ds).to.equal('1208'); + expect(data.dm).to.equal(DISPLAY_MANAGER); + expect(data.dmv).to.equal('$prebid.version$' || '-1'); }); it('Logger : do not log floor fields when prebids floor shows noData in location property', function() { @@ -786,17 +795,19 @@ describe('pubmatic analytics adapter', function () { expect(data.pid).to.equal('1111'); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); - expect(data.pbv).to.equal(getGlobal()?.version || '-1'); + expect(data.dm).to.equal(DISPLAY_MANAGER); + expect(data.dmv).to.equal('$prebid.version$' || '-1'); + expect(data.ctr).not.to.be.null; expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); expect(data.tgid).to.equal(0); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sid).not.to.be.undefined; - expect(data.s[0].ffs).to.equal(1); - expect(data.s[0].fsrc).to.equal(2); - expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); @@ -866,7 +877,9 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(0);// test group id should be between 0-15 else set to 0 expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); - expect(data.pbv).to.equal(getGlobal()?.version || '-1'); + expect(data.dm).to.equal(DISPLAY_MANAGER); + expect(data.dmv).to.equal('$prebid.version$' || '-1'); + expect(data.ctr).not.to.be.null; expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 @@ -914,16 +927,15 @@ describe('pubmatic analytics adapter', function () { expect(requests.length).to.equal(2); // 1 logger and 1 win-tracker let request = requests[1]; // logger is executed late, trackers execute first let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.ctr).not.to.be.null; expect(data.tgid).to.equal(0);// test group id should be an INT between 0-15 else set to 0 + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); - expect(data.s[1].sid).not.to.be.undefined; - - expect(data.s[1].ffs).to.equal(1); - expect(data.s[1].fsrc).to.equal(2); - expect(data.s[1].fp).to.equal('pubmatic'); - expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -1004,12 +1016,13 @@ describe('pubmatic analytics adapter', function () { expect(requests.length).to.equal(1); // 1 logger and 0 win-tracker let request = requests[0]; let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); expect(data.s[1].sid).not.to.be.undefined; - expect(data.s[1].ffs).to.equal(1); - expect(data.s[1].fsrc).to.equal(2); - expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -1119,12 +1132,12 @@ describe('pubmatic analytics adapter', function () { let request = requests[2]; // logger is executed late, trackers execute first expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); expect(data.s[1].sid).not.to.be.undefined; - expect(data.s[1].ffs).to.equal(1); - expect(data.s[1].fsrc).to.equal(2); - expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -1239,12 +1252,12 @@ describe('pubmatic analytics adapter', function () { let request = requests[2]; // logger is executed late, trackers execute first expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); expect(data.s[1].sid).not.to.be.undefined; - expect(data.s[1].ffs).to.equal(1); - expect(data.s[1].fsrc).to.equal(2); - expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -1357,14 +1370,14 @@ describe('pubmatic analytics adapter', function () { let request = requests[1]; // logger is executed late, trackers execute first expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); // slot 2 // Testing only for rejected bid as other scenarios will be covered under other TCs expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); - expect(data.s[1].ffs).to.equal(1); - expect(data.s[1].fsrc).to.equal(2); - expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); @@ -1434,17 +1447,19 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); - expect(data.pbv).to.equal(getGlobal()?.version || '-1'); + expect(data.dm).to.equal(DISPLAY_MANAGER); + expect(data.dmv).to.equal('$prebid.version$' || '-1'); + expect(data.ctr).not.to.be.null; expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); - expect(data.s[0].ffs).to.equal(1); - expect(data.s[0].fsrc).to.equal(2); - expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].sid).not.to.be.undefined; expect(data.s[0].ps).to.be.an('array'); @@ -1477,9 +1492,6 @@ describe('pubmatic analytics adapter', function () { // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); - expect(data.s[1].ffs).to.equal(1); - expect(data.s[1].fsrc).to.equal(2); - expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); @@ -1566,17 +1578,19 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); - expect(data.pbv).to.equal(getGlobal()?.version || '-1'); + expect(data.dm).to.equal(DISPLAY_MANAGER); + expect(data.dmv).to.equal('$prebid.version$' || '-1'); + expect(data.ctr).not.to.be.null; expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); + expect(data.ffs).to.equal(1); + expect(data.fsrc).to.equal(2); + expect(data.fp).to.equal('pubmatic'); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); - expect(data.s[0].ffs).to.equal(1); - expect(data.s[0].fsrc).to.equal(2); - expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].sid).not.to.be.undefined; expect(data.s[0].ps).to.be.an('array'); @@ -1657,8 +1671,55 @@ describe('pubmatic analytics adapter', function () { expect(data.origbidid).to.equal('partnerImpressionID-1'); }); + it('Logger: should use originalRequestId to find the bid', function() { + MOCK.BID_RESPONSE[1]['originalRequestId'] = '3bd4ebb1c900e2'; + MOCK.BID_RESPONSE[1]['requestId'] = '54d4ebb1c9003e'; + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s).to.be.an('array'); + expect(data.s.length).to.equal(2); + + // slot 1 + expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); + expect(data.s[0].ps[0].origbidid).to.equal('partnerImpressionID-1'); + + // slot 2 + expect(data.s[1].ps[0].bidid).to.equal('54d4ebb1c9003e'); + expect(data.s[1].ps[0].origbidid).to.equal('partnerImpressionID-2'); + + // tracker slot1 + let firstTracker = requests[0].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.bidid).to.equal('2ecff0db240757'); + expect(data.origbidid).to.equal('partnerImpressionID-1'); + }); + it('Logger: best case + win tracker. Log bidId when partnerimpressionid is missing', function() { delete MOCK.BID_RESPONSE[1]['partnerImpId']; + MOCK.BID_RESPONSE[1]['requestId'] = '3bd4ebb1c900e2'; MOCK.BID_RESPONSE[1]['prebidBidId'] = 'Prebid-bid-id-1'; sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index ebda4b1767d..c468470d201 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1,4210 +1,1063 @@ import { expect } from 'chai'; -import { spec, checkVideoPlacement, _getDomainFromURL, assignDealTier, prepareMetaObject, getDeviceConnectionType } from 'modules/pubmaticBidAdapter.js'; +import { spec, cpmAdjustment } from 'modules/pubmaticBidAdapter.js'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; -import { createEidsArray } from 'modules/userId/eids.js'; import { bidderSettings } from 'src/bidderSettings.js'; -const constants = require('src/constants.js'); -describe('PubMatic adapter', function () { - let bidRequests; - let videoBidRequests; - let multipleMediaRequests; - let bidResponses; - let nativeBidRequests; - let nativeBidRequestsWithAllParams; - let nativeBidRequestsWithoutAsset; - let nativeBidRequestsWithRequiredParam; - let nativeBidResponse; - let validnativeBidImpression; - let validnativeBidImpressionWithRequiredParam; - let nativeBidImpressionWithoutRequiredParams; - let validnativeBidImpressionWithAllParams; - let bannerAndVideoBidRequests; - let bannerAndNativeBidRequests; - let videoAndNativeBidRequests; - let bannerVideoAndNativeBidRequests; - let bannerBidResponse; - let videoBidResponse; - let schainConfig; - let outstreamBidRequest; - let validOutstreamBidRequest; - let outstreamVideoBidResponse; - - beforeEach(function () { - schainConfig = { - 'ver': '1.0', - 'complete': 1, - 'nodes': [ - { - 'asi': 'indirectseller.com', - 'sid': '00001', - 'hp': 1 - }, - - { - 'asi': 'indirectseller-2.com', - 'sid': '00002', - 'hp': 2 - } - ] - }; - - bidRequests = [ - { - bidder: 'pubmatic', - mediaTypes: { - banner: { - sizes: [[728, 90], [160, 600]] - } - }, - params: { - publisherId: '5670', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD', - dctr: 'key1:val1,val2|key2:val1' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - ortb2Imp: { - ext: { - tid: '92489f71-1bf2-49a0-adf9-000cea934729', - gpid: '/1111/homepage-leftnav' - } - }, - schain: schainConfig - } - ]; - - videoBidRequests = - [ - { - code: 'video1', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'instream' - } - }, - bidder: 'pubmatic', - bidId: '22bddb28db77d', - params: { - publisherId: '5890', - adSlot: 'Div1@0x0', // ad_id or tagid - wiid: 'new-unique-wiid', - video: { - mimes: ['video/mp4', 'video/x-flv'], - skippable: true, - minduration: 5, - maxduration: 30, - startdelay: 5, - playbackmethod: [1, 3], - api: [1, 2], - protocols: [2, 3], - battr: [13, 14], - linearity: 1, - placement: 2, - plcmt: 1, - minbitrate: 10, - maxbitrate: 10 - } - } +describe('PubMatic adapter', () => { + let firstBid, videoBid, firstResponse, response, videoResponse; + let request = {}; + firstBid = { + adUnitCode: 'Div1', + bidder: 'pubmatic', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]], + pos: 1 } - ]; - - multipleMediaRequests = [ - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200' - } + }, + params: { + publisherId: '5670', + adSlot: '/15671365/DMDemo@300x250:0', + kadfloor: '1.2', + pmzoneid: 'aabc, ddef', + // kadpageurl: 'www.publisher.com', + yob: '1986', + gender: 'M', + lat: '12.3', + lon: '23.7', + wiid: '1234567890', + profId: '100', + verId: '200', + currency: 'AUD', + dctr: 'key1:val1,val2|key2:val1', + deals: ['deal-1', 'deal-2'] + }, + placementCode: '/19968336/header-bid-tag-1', + sizes: [ + [300, 250], + [300, 600], + ['fluid'] + ], + bidId: '3736271c3c4b27', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + ortb2: { + device: { + w: 1200, + h: 1800, + sua: {}, + language: 'en', + js: 1, + connectiontype: 6 }, - { - code: 'div-instream', - mediaTypes: { - video: { - context: 'instream', - playerSize: [300, 250] - }, - }, - bidder: 'pubmatic', - params: { - publisherId: '5890', - adSlot: 'Div1@640x480', // ad_id or tagid - wiid: '1234567890', - video: { - mimes: ['video/mp4', 'video/x-flv'], - skippable: true, - minduration: 5, - maxduration: 30, - startdelay: 15, - playbackmethod: [1, 3], - api: [1, 2], - protocols: [2, 3], - w: 640, - h: 480, - battr: [13, 14], - linearity: 1, - placement: 2, - plcmt: 1, - minbitrate: 100, - maxbitrate: 4096 - } - } - } - ]; - - nativeBidRequests = [{ - code: '/19968336/prebid_native_example_1', - sizes: [ - [300, 250] - ], - mediaTypes: { - native: { - title: { - required: true, - length: 80 + site: {domain: 'ebay.com', page: 'https://ebay.com'}, + source: {} + }, + ortb2Imp: { + ext: { + tid: '92489f71-1bf2-49a0-adf9-000cea934729', + gpid: '/1111/homepage-leftnav', + data: { + pbadslot: '/1111/homepage-leftnav', + adserver: { + name: 'gam', + adslot: '/1111/homepage-leftnav' }, - image: { - required: true, - sizes: [300, 250] - }, - sponsoredBy: { - required: true + customData: { + id: 'id-1' } } - }, - nativeParams: { - title: { required: true, length: 80 }, - image: { required: true, sizes: [300, 250] }, - sponsoredBy: { required: true } - }, - nativeOrtbRequest: { - ver: '1.2', - assets: [ - { id: 0, required: 1, title: {len: 140} }, - { id: 1, required: 1, img: { type: 3, w: 300, h: 250 } }, - { id: 2, required: 1, data: { type: 1 } } - ] - }, - bidder: 'pubmatic', - params: { - publisherId: '5670', - adSlot: '/43743431/NativeAutomationPrebid@1x1', - wiid: 'new-unique-wiid' - }, - bidId: '2a5571261281d4', - requestId: 'B68287E1-DC39-4B38-9790-FE4F179739D6', - bidderRequestId: '1c56ad30b9b8ca8', - }]; - - nativeBidRequestsWithAllParams = [{ - code: '/19968336/prebid_native_example_1', - sizes: [ - [300, 250] - ], - mediaTypes: { - native: { - title: {required: true, len: 80, ext: {'title1': 'title2'}}, - icon: {required: true, sizes: [50, 50], ext: {'icon1': 'icon2'}}, - image: {required: true, sizes: [728, 90], ext: {'image1': 'image2'}, 'mimes': ['image/png', 'image/gif']}, - sponsoredBy: {required: true, len: 10, ext: {'sponsor1': 'sponsor2'}}, - body: {required: true, len: 10, ext: {'body1': 'body2'}}, - rating: {required: true, len: 10, ext: {'rating1': 'rating2'}}, - likes: {required: true, len: 10, ext: {'likes1': 'likes2'}}, - downloads: {required: true, len: 10, ext: {'downloads1': 'downloads2'}}, - price: {required: true, len: 10, ext: {'price1': 'price2'}}, - saleprice: {required: true, len: 10, ext: {'saleprice1': 'saleprice2'}}, - phone: {required: true, len: 10, ext: {'phone1': 'phone2'}}, - address: {required: true, len: 10, ext: {'address1': 'address2'}}, - desc2: {required: true, len: 10, ext: {'desc21': 'desc22'}}, - displayurl: {required: true, len: 10, ext: {'displayurl1': 'displayurl2'}} - } - }, - nativeParams: { - title: {required: true, len: 80, ext: {'title1': 'title2'}}, - icon: {required: true, sizes: [50, 50], ext: {'icon1': 'icon2'}}, - image: {required: true, sizes: [728, 90], ext: {'image1': 'image2'}, 'mimes': ['image/png', 'image/gif']}, - sponsoredBy: {required: true, len: 10, ext: {'sponsor1': 'sponsor2'}}, - body: {required: true, len: 10, ext: {'body1': 'body2'}}, - rating: {required: true, len: 10, ext: {'rating1': 'rating2'}}, - likes: {required: true, len: 10, ext: {'likes1': 'likes2'}}, - downloads: {required: true, len: 10, ext: {'downloads1': 'downloads2'}}, - price: {required: true, len: 10, ext: {'price1': 'price2'}}, - saleprice: {required: true, len: 10, ext: {'saleprice1': 'saleprice2'}}, - phone: {required: true, len: 10, ext: {'phone1': 'phone2'}}, - address: {required: true, len: 10, ext: {'address1': 'address2'}}, - desc2: {required: true, len: 10, ext: {'desc21': 'desc22'}}, - displayurl: {required: true, len: 10, ext: {'displayurl1': 'displayurl2'}} - }, - nativeOrtbRequest: { - 'ver': '1.2', - 'assets': [ - {'id': 0, 'required': 1, 'title': {'len': 80}}, - {'id': 1, 'required': 1, 'img': {'type': 1, 'w': 50, 'h': 50}}, - {'id': 2, 'required': 1, 'img': {'type': 3, 'w': 728, 'h': 90}}, - {'id': 3, 'required': 1, 'data': {'type': 1, 'len': 10}}, - {'id': 4, 'required': 1, 'data': {'type': 2, 'len': 10}}, - {'id': 5, 'required': 1, 'data': {'type': 3, 'len': 10}}, - {'id': 6, 'required': 1, 'data': {'type': 4, 'len': 10}}, - {'id': 7, 'required': 1, 'data': {'type': 5, 'len': 10}}, - {'id': 8, 'required': 1, 'data': {'type': 6, 'len': 10}}, - {'id': 9, 'required': 1, 'data': {'type': 8, 'len': 10}}, - {'id': 10, 'required': 1, 'data': {'type': 9, 'len': 10}} - ] - }, - bidder: 'pubmatic', - params: { - publisherId: '5670', - adSlot: '/43743431/NativeAutomationPrebid@1x1', - wiid: 'new-unique-wiid' - }, - bidId: '2a5571261281d4', - requestId: 'B68287E1-DC39-4B38-9790-FE4F179739D6', - bidderRequestId: '1c56ad30b9b8ca8', - }]; - - nativeBidRequestsWithoutAsset = [{ - code: '/19968336/prebid_native_example_1', - sizes: [ - [300, 250] - ], - mediaTypes: { - native: { - type: 'image' - } - }, - nativeParams: { - title: { required: true }, - image: { required: true }, - sponsoredBy: { required: true }, - clickUrl: { required: true } - }, - bidder: 'pubmatic', - params: { - publisherId: '5670', - adSlot: '/43743431/NativeAutomationPrebid@1x1', - wiid: 'new-unique-wiid' } - }]; - - nativeBidRequestsWithRequiredParam = [{ - code: '/19968336/prebid_native_example_1', - sizes: [ - [300, 250] - ], - mediaTypes: { - native: { - title: { - required: false, - length: 80 - }, - image: { - required: false, - sizes: [300, 250] - }, - sponsoredBy: { - required: true - } - } - }, - nativeParams: { - title: { required: false, length: 80 }, - image: { required: false, sizes: [300, 250] }, - sponsoredBy: { required: true } + }, + } + videoBid = { + 'seat': 'seat-id', + 'ext': { + 'buyid': 'BUYER-ID-987' + }, + 'bid': [{ + 'id': '74858439-49D7-4169-BA5D-44A046315B2F', + 'impid': '3736271c3c4b27', + 'price': 1.3, + 'adm': 'Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1https://dsptracker.com/{PSPM}00:00:04https://www.pubmatic.com', + 'adomain': ['blackrock.com'], + 'h': 250, + 'w': 300, + 'ext': { + 'deal_channel': 6, + 'advid': 976, + 'dspid': 123 }, - nativeOrtbRequest: { - ver: '1.2', - assets: [ - { id: 0, required: 0, title: {len: 140} }, - { id: 1, required: 0, img: {type: 3, w: 300, h: 250} }, - { id: 2, required: 1, data: {type: 1} } - ] + 'dealid': 'PUBDEAL1', + 'mtype': 2 + }] + }; + firstResponse = { + 'seat': 'seat-id', + 'ext': { + 'buyid': 'BUYER-ID-987' + }, + 'bid': [{ + 'id': '74858439-49D7-4169-BA5D-44A046315B2F', + 'impid': '3736271c3c4b27', + 'price': 1.3, + 'adm': 'image3.pubmatic.com Layer based creative', + 'adomain': ['blackrock.com'], + 'h': 250, + 'w': 300, + 'ext': { + 'deal_channel': 6, + 'advid': 976, + 'dspid': 123 }, - bidder: 'pubmatic', - params: { - publisherId: '5670', - adSlot: '/43743431/NativeAutomationPrebid@1x1', - wiid: 'new-unique-wiid' - } - }]; - - bannerAndVideoBidRequests = [ - { - code: 'div-banner-video', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'instream' - }, - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD', - dctr: 'key1:val1,val2|key2:val1', - video: { - mimes: ['video/mp4', 'video/x-flv'], - skippable: true, - minduration: 5, - maxduration: 30, - startdelay: 15, - playbackmethod: [1, 3], - api: [1, 2], - protocols: [2, 3], - w: 640, - h: 480, - battr: [13, 14], - linearity: 1, - placement: 2, - plcmt: 1, - minbitrate: 100, - maxbitrate: 4096 - } - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[728, 90]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; - - bannerAndNativeBidRequests = [ - { - code: 'div-banner-native', - mediaTypes: { - native: { - title: { - required: true, - length: 80 - }, - image: { - required: true, - sizes: [300, 250] - }, - sponsoredBy: { - required: true - } - }, - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - nativeParams: { - title: { required: true, length: 80 }, - image: { required: true, sizes: [300, 250] }, - sponsoredBy: { required: true } - }, - nativeOrtbRequest: { - ver: '1.2', - assets: [ - {id: 0, required: 1, title: {len: 140}}, - {id: 1, required: 1, img: {type: 3, w: 300, h: 250}}, - {id: 2, required: 1, data: {type: 1}} - ] - }, - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD', - dctr: 'key1:val1,val2|key2:val1' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[728, 90]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; - - videoAndNativeBidRequests = [ - { - code: 'div-video-native', - mediaTypes: { - native: { - title: { - required: true, - length: 80 - }, - image: { - required: true, - sizes: [300, 250] - }, - sponsoredBy: { - required: true - } - }, - video: { - playerSize: [640, 480], - context: 'instream' - } - }, - nativeParams: { - title: { required: true, length: 80 }, - image: { required: true, sizes: [300, 250] }, - sponsoredBy: { required: true } - }, - nativeOrtbRequest: { - ver: '1.2', - assets: [ - { id: 0, required: 1, title: {len: 140} }, - { id: 1, required: 1, img: { type: 3, w: 300, h: 250 } }, - { id: 2, required: 1, data: { type: 1 } } - ] - }, - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - wiid: 'new-unique-wiid', - video: { - mimes: ['video/mp4', 'video/x-flv'], - skippable: true, - minduration: 5, - maxduration: 30, - startdelay: 15, - playbackmethod: [1, 3], - api: [1, 2], - protocols: [2, 3], - w: 640, - h: 480, - battr: [13, 14], - linearity: 1, - placement: 2, - plcmt: 1, - minbitrate: 100, - maxbitrate: 4096 - } - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[728, 90]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; - - bannerVideoAndNativeBidRequests = [ - { - code: 'div-video-native', - mediaTypes: { - native: { - title: { - required: true, - length: 80 - }, - image: { - required: true, - sizes: [300, 250] - }, - sponsoredBy: { - required: true - } - }, - video: { - playerSize: [640, 480], - context: 'instream' - }, - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - nativeParams: { - title: { required: true, length: 80 }, - image: { required: true, sizes: [300, 250] }, - sponsoredBy: { required: true } - }, - nativeOrtbRequest: { - ver: '1.2', - assets: [ - { id: 0, required: 1, title: {len: 80} }, - { id: 1, required: 1, img: { type: 3, w: 300, h: 250 } }, - { id: 2, required: 1, data: { type: 1 } } - ] - }, - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - wiid: 'new-unique-wiid', - video: { - mimes: ['video/mp4', 'video/x-flv'], - skippable: true, - minduration: 5, - maxduration: 30, - startdelay: 15, - playbackmethod: [1, 3], - api: [1, 2], - protocols: [2, 3], - w: 640, - h: 480, - battr: [13, 14], - linearity: 1, - placement: 2, - plcmt: 1, - minbitrate: 100, - maxbitrate: 4096 - } - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[728, 90]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; - - bidResponses = { - 'body': { - 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', - 'seatbid': [{ - 'seat': 'seat-id', - 'ext': { - 'buyid': 'BUYER-ID-987' - }, - 'bid': [{ - 'id': '74858439-49D7-4169-BA5D-44A046315B2F', - 'impid': '22bddb28db77d', - 'price': 1.3, - 'adm': 'image3.pubmatic.com Layer based creative', - 'adomain': ['blackrock.com'], - 'h': 250, - 'w': 300, - 'ext': { - 'deal_channel': 6, - 'advid': 976, - 'dspid': 123, - 'dchain': 'dchain' - } - }] - }, { - 'ext': { - 'buyid': 'BUYER-ID-789' - }, - 'bid': [{ - 'id': '74858439-49D7-4169-BA5D-44A046315BEF', - 'impid': '22bddb28db77e', - 'price': 1.7, - 'adm': 'image3.pubmatic.com Layer based creative', - 'adomain': ['hivehome.com'], - 'h': 250, - 'w': 300, - 'ext': { - 'deal_channel': 5, - 'advid': 832, - 'dspid': 422 - } - }] - }] - } - }; - - nativeBidResponse = { - 'body': { - 'id': '1544691825939', - 'seatbid': [{ - 'bid': [{ - 'id': 'B68287E1-DC39-4B38-9790-FE4F179739D6', - 'impid': '2a5571261281d4', - 'price': 0.01, - 'adm': "{\"native\":{\"assets\":[{\"id\":1,\"title\":{\"text\":\"Native Test Title\"}},{\"id\":2,\"img\":{\"h\":627,\"url\":\"http://stagingpub.net/native_ads/PM-Native-Ad-1200x627.png\",\"w\":1200}},{\"data\":{\"value\":\"Sponsored By PubMatic\"},\"id\":4}],\"imptrackers\":[\"http://imptracker.com/main/9bde02d0-6017-11e4-9df7-005056967c35\",\"http://172.16.4.213/AdServer/AdDisplayTrackerServlet?operId=1&pubId=5890&siteId=5892&adId=6016&adType=12&adServerId=243&kefact=0.010000&kaxefact=0.010000&kadNetFrequecy=0&kadwidth=0&kadheight=0&kadsizeid=7&kltstamp=1544692761&indirectAdId=0&adServerOptimizerId=2&ranreq=0.1&kpbmtpfact=1.000000&dcId=1&tldId=0&passback=0&svr=MADS1107&ekefact=GSQSXOLKDgBAvRnoiNj0LxtpAnNEO30u1ZI5sITloOsP7gzh&ekaxefact=GSQSXAXLDgD0fOZLCjgbnVJiyS3D65dqDkxfs2ArpC3iugXw&ekpbmtpfact=GSQSXCDLDgB5mcooOvXtCKmx7TnNDJDY2YuHFOL3o9ceoU4H&crID=campaign111&lpu=advertiserdomain.com&ucrid=273354366805642829&campaignId=16981&creativeId=0&pctr=0.000000&wDSPByrId=511&wDspId=6&wbId=0&wrId=0&wAdvID=1&isRTB=1&rtbId=C09BB577-B8C1-4C3E-A0FF-73F6F631C80A&imprId=B68287E1-DC39-4B38-9790-FE4F179739D6&oid=B68287E1-DC39-4B38-9790-FE4F179739D6&pageURL=http%3A%2F%2Ftest.com%2FTestPages%2Fnativead.html\"],\"jstracker\":\" ', - 'h': 250, - 'w': 300, - 'ext': { - 'deal_channel': 6 - } - }] - }] - } - }; - - videoBidResponse = { - 'body': { - 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', - 'seatbid': [{ - 'bid': [{ - 'id': '74858439-49D7-4169-BA5D-44A046315B2F', - 'impid': '22bddb28db77d', - 'price': 1.3, - 'adm': 'Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1https://dsptracker.com/{PSPM}00:00:04https://www.pubmatic.com', - 'h': 250, - 'w': 300, - 'ext': { - 'deal_channel': 6 - } - }] - }] - } - }; - outstreamBidRequest = - [ - { - code: 'video1', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream' - } - }, - bidder: 'pubmatic', - bidId: '47acc48ad47af5', - requestId: '0fb4905b-1234-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', - params: { - publisherId: '5670', - outstreamAU: 'pubmatic-test', - adSlot: 'Div1@0x0', // ad_id or tagid - video: { - mimes: ['video/mp4', 'video/x-flv'], - skippable: true, - minduration: 5, - maxduration: 30 - } - } - } - ]; - - validOutstreamBidRequest = { - auctionId: '92489f71-1bf2-49a0-adf9-000cea934729', - auctionStart: 1585918458868, - bidderCode: 'pubmatic', - bidderRequestId: '47acc48ad47af5', - bids: [{ - adUnitCode: 'video1', - auctionId: '92489f71-1bf2-49a0-adf9-000cea934729', - bidId: '47acc48ad47af5', - bidRequestsCount: 1, - bidder: 'pubmatic', - bidderRequestId: '47acc48ad47af5', - mediaTypes: { - video: { - context: 'outstream' - } - }, - params: { - publisherId: '5670', - outstreamAU: 'pubmatic-test', - adSlot: 'Div1@0x0', // ad_id or tagid - video: { - mimes: ['video/mp4', 'video/x-flv'], - skippable: true, - minduration: 5, - maxduration: 30 - } - }, - sizes: [[768, 432], [640, 480], [630, 360]], - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - }], - start: 11585918458869, - timeout: 3000 - }; - - outstreamVideoBidResponse = { - 'body': { - 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', - 'seatbid': [{ - 'bid': [{ - 'id': '0fb4905b-1234-4152-86be-c6f6d259ba99', - 'impid': '47acc48ad47af5', - 'price': 1.3, - 'adm': 'Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1https://dsptracker.com/{PSPM}00:00:04https://www.pubmatic.com', - 'h': 250, - 'w': 300, - 'ext': { - 'deal_channel': 6 - } - }] - }] - } - }; - }); - - describe('implementation', function () { - describe('Bid validations', function () { - it('valid bid case', function () { - let validBid = { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0' - } - }, - isValid = spec.isBidRequestValid(validBid); - expect(isValid).to.equal(true); - }); - - it('invalid bid case: publisherId not passed', function () { - let validBid = { - bidder: 'pubmatic', - params: { - adSlot: '/15671365/DMDemo@300x250:0' - } - }, - isValid = spec.isBidRequestValid(validBid); - expect(isValid).to.equal(false); - }); - - it('invalid bid case: publisherId is not string', function () { - let validBid = { - bidder: 'pubmatic', - params: { - publisherId: 301, - adSlot: '/15671365/DMDemo@300x250:0' - } - }, - isValid = spec.isBidRequestValid(validBid); - expect(isValid).to.equal(false); - }); - - it('valid bid case: adSlot is not passed', function () { - let validBid = { - bidder: 'pubmatic', - params: { - publisherId: '301' - } - }, - isValid = spec.isBidRequestValid(validBid); - expect(isValid).to.equal(true); - }); - - if (FEATURES.VIDEO) { - it('should check for context if video is present', function() { - let bid = { - 'bidder': 'pubmatic', - 'params': { - 'adSlot': 'SLOT_NHB1@728x90', - 'publisherId': '5890' - }, - 'mediaTypes': { - 'video': { - 'playerSize': [ - [640, 480] - ], - 'protocols': [1, 2, 5], - 'context': 'instream', - 'mimes': ['video/flv'], - 'skippable': false, - 'skip': 1, - 'linearity': 2 - } - }, - 'adUnitCode': 'video1', - 'transactionId': '803e3750-0bbe-4ffe-a548-b6eca15087bf', - 'sizes': [ - [640, 480] - ], - 'bidId': '2c95df014cfe97', - 'bidderRequestId': '1fe59391566442', - 'auctionId': '3a4118ef-fb96-4416-b0b0-3cfc1cebc142', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }, - isValid = spec.isBidRequestValid(bid); - expect(isValid).to.equal(true); - }) - - it('should return false if context is not present in video', function() { - let bid = { - 'bidder': 'pubmatic', - 'params': { - 'adSlot': 'SLOT_NHB1@728x90', - 'publisherId': '5890' - }, - 'mediaTypes': { - 'video': { - 'w': 640, - 'h': 480, - 'protocols': [1, 2, 5], - 'mimes': ['video/flv'], - 'skippable': false, - 'skip': 1, - 'linearity': 2 - } - }, - 'adUnitCode': 'video1', - 'transactionId': '803e3750-0bbe-4ffe-a548-b6eca15087bf', - 'sizes': [ - [640, 480] - ], - 'bidId': '2c95df014cfe97', - 'bidderRequestId': '1fe59391566442', - 'auctionId': '3a4118ef-fb96-4416-b0b0-3cfc1cebc142', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }, - isValid = spec.isBidRequestValid(bid); - expect(isValid).to.equal(false); - }) - - it('bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array', function() { - let bid = { - 'bidder': 'pubmatic', - 'params': { - 'adSlot': 'SLOT_NHB1@728x90', - 'publisherId': '5890', - 'video': {} - }, - 'mediaTypes': { - 'video': { - 'playerSize': [ - [640, 480] - ], - 'protocols': [1, 2, 5], - 'context': 'instream', - 'skippable': false, - 'skip': 1, - 'linearity': 2 - } - }, - 'adUnitCode': 'video1', - 'transactionId': '803e3750-0bbe-4ffe-a548-b6eca15087bf', - 'sizes': [ - [640, 480] - ], - 'bidId': '2c95df014cfe97', - 'bidderRequestId': '1fe59391566442', - 'auctionId': '3a4118ef-fb96-4416-b0b0-3cfc1cebc142', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }; - - delete bid.params.video.mimes; // Undefined - bid.mediaTypes.video.mimes = 'string'; // NOT array - expect(spec.isBidRequestValid(bid)).to.equal(false); - - delete bid.params.video.mimes; // Undefined - delete bid.mediaTypes.video.mimes; // Undefined - expect(spec.isBidRequestValid(bid)).to.equal(false); - - delete bid.params.video.mimes; // Undefined - bid.mediaTypes.video.mimes = ['video/flv']; // Valid - expect(spec.isBidRequestValid(bid)).to.equal(true); - - delete bid.mediaTypes.video.mimes; // mediaTypes.video.mimes undefined - bid.params.video = {mimes: 'string'}; // NOT array - expect(spec.isBidRequestValid(bid)).to.equal(false); - - delete bid.mediaTypes.video.mimes; // mediaTypes.video.mimes undefined - delete bid.params.video.mimes; // Undefined - expect(spec.isBidRequestValid(bid)).to.equal(false); - - delete bid.mediaTypes.video.mimes; // mediaTypes.video.mimes undefined - bid.params.video.mimes = ['video/flv']; // Valid - expect(spec.isBidRequestValid(bid)).to.equal(true); - - delete bid.mediaTypes.video.mimes; // Undefined - bid.params.video.mimes = ['video/flv']; // Valid - expect(spec.isBidRequestValid(bid)).to.equal(true); - - delete bid.mediaTypes.video.mimes; // Undefined - delete bid.params.video.mimes; // Undefined - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('checks on bid.params.outstreamAU & bid.renderer & bid.mediaTypes.video.renderer', function() { - const getThebid = function() { - let bid = utils.deepClone(validOutstreamBidRequest.bids[0]); - bid.params.outstreamAU = 'pubmatic-test'; - bid.renderer = ' '; // we are only checking if this key is set or not - bid.mediaTypes.video.renderer = ' '; // we are only checking if this key is set or not - return bid; - } - - // true: when all are present - // mdiatype: outstream - // bid.params.outstreamAU : Y - // bid.renderer : Y - // bid.mediaTypes.video.renderer : Y - let bid = getThebid(); - expect(spec.isBidRequestValid(bid)).to.equal(true); - - // true: atleast one is present; 3 cases - // mdiatype: outstream - // bid.params.outstreamAU : Y - // bid.renderer : N - // bid.mediaTypes.video.renderer : N - bid = getThebid(); - delete bid.renderer; - delete bid.mediaTypes.video.renderer; - expect(spec.isBidRequestValid(bid)).to.equal(true); - - // true: atleast one is present; 3 cases - // mdiatype: outstream - // bid.params.outstreamAU : N - // bid.renderer : Y - // bid.mediaTypes.video.renderer : N - bid = getThebid(); - delete bid.params.outstreamAU; - delete bid.mediaTypes.video.renderer; - expect(spec.isBidRequestValid(bid)).to.equal(true); - - // true: atleast one is present; 3 cases - // mdiatype: outstream - // bid.params.outstreamAU : N - // bid.renderer : N - // bid.mediaTypes.video.renderer : Y - bid = getThebid(); - delete bid.params.outstreamAU; - delete bid.renderer; - expect(spec.isBidRequestValid(bid)).to.equal(true); - - // false: none present; only outstream - // mdiatype: outstream - // bid.params.outstreamAU : N - // bid.renderer : N - // bid.mediaTypes.video.renderer : N - bid = getThebid(); - delete bid.params.outstreamAU; - delete bid.renderer; - delete bid.mediaTypes.video.renderer; - expect(spec.isBidRequestValid(bid)).to.equal(false); - - // true: none present; outstream + Banner - // mdiatype: outstream, banner - // bid.params.outstreamAU : N - // bid.renderer : N - // bid.mediaTypes.video.renderer : N - bid = getThebid(); - delete bid.params.outstreamAU; - delete bid.renderer; - delete bid.mediaTypes.video.renderer; - bid.mediaTypes.banner = {sizes: [ [300, 250], [300, 600] ]}; - expect(spec.isBidRequestValid(bid)).to.equal(true); - - // true: none present; outstream + Native - // mdiatype: outstream, native - // bid.params.outstreamAU : N - // bid.renderer : N - // bid.mediaTypes.video.renderer : N - bid = getThebid(); - delete bid.params.outstreamAU; - delete bid.renderer; - delete bid.mediaTypes.video.renderer; - bid.mediaTypes.native = {} - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - } + } + let validBidRequests = [firstBid]; + let bidderRequest = { + bids: [firstBid], + auctionId: 'ee3074fe-97ce-4681-9235-d7622aede74c', + auctionStart: 1725514077194, + bidderCode: 'pubmatic', + bidderRequestId: '1c56ad30b9b8ca8', + refererInfo: { + page: 'https://ebay.com', + ref: '' + }, + ortb2: { + device: { + w: 1200, + h: 1800, + sua: {}, + language: 'en', + js: 1, + connectiontype: 6 + }, + site: {domain: 'ebay.com', page: 'https://ebay.com'}, + source: {} + }, + timeout: 2000 + }; + let videoBidRequest, videoBidderRequest, utilsLogWarnMock, nativeBidderRequest; + + describe('Bid validations', () => { + it('should return true if publisherId is present in params', () => { + const isValid = spec.isBidRequestValid(validBidRequests[0]); + expect(isValid).to.equal(true); }); - describe('Request formation', function () { - it('buildRequests function should not modify original bidRequests object', function () { - let originalBidRequests = utils.deepClone(bidRequests); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - expect(bidRequests).to.deep.equal(originalBidRequests); - }); - - it('buildRequests function should not modify original nativebidRequests object', function () { - let originalBidRequests = utils.deepClone(nativeBidRequests); - let request = spec.buildRequests(nativeBidRequests, { - auctionId: 'new-auction-id' - }); - expect(nativeBidRequests).to.deep.equal(originalBidRequests); - }); - - it('Endpoint checking', function () { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - expect(request.url).to.equal('https://hbopenbid.pubmatic.com/translator?source=prebid-client'); - expect(request.method).to.equal('POST'); - }); - - it('should return bidderRequest property', function() { - let request = spec.buildRequests(bidRequests, validOutstreamBidRequest); - expect(request.bidderRequest).to.equal(validOutstreamBidRequest); - }); - - it('bidderRequest should be undefined if bidderRequest is not present', function() { - let request = spec.buildRequests(bidRequests); - expect(request.bidderRequest).to.be.undefined; - }); - - it('test flag not sent when pubmaticTest=true is absent in page url', function() { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.test).to.equal(undefined); - }); - - it('Request params check', function () { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', - ortb2: { - source: { - tid: 'source-tid' - }, - device: { - geo: { - lat: '36.5189', - lon: '-76.4063' - } - }, - user: { - geo: { - lat: '26.8915', - lon: '-56.6340' - } - }, - } - }); - let data = JSON.parse(request.data); - expect(data.at).to.equal(1); // auction type - expect(data.cur[0]).to.equal('USD'); // currency - expect(data.site.domain).to.be.a('string'); // domain should be set - expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL - expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id - expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB - expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.equal('36.5189'); // Latitude - expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude - expect(data.user.geo.lat).to.equal('26.8915'); // Latitude - expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude - expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version - expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId - expect(data.source.tid).to.equal('source-tid'); // Prebid TransactionId - expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID - expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID - expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID - - expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id - expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor - expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid - expect(data.imp[0].banner.w).to.equal(300); // width - expect(data.imp[0].banner.h).to.equal(250); // height - expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); - expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid - expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); - expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); - expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); - expect(data.ext.epoch).to.exist; - }); - - it('Set tmax from global config if not set by requestBids method', function() { - let sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - bidderTimeout: 3000 - }; - return config[key]; - }); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', timeout: 3000 - }); - let data = JSON.parse(request.data); - expect(data.tmax).to.deep.equal(3000); - sandbox.restore(); - }); - describe('Marketplace parameters', function() { - let bidderSettingStub; - beforeEach(function() { - bidderSettingStub = sinon.stub(bidderSettings, 'get'); - }); - - afterEach(function() { - bidderSettingStub.restore(); - }); - - it('should not be present when allowAlternateBidderCodes is undefined', function () { - bidderSettingStub.returns(undefined); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.ext.marketplace).to.equal(undefined); - }); - - it('should be pubmatic and groupm when allowedAlternateBidderCodes is \'groupm\'', function () { - bidderSettingStub.withArgs('pubmatic', 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs('pubmatic', 'allowedAlternateBidderCodes').returns(['groupm']); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', - bidderCode: 'pubmatic' - }); - let data = JSON.parse(request.data); - expect(data.ext.marketplace.allowedbidders).to.be.an('array'); - expect(data.ext.marketplace.allowedbidders.length).to.equal(2); - expect(data.ext.marketplace.allowedbidders[0]).to.equal('pubmatic'); - expect(data.ext.marketplace.allowedbidders[1]).to.equal('groupm'); - }); + it('should return false if publisherId is missing', () => { + const bid = utils.deepClone(validBidRequests[0]); + delete bid.params.publisherId; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equal(false); + }); - it('should be ALL by default', function () { - bidderSettingStub.returns(true); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.ext.marketplace.allowedbidders).to.be.an('array'); - expect(data.ext.marketplace.allowedbidders[0]).to.equal('all'); - }); + it('should return false if publisherId is not of type string', () => { + const bid = utils.deepClone(validBidRequests[0]); + bid.params.publisherId = 5890; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equal(false); + }); - it('should be ALL when allowedAlternateBidderCodes is \'*\'', function () { - bidderSettingStub.withArgs('pubmatic', 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs('pubmatic', 'allowedAlternateBidderCodes').returns(['*']); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', - bidderCode: 'pubmatic' - }); - let data = JSON.parse(request.data); - expect(data.ext.marketplace.allowedbidders).to.be.an('array'); - expect(data.ext.marketplace.allowedbidders[0]).to.equal('all'); - }); - }) - - it('Set content from config, set site.content', function() { - let sandbox = sinon.sandbox.create(); - const content = { - 'id': 'alpha-numeric-id' - }; - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - content: content - }; - return config[key]; - }); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.site.content).to.deep.equal(content); - sandbox.restore(); - }); - - it('Merge the device info from config', function() { - let sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - device: { - 'newkey': 'new-device-data' - } - }; - return config[key]; - }); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.device.js).to.equal(1); - expect(data.device.dnt).to.equal((navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0); - expect(data.device.h).to.equal(screen.height); - expect(data.device.w).to.equal(screen.width); - expect(data.device.language).to.equal(navigator.language.split('-')[0]); - expect(data.device.newkey).to.equal('new-device-data');// additional data from config - sandbox.restore(); - }); - - it('Merge the device info from config; data from config overrides the info we have gathered', function() { - let sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - device: { - newkey: 'new-device-data', - language: 'MARATHI' - } - }; - return config[key]; - }); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.device.js).to.equal(1); - expect(data.device.dnt).to.equal((navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0); - expect(data.device.h).to.equal(screen.height); - expect(data.device.w).to.equal(screen.width); - expect(data.device.language).to.equal('MARATHI');// // data overriding from config - expect(data.device.newkey).to.equal('new-device-data');// additional data from config - sandbox.restore(); - }); - - it('Set app from config, copy publisher and ext from site, unset site', function() { - let sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - app: { - bundle: 'org.prebid.mobile.demoapp', - domain: 'prebid.org' - } - }; - return config[key]; - }); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); - expect(data.app.domain).to.equal('prebid.org'); - expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); - expect(data.site).to.not.exist; - sandbox.restore(); - }); - - it('Set app, content from config, copy publisher and ext from site, unset site, config.content in app.content', function() { - let sandbox = sinon.sandbox.create(); - const content = { - 'id': 'alpha-numeric-id' - }; - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - content: content, - app: { - bundle: 'org.prebid.mobile.demoapp', - domain: 'prebid.org' - } - }; - return config[key]; - }); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); - expect(data.app.domain).to.equal('prebid.org'); - expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); - expect(data.app.content).to.deep.equal(content); - expect(data.site).to.not.exist; - sandbox.restore(); - }); - - it('Set app.content, content from config, copy publisher and ext from site, unset site, config.app.content in app.content', function() { - let sandbox = sinon.sandbox.create(); - const content = { - 'id': 'alpha-numeric-id' - }; - const appContent = { - id: 'app-content-id-2' - }; - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - content: content, - app: { - bundle: 'org.prebid.mobile.demoapp', - domain: 'prebid.org', - content: appContent - } - }; - return config[key]; - }); - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); - expect(data.app.domain).to.equal('prebid.org'); - expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); - expect(data.app.content).to.deep.equal(appContent); - expect(data.site).to.not.exist; - sandbox.restore(); - }); - - it('Request params check: without adSlot', function () { - delete bidRequests[0].params.adSlot; - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', - ortb2: { - device: { - geo: { - lat: '36.5189', - lon: '-76.4063' - } - }, - user: { - geo: { - lat: '26.8915', - lon: '-56.6340' - } - }, - } - }); - let data = JSON.parse(request.data); - expect(data.at).to.equal(1); // auction type - expect(data.cur[0]).to.equal('USD'); // currency - expect(data.site.domain).to.be.a('string'); // domain should be set - expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL - expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id - expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB - expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.equal('36.5189'); // Latitude - expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude - expect(data.user.geo.lat).to.equal('26.8915'); // Latitude - expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude - expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version - expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId - expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID - expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID - expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID - - expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id - expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor - expect(data.imp[0].tagid).to.deep.equal(undefined); // tagid - expect(data.imp[0].banner.w).to.equal(728); // width - expect(data.imp[0].banner.h).to.equal(90); // height - expect(data.imp[0].banner.format).to.deep.equal([{w: 160, h: 600}]); - expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); - expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); - expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid - expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); - }); - - it('Request params multi size format object check', function () { - let bidRequests = [ - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD' - }, - placementCode: '/19968336/header-bid-tag-1', - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; - /* case 1 - size passed in adslot */ - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - - expect(data.imp[0].banner.w).to.equal(300); // width - expect(data.imp[0].banner.h).to.equal(250); // height - - /* case 2 - size passed in adslot as well as in sizes array */ - bidRequests[0].sizes = [[300, 600], [300, 250]]; - bidRequests[0].mediaTypes = { - banner: { - sizes: [[300, 600], [300, 250]] - } - }; - request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - - expect(data.imp[0].banner.w).to.equal(300); // width - expect(data.imp[0].banner.h).to.equal(250); // height - - /* case 3 - size passed in sizes but not in adslot */ - bidRequests[0].params.adSlot = '/15671365/DMDemo'; - bidRequests[0].sizes = [[300, 250], [300, 600]]; - bidRequests[0].mediaTypes = { - banner: { - sizes: [[300, 250], [300, 600]] - } - }; - request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - - expect(data.imp[0].banner.w).to.equal(300); // width - expect(data.imp[0].banner.h).to.equal(250); // height - expect(data.imp[0].banner.format).exist.and.to.be.an('array'); - expect(data.imp[0].banner.format[0]).exist.and.to.be.an('object'); - expect(data.imp[0].banner.format[0].w).to.equal(300); // width - expect(data.imp[0].banner.format[0].h).to.equal(600); // height - }); - - it('Request params currency check', function () { - let multipleBidRequests = [ - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - }, - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'GBP' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; - - /* case 1 - - currency specified in both adunits - output: imp[0] and imp[1] both use currency specified in bidRequests[0].params.currency - - */ - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - - expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); - expect(data.imp[1].bidfloorcur).to.equal(bidRequests[0].params.currency); - - /* case 2 - - currency specified in only 1st adunit - output: imp[0] and imp[1] both use currency specified in bidRequests[0].params.currency - - */ - delete multipleBidRequests[1].params.currency; - request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); - expect(data.imp[1].bidfloorcur).to.equal(bidRequests[0].params.currency); - - /* case 3 - - currency specified in only 1st adunit - output: imp[0] and imp[1] both use default currency - USD - - */ - delete multipleBidRequests[0].params.currency; - request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - expect(data.imp[0].bidfloorcur).to.equal('USD'); - expect(data.imp[1].bidfloorcur).to.equal('USD'); - - /* case 4 - - currency not specified in 1st adunit but specified in 2nd adunit - output: imp[0] and imp[1] both use default currency - USD - - */ - multipleBidRequests[1].params.currency = 'AUD'; - request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - expect(data.imp[0].bidfloorcur).to.equal('USD'); - expect(data.imp[1].bidfloorcur).to.equal('USD'); - }); - - it('Pass auctiondId as wiid if wiid is not passed in params', function () { - let bidRequest = { - auctionId: 'new-auction-id', - ortb2: { - device: { - geo: { - lat: '36.5189', - lon: '-76.4063' - } - }, - user: { - geo: { - lat: '26.8915', - lon: '-56.6340' - } - }, - } - }; - delete bidRequests[0].params.wiid; - let request = spec.buildRequests(bidRequests, bidRequest); - let data = JSON.parse(request.data); - expect(data.at).to.equal(1); // auction type - expect(data.cur[0]).to.equal('USD'); // currency - expect(data.site.domain).to.be.a('string'); // domain should be set - expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL - expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id - expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB - expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.equal('36.5189'); // Latitude - expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude - expect(data.user.geo.lat).to.equal('26.8915'); // Latitude - expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude - expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version - expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId - expect(data.ext.wrapper.wiid).to.equal('new-auction-id'); // OpenWrap: Wrapper Impression ID - expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID - expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID - - expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id - expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor - expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid - expect(data.imp[0].banner.w).to.equal(300); // width - expect(data.imp[0].banner.h).to.equal(250); // height - expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); - expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid - }); - - it('Request params check with GDPR Consent', function () { - let bidRequest = { - gdprConsent: { - consentString: 'kjfdniwjnifwenrif3', - gdprApplies: true - }, - ortb2: { - device: { - geo: { - lat: '36.5189', - lon: '-76.4063' - } - }, - user: { - geo: { - lat: '26.8915', - lon: '-56.6340' - } - }, - } - }; - let request = spec.buildRequests(bidRequests, bidRequest); - let data = JSON.parse(request.data); - expect(data.user.ext.consent).to.equal('kjfdniwjnifwenrif3'); - expect(data.regs.ext.gdpr).to.equal(1); - expect(data.at).to.equal(1); // auction type - expect(data.cur[0]).to.equal('USD'); // currency - expect(data.site.domain).to.be.a('string'); // domain should be set - expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL - expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id - expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB - expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.equal('36.5189'); // Latitude - expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude - expect(data.user.geo.lat).to.equal('26.8915'); // Latitude - expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude - expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version - expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId - expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID - expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID - expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID - - expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id - expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor - expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid - expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); - expect(data.imp[0].banner.w).to.equal(300); // width - expect(data.imp[0].banner.h).to.equal(250); // height - expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid - }); - - it('Request params check with USP/CCPA Consent', function () { - let bidRequest = { - uspConsent: '1NYN', - ortb2: { - device: { - geo: { - lat: '36.5189', - lon: '-76.4063' - } - }, - user: { - geo: { - lat: '26.8915', - lon: '-56.6340' - } - }, - } - }; - let request = spec.buildRequests(bidRequests, bidRequest); - let data = JSON.parse(request.data); - expect(data.regs.ext.us_privacy).to.equal('1NYN');// USP/CCPAs - expect(data.at).to.equal(1); // auction type - expect(data.cur[0]).to.equal('USD'); // currency - expect(data.site.domain).to.be.a('string'); // domain should be set - expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL - expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id - expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB - expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.equal('36.5189'); // Latitude - expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude - expect(data.user.geo.lat).to.equal('26.8915'); // Latitude - expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude - expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version - expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId - expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID - expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID - expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID - - expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id - expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor - expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid - expect(data.imp[0].banner.w).to.equal(300); // width - expect(data.imp[0].banner.h).to.equal(250); // height - expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); - expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid - - // second request without USP/CCPA - let request2 = spec.buildRequests(bidRequests, {}); - let data2 = JSON.parse(request2.data); - expect(data2.regs).to.equal(undefined);// USP/CCPAs - }); - - it('Request params should include DSA signals if present', function () { - const dsa = { - dsarequired: 3, - pubrender: 0, - datatopub: 2, - transparency: [ - { - domain: 'platform1domain.com', - dsaparams: [1] - }, - { - domain: 'SSP2domain.com', - dsaparams: [1, 2] - } - ] - }; - - let bidRequest = { - ortb2: { - regs: { - ext: { - dsa - } - } - } - }; - let request = spec.buildRequests(bidRequests, bidRequest); - let data = JSON.parse(request.data); - assert.deepEqual(data.regs.ext.dsa, dsa); - }); - - it('Request params check with JW player params', function() { - let bidRequests = [ - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - dctr: 'key1=val1|key2=val2,val3' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', - rtd: { - jwplayer: { - targeting: { - content: { id: 'jw_d9J2zcaA' }, - segments: ['80011026', '80011035'] - } - } - } - }]; - let key_val_output = 'key1=val1|key2=val2,val3|jw-id=jw_d9J2zcaA|jw-80011026=1|jw-80011035=1' - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.imp[0].ext).to.exist.and.to.be.an('object'); - expect(data.imp[0].ext.key_val).to.exist.and.to.equal(key_val_output); - - // jw player data not available. Only dctr sent. - delete bidRequests[0].rtd; - request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - - expect(data.imp[0].ext).to.exist.and.to.be.an('object'); // dctr parameter - expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); - - // jw player data is available, but dctr is not present - bidRequests[0].rtd = { - jwplayer: { - targeting: { - content: { id: 'jw_d9J2zcaA' }, - segments: ['80011026', '80011035'] - } - } - }; - - delete bidRequests[0].params.dctr; - key_val_output = 'jw-id=jw_d9J2zcaA|jw-80011026=1|jw-80011035=1'; - request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - - expect(data.imp[0].ext).to.exist.and.to.be.an('object'); - expect(data.imp[0].ext.key_val).to.exist.and.to.equal(key_val_output); - }); - - describe('FPD', function() { - let newRequest; - - describe('ortb2.site should not override page, domain & ref values', function() { - it('When above properties are present in ortb2.site', function() { - const ortb2 = { - site: { - domain: 'page.example.com', - page: 'https://page.example.com/here.html', - ref: 'https://page.example.com/here.html' - } - }; - const request = spec.buildRequests(bidRequests, {ortb2}); - let data = JSON.parse(request.data); - expect(data.site.domain).not.equal('page.example.com'); - expect(data.site.page).not.equal('https://page.example.com/here.html'); - expect(data.site.ref).not.equal('https://page.example.com/here.html'); - }); - - it('When above properties are absent in ortb2.site', function () { - const ortb2 = { - site: {} - }; - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', - ortb2 - }); - let data = JSON.parse(request.data); - let response = spec.interpretResponse(bidResponses, request); - expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); - expect(data.site.domain).to.equal(_getDomainFromURL(data.site.page)); - expect(response[0].referrer).to.equal(data.site.ref); - }); - - it('With some extra properties in ortb2.site', function() { - const ortb2 = { - site: { - domain: 'page.example.com', - page: 'https://page.example.com/here.html', - ref: 'https://page.example.com/here.html', - cat: ['IAB2'], - sectioncat: ['IAB2-2'] - } - }; - const request = spec.buildRequests(bidRequests, {ortb2}); - let data = JSON.parse(request.data); - expect(data.site.domain).not.equal('page.example.com'); - expect(data.site.page).not.equal('https://page.example.com/here.html'); - expect(data.site.ref).not.equal('https://page.example.com/here.html'); - expect(data.site.cat).to.deep.equal(['IAB2']); - expect(data.site.sectioncat).to.deep.equal(['IAB2-2']); - }); - }); - - it('ortb2.site should be merged except page, domain & ref in the request', function() { - const ortb2 = { - site: { - cat: ['IAB2'], - sectioncat: ['IAB2-2'] - } - }; - const request = spec.buildRequests(bidRequests, {ortb2}); - let data = JSON.parse(request.data); - expect(data.site.cat).to.deep.equal(['IAB2']); - expect(data.site.sectioncat).to.deep.equal(['IAB2-2']); - }); - - it('ortb2.user should be merged in the request', function() { - const ortb2 = { - user: { - yob: 1985 - } - }; - const request = spec.buildRequests(bidRequests, {ortb2}); - let data = JSON.parse(request.data); - expect(data.user.yob).to.equal(1985); - }); - - it('ortb2.badv should be merged in the request', function() { - const ortb2 = { - badv: ['example.com'] - }; - const request = spec.buildRequests(bidRequests, {ortb2}); - let data = JSON.parse(request.data); - expect(data.badv).to.deep.equal(['example.com']); - }); - - describe('ortb2Imp', function() { - describe('ortb2Imp.ext.gpid', function() { - beforeEach(function () { - if (bidRequests[0].hasOwnProperty('ortb2Imp')) { - delete bidRequests[0].ortb2Imp; - } - }); - - it('should send gpid if imp[].ext.gpid is specified', function() { - bidRequests[0].ortb2Imp = { - ext: { - gpid: 'ortb2Imp.ext.gpid' - } - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.imp[0].ext).to.have.property('gpid'); - expect(data.imp[0].ext.gpid).to.equal('ortb2Imp.ext.gpid'); - }); - - it('should not send if imp[].ext.gpid is not specified', function() { - bidRequests[0].ortb2Imp = { ext: { } }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.imp[0].ext).to.not.have.property('gpid'); - }); - }); - - describe('ortb2Imp.ext.data.pbadslot', function() { - beforeEach(function () { - if (bidRequests[0].hasOwnProperty('ortb2Imp')) { - delete bidRequests[0].ortb2Imp; - } - }); - - it('should not send if imp[].ext.data object is invalid', function() { - bidRequests[0].ortb2Imp = { - ext: {} - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.imp[0].ext).to.not.have.property('data'); - }); - - it('should not send if imp[].ext.data.pbadslot is undefined', function() { - bidRequests[0].ortb2Imp = { - ext: { - data: { - } - } - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - if (data.imp[0].ext.data) { - expect(data.imp[0].ext.data).to.not.have.property('pbadslot'); - } else { - expect(data.imp[0].ext).to.not.have.property('data'); - } - }); - - it('should not send if imp[].ext.data.pbadslot is empty string', function() { - bidRequests[0].ortb2Imp = { - ext: { - data: { - pbadslot: '' - } - } - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - if (data.imp[0].ext.data) { - expect(data.imp[0].ext.data).to.not.have.property('pbadslot'); - } else { - expect(data.imp[0].ext).to.not.have.property('data'); - } - }); - - it('should send if imp[].ext.data.pbadslot is string', function() { - bidRequests[0].ortb2Imp = { - ext: { - data: { - pbadslot: 'abcd' - } - } - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.imp[0].ext.data).to.have.property('pbadslot'); - expect(data.imp[0].ext.data.pbadslot).to.equal('abcd'); - }); - }); - - describe('ortb2Imp.ext.data.adserver', function() { - beforeEach(function () { - if (bidRequests[0].hasOwnProperty('ortb2Imp')) { - delete bidRequests[0].ortb2Imp; - } - }); - - it('should not send if imp[].ext.data object is invalid', function() { - bidRequests[0].ortb2Imp = { - ext: {} - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.imp[0].ext).to.not.have.property('data'); - }); - - it('should not send if imp[].ext.data.adserver is undefined', function() { - bidRequests[0].ortb2Imp = { - ext: { - data: { - } - } - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - if (data.imp[0].ext.data) { - expect(data.imp[0].ext.data).to.not.have.property('adserver'); - } else { - expect(data.imp[0].ext).to.not.have.property('data'); - } - }); - - it('should send', function() { - let adSlotValue = 'abc'; - bidRequests[0].ortb2Imp = { - ext: { - data: { - adserver: { - name: 'GAM', - adslot: adSlotValue - } - } - } - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.imp[0].ext.data.adserver.name).to.equal('GAM'); - expect(data.imp[0].ext.data.adserver.adslot).to.equal(adSlotValue); - expect(data.imp[0].ext.dfp_ad_unit_code).to.equal(adSlotValue); - }); - }); - - describe('ortb2Imp.ext.data.other', function() { - beforeEach(function () { - if (bidRequests[0].hasOwnProperty('ortb2Imp')) { - delete bidRequests[0].ortb2Imp; - } - }); - - it('should not send if imp[].ext.data object is invalid', function() { - bidRequests[0].ortb2Imp = { - ext: {} - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.imp[0].ext).to.not.have.property('data'); - }); - - it('should not send if imp[].ext.data.other is undefined', function() { - bidRequests[0].ortb2Imp = { - ext: { - data: { - } - } - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - if (data.imp[0].ext.data) { - expect(data.imp[0].ext.data).to.not.have.property('other'); - } else { - expect(data.imp[0].ext).to.not.have.property('data'); - } - }); - - it('ortb2Imp.ext.data.other', function() { - bidRequests[0].ortb2Imp = { - ext: { - data: { - other: 1234 - } - } - }; - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.imp[0].ext.data.other).to.equal(1234); - }); - }); - }); - }); - - describe('setting imp.floor using floorModule', function() { - /* - Use the minimum value among floor from floorModule per mediaType - If params.adfloor is set then take max(kadfloor, min(floors from floorModule)) - set imp.bidfloor only if it is more than 0 - */ - - let newRequest; - let floorModuleTestData; - let getFloor = function(req) { - // actual getFloor module does not work like this :) - // special treatment for banner since for other mediaTypes we pass * - if (req.mediaType === 'banner') { - return floorModuleTestData[req.mediaType][ req.size[0] + 'x' + req.size[1] ] || {}; - } - return floorModuleTestData[req.mediaType] || {}; - }; - - beforeEach(() => { - floorModuleTestData = { - 'banner': { - '300x250': { - 'currency': 'USD', - 'floor': 1.50 - }, - '300x600': { - 'currency': 'USD', - 'floor': 2.0 - } - }, - 'video': { - 'currency': 'USD', - 'floor': 2.50 - }, - 'native': { - 'currency': 'USD', - 'floor': 3.50 - } - }; - newRequest = utils.deepClone(bannerVideoAndNativeBidRequests); - newRequest[0].getFloor = getFloor; - }); - - it('bidfloor should be undefined if calculation is <= 0', function() { - floorModuleTestData.banner['300x250'].floor = 0; // lowest of them all - newRequest[0].params.kadfloor = undefined; - let request = spec.buildRequests(newRequest, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.bidfloor).to.equal(undefined); - }); - - if (FEATURES.VIDEO) { - it('ignore floormodule o/p if floor is not number', function() { - floorModuleTestData.banner['300x250'].floor = 'Not-a-Number'; - floorModuleTestData.banner['300x600'].floor = 'Not-a-Number'; - newRequest[0].params.kadfloor = undefined; - let request = spec.buildRequests(newRequest, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.bidfloor).to.equal(2.5); // video will be lowest now - }); - - it('ignore floormodule o/p if currency is not matched', function() { - floorModuleTestData.banner['300x250'].currency = 'INR'; - floorModuleTestData.banner['300x600'].currency = 'INR'; - newRequest[0].params.kadfloor = undefined; - let request = spec.buildRequests(newRequest, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.bidfloor).to.equal(2.5); // video will be lowest now - }); - } - - it('kadfloor is not passed, use minimum from floorModule', function() { - newRequest[0].params.kadfloor = undefined; - let request = spec.buildRequests(newRequest, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.bidfloor).to.equal(1.5); - }); - - it('kadfloor is passed as 3, use kadfloor as it is highest', function() { - newRequest[0].params.kadfloor = '3.0';// yes, we want it as a string - let request = spec.buildRequests(newRequest, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.bidfloor).to.equal(3); - }); - - it('kadfloor is passed as 1, use min of floorModule as it is highest', function() { - newRequest[0].params.kadfloor = '1.0';// yes, we want it as a string - let request = spec.buildRequests(newRequest, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.bidfloor).to.equal(1.5); - }); - }); - - it('should NOT include coppa flag in bid request if coppa config is not present', () => { - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - if (data.regs) { - // in case GDPR is set then data.regs will exist - expect(data.regs.coppa).to.equal(undefined); - } else { - expect(data.regs).to.equal(undefined); - } - }); - - it('should include coppa flag in bid request if coppa is set to true', () => { - let sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake(key => { - const config = { - 'coppa': true - }; - return config[key]; - }); - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.regs.coppa).to.equal(1); - sandbox.restore(); - }); - - it('should NOT include coppa flag in bid request if coppa is set to false', () => { - let sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake(key => { - const config = { - 'coppa': false - }; - return config[key]; - }); - const request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - if (data.regs) { - // in case GDPR is set then data.regs will exist - expect(data.regs.coppa).to.equal(undefined); - } else { - expect(data.regs).to.equal(undefined); - } - sandbox.restore(); - }); - - describe('userIdAsEids', function() { - let sandbox; + if (FEATURES.VIDEO) { + describe('VIDEO', () => { beforeEach(() => { - sandbox = sinon.sandbox.create(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('Request should have EIDs', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.tdid = 'TTD_ID_FROM_USER_ID_MODULE'; - bidRequests[0].userIdAsEids = [{ - 'source': 'adserver.org', - 'uids': [{ - 'id': 'TTD_ID_FROM_USER_ID_MODULE', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - }] - }]; - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal(bidRequests[0].userIdAsEids); - }); - - it('Request should NOT have EIDs userIdAsEids is NOT object', function() { - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal(undefined); - }); - }); - - it('should pass device.sua if present in bidderRequest fpd ortb2 object', function () { - const suaObject = {'source': 2, 'platform': {'brand': 'macOS', 'version': ['12', '4', '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': 0, 'model': '', 'bitness': '64', 'architecture': 'x86'}; - let request = spec.buildRequests(multipleMediaRequests, { - auctionId: 'new-auction-id', - ortb2: { - device: { - sua: suaObject - } - } - }); - let data = JSON.parse(request.data); - expect(data.device.sua).to.exist.and.to.be.an('object'); - expect(data.device.sua).to.deep.equal(suaObject); - }); - - it('should pass device.ext.cdep if present in bidderRequest fpd ortb2 object', function () { - const cdepObj = { - cdep: 'example_label_1' - }; - let request = spec.buildRequests(multipleMediaRequests, { - auctionId: 'new-auction-id', - ortb2: { - device: { - ext: cdepObj - } - } - }); - let data = JSON.parse(request.data); - expect(data.device.ext.cdep).to.exist.and.to.be.an('string'); - expect(data.device.ext).to.deep.equal(cdepObj); - }); - - it('Request params should have valid native bid request for all valid params', function () { - let request = spec.buildRequests(nativeBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.imp[0].native).to.exist; - expect(data.imp[0].native['request']).to.exist; - expect(data.imp[0].tagid).to.equal('/43743431/NativeAutomationPrebid'); - expect(data.imp[0]['native']['request']).to.exist.and.to.be.an('string'); - expect(data.imp[0]['native']['request']).to.exist.and.to.equal(validnativeBidImpression.native.request); - }); - - it('Request params should not have valid native bid request for non native request', function () { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.imp[0].native).to.not.exist; - }); - - it('Request params should have valid native bid request with valid required param values for all valid params', function () { - let request = spec.buildRequests(nativeBidRequestsWithRequiredParam, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.imp[0].native).to.exist; - expect(data.imp[0].native['request']).to.exist; - expect(data.imp[0].tagid).to.equal('/43743431/NativeAutomationPrebid'); - expect(data.imp[0]['native']['request']).to.exist.and.to.be.an('string'); - expect(data.imp[0]['native']['request']).to.exist.and.to.equal(validnativeBidImpressionWithRequiredParam.native.request); - }); - - it('Request params should have valid native bid request for all native params', function () { - let request = spec.buildRequests(nativeBidRequestsWithAllParams, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.imp[0].native).to.exist; - expect(data.imp[0].native['request']).to.exist; - expect(data.imp[0].tagid).to.equal('/43743431/NativeAutomationPrebid'); - expect(data.imp[0]['native']['request']).to.exist.and.to.be.an('string'); - expect(data.imp[0]['native']['request']).to.exist.and.to.equal(validnativeBidImpressionWithAllParams.native.request); - }); - - it('Request params - should handle banner and native format in single adunit', function() { - let request = spec.buildRequests(bannerAndNativeBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.banner).to.exist; - expect(data.banner.w).to.equal(300); - expect(data.banner.h).to.equal(250); - expect(data.banner.format).to.exist; - expect(data.banner.format.length).to.equal(bannerAndNativeBidRequests[0].mediaTypes.banner.sizes.length); - - expect(data.native).to.exist; - expect(data.native.request).to.exist; - }); - - it('Request params - banner and native multiformat request - should have native object incase of invalid config present', function() { - bannerAndNativeBidRequests[0].mediaTypes.native = { - title: { required: true }, - image: { required: true }, - sponsoredBy: { required: true }, - clickUrl: { required: true } - }; - bannerAndNativeBidRequests[0].nativeParams = { - title: { required: true }, - image: { required: true }, - sponsoredBy: { required: true }, - clickUrl: { required: true } - } - let request = spec.buildRequests(bannerAndNativeBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.banner).to.exist; - expect(data.native).to.exist; - }); - - it('Request params - should not add banner object if mediaTypes.banner is missing, but adunits.sizes is present', function() { - delete bannerAndNativeBidRequests[0].mediaTypes.banner; - bannerAndNativeBidRequests[0].sizes = [729, 90]; - - let request = spec.buildRequests(bannerAndNativeBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.banner).to.not.exist; - - expect(data.native).to.exist; - expect(data.native.request).to.exist; - }); - - if (FEATURES.VIDEO) { - it('Request params - should not contain banner imp if mediaTypes.banner is not present and sizes is specified in bid.sizes', function() { - delete bannerAndVideoBidRequests[0].mediaTypes.banner; - bannerAndVideoBidRequests[0].params.sizes = [300, 250]; - - let request = spec.buildRequests(bannerAndVideoBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.banner).to.not.exist; - }); - - it('Request params check for 1 banner and 1 video ad', function () { - let request = spec.buildRequests(multipleMediaRequests, { - auctionId: 'new-auction-id', - ortb2: { - device: { - geo: { - lat: '36.5189', - lon: '-76.4063' - } - }, - user: { - geo: { - lat: '26.8915', - lon: '-56.6340' - } - }, - } - }); - let data = JSON.parse(request.data); - - expect(data.imp).to.be.an('array') - expect(data.imp).with.length.above(1); - - expect(data.at).to.equal(1); // auction type - expect(data.cur[0]).to.equal('USD'); // currency - expect(data.site.domain).to.be.a('string'); // domain should be set - expect(data.site.page).to.equal(multipleMediaRequests[0].params.kadpageurl); // forced pageURL - expect(data.site.publisher.id).to.equal(multipleMediaRequests[0].params.publisherId); // publisher Id - expect(data.user.yob).to.equal(parseInt(multipleMediaRequests[0].params.yob)); // YOB - expect(data.user.gender).to.equal(multipleMediaRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.equal('36.5189'); // Latitude - expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude - expect(data.user.geo.lat).to.equal('26.8915'); // Latitude - expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude - expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version - expect(data.ext.wrapper.transactionId).to.equal(multipleMediaRequests[0].transactionId); // Prebid TransactionId - expect(data.ext.wrapper.wiid).to.equal(multipleMediaRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID - expect(data.ext.wrapper.profile).to.equal(parseInt(multipleMediaRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID - expect(data.ext.wrapper.version).to.equal(parseInt(multipleMediaRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID - - // banner imp object check - expect(data.imp[0].id).to.equal(multipleMediaRequests[0].bidId); // Prebid bid id is passed as id - expect(data.imp[0].bidfloor).to.equal(parseFloat(multipleMediaRequests[0].params.kadfloor)); // kadfloor - expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid - expect(data.imp[0].banner.w).to.equal(300); // width - expect(data.imp[0].banner.h).to.equal(250); // height - expect(data.imp[0].ext.pmZoneId).to.equal(multipleMediaRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid - - // video imp object check - expect(data.imp[1].video).to.exist; - expect(data.imp[1].tagid).to.equal('Div1'); - expect(data.imp[1]['video']['mimes']).to.exist.and.to.be.an('array'); - expect(data.imp[1]['video']['mimes'][0]).to.equal(multipleMediaRequests[1].params.video['mimes'][0]); - expect(data.imp[1]['video']['mimes'][1]).to.equal(multipleMediaRequests[1].params.video['mimes'][1]); - expect(data.imp[1]['video']['minduration']).to.equal(multipleMediaRequests[1].params.video['minduration']); - expect(data.imp[1]['video']['maxduration']).to.equal(multipleMediaRequests[1].params.video['maxduration']); - expect(data.imp[1]['video']['startdelay']).to.equal(multipleMediaRequests[1].params.video['startdelay']); - - expect(data.imp[1]['video']['playbackmethod']).to.exist.and.to.be.an('array'); - expect(data.imp[1]['video']['playbackmethod'][0]).to.equal(multipleMediaRequests[1].params.video['playbackmethod'][0]); - expect(data.imp[1]['video']['playbackmethod'][1]).to.equal(multipleMediaRequests[1].params.video['playbackmethod'][1]); - - expect(data.imp[1]['video']['api']).to.exist.and.to.be.an('array'); - expect(data.imp[1]['video']['api'][0]).to.equal(multipleMediaRequests[1].params.video['api'][0]); - expect(data.imp[1]['video']['api'][1]).to.equal(multipleMediaRequests[1].params.video['api'][1]); - - expect(data.imp[1]['video']['protocols']).to.exist.and.to.be.an('array'); - expect(data.imp[1]['video']['protocols'][0]).to.equal(multipleMediaRequests[1].params.video['protocols'][0]); - expect(data.imp[1]['video']['protocols'][1]).to.equal(multipleMediaRequests[1].params.video['protocols'][1]); - - expect(data.imp[1]['video']['battr']).to.exist.and.to.be.an('array'); - expect(data.imp[1]['video']['battr'][0]).to.equal(multipleMediaRequests[1].params.video['battr'][0]); - expect(data.imp[1]['video']['battr'][1]).to.equal(multipleMediaRequests[1].params.video['battr'][1]); - - expect(data.imp[1]['video']['linearity']).to.equal(multipleMediaRequests[1].params.video['linearity']); - expect(data.imp[1]['video']['placement']).to.equal(multipleMediaRequests[1].params.video['placement']); - expect(data.imp[1]['video']['plcmt']).to.equal(multipleMediaRequests[1].params.video['plcmt']); - expect(data.imp[1]['video']['minbitrate']).to.equal(multipleMediaRequests[1].params.video['minbitrate']); - expect(data.imp[1]['video']['maxbitrate']).to.equal(multipleMediaRequests[1].params.video['maxbitrate']); - - expect(data.imp[1]['video']['w']).to.equal(multipleMediaRequests[1].mediaTypes.video.playerSize[0]); - expect(data.imp[1]['video']['h']).to.equal(multipleMediaRequests[1].mediaTypes.video.playerSize[1]); - }); - - // ================================================ - it('Request params - should handle banner and video format in single adunit', function() { - let request = spec.buildRequests(bannerAndVideoBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.banner).to.exist; - expect(data.banner.w).to.equal(300); - expect(data.banner.h).to.equal(250); - expect(data.banner.format).to.exist; - expect(data.banner.format.length).to.equal(bannerAndVideoBidRequests[0].mediaTypes.banner.sizes.length); - - // Case: when size is not present in adslo - bannerAndVideoBidRequests[0].params.adSlot = '/15671365/DMDemo'; - request = spec.buildRequests(bannerAndVideoBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.banner).to.exist; - expect(data.banner.w).to.equal(bannerAndVideoBidRequests[0].mediaTypes.banner.sizes[0][0]); - expect(data.banner.h).to.equal(bannerAndVideoBidRequests[0].mediaTypes.banner.sizes[0][1]); - expect(data.banner.format).to.exist; - expect(data.banner.format.length).to.equal(bannerAndVideoBidRequests[0].mediaTypes.banner.sizes.length - 1); - - expect(data.video).to.exist; - expect(data.video.w).to.equal(bannerAndVideoBidRequests[0].mediaTypes.video.playerSize[0]); - expect(data.video.h).to.equal(bannerAndVideoBidRequests[0].mediaTypes.video.playerSize[1]); - }); - - it('Request params - should handle banner, video and native format in single adunit', function() { - let request = spec.buildRequests(bannerVideoAndNativeBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.banner).to.exist; - expect(data.banner.w).to.equal(300); - expect(data.banner.h).to.equal(250); - expect(data.banner.format).to.exist; - expect(data.banner.format.length).to.equal(bannerAndNativeBidRequests[0].mediaTypes.banner.sizes.length); - - expect(data.video).to.exist; - expect(data.video.w).to.equal(bannerAndVideoBidRequests[0].mediaTypes.video.playerSize[0]); - expect(data.video.h).to.equal(bannerAndVideoBidRequests[0].mediaTypes.video.playerSize[1]); - - expect(data.native).to.exist; - expect(data.native.request).to.exist; - }); - - it('Request params - should handle video and native format in single adunit', function() { - let request = spec.buildRequests(videoAndNativeBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.video).to.exist; - expect(data.video.w).to.equal(bannerAndVideoBidRequests[0].mediaTypes.video.playerSize[0]); - expect(data.video.h).to.equal(bannerAndVideoBidRequests[0].mediaTypes.video.playerSize[1]); - - expect(data.native).to.exist; - expect(data.native.request).to.exist; - }); - - it('Request params - banner and video req in single adslot - should ignore banner imp if banner size is set to fluid and send video imp object', function () { - /* Adslot configured for banner and video. - banner size is set to [['fluid'], [300, 250]] - adslot specifies a size as 300x250 - => banner imp object should have primary w and h set to 300 and 250. fluid is ignored - */ - bannerAndVideoBidRequests[0].mediaTypes.banner.sizes = [['fluid'], [160, 600]]; - - let request = spec.buildRequests(bannerAndVideoBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.banner).to.exist; - expect(data.banner.w).to.equal(300); - expect(data.banner.h).to.equal(250); - expect(data.banner.format).to.exist; - expect(data.banner.format[0].w).to.equal(160); - expect(data.banner.format[0].h).to.equal(600); - - /* Adslot configured for banner and video. - banner size is set to [['fluid'], [300, 250]] - adslot does not specify any size - => banner imp object should have primary w and h set to 300 and 250. fluid is ignored - */ - bannerAndVideoBidRequests[0].mediaTypes.banner.sizes = [['fluid'], [160, 600]]; - bannerAndVideoBidRequests[0].params.adSlot = '/15671365/DMDemo'; - - request = spec.buildRequests(bannerAndVideoBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.banner).to.exist; - expect(data.banner.w).to.equal(160); - expect(data.banner.h).to.equal(600); - expect(data.banner.format).to.not.exist; - - /* Adslot configured for banner and video. - banner size is set to [[728 90], ['fluid'], [300, 250]] - adslot does not specify any size - => banner imp object should have primary w and h set to 728 and 90. - banner.format should have 300, 250 set in it - fluid is ignore - */ - - bannerAndVideoBidRequests[0].mediaTypes.banner.sizes = [[728, 90], ['fluid'], [300, 250]]; - request = spec.buildRequests(bannerAndVideoBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.banner).to.exist; - expect(data.banner.w).to.equal(728); - expect(data.banner.h).to.equal(90); - expect(data.banner.format).to.exist; - expect(data.banner.format[0].w).to.equal(300); - expect(data.banner.format[0].h).to.equal(250); - - /* Adslot configured for banner and video. - banner size is set to [['fluid']] - adslot does not specify any size - => banner object should not be sent in the request. only video should be sent. - */ - - bannerAndVideoBidRequests[0].mediaTypes.banner.sizes = [['fluid']]; - request = spec.buildRequests(bannerAndVideoBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.banner).to.not.exist; - expect(data.video).to.exist; - }); - - it('Request params - video and native multiformat request - should have native object incase of invalid config present', function() { - videoAndNativeBidRequests[0].mediaTypes.native = { - title: { required: true }, - image: { required: true }, - sponsoredBy: { required: true }, - clickUrl: { required: true } - }; - videoAndNativeBidRequests[0].nativeParams = { - title: { required: true }, - image: { required: true }, - sponsoredBy: { required: true }, - clickUrl: { required: true } - } - let request = spec.buildRequests(videoAndNativeBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.video).to.exist; - expect(data.native).to.exist; - }); - - it('should build video impression if video params are present in adunit.mediaTypes instead of bid.params', function() { - let videoReq = [{ - 'bidder': 'pubmatic', - 'params': { - 'adSlot': 'SLOT_NHB1@728x90', - 'publisherId': '5890', - }, - 'mediaTypes': { - 'video': { - 'playerSize': [ - [640, 480] - ], - 'protocols': [1, 2, 5], - 'context': 'instream', - 'mimes': ['video/flv'], - 'skip': 1, - 'linearity': 2 - } - }, - 'adUnitCode': 'video1', - 'transactionId': 'adc36682-887c-41e9-9848-8b72c08332c0', - 'sizes': [ - [640, 480] - ], - 'bidId': '21b59b1353ba82', - 'bidderRequestId': '1a08245305e6dd', - 'auctionId': 'bad3a743-7491-4d19-9a96-b0a69dd24a67', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }] - let request = spec.buildRequests(videoReq, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - expect(data.video).to.exist; - }); - - it('should build video impression with overwriting video params present in adunit.mediaTypes with bid.params', function() { - let videoReq = [{ - 'bidder': 'pubmatic', - 'params': { - 'adSlot': 'SLOT_NHB1@728x90', - 'publisherId': '5890', - 'video': { - 'mimes': ['video/mp4'], - 'protocols': [1, 2, 5], - 'linearity': 1 - } - }, - 'mediaTypes': { - 'video': { - 'playerSize': [ - [640, 480] - ], - 'protocols': [1, 2, 5], - 'context': 'instream', - 'mimes': ['video/flv'], - 'skip': 1, - 'linearity': 2 - } - }, - 'adUnitCode': 'video1', - 'transactionId': 'adc36682-887c-41e9-9848-8b72c08332c0', - 'sizes': [ - [640, 480] - ], - 'bidId': '21b59b1353ba82', - 'bidderRequestId': '1a08245305e6dd', - 'auctionId': 'bad3a743-7491-4d19-9a96-b0a69dd24a67', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }] - let request = spec.buildRequests(videoReq, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - data = data.imp[0]; - - expect(data.video).to.exist; - expect(data.video.linearity).to.equal(1); - }); - - it('Request params check for video ad', function () { - let request = spec.buildRequests(videoBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist; - expect(data.imp[0].tagid).to.equal('Div1'); - expect(data.imp[0]['video']['mimes']).to.exist.and.to.be.an('array'); - expect(data.imp[0]['video']['mimes'][0]).to.equal(videoBidRequests[0].params.video['mimes'][0]); - expect(data.imp[0]['video']['mimes'][1]).to.equal(videoBidRequests[0].params.video['mimes'][1]); - expect(data.imp[0]['video']['minduration']).to.equal(videoBidRequests[0].params.video['minduration']); - expect(data.imp[0]['video']['maxduration']).to.equal(videoBidRequests[0].params.video['maxduration']); - expect(data.imp[0]['video']['startdelay']).to.equal(videoBidRequests[0].params.video['startdelay']); - - expect(data.imp[0]['video']['playbackmethod']).to.exist.and.to.be.an('array'); - expect(data.imp[0]['video']['playbackmethod'][0]).to.equal(videoBidRequests[0].params.video['playbackmethod'][0]); - expect(data.imp[0]['video']['playbackmethod'][1]).to.equal(videoBidRequests[0].params.video['playbackmethod'][1]); - - expect(data.imp[0]['video']['api']).to.exist.and.to.be.an('array'); - expect(data.imp[0]['video']['api'][0]).to.equal(videoBidRequests[0].params.video['api'][0]); - expect(data.imp[0]['video']['api'][1]).to.equal(videoBidRequests[0].params.video['api'][1]); - - expect(data.imp[0]['video']['protocols']).to.exist.and.to.be.an('array'); - expect(data.imp[0]['video']['protocols'][0]).to.equal(videoBidRequests[0].params.video['protocols'][0]); - expect(data.imp[0]['video']['protocols'][1]).to.equal(videoBidRequests[0].params.video['protocols'][1]); - - expect(data.imp[0]['video']['battr']).to.exist.and.to.be.an('array'); - expect(data.imp[0]['video']['battr'][0]).to.equal(videoBidRequests[0].params.video['battr'][0]); - expect(data.imp[0]['video']['battr'][1]).to.equal(videoBidRequests[0].params.video['battr'][1]); - - expect(data.imp[0]['video']['linearity']).to.equal(videoBidRequests[0].params.video['linearity']); - expect(data.imp[0]['video']['placement']).to.equal(videoBidRequests[0].params.video['placement']); - expect(data.imp[0]['video']['plcmt']).to.equal(videoBidRequests[0].params.video['plcmt']); - expect(data.imp[0]['video']['minbitrate']).to.equal(videoBidRequests[0].params.video['minbitrate']); - expect(data.imp[0]['video']['maxbitrate']).to.equal(videoBidRequests[0].params.video['maxbitrate']); - - expect(data.imp[0]['video']['w']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[0]); - expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]); - }); - } - - describe('GPP', function() { - it('Request params check with GPP Consent', function () { - let bidRequest = { - gppConsent: { - 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', - 'fullGppData': { - 'sectionId': 3, - 'gppVersion': 1, - 'sectionList': [ - 5, - 7 - ], - 'applicableSections': [ - 5 - ], - 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', - 'pingData': { - 'cmpStatus': 'loaded', - 'gppVersion': '1.0', - 'cmpDisplayStatus': 'visible', - 'supportedAPIs': [ - 'tcfca', - 'usnat', - 'usca', - 'usva', - 'usco', - 'usut', - 'usct' - ], - 'cmpId': 31 - }, - 'eventName': 'sectionChange' - }, - 'applicableSections': [ - 5 - ], - 'apiVersion': 1 - } - }; - let request = spec.buildRequests(bidRequests, bidRequest); - let data = JSON.parse(request.data); - expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); - expect(data.regs.gpp_sid[0]).to.equal(5); + videoBidRequest = utils.deepClone(validBidRequests[0]); + delete videoBidRequest.mediaTypes.banner; + videoBidRequest.mediaTypes.video = { + playerSize: [ + [640, 480] + ], + protocols: [1, 2, 5], + context: 'instream', + skippable: false, + skip: 1, + linearity: 2 + } }); - - it('Request params check without GPP Consent', function () { - let bidRequest = {}; - let request = spec.buildRequests(bidRequests, bidRequest); - let data = JSON.parse(request.data); - expect(data.regs).to.equal(undefined); + it('should return false if mimes are missing in a video impression request', () => { + const isValid = spec.isBidRequestValid(videoBidRequest); + expect(isValid).to.equal(false); }); - it('Request params check with GPP Consent read from ortb2', function () { - let bidRequest = { - ortb2: { - regs: { - 'gpp': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', - 'gpp_sid': [ - 5 - ] - } - } - }; - let request = spec.buildRequests(bidRequests, bidRequest); - let data = JSON.parse(request.data); - expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); - expect(data.regs.gpp_sid[0]).to.equal(5); - }); - }); + it('should return false if context is missing in a video impression request', () => { + delete videoBidRequest.mediaTypes.context; + const isValid = spec.isBidRequestValid(videoBidRequest); + expect(isValid).to.equal(false); + }) - describe('Fledge', function() { - it('should not send imp.ext.ae when FLEDGE is disabled, ', function () { - let bidRequest = Object.assign([], bidRequests); - bidRequest[0].ortb2Imp = { - ext: { ae: 1 } - }; - const req = spec.buildRequests(bidRequest, { ...bidRequest, fledgeEnabled: false }); - let data = JSON.parse(req.data); - if (data.imp[0].ext) { - expect(data.imp[0].ext).to.not.have.property('ae'); + it('should return true if banner/native present, but outstreamAU or renderer is missing', () => { + videoBidRequest.mediaTypes.video.mimes = ['video/flv']; + videoBidRequest.mediaTypes.video.context = 'outstream'; + videoBidRequest.mediaTypes.banner = { + sizes: [[728, 90], [160, 600]] } + const isValid = spec.isBidRequestValid(videoBidRequest); + expect(isValid).to.equal(true); }); - it('when FLEDGE is enabled, should send whatever is set in ortb2imp.ext.ae in all bid requests', function () { - let bidRequest = Object.assign([], bidRequests); - delete bidRequest[0].params.test; - bidRequest[0].ortb2Imp = { - ext: { ae: 1 } - }; - const req = spec.buildRequests(bidRequest, { ...bidRequest, fledgeEnabled: true }); - let data = JSON.parse(req.data); - expect(data.imp[0].ext.ae).to.equal(1); + it('should return false if outstreamAU or renderer is missing', () => { + const isValid = spec.isBidRequestValid(videoBidRequest); + expect(isValid).to.equal(false); }); }); + } + }); - it('should send connectiontype parameter if browser contains navigator.connection property', function () { - const bidRequest = spec.buildRequests(bidRequests); - let data = JSON.parse(bidRequest.data); - if (window.navigator && window.navigator.connection) { - expect(data.device).to.include.any.keys('connectiontype'); - } - }); - }); - - it('Request params dctr check', function () { - let multipleBidRequests = [ - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - dctr: 'key1=val1|key2=val2,!val3' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - }, - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'GBP', - dctr: 'key1=val3|key2=val1,!val3|key3=val123' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; - - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - - /* case 1 - - dctr is found in adunit[0] - */ - - expect(data.imp[0].ext).to.exist.and.to.be.an('object'); // dctr parameter - expect(data.imp[0].ext.key_val).to.exist.and.to.equal(multipleBidRequests[0].params.dctr); - - /* case 2 - - dctr not present in adunit[0] but present in adunit[1] - */ - delete multipleBidRequests[0].params.dctr; - request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' + describe('Request formation', () => { + describe('IMP', () => { + it('should generate request with banner', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('banner'); + expect(imp[0]).to.have.property('id').equal('3736271c3c4b27'); }); - data = JSON.parse(request.data); - - expect(data.imp[0].ext).to.exist.and.to.deep.equal({}); - expect(data.imp[1].ext).to.exist.and.to.be.an('object'); // dctr parameter - expect(data.imp[1].ext.key_val).to.exist.and.to.equal(multipleBidRequests[1].params.dctr); - /* case 3 - - dctr is present in adunit[0], but is not a string value - */ - multipleBidRequests[0].params.dctr = 123; - request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' + it('should add pmp if deals are present in parameters', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('pmp'); + expect(imp[0]).to.have.property('pmp').to.have.property('deals').with.lengthOf(2); }); - data = JSON.parse(request.data); - expect(data.imp[0].ext).to.exist.and.to.deep.equal({}); - }); - - it('Request params deals check', function () { - let multipleBidRequests = [ - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD', - deals: ['deal-id-1', 'deal-id-2', 'dea'] // "dea" will not be passed as more than 3 characters needed - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - }, - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'GBP', - deals: ['deal-id-100', 'deal-id-200'] - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; - - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - // case 1 - deals are passed as expected, ['', ''] , in both adUnits - expect(data.imp[0].pmp).to.deep.equal({ - 'private_auction': 0, - 'deals': [ - { - 'id': 'deal-id-1' - }, - { - 'id': 'deal-id-2' - } - ] - }); - expect(data.imp[1].pmp).to.deep.equal({ - 'private_auction': 0, - 'deals': [ - { - 'id': 'deal-id-100' - }, - { - 'id': 'deal-id-200' - } - ] + it('should not add pmp if deals are absent in parameters', () => { + delete validBidRequests[0].params.deals; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.not.have.property('pmp'); }); - // case 2 - deals not present in adunit[0] - delete multipleBidRequests[0].params.deals; - request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - expect(data.imp[0].pmp).to.not.exist; + it('should add key_val property if dctr is present in parameters', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('ext'); + expect(imp[0]).to.have.property('ext').to.have.property('key_val'); + }); - // case 3 - deals is present in adunit[0], but is not an array - multipleBidRequests[0].params.deals = 123; - request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - expect(data.imp[0].pmp).to.not.exist; + it('should not add key_val if dctr is absent in parameters', () => { + delete validBidRequests[0].params.dctr; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('ext').to.not.have.property('key_val'); + }); + + it('should set w and h to the primary size for banner', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('banner'); + expect(imp[0]).to.have.property('banner').to.have.property('w').equal(300); + expect(imp[0]).to.have.property('banner').to.have.property('h').equal(250); + }); + + it('should have 1 size in the banner.format array', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('banner').to.have.property('format'); + expect(imp[0]).to.have.property('banner').to.have.property('format').with.lengthOf(2); + }); + + it('should add pmZoneId in ext if pmzoneid is present in parameters', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('ext'); + expect(imp[0]).to.have.property('ext').to.have.property('pmZoneId'); + }); + + it('should add bidfloor if kadfloor is present in parameters', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('bidfloor'); + expect(imp[0]).to.have.property('bidfloor').equal(1.2); + }); + + it('should add bidfloorcur if currency is present in parameters', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('bidfloorcur'); + expect(imp[0]).to.have.property('bidfloorcur').equal('AUD'); + }); + + it('should add bidfloorcur with default value if currency is missing in parameters', () => { + delete validBidRequests[0].params.currency; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('bidfloorcur'); + expect(imp[0]).to.have.property('bidfloorcur').equal('USD'); + }); + + it('should add tagid', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('tagid'); + expect(imp[0]).to.have.property('tagid').equal('/15671365/DMDemo'); + }); + + it('should add secure, displaymanager & displaymanagerver', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('secure').equal(1); + expect(imp[0]).to.have.property('displaymanager').equal('Prebid.js'); + expect(imp[0]).to.have.property('displaymanagerver'); + }); + + it('should include the properties topframe and format as an array', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('banner').to.have.property('topframe'); + expect(imp[0]).to.have.property('banner').to.have.property('format').to.be.an('array'); + }); + + it('should respect the publisher-provided pos for the banner impression', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('banner').to.have.property('pos'); + expect(imp[0]).to.have.property('banner').to.have.property('pos').equal(1); + }); + + it('should default pos to 0 if not explicitly provided by the publisher', () => { + delete bidderRequest.bids[0].mediaTypes.banner.pos; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('banner').to.have.property('pos'); + expect(imp[0]).to.have.property('banner').to.have.property('pos').equal(0); + }); + + if (FEATURES.VIDEO) { + describe('VIDEO', () => { + beforeEach(() => { + utilsLogWarnMock = sinon.stub(utils, 'logWarn'); + videoBidderRequest = utils.deepClone(bidderRequest); + delete videoBidderRequest.bids[0].mediaTypes.banner; + videoBidderRequest.bids[0].mediaTypes.video = { + skip: 1, + mimes: ['video/mp4', 'video/x-flv'], + minduration: 5, + maxduration: 30, + startdelay: 5, + playbackmethod: [1, 3], + api: [1, 2], + protocols: [2, 3], + battr: [13, 14], + linearity: 1, + placement: 2, + plcmt: 1, + minbitrate: 10, + maxbitrate: 10, + playerSize: [640, 480] + } + }); - // case 4 - deals is present in adunit[0] as an array but one of the value is not a string - multipleBidRequests[0].params.deals = [123, 'deal-id-1']; - request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - data = JSON.parse(request.data); - expect(data.imp[0].pmp).to.deep.equal({ - 'private_auction': 0, - 'deals': [ - { - 'id': 'deal-id-1' - } - ] - }); - }); + afterEach(() => { + utilsLogWarnMock.restore(); + }) - describe('Request param acat checking', function() { - let multipleBidRequests = [ - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD', - dctr: 'key1=val1|key2=val2,!val3' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - }, - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'GBP', - dctr: 'key1=val3|key2=val1,!val3|key3=val123' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; + it('should generate request with mediatype video', () => { + const request = spec.buildRequests(validBidRequests, videoBidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('video'); + }); - it('acat: pass only strings', function() { - multipleBidRequests[0].params.acat = [1, 2, 3, 'IAB1', 'IAB2']; - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.ext.acat).to.exist.and.to.deep.equal(['IAB1', 'IAB2']); - }); + it('should log a warning if playerSize is missing', () => { + delete videoBidderRequest.bids[0].mediaTypes.video.playerSize; + const request = spec.buildRequests(validBidRequests, videoBidderRequest); + sinon.assert.called(utils.logWarn); + }); - it('acat: trim the strings', function() { - multipleBidRequests[0].params.acat = [' IAB1 ', ' IAB2 ']; - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' - }); - let data = JSON.parse(request.data); - expect(data.ext.acat).to.exist.and.to.deep.equal(['IAB1', 'IAB2']); - }); + it('should log a warning if plcmt is missing', () => { + delete videoBidderRequest.bids[0].mediaTypes.video.plcmt; + const request = spec.buildRequests(validBidRequests, videoBidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + sinon.assert.called(utils.logWarn); + expect(imp.video).to.be.undefined; + }); - it('acat: pass only unique strings', function() { - multipleBidRequests[0].params.acat = ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB2']; - multipleBidRequests[1].params.acat = ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB3']; - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' + it('should have all supporting parameters', () => { + const request = spec.buildRequests(validBidRequests, videoBidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('video'); + expect(imp[0]).to.have.property('video').to.have.property('mimes'); + expect(imp[0]).to.have.property('video').to.have.property('minbitrate'); + expect(imp[0]).to.have.property('video').to.have.property('maxbitrate'); + expect(imp[0]).to.have.property('video').to.have.property('minduration'); + expect(imp[0]).to.have.property('video').to.have.property('maxduration'); + expect(imp[0]).to.have.property('video').to.have.property('plcmt'); + expect(imp[0]).to.have.property('video').to.have.property('battr'); + expect(imp[0]).to.have.property('video').to.have.property('startdelay'); + expect(imp[0]).to.have.property('video').to.have.property('playbackmethod'); + expect(imp[0]).to.have.property('video').to.have.property('api'); + expect(imp[0]).to.have.property('video').to.have.property('protocols'); + expect(imp[0]).to.have.property('video').to.have.property('linearity'); + expect(imp[0]).to.have.property('video').to.have.property('placement'); + expect(imp[0]).to.have.property('video').to.have.property('skip'); + expect(imp[0]).to.have.property('video').to.have.property('w'); + expect(imp[0]).to.have.property('video').to.have.property('h'); + }); }); - let data = JSON.parse(request.data); - expect(data.ext.acat).to.exist.and.to.deep.equal(['IAB1', 'IAB2', 'IAB3']); - }); - it('ortb2.ext.prebid.bidderparams.pubmatic.acat should be passed in request payload', function() { - const ortb2 = { - ext: { - prebid: { - bidderparams: { - pubmatic: { - acat: ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB2'] - } + } + if (FEATURES.NATIVE) { + describe('NATIVE', () => { + beforeEach(() => { + utilsLogWarnMock = sinon.stub(utils, 'logWarn'); + nativeBidderRequest = utils.deepClone(bidderRequest); + delete nativeBidderRequest.bids[0].mediaTypes.banner; + nativeBidderRequest.bids[0].nativeOrtbRequest = { + ver: '1.2', + assets: [{ + id: 0, + img: { + 'type': 3, + 'w': 300, + 'h': 250 + }, + required: 1, + }] + }; + nativeBidderRequest.bids[0].mediaTypes.native = { + title: { + required: true, + length: 80 + }, + image: { + required: true, + sizes: [300, 250] + }, + sponsoredBy: { + required: true } } - } - }; - const request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', - bidderCode: 'pubmatic', - ortb2 - }); - let data = JSON.parse(request.data); - expect(data.ext.acat).to.deep.equal(['IAB1', 'IAB2']); - }); - }); + }); - describe('Request param bcat checking', function() { - let multipleBidRequests = [ - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD', - dctr: 'key1=val1|key2=val2,!val3' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - }, - { - bidder: 'pubmatic', - params: { - publisherId: '301', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'GBP', - dctr: 'key1=val3|key2=val1,!val3|key3=val123' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: '23acc48ad47af5', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; + afterEach(() => { + utilsLogWarnMock.restore(); + }) - it('bcat: pass only strings', function() { - multipleBidRequests[0].params.bcat = [1, 2, 3, 'IAB1', 'IAB2']; - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' + it('should generate request with mediatype native', () => { + const request = spec.buildRequests(validBidRequests, nativeBidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('native'); + }); }); - let data = JSON.parse(request.data); - expect(data.bcat).to.exist.and.to.deep.equal(['IAB1', 'IAB2']); - }); + } + // describe('MULTIFORMAT', () => { + // let multiFormatBidderRequest; + // it('should have both banner & video impressions', () => { + // multiFormatBidderRequest = utils.deepClone(bidderRequest); + // multiFormatBidderRequest.bids[0].mediaTypes.video = { + // skip: 1, + // mimes: ['video/mp4', 'video/x-flv'], + // minduration: 5, + // maxduration: 30, + // startdelay: 5, + // playbackmethod: [1, 3], + // api: [1, 2], + // protocols: [2, 3], + // battr: [13, 14], + // linearity: 1, + // placement: 2, + // plcmt: 1, + // minbitrate: 10, + // maxbitrate: 10, + // playerSize: [640, 480] + // } + // const request = spec.buildRequests(validBidRequests, multiFormatBidderRequest); + // const { imp } = request?.data; + // expect(imp).to.be.an('array'); + // expect(imp[0]).to.have.property('banner'); + // expect(imp[0].banner).to.have.property('topframe'); + // expect(imp[0].banner).to.have.property('format'); + // expect(imp[0]).to.have.property('video'); + // }); + + // it('should have both banner & native impressions', () => { + // multiFormatBidderRequest = utils.deepClone(bidderRequest); + // multiFormatBidderRequest.bids[0].nativeOrtbRequest = { + // ver: '1.2', + // assets: [{ + // id: 0, + // img: { + // 'type': 3, + // 'w': 300, + // 'h': 250 + // }, + // required: 1, + // }] + // }; + // const request = spec.buildRequests(validBidRequests, multiFormatBidderRequest); + // const { imp } = request?.data; + // expect(imp).to.be.an('array'); + // expect(imp[0]).to.have.property('banner'); + // expect(imp[0].banner).to.have.property('topframe'); + // expect(imp[0].banner).to.have.property('format'); + // expect(imp[0]).to.have.property('native'); + // }); + // }); + }); - it('bcat: pass strings with length greater than 3', function() { - multipleBidRequests[0].params.bcat = ['AB', 'CD', 'IAB1', 'IAB2']; - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' + describe('rest of ORTB request', () => { + describe('BCAT', () => { + it('should contain only string values', () => { + validBidRequests[0].params.bcat = [1, 2, 3, 'IAB1', 'IAB2']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('bcat'); + expect(request.data).to.have.property('bcat').to.deep.equal(['IAB1', 'IAB2']); }); - let data = JSON.parse(request.data); - expect(data.bcat).to.exist.and.to.deep.equal(['IAB1', 'IAB2']); - }); - it('bcat: trim the strings', function() { - multipleBidRequests[0].params.bcat = [' IAB1 ', ' IAB2 ']; - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' + it('should contain string values with length greater than 3', function() { + validBidRequests[0].params.bcat = ['AB', 'CD', 'IAB1', 'IAB2']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('bcat'); + expect(request.data).to.have.property('bcat').to.deep.equal(['IAB1', 'IAB2']); }); - let data = JSON.parse(request.data); - expect(data.bcat).to.exist.and.to.deep.equal(['IAB1', 'IAB2']); - }); - it('bcat: pass only unique strings', function() { - // multi slot - multipleBidRequests[0].params.bcat = ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB2']; - multipleBidRequests[1].params.bcat = ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB3']; - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' + it('should trim strings', function() { + validBidRequests[0].params.bcat = [' IAB1 ', ' IAB2 ']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('bcat'); + expect(request.data).to.have.property('bcat').to.deep.equal(['IAB1', 'IAB2']); }); - let data = JSON.parse(request.data); - expect(data.bcat).to.exist.and.to.deep.equal(['IAB1', 'IAB2', 'IAB3']); - }); - it('bcat: do not pass bcat if all entries are invalid', function() { - // multi slot - multipleBidRequests[0].params.bcat = ['', 'IAB', 'IAB']; - multipleBidRequests[1].params.bcat = [' ', 22, 99999, 'IA']; - let request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id' + it('should pass unique strings', function() { + validBidRequests[0].params.bcat = ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB2']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('bcat'); + expect(request.data).to.have.property('bcat').to.deep.equal(['IAB1', 'IAB2']); }); - let data = JSON.parse(request.data); - expect(data.bcat).to.deep.equal(undefined); - }); - it('ortb2.bcat should merged with slot level bcat param', function() { - multipleBidRequests[0].params.bcat = ['IAB-1', 'IAB-2']; - const ortb2 = { - bcat: ['IAB-3', 'IAB-4'] - }; - const request = spec.buildRequests(multipleBidRequests, { - auctionId: 'new-auction-id', - bidderCode: 'pubmatic', - ortb2 + it('should fail if validations are not met', function() { + validBidRequests[0].params.bcat = ['', 'IA', 'IB']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.not.have.property('bcat'); }); - let data = JSON.parse(request.data); - expect(data.bcat).to.deep.equal(['IAB-1', 'IAB-2', 'IAB-3', 'IAB-4']); }); - }); - describe('Response checking', function () { - it('should check for valid response values', function () { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' + describe('ACAT', () => { + it('should contain only string values', () => { + validBidRequests[0].params.acat = [1, 2, 3, 'IAB1', 'IAB2']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('acat'); + expect(request.data).to.have.property('acat').to.deep.equal(['IAB1', 'IAB2']); }); - let data = JSON.parse(request.data); - let response = spec.interpretResponse(bidResponses, request); - expect(response).to.be.an('array').with.length.above(0); - expect(response[0].requestId).to.equal(bidResponses.body.seatbid[0].bid[0].impid); - expect(response[0].cpm).to.equal(parseFloat((bidResponses.body.seatbid[0].bid[0].price).toFixed(2))); - expect(response[0].width).to.equal(bidResponses.body.seatbid[0].bid[0].w); - expect(response[0].height).to.equal(bidResponses.body.seatbid[0].bid[0].h); - if (bidResponses.body.seatbid[0].bid[0].crid) { - expect(response[0].creativeId).to.equal(bidResponses.body.seatbid[0].bid[0].crid); - } else { - expect(response[0].creativeId).to.equal(bidResponses.body.seatbid[0].bid[0].id); - } - expect(response[0].dealId).to.equal(bidResponses.body.seatbid[0].bid[0].dealid); - expect(response[0].currency).to.equal('USD'); - expect(response[0].netRevenue).to.equal(true); - expect(response[0].ttl).to.equal(300); - expect(response[0].meta.networkId).to.equal(123); - expect(response[0].adserverTargeting.hb_buyid_pubmatic).to.equal('BUYER-ID-987'); - expect(response[0].meta.buyerId).to.equal('seat-id'); - expect(response[0].meta.dchain).to.equal('dchain'); - expect(response[0].meta.clickUrl).to.equal('blackrock.com'); - expect(response[0].meta.advertiserDomains[0]).to.equal('blackrock.com'); - expect(response[0].referrer).to.include(data.site.ref); - expect(response[0].ad).to.equal(bidResponses.body.seatbid[0].bid[0].adm); - expect(response[0].pm_seat).to.equal(bidResponses.body.seatbid[0].seat); - expect(response[0].pm_dspid).to.equal(bidResponses.body.seatbid[0].bid[0].ext.dspid); - expect(response[0].partnerImpId).to.equal(bidResponses.body.seatbid[0].bid[0].id); - - expect(response[1].requestId).to.equal(bidResponses.body.seatbid[1].bid[0].impid); - expect(response[1].cpm).to.equal(parseFloat((bidResponses.body.seatbid[1].bid[0].price).toFixed(2))); - expect(response[1].width).to.equal(bidResponses.body.seatbid[1].bid[0].w); - expect(response[1].height).to.equal(bidResponses.body.seatbid[1].bid[0].h); - if (bidResponses.body.seatbid[1].bid[0].crid) { - expect(response[1].creativeId).to.equal(bidResponses.body.seatbid[1].bid[0].crid); - } else { - expect(response[1].creativeId).to.equal(bidResponses.body.seatbid[1].bid[0].id); - } - expect(response[1].dealId).to.equal(bidResponses.body.seatbid[1].bid[0].dealid); - expect(response[1].currency).to.equal('USD'); - expect(response[1].netRevenue).to.equal(true); - expect(response[1].ttl).to.equal(300); - expect(response[1].meta.networkId).to.equal(422); - expect(response[1].adserverTargeting.hb_buyid_pubmatic).to.equal('BUYER-ID-789'); - expect(response[1].meta.buyerId).to.equal(832); - expect(response[1].meta.clickUrl).to.equal('hivehome.com'); - expect(response[1].meta.advertiserDomains[0]).to.equal('hivehome.com'); - expect(response[1].referrer).to.include(data.site.ref); - expect(response[1].ad).to.equal(bidResponses.body.seatbid[1].bid[0].adm); - expect(response[1].pm_seat).to.equal(bidResponses.body.seatbid[1].seat || null); - expect(response[1].pm_dspid).to.equal(bidResponses.body.seatbid[1].bid[0].ext.dspid); - expect(response[0].partnerImpId).to.equal(bidResponses.body.seatbid[0].bid[0].id); - }); - it('should check for dealChannel value selection', function () { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' + it('should trim strings', () => { + validBidRequests[0].params.acat = [' IAB1 ', ' IAB2 ']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('acat'); + expect(request.data).to.have.property('acat').to.deep.equal(['IAB1', 'IAB2']); }); - let response = spec.interpretResponse(bidResponses, request); - expect(response).to.be.an('array').with.length.above(0); - expect(response[0].dealChannel).to.equal('PMPG'); - expect(response[1].dealChannel).to.equal('PREF'); - }); - it('should check for unexpected dealChannel value selection', function () { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' + it('should pass unique strings', () => { + validBidRequests[0].params.acat = ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB2']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('acat'); + expect(request.data).to.have.property('acat').to.deep.equal(['IAB1', 'IAB2']); }); - let updateBiResponse = bidResponses; - updateBiResponse.body.seatbid[0].bid[0].ext.deal_channel = 11; - let response = spec.interpretResponse(updateBiResponse, request); - - expect(response).to.be.an('array').with.length.above(0); - expect(response[0].dealChannel).to.equal(null); + it('should fail if validations are not met', () => { + validBidRequests[0].params.acat = ['', 'IA', 'IB']; + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('acat'); + }); }); - it('should have a valid native bid response', function() { - let request = spec.buildRequests(nativeBidRequests, { - auctionId: 'new-auction-id' + describe('TMAX, ID, AT, CUR, EXT', () => { + it('should have tmax', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('tmax').to.equal(2000); }); - let data = JSON.parse(request.data); - data.imp[0].id = '2a5571261281d4'; - request.data = JSON.stringify(data); - let response = spec.interpretResponse(nativeBidResponse, request); - let assets = response[0].native.ortb.assets; - expect(response).to.be.an('array').with.length.above(0); - expect(response[0].native).to.exist.and.to.be.an('object'); - expect(response[0].mediaType).to.exist.and.to.equal('native'); - expect(assets).to.be.an('array').with.length.above(0); - expect(assets[0].title).to.exist.and.to.be.an('object'); - expect(assets[1].img).to.exist.and.to.be.an('object'); - expect(assets[1].img.url).to.exist.and.to.be.an('string'); - expect(assets[1].img.h).to.exist; - expect(assets[1].img.w).to.exist; - expect(assets[2].data).to.exist.and.to.be.an('object'); - }); - it('should check for valid banner mediaType in case of multiformat request', function() { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' + it('should remove test if pubmaticTest is not set', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('test').to.equal(undefined); }); - let response = spec.interpretResponse(bannerBidResponse, request); - expect(response[0].mediaType).to.equal('banner'); - }); + it('should have id', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('id'); + }); - it('should check for valid native mediaType in case of multiformat request', function() { - let request = spec.buildRequests(nativeBidRequests, { - auctionId: 'new-auction-id' + it('should set at to 1', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('at').to.equal(1); }); - let response = spec.interpretResponse(nativeBidResponse, request); - expect(response[0].mediaType).to.equal('native'); - }); + it('should have cur', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('cur').to.be.an('array').to.have.lengthOf(1); + expect(request.data).to.have.property('cur').to.include.members(['USD']); + }); - it('should not assign renderer if bid is native', function() { - let request = spec.buildRequests(nativeBidRequests, { - auctionId: 'new-auction-id' + it('should have ext', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('ext').to.have.property('epoch'); + expect(request.data).to.have.property('ext').to.have.property('wrapper'); + expect(request.data).to.have.property('ext').to.have.property('wrapper').to.have.property('profile'); + expect(request.data).to.have.property('ext').to.have.property('wrapper').to.have.property('wiid'); + expect(request.data).to.have.property('ext').to.have.property('wrapper').to.have.property('wv'); + expect(request.data).to.have.property('ext').to.have.property('wrapper').to.have.property('wp'); }); - let response = spec.interpretResponse(nativeBidResponse, request); - expect(response[0].renderer).to.not.exist; - }); - it('should not assign renderer if bid is of banner', function() { - let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' + it('should have url with post method', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request).to.have.property('method').to.equal('POST'); + expect(request).to.have.property('url').to.equal('https://hbopenbid.pubmatic.com/translator?source=prebid-client'); }); - let response = spec.interpretResponse(bidResponses, request); - expect(response[0].renderer).to.not.exist; }); - if (FEATURES.VIDEO) { - it('should check for valid video mediaType in case of multiformat request', function() { - let request = spec.buildRequests(videoBidRequests, { - auctionId: 'new-auction-id' - }); - let response = spec.interpretResponse(videoBidResponse, request); - expect(response[0].mediaType).to.equal('video'); + describe('GROUPM', () => { + let bidderSettingStub; + beforeEach(() => { + bidderSettingStub = sinon.stub(bidderSettings, 'get'); }); - it('should assign renderer if bid is video and request is for outstream', function() { - let request = spec.buildRequests(outstreamBidRequest, validOutstreamBidRequest); - let response = spec.interpretResponse(outstreamVideoBidResponse, request); - expect(response[0].renderer).to.exist; + afterEach(() => { + bidderSettingStub.restore(); }); - it('should not assign renderer if bidderRequest is not present', function() { - let request = spec.buildRequests(outstreamBidRequest, { - auctionId: 'new-auction-id' - }); - let response = spec.interpretResponse(outstreamVideoBidResponse, request); - expect(response[0].renderer).to.not.exist; + it('should skip setting the marketplace object in extension if allowAlternateBidderCodes is not defined', () => { + bidderSettingStub.returns(undefined); + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('ext').to.not.have.property('marketplace'); }); - it('should not assign renderer if bid is video and request is for instream', function() { - let request = spec.buildRequests(videoBidRequests, { - auctionId: 'new-auction-id' - }); - let response = spec.interpretResponse(videoBidResponse, request); - expect(response[0].renderer).to.not.exist; + it('should set the marketplace object in the extension when allowAlternateBidderCodes is set to "groupm"', () => { + bidderSettingStub.withArgs('pubmatic', 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs('pubmatic', 'allowedAlternateBidderCodes').returns(['groupm']); + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('ext').to.have.property('marketplace'); + expect(request.data).to.have.property('ext').to.have.property('marketplace').to.have.property('allowedbidders').to.be.an('array'); + expect(request.data.ext.marketplace.allowedbidders.length).to.equal(2); + expect(request.data.ext.marketplace.allowedbidders[0]).to.equal('pubmatic'); + expect(request.data.ext.marketplace.allowedbidders[1]).to.equal('groupm'); }); - it('should assign mediaType by reading bid.ext.mediaType', function() { - let newvideoRequests = [{ - 'bidder': 'pubmatic', - 'params': { - 'adSlot': 'SLOT_NHB1@728x90', - 'publisherId': '5670', - 'video': { - 'mimes': ['video/mp4'], - 'skippable': true, - 'protocols': [1, 2, 5], - 'linearity': 1 - } - }, - 'mediaTypes': { - 'video': { - 'playerSize': [ - [640, 480] - ], - 'protocols': [1, 2, 5], - 'context': 'instream', - 'mimes': ['video/flv'], - 'skippable': false, - 'skip': 1, - 'linearity': 2 - } - }, - 'adUnitCode': 'video1', - 'transactionId': '803e3750-0bbe-4ffe-a548-b6eca15087bf', - 'sizes': [ - [640, 480] - ], - 'bidId': '2c95df014cfe97', - 'bidderRequestId': '1fe59391566442', - 'auctionId': '3a4118ef-fb96-4416-b0b0-3cfc1cebc142', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }]; - let newvideoBidResponses = { - 'body': { - 'id': '1621441141473', - 'cur': 'USD', - 'customdata': 'openrtb1', - 'ext': { - 'buyid': 'myBuyId' - }, - 'seatbid': [{ - 'bid': [{ - 'id': '2c95df014cfe97', - 'impid': '2c95df014cfe97', - 'price': 4.2, - 'cid': 'test1', - 'crid': 'test2', - 'adm': "Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1", - 'w': 0, - 'h': 0, - 'dealId': 'ASEA-MS-KLY-TTD-DESKTOP-ID-VID-6S-030420', - 'ext': { - 'bidtype': 1 - } - }], - 'ext': { - 'buyid': 'myBuyId' - } - }] - }, - 'headers': {} - } - let newrequest = spec.buildRequests(newvideoRequests, { - auctionId: 'new-auction-id' - }); - let newresponse = spec.interpretResponse(newvideoBidResponses, newrequest); - expect(newresponse[0].mediaType).to.equal('video') - }) - - it('should assign mediaType even if bid.ext.mediaType does not exists', function() { - let newvideoRequests = [{ - 'bidder': 'pubmatic', - 'params': { - 'adSlot': 'SLOT_NHB1@728x90', - 'publisherId': '5670', - 'video': { - 'mimes': ['video/mp4'], - 'skippable': true, - 'protocols': [1, 2, 5], - 'linearity': 1 - } - }, - 'mediaTypes': { - 'video': { - 'playerSize': [ - [640, 480] - ], - 'protocols': [1, 2, 5], - 'context': 'instream', - 'mimes': ['video/flv'], - 'skippable': false, - 'skip': 1, - 'linearity': 2 - } - }, - 'adUnitCode': 'video1', - 'transactionId': '803e3750-0bbe-4ffe-a548-b6eca15087bf', - 'sizes': [ - [640, 480] - ], - 'bidId': '2c95df014cfe97', - 'bidderRequestId': '1fe59391566442', - 'auctionId': '3a4118ef-fb96-4416-b0b0-3cfc1cebc142', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }]; - let newvideoBidResponses = { - 'body': { - 'id': '1621441141473', - 'cur': 'USD', - 'customdata': 'openrtb1', - 'ext': { - 'buyid': 'myBuyId' - }, - 'seatbid': [{ - 'bid': [{ - 'id': '2c95df014cfe97', - 'impid': '2c95df014cfe97', - 'price': 4.2, - 'cid': 'test1', - 'crid': 'test2', - 'adm': "Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1", - 'w': 0, - 'h': 0, - 'dealId': 'ASEA-MS-KLY-TTD-DESKTOP-ID-VID-6S-030420' - }], - 'ext': { - 'buyid': 'myBuyId' - } - }] - }, - 'headers': {} - } - let newrequest = spec.buildRequests(newvideoRequests, { - auctionId: 'new-auction-id' - }); - let newresponse = spec.interpretResponse(newvideoBidResponses, newrequest); - expect(newresponse[0].mediaType).to.equal('video') + it('should be ALL when allowedAlternateBidderCodes is \'*\'', () => { + bidderSettingStub.withArgs('pubmatic', 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs('pubmatic', 'allowedAlternateBidderCodes').returns(['*']); + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data.ext.marketplace.allowedbidders).to.be.an('array'); + expect(request.data.ext.marketplace.allowedbidders[0]).to.equal('all'); }); - } - }); - - describe('Fledge Auction config Response', function () { - let response; - let bidRequestConfigs = [ - { - bidder: 'pubmatic', - mediaTypes: { - banner: { - sizes: [[728, 90], [160, 600]] - } - }, - params: { - publisherId: '5670', - adSlot: '/15671365/DMDemo@300x250:0', - kadfloor: '1.2', - pmzoneid: 'aabc, ddef', - kadpageurl: 'www.publisher.com', - yob: '1986', - gender: 'M', - lat: '12.3', - lon: '23.7', - wiid: '1234567890', - profId: '100', - verId: '200', - currency: 'AUD', - dctr: 'key1:val1,val2|key2:val1' - }, - placementCode: '/19968336/header-bid-tag-1', - sizes: [[300, 250], [300, 600]], - bidId: 'test_bid_id', - requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - ortb2Imp: { - ext: { - tid: '92489f71-1bf2-49a0-adf9-000cea934729', - ae: 1 - } - }, - } - ]; - - let bidRequest = spec.buildRequests(bidRequestConfigs, {}); - let bidResponse = { - seatbid: [{ - bid: [{ - impid: 'test_bid_id', - price: 2, - w: 728, - h: 250, - crid: 'test-creative-id', - dealid: 'test-deal-id', - adm: 'test-ad-markup' - }] - }], - cur: 'AUS', - ext: { - fledge_auction_configs: { - 'test_bid_id': { - seller: 'ads.pubmatic.com', - interestGroupBuyers: ['dsp1.com'], - sellerTimeout: 0, - perBuyerSignals: { - 'dsp1.com': { - bid_macros: 0.1, - disallowed_adv_ids: [ - '5678', - '5890' - ], - } - } - } - } - } - }; - - response = spec.interpretResponse({ body: bidResponse }, bidRequest); - it('should return FLEDGE auction_configs alongside bids', function () { - expect(response).to.have.property('bids'); - expect(response).to.have.property('fledgeAuctionConfigs'); - expect(response.fledgeAuctionConfigs.length).to.equal(1); - expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test_bid_id'); - }); - }); - - describe('Preapare metadata', function () { - it('Should copy all fields from ext to meta', function () { - const dsa = { - behalf: 'Advertiser', - paid: 'Advertiser', - transparency: [{ - domain: 'dsp1domain.com', - dsaparams: [1, 2] - }], - adrender: 1 - }; - - const bid = { - 'adomain': [ - 'mystartab.com' - ], - cat: ['IAB_CATEGORY'], - ext: { - advid: '12', - 'dspid': 6, - 'deal_channel': 1, - 'bidtype': 0, - advertiserId: 'adid', - dsa, - // networkName: 'nwnm', - // primaryCatId: 'pcid', - // advertiserName: 'adnm', - // agencyId: 'agid', - // agencyName: 'agnm', - // brandId: 'brid', - // brandName: 'brnm', - // dchain: 'dc', - // demandSource: 'ds', - // secondaryCatIds: ['secondaryCatIds'] - } - }; - - const br = {}; - prepareMetaObject(br, bid, null); - expect(br.meta.networkId).to.equal(6); // dspid - expect(br.meta.buyerId).to.equal('12'); // adid - expect(br.meta.advertiserId).to.equal('12'); - // expect(br.meta.networkName).to.equal('nwnm'); - expect(br.meta.primaryCatId).to.equal('IAB_CATEGORY'); - // expect(br.meta.advertiserName).to.equal('adnm'); - expect(br.meta.agencyId).to.equal('12'); - // expect(br.meta.agencyName).to.equal('agnm'); - expect(br.meta.brandId).to.equal('mystartab.com'); - // expect(br.meta.brandName).to.equal('brnm'); - // expect(br.meta.dchain).to.equal('dc'); - expect(br.meta.demandSource).to.equal(6); - expect(br.meta.secondaryCatIds).to.be.an('array').with.length.above(0); - expect(br.meta.secondaryCatIds[0]).to.equal('IAB_CATEGORY'); - expect(br.meta.advertiserDomains).to.be.an('array').with.length.above(0); // adomain - expect(br.meta.clickUrl).to.equal('mystartab.com'); // adomain - expect(br.meta.dsa).to.equal(dsa); // dsa - }); - - it('Should be empty, when ext and adomain is absent in bid object', function () { - const bid = {}; - const br = {}; - prepareMetaObject(br, bid, null); - expect(Object.keys(br.meta).length).to.equal(0); - }); - - it('Should be empty, when ext and adomain will not have properties', function () { - const bid = { - 'adomain': [], - ext: {} - }; - const br = {}; - prepareMetaObject(br, bid, null); - expect(Object.keys(br.meta).length).to.equal(0); - expect(br.meta.advertiserDomains).to.equal(undefined); // adomain - expect(br.meta.clickUrl).to.equal(undefined); // adomain - }); - - it('Should have buyerId,advertiserId, agencyId value of site ', function () { - const bid = { - 'adomain': [], - ext: { - advid: '12', - } - }; - const br = {}; - prepareMetaObject(br, bid, '5100'); - expect(br.meta.buyerId).to.equal('5100'); // adid - expect(br.meta.advertiserId).to.equal('5100'); - expect(br.meta.agencyId).to.equal('5100'); - }); - }); - - describe('getUserSyncs', function() { - const syncurl_iframe = 'https://ads.pubmatic.com/AdServer/js/user_sync.html?kdntuid=1&p=5670'; - const syncurl_image = 'https://image8.pubmatic.com/AdServer/ImgSync?p=5670'; - let sandbox; - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - afterEach(function() { - sandbox.restore(); - }); - - it('execute as per config', function() { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: syncurl_iframe - }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ - type: 'image', url: syncurl_image - }]); - }); - - it('CCPA/USP', function() { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&us_privacy=1NYN` - }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, '1NYN')).to.deep.equal([{ - type: 'image', url: `${syncurl_image}&us_privacy=1NYN` - }]); }); - it('GDPR', function() { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo` - }]); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=foo` - }]); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` - }]); - - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ - type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=foo` - }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ - type: 'image', url: `${syncurl_image}&gdpr=0&gdpr_consent=foo` - }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ - type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=` - }]); - }); + describe('SITE', () => { + it('should have site object', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('site'); + }); - it('COPPA: true', function() { - sandbox.stub(config, 'getConfig').callsFake(key => { - const config = { - 'coppa': true - }; - return config[key]; + it('should have site object with page, domain', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('site').to.have.property('page').to.equal('https://ebay.com'); + expect(request.data).to.have.property('site').to.have.property('domain').to.equal('ebay.com'); }); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&coppa=1` - }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ - type: 'image', url: `${syncurl_image}&coppa=1` - }]); }); - it('COPPA: false', function() { - sandbox.stub(config, 'getConfig').callsFake(key => { - const config = { - 'coppa': false - }; - return config[key]; + describe('DEVICE', () => { + it('should have device object', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('device'); + expect(request.data.device).to.have.property('w').to.equal(1200); + expect(request.data.device).to.have.property('h').to.equal(1800); + expect(request.data.device).to.have.property('js').to.equal(1); + expect(request.data.device).to.have.property('connectiontype'); + expect(request.data.device).to.have.property('language').to.equal('en'); }); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}` - }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ - type: 'image', url: `${syncurl_image}` - }]); }); - it('GDPR + COPPA:true + CCPA/USP', function() { - sandbox.stub(config, 'getConfig').callsFake(key => { - const config = { - 'coppa': true + describe('CONFIG/BADV', () => { + let copiedBidderRequest; + beforeEach(() => { + copiedBidderRequest = utils.deepClone(bidderRequest); + copiedBidderRequest.ortb2.app = { + id: 'app-id', + name: 'app-name', }; - return config[key]; + copiedBidderRequest.ortb2.site.ext = { + id: 'site-id', + name: 'site-name', + } + copiedBidderRequest.ortb2.badv = ['example.com']; }); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` - }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ - type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` - }]); - }); - describe('GPP', function() { - it('should return userSync url without Gpp consent if gppConsent is undefined', () => { - const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, undefined); - expect(result).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}` - }]); + it('should have app if it is set in ortb2', () => { + const request = spec.buildRequests(validBidRequests, copiedBidderRequest); + expect(request.data).to.have.property('app'); }); - it('should return userSync url without Gpp consent if gppConsent.gppString is undefined', () => { - const gppConsent = { applicableSections: ['5'] }; - const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); - expect(result).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}` - }]); + it('should include app if it is defined in ortb2 but not site', () => { + const request = spec.buildRequests(validBidRequests, copiedBidderRequest); + expect(request.data).to.have.property('app'); + expect(request.data).to.not.have.property('site'); }); - it('should return userSync url without Gpp consent if gppConsent.applicableSections is undefined', () => { - const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' }; - const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); - expect(result).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}` - }]); + it('should have badv if it is set in ortb2', () => { + const request = spec.buildRequests(validBidRequests, copiedBidderRequest); + expect(request.data).to.have.property('badv'); + expect(request.data.badv).to.deep.equal(['example.com']); }); + }); - it('should return userSync url without Gpp consent if gppConsent.applicableSections is an empty array', () => { - const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [] }; - const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); - expect(result).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}` - }]); + describe('AUCTION ID', () => { + it('should use auctionId as wiid when it is not provided in params', () => { + const copiedValidBidRequests = utils.deepClone(validBidRequests); + delete copiedValidBidRequests[0].params.wiid; + const request = spec.buildRequests(copiedValidBidRequests, bidderRequest); + expect(request.data).to.have.property('ext'); + expect(request.data.ext).to.have.property('wrapper'); + expect(request.data.ext.wrapper).to.have.property('wiid'); + expect(request.data.ext.wrapper.wiid).to.equal('ee3074fe-97ce-4681-9235-d7622aede74c'); }); - it('should concatenate gppString and applicableSections values in the returned userSync iframe url', () => { - const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [5] }; - const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); - expect(result).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gpp=${encodeURIComponent(gppConsent.gppString)}&gpp_sid=${encodeURIComponent(gppConsent.applicableSections)}` - }]); + it('should use auctionId as wiid from params', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('ext'); + expect(request.data.ext).to.have.property('wrapper'); + expect(request.data.ext.wrapper).to.have.property('wiid'); + expect(request.data.ext.wrapper.wiid).to.equal('1234567890'); }); + }); - it('should concatenate gppString and applicableSections values in the returned userSync image url', () => { - const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [5] }; - const result = spec.getUserSyncs({iframeEnabled: false}, undefined, undefined, undefined, gppConsent); - expect(result).to.deep.equal([{ - type: 'image', url: `${syncurl_image}&gpp=${encodeURIComponent(gppConsent.gppString)}&gpp_sid=${encodeURIComponent(gppConsent.applicableSections)}` - }]); + describe('GDPR', () => { + let copiedBidderRequest; + beforeEach(() => { + copiedBidderRequest = utils.deepClone(bidderRequest); + copiedBidderRequest.ortb2.user = { + ext: { + consent: 'kjfdniwjnifwenrif3' + } + } }); - }); - }); - describe('getDeviceConnectionType', function() { - it('is a function', function(done) { - getDeviceConnectionType.should.be.a('function'); - done(); + it('should have GDPR string', () => { + const request = spec.buildRequests(validBidRequests, copiedBidderRequest); + expect(request.data).to.have.property('user'); + expect(request.data.user).to.have.property('ext'); + expect(request.data.user.ext).to.have.property('consent').to.equal('kjfdniwjnifwenrif3'); + }); }); - it('should return matched value if navigator.connection is present', function(done) { - const connectionValue = getDeviceConnectionType(); - if (window?.navigator?.connection) { - expect(connectionValue).to.be.a('number'); - } - done(); + describe('GPP', () => { + it('should have gpp & gpp_sid in request if set using ortb2 and not present in request', () => { + let copiedBidderRequest = utils.deepClone(bidderRequest); + copiedBidderRequest.ortb2.regs = { + gpp: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + gpp_sid: [5] + } + const request = spec.buildRequests(validBidRequests, copiedBidderRequest); + expect(request.data).to.have.property('regs'); + expect(request.data.regs).to.have.property('gpp').to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + expect(request.data.regs).to.have.property('gpp_sid').that.eql([5]); + }); }); - }); - if (FEATURES.VIDEO) { - describe('Checking for Video.Placement property', function() { - let sandbox, utilsMock; - const adUnit = 'Div1'; - const msg_placement_missing = 'Video.Placement param missing for Div1'; - let videoData = { - battr: [6, 7], - skipafter: 15, - maxduration: 50, - context: 'instream', - playerSize: [640, 480], - skip: 0, - connectiontype: [1, 2, 6], - skipmin: 10, - minduration: 10, - mimes: ['video/mp4', 'video/x-flv'], - } + describe('DSA', () => { + const dsa = { + dsarequired: 3, + pubrender: 0, + datatopub: 2, + transparency: [ + { + domain: 'platform1domain.com', + dsaparams: [1] + }, + { + domain: 'SSP2domain.com', + dsaparams: [1, 2] + } + ] + }; beforeEach(() => { - utilsMock = sinon.mock(utils); - sandbox = sinon.sandbox.create(); - sandbox.spy(utils, 'logWarn'); + bidderRequest.ortb2.regs = {ext: { dsa }}; }); - afterEach(() => { - utilsMock.restore(); - sandbox.restore(); - }) - - it('should log Video.Placement param missing', function() { - checkVideoPlacement(videoData, adUnit); - sinon.assert.calledWith(utils.logWarn, msg_placement_missing); - }) - it('shoud not log Video.Placement param missing', function() { - videoData['placement'] = 1; - checkVideoPlacement(videoData, adUnit); - sinon.assert.neverCalledWith(utils.logWarn, msg_placement_missing); - }) - }); - } - }); - - if (FEATURES.VIDEO) { - describe('Video request params', function() { - let sandbox, utilsMock, newVideoRequest; - beforeEach(() => { - utilsMock = sinon.mock(utils); - sandbox = sinon.sandbox.create(); - sandbox.spy(utils, 'logWarn'); - newVideoRequest = utils.deepClone(videoBidRequests) + it('should have DSA in regs.ext', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('regs'); + expect(request.data.regs).to.have.property('ext'); + expect(request.data.regs.ext).to.have.property('dsa').to.deep.equal(dsa); + }); }); - afterEach(() => { - utilsMock.restore(); - sandbox.restore(); - }) - - it('Should log warning if video params from mediaTypes and params obj of bid are not present', function () { - delete newVideoRequest[0].mediaTypes.video; - delete newVideoRequest[0].params.video; - - let request = spec.buildRequests(newVideoRequest, { - auctionId: 'new-auction-id' + describe('ORTB2IMP', () => { + it('should send gpid if specified', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('imp'); + expect(request.data.imp[0]).to.have.property('ext'); + expect(request.data.imp[0].ext).to.have.property('gpid'); + expect(request.data.imp[0].ext.gpid).to.equal('/1111/homepage-leftnav'); }); - sinon.assert.calledOnce(utils.logWarn); - expect(request).to.equal(undefined); - }); + it('should send pbadslot if specified', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('imp'); + expect(request.data.imp[0]).to.have.property('ext'); + expect(request.data.imp[0].ext).to.have.property('data'); + expect(request.data.imp[0].ext.data).to.have.property('pbadslot'); + expect(request.data.imp[0].ext.data.pbadslot).to.equal('/1111/homepage-leftnav'); + }); - it('Should consider video params from mediaType object of bid', function () { - delete newVideoRequest[0].params.video; + it('should send adserver if specified', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('imp'); + expect(request.data.imp[0]).to.have.property('ext'); + expect(request.data.imp[0].ext).to.have.property('data'); + expect(request.data.imp[0].ext.data).to.have.property('adserver'); + expect(request.data.imp[0].ext.data.adserver).to.have.property('name'); + expect(request.data.imp[0].ext.data.adserver.name).to.equal('gam'); + expect(request.data.imp[0].ext.data.adserver).to.have.property('adslot'); + expect(request.data.imp[0].ext.data.adserver.adslot).to.equal('/1111/homepage-leftnav'); + }); - let request = spec.buildRequests(newVideoRequest, { - auctionId: 'new-auction-id' + it('should send custom data if specified', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('imp'); + expect(request.data.imp[0]).to.have.property('ext'); + expect(request.data.imp[0].ext).to.have.property('data'); + expect(request.data.imp[0].ext.data).to.have.property('customData'); + expect(request.data.imp[0].ext.data.customData).to.have.property('id').to.equal('id-1'); }); - let data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist; - expect(data.imp[0]['video']['w']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[0]); - expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]); - expect(data.imp[0]['video']['battr']).to.equal(undefined); }); - describe('Assign Deal Tier (i.e. prebidDealPriority)', function () { - let videoSeatBid, request, newBid; - // let data = JSON.parse(request.data); - beforeEach(function () { - videoSeatBid = videoBidResponse.body.seatbid[0].bid[0]; - // const adpodValidOutstreamBidRequest = validOutstreamBidRequest.bids[0].mediaTypes.video.context = 'adpod'; - request = spec.buildRequests(bidRequests, validOutstreamBidRequest); - newBid = { - requestId: '47acc48ad47af5' - }; - videoSeatBid.ext = videoSeatBid.ext || {}; - videoSeatBid.ext.video = videoSeatBid.ext.video || {}; - // videoBidRequests[0].mediaTypes.video = videoBidRequests[0].mediaTypes.video || {}; + describe('FLEDGE', () => { + it('should not send imp.ext.ae when FLEDGE is disabled, ', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.have.property('imp'); + expect(request.data.imp).to.be.an('array'); + expect(request.data.imp[0]).to.have.property('ext'); + expect(request.data.imp[0].ext).to.not.have.property('ae'); }); + }) - it('should not assign video object if deal priority is missing', function () { - assignDealTier(newBid, videoSeatBid, request); - expect(newBid.video).to.equal(undefined); - expect(newBid.video).to.not.exist; + describe('cpm adjustment', () => { + beforeEach(() => { + global.cpmAdjustment = {}; }); - it('should not assign video object if context is not a adpod', function () { - videoSeatBid.ext.prebiddealpriority = 5; - assignDealTier(newBid, videoSeatBid, request); - expect(newBid.video).to.equal(undefined); - expect(newBid.video).to.not.exist; + it('should not perform any action if the bid is undefined', () => { + spec.onBidWon(undefined); + expect(global.cpmAdjustment).to.deep.equal({}); }); - describe('when video deal tier object is present', function () { - beforeEach(function () { - videoSeatBid.ext.prebiddealpriority = 5; - request.bidderRequest.bids[0].mediaTypes.video = { - ...request.bidderRequest.bids[0].mediaTypes.video, - context: 'adpod', - maxduration: 50 - }; - }); - - it('should set video deal tier object, when maxduration is present in ext', function () { - assignDealTier(newBid, videoSeatBid, request); - expect(newBid.video.durationSeconds).to.equal(50); - expect(newBid.video.context).to.equal('adpod'); - expect(newBid.video.dealTier).to.equal(5); - }); + it('should not perform any action if the bid is null', () => { + spec.onBidWon(null); + expect(global.cpmAdjustment).to.deep.equal({}); + }); + it('should invoke _calculateBidCpmAdjustment and correctly update cpmAdjustment', () => { + const bid = { + cpm: 2.5, + originalCpm: 3, + originalCurrency: 'USD', + currency: 'USD', + mediaType: 'banner', + meta: { mediaType: 'banner' } + }; - it('should set video deal tier object, when duration is present in ext', function () { - videoSeatBid.ext.video.duration = 20; - assignDealTier(newBid, videoSeatBid, request); - expect(newBid.video.durationSeconds).to.equal(20); - expect(newBid.video.context).to.equal('adpod'); - expect(newBid.video.dealTier).to.equal(5); + spec.onBidWon(bid); + + expect(cpmAdjustment).to.deep.equal({ + currency: 'USD', + originalCurrency: 'USD', + adjustment: [ + { + cpmAdjustment: Number(((3 - 2.5) / 3).toFixed(2)), // Expected: 0.17 + mediaType: 'banner', + metaMediaType: 'banner', + cpm: 2.5, + originalCpm: 3 + } + ] }); }); }); + + // describe('USER ID/ EIDS', () => { + // let copiedBidderRequest; + // beforeEach(() => { + // copiedBidderRequest = utils.deepClone(bidderRequest); + // copiedBidderRequest.bids[0].userId = { + // id5id : { + // uid: 'id5id-xyz-user-id' + // } + // } + // copiedBidderRequest.bids[0].userIdAsEids = [{ + // source: 'id5-sync.com', + // uids: [{ + // 'id': "ID5*G3_osFE_-UHoUjSuA4T8-f51U-JTNOoGcb2aMpx1APnDy8pDwkKCzXCcoSb1HXIIw9AjWBOWmZ3QbMUDTXKq8MPPW8h0II9mBYkP4F_IXkvD-XG64NuFFDPKvez1YGGx", + // 'atype': 1, + // 'ext': { + // 'linkType': 2, + // 'pba': 'q6Vzr0jEebxzmvS8aSrVQJFoJnOxs9gKBKCOLw1y6ew=' + // } + // }] + // }] + // }); + + // it('should send gpid if specified', () => { + // const request = spec.buildRequests(validBidRequests, copiedBidderRequest); + // expect(request.data).to.have.property('user'); + // expect(request.data.user).to.have.property('eids'); + // }); + // }); }); - } + }); - describe('Marketplace params', function () { - let sandbox, utilsMock, newBidRequests, newBidResponses; - beforeEach(() => { - utilsMock = sinon.mock(utils); - sandbox = sinon.sandbox.create(); - sandbox.spy(utils, 'logInfo'); - newBidRequests = utils.deepClone(bidRequests) - newBidRequests[0].bidder = 'groupm'; - newBidResponses = utils.deepClone(bidResponses); - newBidResponses.body.seatbid[0].bid[0].ext.marketplace = 'groupm' + describe('Response', () => { + it('should return response in prebid format', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const bidResponse = spec.interpretResponse(response, request); + expect(bidResponse).to.be.an('array'); + expect(bidResponse[0]).to.be.an('object'); + expect(bidResponse[0]).to.have.property('ad'); + expect(bidResponse[0]).to.have.property('dealId'); + expect(bidResponse[0]).to.have.property('dealChannel'); + expect(bidResponse[0]).to.have.property('currency'); + expect(bidResponse[0]).to.have.property('meta'); + expect(bidResponse[0]).to.have.property('mediaType'); + expect(bidResponse[0]).to.have.property('referrer'); + expect(bidResponse[0]).to.have.property('cpm'); + expect(bidResponse[0]).to.have.property('pm_seat'); + expect(bidResponse[0]).to.have.property('pm_dspid'); + expect(bidResponse[0]).to.have.property('sspID'); + expect(bidResponse[0]).to.have.property('partnerImpId'); + expect(bidResponse[0]).to.have.property('requestId'); + expect(bidResponse[0]).to.have.property('width'); + expect(bidResponse[0]).to.have.property('height'); + expect(bidResponse[0]).to.have.property('ttl'); + expect(bidResponse[0]).to.have.property('netRevenue'); + expect(bidResponse[0]).to.have.property('creativeId'); }); - afterEach(() => { - utilsMock.restore(); - sandbox.restore(); - }) + it('should return response and match with input values', () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const bidResponse = spec.interpretResponse(response, request); + expect(bidResponse).to.be.an('array'); + expect(bidResponse[0]).to.be.an('object'); + expect(bidResponse[0]).to.have.property('currency').to.be.equal('USD'); + expect(bidResponse[0]).to.have.property('dealId').to.equal('PUBDEAL1'); + expect(bidResponse[0]).to.have.property('dealChannel').to.equal('PMPG'); + expect(bidResponse[0]).to.have.property('meta').to.be.an('object'); + expect(bidResponse[0]).to.have.property('mediaType').to.equal('banner'); + expect(bidResponse[0]).to.have.property('cpm').to.equal(1.3); + expect(bidResponse[0]).to.have.property('pm_seat').to.equal('seat-id'); + expect(bidResponse[0]).to.have.property('pm_dspid').to.equal(123); + expect(bidResponse[0]).to.have.property('sspID').to.equal('74858439-49D7-4169-BA5D-44A046315B2F'); + expect(bidResponse[0]).to.have.property('requestId').to.equal('3736271c3c4b27'); + expect(bidResponse[0]).to.have.property('width').to.equal(300); + expect(bidResponse[0]).to.have.property('height').to.equal(250); + expect(bidResponse[0]).to.have.property('ttl').to.equal(360); + }); - it('Should add bidder code as groupm for marketplace groupm response ', function () { - let request = spec.buildRequests(newBidRequests, { - auctionId: 'new-auction-id' + describe('DEALID', () => { + it('should set deal_channel to PMP if ext.deal_channel is missing', () => { + const copiedResponse = utils.deepClone(response); + delete copiedResponse.body.seatbid[0].bid[0].ext.deal_channel; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const bidResponse = spec.interpretResponse(copiedResponse, request); + expect(bidResponse).to.be.an('array'); + expect(bidResponse[0]).to.be.an('object'); + expect(bidResponse[0]).to.have.property('dealChannel').to.equal('PMP'); + }); + + it('should exclude deal_id and deal_channel from the response if the deal id is missing', () => { + const copiedResponse = utils.deepClone(response); + delete copiedResponse.body.seatbid[0].bid[0].ext.deal_channel; + delete copiedResponse.body.seatbid[0].bid[0].dealid; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const bidResponse = spec.interpretResponse(copiedResponse, request); + expect(bidResponse).to.be.an('array'); + expect(bidResponse[0]).to.be.an('object'); + expect(bidResponse[0]).to.not.have.property('dealId'); + expect(bidResponse[0]).to.not.have.property('dealChannel'); }); - let response = spec.interpretResponse(newBidResponses, request); - expect(response).to.be.an('array').with.length.above(0); - expect(response[0].bidderCode).to.equal('groupm'); }); - }); -}); + if (FEATURES.VIDEO) { + describe('VIDEO', () => { + beforeEach(() => { + let videoBidderRequest = utils.deepClone(bidderRequest); + delete videoBidderRequest.bids[0].mediaTypes.banner; + videoBidderRequest.bids[0].mediaTypes.video = { + skip: 1, + mimes: ['video/mp4', 'video/x-flv'], + minduration: 5, + maxduration: 30, + startdelay: 5, + playbackmethod: [1, 3], + api: [1, 2], + protocols: [2, 3], + battr: [13, 14], + linearity: 1, + placement: 2, + plcmt: 1, + context: 'outstream', + minbitrate: 10, + maxbitrate: 10, + playerSize: [640, 480] + } + }); + + it('should generate video response', () => { + const request = spec.buildRequests(validBidRequests, videoBidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('video'); + const bidResponse = spec.interpretResponse(videoResponse, request); + expect(bidResponse).to.be.an('array'); + expect(bidResponse[0]).to.be.an('object'); + expect(bidResponse[0]).to.have.property('vastXml'); + expect(bidResponse[0]).to.have.property('mediaType'); + expect(bidResponse[0]).to.have.property('playerHeight'); + expect(bidResponse[0]).to.have.property('playerWidth'); + }); + + it('should generate video response with input values', () => { + const request = spec.buildRequests(validBidRequests, videoBidderRequest); + const { imp } = request?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('video'); + const bidResponse = spec.interpretResponse(videoResponse, request); + expect(bidResponse).to.be.an('array'); + expect(bidResponse[0]).to.be.an('object'); + expect(bidResponse[0]).to.have.property('mediaType').to.equal('video'); + expect(bidResponse[0]).to.have.property('playerHeight').to.equal(480); + expect(bidResponse[0]).to.have.property('playerWidth').to.equal(640); + }); + }); + } + }) +}) diff --git a/test/spec/modules/pubmaticIdSystem_spec.js b/test/spec/modules/pubmaticIdSystem_spec.js new file mode 100644 index 00000000000..70aa6df7337 --- /dev/null +++ b/test/spec/modules/pubmaticIdSystem_spec.js @@ -0,0 +1,192 @@ +import { pubmaticIdSubmodule, storage } from 'modules/pubmaticIdSystem.js'; +import * as utils from 'src/utils.js'; +import { server } from 'test/mocks/xhr.js'; +import { uspDataHandler, coppaDataHandler, gppDataHandler } from 'src/adapterManager.js'; +import { expect } from 'chai/index.mjs'; +import { attachIdSystem } from '../../../modules/userId/index.js'; +import { createEidsArray } from '../../../modules/userId/eids.js'; + +const validCookieConfig = { + params: { + publisherId: 12345 + }, + storage: { + type: 'cookie', + name: 'pubmaticId', + expires: 30, + refreshInSeconds: 24 * 3600 // 24 Hours + } +}; + +describe('pubmaticIdSystem', () => { + describe('name', () => { + it('should expose the name of the submodule', () => { + expect(pubmaticIdSubmodule.name).to.equal('pubmaticId'); + }); + }); + + describe('gvlid', () => { + it('should expose the vendor id', () => { + expect(pubmaticIdSubmodule.gvlid).to.equal(76); + }); + }); + + describe('getId', () => { + it('should call endpoint and handle valid response', () => { + const completeCallback = sinon.spy(function() {}); + + const { callback } = pubmaticIdSubmodule.getId(utils.mergeDeep({}, validCookieConfig)); + + callback(completeCallback); + + const [request] = server.requests; + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + id: '6C3F0AB9-AE82-45C2-AD6F-9721E542DC4A' + })); + + expect(request.method).to.equal('GET'); + expect(request.withCredentials).to.be.true; + + const expectedURL = 'https://image6.pubmatic.com/AdServer/UCookieSetPug?oid=5&p=12345&publisherId=12345&gdpr=0&gdpr_consent=&src=pbjs_uid&ver=1&coppa=0&us_privacy=&gpp=&gpp_sid='; + expect(request.url).to.equal(expectedURL); + expect(completeCallback.calledOnceWithExactly({id: '6C3F0AB9-AE82-45C2-AD6F-9721E542DC4A'})).to.be.true; + }); + + it('should log an error if configuration is invalid', () => { + const logErrorSpy = sinon.spy(utils, 'logError'); + pubmaticIdSubmodule.getId({}); + expect(logErrorSpy.called).to.be.true; + logErrorSpy.restore(); + }); + + context('when GDPR applies', () => { + it('should call endpoint with gdpr=1 when GDPR applies and consent string is provided', () => { + const completeCallback = sinon.spy(); + const { callback } = pubmaticIdSubmodule.getId(utils.mergeDeep({}, validCookieConfig), { + gdprApplies: true, + consentString: 'foo' + }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('gdpr=1'); + expect(request.url).to.contain('gdpr_consent=foo'); + }); + }); + + context('when GDPR doesn\'t apply', () => { + it('should call endpoint with \'gdpr=0\'', () => { + const completeCallback = () => {}; + const { callback } = pubmaticIdSubmodule.getId(utils.mergeDeep({}, validCookieConfig), { + gdprApplies: false + }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('gdpr=0'); + }); + }); + + context('when a valid US Privacy string is given', () => { + it('should call endpoint with the US Privacy parameter', () => { + const completeCallback = () => {}; + const { callback } = pubmaticIdSubmodule.getId(utils.mergeDeep({}, validCookieConfig)); + + sinon.stub(uspDataHandler, 'getConsentData').returns('1YYY'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('us_privacy=1YYY'); + + uspDataHandler.getConsentData.restore(); + }); + }); + + context('when coppa is enabled', () => { + it('should call endpoint with an enabled coppa signal', () => { + const completeCallback = () => {}; + const { callback } = pubmaticIdSubmodule.getId(utils.mergeDeep({}, validCookieConfig)); + + sinon.stub(coppaDataHandler, 'getCoppa').returns(true); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('coppa=1'); + + coppaDataHandler.getCoppa.restore(); + }); + }); + + context('when a GPP consent string is given', () => { + it('should call endpoint with the GPP consent string and GPP applicable sections', () => { + const completeCallback = () => {}; + const { callback } = pubmaticIdSubmodule.getId(utils.mergeDeep({}, validCookieConfig)); + + sinon.stub(gppDataHandler, 'getConsentData').returns({ gppString: 'foo', applicableSections: ['1', '2'] }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('gpp=foo&gpp_sid=1%2C2'); + + gppDataHandler.getConsentData.restore(); + }); + + it('should call endpoint with the GPP consent and GPP applicable sections keys still if both values are not present', () => { + const completeCallback = () => {}; + const { callback } = pubmaticIdSubmodule.getId(utils.mergeDeep({}, validCookieConfig)); + + sinon.stub(gppDataHandler, 'getConsentData').returns({ gppString: undefined, applicableSections: undefined }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('gpp=&gpp_sid='); + + gppDataHandler.getConsentData.restore(); + }); + }); + }); + + describe('decode', () => { + it('should wrap the given value inside an object literal', () => { + expect(pubmaticIdSubmodule.decode({ id: 'foo' })).to.deep.equal({ + [pubmaticIdSubmodule.name]: 'foo' + }); + }); + }); + + describe('eid', () => { + before(() => { + attachIdSystem(pubmaticIdSubmodule); + }); + + it('should create the correct EIDs', () => { + const userId = { + 'pubmaticId': 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'esp.pubmatic.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + }); +}); diff --git a/test/spec/modules/pubmaticRtdProvider_spec.js b/test/spec/modules/pubmaticRtdProvider_spec.js new file mode 100644 index 00000000000..2506ddc7598 --- /dev/null +++ b/test/spec/modules/pubmaticRtdProvider_spec.js @@ -0,0 +1,612 @@ +import { expect } from 'chai'; +import * as priceFloors from '../../../modules/priceFloors'; +import * as utils from '../../../src/utils.js'; +import * as suaModule from '../../../src/fpd/sua.js'; +import { config as conf } from '../../../src/config'; +import * as hook from '../../../src/hook.js'; +import { + registerSubModule, pubmaticSubmodule, getFloorsConfig, fetchData, + getCurrentTimeOfDay, getBrowserType, getOs, getDeviceType, getCountry, getUtm, _country, + _profileConfigs, _floorsData, defaultValueTemplate, withTimeout, configMerged +} from '../../../modules/pubmaticRtdProvider.js'; +import sinon from 'sinon'; + +describe('Pubmatic RTD Provider', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(conf, 'getConfig').callsFake(() => { + return { + floors: { + 'enforcement': { + 'floorDeals': true, + 'enforceJS': true + } + }, + realTimeData: { + auctionDelay: 100 + } + }; + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('registerSubModule', () => { + it('should register RTD submodule provider', () => { + let submoduleStub = sinon.stub(hook, 'submodule'); + registerSubModule(); + assert(submoduleStub.calledOnceWith('realTimeData', pubmaticSubmodule)); + submoduleStub.restore(); + }); + }); + + describe('submodule', () => { + describe('name', () => { + it('should be pubmatic', () => { + expect(pubmaticSubmodule.name).to.equal('pubmatic'); + }); + }); + }); + + describe('init', () => { + let logErrorStub; + let continueAuctionStub; + + const getConfig = () => ({ + params: { + publisherId: 'test-publisher-id', + profileId: 'test-profile-id' + }, + }); + + beforeEach(() => { + logErrorStub = sandbox.stub(utils, 'logError'); + continueAuctionStub = sandbox.stub(priceFloors, 'continueAuction'); + }); + + it('should return false if publisherId is missing', () => { + const config = { + params: { + profileId: 'test-profile-id' + } + }; + expect(pubmaticSubmodule.init(config)).to.be.false; + }); + + it('should return false if profileId is missing', () => { + const config = { + params: { + publisherId: 'test-publisher-id' + } + }; + expect(pubmaticSubmodule.init(config)).to.be.false; + }); + + it('should return false if publisherId is not a string', () => { + const config = { + params: { + publisherId: 123, + profileId: 'test-profile-id' + } + }; + expect(pubmaticSubmodule.init(config)).to.be.false; + }); + + it('should return false if profileId is not a string', () => { + const config = { + params: { + publisherId: 'test-publisher-id', + profileId: 345 + } + }; + expect(pubmaticSubmodule.init(config)).to.be.false; + }); + + it('should initialize successfully with valid config', () => { + expect(pubmaticSubmodule.init(getConfig())).to.be.true; + }); + + it('should handle empty config object', () => { + expect(pubmaticSubmodule.init({})).to.be.false; + expect(logErrorStub.calledWith(sinon.match(/Missing publisher Id/))).to.be.true; + }); + + it('should return false if continueAuction is not a function', () => { + continueAuctionStub.value(undefined); + expect(pubmaticSubmodule.init(getConfig())).to.be.false; + expect(logErrorStub.calledWith(sinon.match(/continueAuction is not a function/))).to.be.true; + }); + }); + + describe('getCurrentTimeOfDay', () => { + let clock; + + beforeEach(() => { + clock = sandbox.useFakeTimers(new Date('2024-01-01T12:00:00')); // Set fixed time for testing + }); + + afterEach(() => { + clock.restore(); + }); + + const testTimes = [ + { hour: 6, expected: 'morning' }, + { hour: 13, expected: 'afternoon' }, + { hour: 18, expected: 'evening' }, + { hour: 22, expected: 'night' }, + { hour: 4, expected: 'night' } + ]; + + testTimes.forEach(({ hour, expected }) => { + it(`should return ${expected} at ${hour}:00`, () => { + clock.setSystemTime(new Date().setHours(hour)); + const result = getCurrentTimeOfDay(); + expect(result).to.equal(expected); + }); + }); + }); + + describe('getBrowserType', () => { + let userAgentStub, getLowEntropySUAStub; + + const USER_AGENTS = { + chrome: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + firefox: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0', + edge: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edg/91.0.864.67 Safari/537.36', + safari: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.6 Mobile/15E148 Safari/604.1', + ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', + opera: 'Opera/9.80 (Windows NT 6.1; WOW64) Presto/2.12.388 Version/12.16', + unknown: 'UnknownBrowser/1.0' + }; + + beforeEach(() => { + userAgentStub = sandbox.stub(navigator, 'userAgent'); + getLowEntropySUAStub = sandbox.stub(suaModule, 'getLowEntropySUA').returns(undefined); + }); + + afterEach(() => { + userAgentStub.restore(); + getLowEntropySUAStub.restore(); + }); + + it('should detect Chrome', () => { + userAgentStub.value(USER_AGENTS.chrome); + expect(getBrowserType()).to.equal('9'); + }); + + it('should detect Firefox', () => { + userAgentStub.value(USER_AGENTS.firefox); + expect(getBrowserType()).to.equal('12'); + }); + + it('should detect Edge', () => { + userAgentStub.value(USER_AGENTS.edge); + expect(getBrowserType()).to.equal('2'); + }); + + it('should detect Internet Explorer', () => { + userAgentStub.value(USER_AGENTS.ie); + expect(getBrowserType()).to.equal('4'); + }); + + it('should detect Opera', () => { + userAgentStub.value(USER_AGENTS.opera); + expect(getBrowserType()).to.equal('3'); + }); + + it('should return 0 for unknown browser', () => { + userAgentStub.value(USER_AGENTS.unknown); + expect(getBrowserType()).to.equal('0'); + }); + + it('should return -1 when userAgent is null', () => { + userAgentStub.value(null); + expect(getBrowserType()).to.equal('-1'); + }); + }); + + describe('Utility functions', () => { + it('should set browser correctly', () => { + expect(getBrowserType()).to.be.a('string'); + }); + + it('should set OS correctly', () => { + expect(getOs()).to.be.a('string'); + }); + + it('should set device type correctly', () => { + expect(getDeviceType()).to.be.a('string'); + }); + + it('should set time of day correctly', () => { + expect(getCurrentTimeOfDay()).to.be.a('string'); + }); + + it('should set country correctly', () => { + expect(getCountry()).to.satisfy(value => typeof value === 'string' || value === undefined); + }); + + it('should set UTM correctly', () => { + expect(getUtm()).to.be.a('string'); + expect(getUtm()).to.be.oneOf(['0', '1']); + }); + }); + + describe('getFloorsConfig', () => { + let floorsData, profileConfigs; + let sandbox; + let logErrorStub; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + logErrorStub = sandbox.stub(utils, 'logError'); + floorsData = { + "currency": "USD", + "floorProvider": "PM", + "floorsSchemaVersion": 2, + "modelGroups": [ + { + "modelVersion": "M_1", + "modelWeight": 100, + "schema": { + "fields": [ + "domain" + ] + }, + "values": { + "*": 2.00 + } + } + ], + "skipRate": 0 + }; + profileConfigs = { + 'plugins': { + 'dynamicFloors': { + 'enabled': true, + 'config': { + 'enforcement': { + 'floorDeals': false, + 'enforceJS': false + }, + 'floorMin': 0.1111, + 'skipRate': 11, + 'defaultValues': { + "*|*": 0.2 + } + } + } + } + } + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return correct config structure', () => { + const result = getFloorsConfig(floorsData, profileConfigs); + + expect(result.floors).to.be.an('object'); + expect(result.floors).to.be.an('object'); + expect(result.floors).to.have.property('enforcement'); + expect(result.floors.enforcement).to.have.property('floorDeals', false); + expect(result.floors.enforcement).to.have.property('enforceJS', false); + expect(result.floors).to.have.property('floorMin', 0.1111); + + // Verify the additionalSchemaFields structure + expect(result.floors.additionalSchemaFields).to.have.all.keys([ + 'deviceType', + 'timeOfDay', + 'browser', + 'os', + 'country', + 'utm' + ]); + + Object.values(result.floors.additionalSchemaFields).forEach(field => { + expect(field).to.be.a('function'); + }); + }); + + it('should return undefined when plugin is disabled', () => { + profileConfigs.plugins.dynamicFloors.enabled = false; + const result = getFloorsConfig(floorsData, profileConfigs); + + expect(result).to.equal(undefined); + }); + + it('should initialise default values to empty object when not available', () => { + profileConfigs.plugins.dynamicFloors.config.defaultValues = undefined; + floorsData = undefined; + const result = getFloorsConfig(floorsData, profileConfigs); + + expect(result.floors.data).to.have.property('currency', 'USD'); + expect(result.floors.data).to.have.property('skipRate', 11); + expect(result.floors.data.schema).to.deep.equal(defaultValueTemplate.schema); + expect(result.floors.data.value).to.deep.equal(defaultValueTemplate.value); + }); + + it('should replace skipRate from config to data when avaialble', () => { + const result = getFloorsConfig(floorsData, profileConfigs); + + expect(result.floors.data).to.have.property('skipRate', 11); + }); + + it('should not replace skipRate from config to data when not avaialble', () => { + delete profileConfigs.plugins.dynamicFloors.config.skipRate; + const result = getFloorsConfig(floorsData, profileConfigs); + + expect(result.floors.data).to.have.property('skipRate', 0); + }); + + it('should maintain correct function references', () => { + const result = getFloorsConfig(floorsData, profileConfigs); + + expect(result.floors.additionalSchemaFields.deviceType).to.equal(getDeviceType); + expect(result.floors.additionalSchemaFields.timeOfDay).to.equal(getCurrentTimeOfDay); + expect(result.floors.additionalSchemaFields.browser).to.equal(getBrowserType); + expect(result.floors.additionalSchemaFields.os).to.equal(getOs); + expect(result.floors.additionalSchemaFields.country).to.equal(getCountry); + expect(result.floors.additionalSchemaFields.utm).to.equal(getUtm); + }); + + it('should log error when profileConfigs is not an object', () => { + profileConfigs = 'invalid'; + const result = getFloorsConfig(floorsData, profileConfigs); + expect(result).to.be.undefined; + expect(logErrorStub.calledWith(sinon.match(/profileConfigs is not an object or is empty/))).to.be.true; + }); + }); + + describe('fetchData for configs', () => { + let logErrorStub; + let fetchStub; + let confStub; + + beforeEach(() => { + logErrorStub = sandbox.stub(utils, 'logError'); + fetchStub = sandbox.stub(window, 'fetch'); + confStub = sandbox.stub(conf, 'setConfig'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should successfully fetch profile configs', async () => { + const mockApiResponse = { + "profileName": "profie name", + "desc": "description", + "plugins": { + "dynamicFloors": { + "enabled": false + } + } + }; + + fetchStub.resolves(new Response(JSON.stringify(mockApiResponse), { status: 200 })); + + const result = await fetchData('1234', '123', 'CONFIGS'); + expect(result).to.deep.equal(mockApiResponse); + }); + + it('should log error when JSON parsing fails', async () => { + fetchStub.resolves(new Response('Invalid JSON', { status: 200 })); + + await fetchData('1234', '123', 'CONFIGS'); + expect(logErrorStub.called).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.include('Error while fetching CONFIGS:'); + }); + + it('should log error when response is not ok', async () => { + fetchStub.resolves(new Response(null, { status: 500 })); + + await fetchData('1234', '123', 'CONFIGS'); + expect(logErrorStub.calledWith(sinon.match(/Error while fetching CONFIGS: Not ok/))).to.be.true; + }); + + it('should log error on network failure', async () => { + fetchStub.rejects(new Error('Network Error')); + + await fetchData('1234', '123', 'CONFIGS'); + expect(logErrorStub.called).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.include('Error while fetching CONFIGS'); + }); + }); + + describe('fetchData for floors', () => { + let logErrorStub; + let fetchStub; + let confStub; + + beforeEach(() => { + logErrorStub = sandbox.stub(utils, 'logError'); + fetchStub = sandbox.stub(window, 'fetch'); + confStub = sandbox.stub(conf, 'setConfig'); + global._country = undefined; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should successfully fetch and parse floor rules', async () => { + const mockApiResponse = { + data: { + currency: 'USD', + modelGroups: [], + values: {} + } + }; + + fetchStub.resolves(new Response(JSON.stringify(mockApiResponse), { status: 200, headers: { 'country_code': 'US' } })); + + const result = await fetchData('1234', '123', 'FLOORS'); + expect(result).to.deep.equal(mockApiResponse); + expect(_country).to.equal('US'); + }); + + it('should correctly extract the first unique country code from response headers', async () => { + fetchStub.resolves(new Response(JSON.stringify({}), { + status: 200, + headers: { 'country_code': 'US,IN,US' } + })); + + await fetchData('1234', '123', 'FLOORS'); + expect(_country).to.equal('US'); + }); + + it('should set _country to undefined if country_code header is missing', async () => { + fetchStub.resolves(new Response(JSON.stringify({}), { + status: 200 + })); + + await fetchData('1234', '123', 'FLOORS'); + expect(_country).to.be.undefined; + }); + + it('should log error when JSON parsing fails', async () => { + fetchStub.resolves(new Response('Invalid JSON', { status: 200 })); + + await fetchData('1234', '123', 'FLOORS'); + expect(logErrorStub.called).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.include('Error while fetching FLOORS'); + }); + + it('should log error when response is not ok', async () => { + fetchStub.resolves(new Response(null, { status: 500 })); + + await fetchData('1234', '123', 'FLOORS'); + expect(logErrorStub.firstCall.args[0]).to.include('Error while fetching FLOORS'); + }); + + it('should log error on network failure', async () => { + fetchStub.rejects(new Error('Network Error')); + + await fetchData('1234', '123', 'FLOORS'); + expect(logErrorStub.called).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.include('Error while fetching FLOORS'); + }); + }); + + describe('getBidRequestData', function () { + let callback, continueAuctionStub, mergeDeepStub, logErrorStub; + + const reqBidsConfigObj = { + adUnits: [{ code: 'ad-slot-code-0' }], + auctionId: 'auction-id-0', + ortb2Fragments: { + bidder: { + user: { + ext: { + ctr: 'US', + } + } + } + } + }; + + const ortb2 = { + user: { + ext: { + ctr: 'US', + } + } + } + + const hookConfig = { + reqBidsConfigObj, + context: this, + nextFn: () => true, + haveExited: false, + timer: null + }; + + beforeEach(() => { + callback = sinon.spy(); + continueAuctionStub = sandbox.stub(priceFloors, 'continueAuction'); + logErrorStub = sandbox.stub(utils, 'logError'); + + global.configMergedPromise = Promise.resolve(); + }); + + afterEach(() => { + sandbox.restore(); // Restore all stubs/spies + }); + + it('should call continueAuction with correct hookConfig', async function () { + configMerged(); + await pubmaticSubmodule.getBidRequestData(reqBidsConfigObj, callback); + + expect(continueAuctionStub.called).to.be.true; + expect(continueAuctionStub.firstCall.args[0]).to.have.property('reqBidsConfigObj', reqBidsConfigObj); + expect(continueAuctionStub.firstCall.args[0]).to.have.property('haveExited', false); + }); + + // it('should merge country data into ortb2Fragments.bidder', async function () { + // configMerged(); + // global._country = 'US'; + // pubmaticSubmodule.getBidRequestData(reqBidsConfigObj, callback); + + // expect(reqBidsConfigObj.ortb2Fragments.bidder).to.have.property('pubmatic'); + // // expect(reqBidsConfigObj.ortb2Fragments.bidder.pubmatic.user.ext.ctr).to.equal('US'); + // }); + + it('should call callback once after execution', async function () { + configMerged(); + await pubmaticSubmodule.getBidRequestData(reqBidsConfigObj, callback); + + expect(callback.called).to.be.true; + }); + }); + + describe('withTimeout', function () { + it('should resolve with the original promise value if it resolves before the timeout', async function () { + const promise = new Promise((resolve) => setTimeout(() => resolve('success'), 50)); + const result = await withTimeout(promise, 100); + expect(result).to.equal('success'); + }); + + it('should resolve with undefined if the promise takes longer than the timeout', async function () { + const promise = new Promise((resolve) => setTimeout(() => resolve('success'), 200)); + const result = await withTimeout(promise, 100); + expect(result).to.be.undefined; + }); + + it('should properly handle rejected promises', async function () { + const promise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Failure')), 50)); + try { + await withTimeout(promise, 100); + } catch (error) { + expect(error.message).to.equal('Failure'); + } + }); + + it('should resolve with undefined if the original promise is rejected but times out first', async function () { + const promise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Failure')), 200)); + const result = await withTimeout(promise, 100); + expect(result).to.be.undefined; + }); + + it('should clear the timeout when the promise resolves before the timeout', async function () { + const clock = sinon.useFakeTimers(); + const clearTimeoutSpy = sinon.spy(global, 'clearTimeout'); + + const promise = new Promise((resolve) => setTimeout(() => resolve('success'), 50)); + const resultPromise = withTimeout(promise, 100); + + clock.tick(50); + await resultPromise; + + expect(clearTimeoutSpy.called).to.be.true; + + clearTimeoutSpy.restore(); + clock.restore(); + }); + }); +}); diff --git a/test/spec/modules/pubriseBidAdapter_spec.js b/test/spec/modules/pubriseBidAdapter_spec.js new file mode 100644 index 00000000000..37f1c742c65 --- /dev/null +++ b/test/spec/modules/pubriseBidAdapter_spec.js @@ -0,0 +1,519 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/pubriseBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'pubrise'; + +describe('PubriseBidAdapter', 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 valid URL', function () { + expect(serverRequest.url).to.equal('https://backend.pubrise.ai/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys( + 'device', + 'deviceWidth', + 'deviceHeight', + '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 + } + ]; + + let 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); + let 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); + let 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] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + + 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 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let 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 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let 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' + }] + }; + + let 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' + }] + }; + let 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', + }] + }; + let 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' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.pubrise.ai/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.pubrise.ai/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sync.pubrise.ai/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js index 688a827de03..58c539ae3c8 100644 --- a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js @@ -79,7 +79,7 @@ describe('PubWise Prebid Analytics', function () { /* check for critical values */ let request = requests[0]; let data = JSON.parse(request.requestBody); - // eslint-disable-next-line + // console.log(data.metaData); expect(data.metaData, 'metaData property').to.exist; expect(data.metaData.pbjs_version, 'pbjs version').to.equal('$prebid.version$') @@ -133,7 +133,7 @@ describe('PubWise Prebid Analytics', function () { expect(data.eventList[0], 'eventList property').to.exist; expect(data.eventList[0].args, 'eventList property').to.exist; - // eslint-disable-next-line + // console.log(data.eventList[0].args); let eventArgs = data.eventList[0].args; diff --git a/test/spec/modules/pubxBidAdapter_spec.js b/test/spec/modules/pubxBidAdapter_spec.js index b387264bf91..38efccac2a6 100644 --- a/test/spec/modules/pubxBidAdapter_spec.js +++ b/test/spec/modules/pubxBidAdapter_spec.js @@ -26,10 +26,10 @@ describe('pubxAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 9af1ef185e1..f9f4005db41 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -1,675 +1,735 @@ -import pubxaiAnalyticsAdapter, {getBrowser, getDeviceType, getOS} from 'modules/pubxaiAnalyticsAdapter.js'; -import {expect} from 'chai'; -import adapterManager from 'src/adapterManager.js'; -import * as utils from 'src/utils.js'; -import {server} from 'test/mocks/xhr.js'; -import {getGptSlotInfoForAdUnitCode} from '../../../libraries/gptUtils/gptUtils.js'; +/* globals describe, beforeEach, afterEach, sinon */ +import { expect } from 'chai'; +import { getGptSlotInfoForAdUnitCode } from 'libraries/gptUtils/gptUtils.js'; +import { getDeviceType, getBrowser, getOS } from 'libraries/userAgentUtils'; +import pubxaiAnalyticsAdapter, { + auctionCache, +} from 'modules/pubxaiAnalyticsAdapter.js'; import { EVENTS } from 'src/constants.js'; +import adapterManager from 'src/adapterManager.js'; +import { getWindowLocation } from 'src/utils.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +import * as events from 'src/events.js' +import 'modules/userId/index.js' -let events = require('src/events'); +const readBlobSafariCompat = (blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result) + reader.onerror = reject + reader.readAsText(blob) + }) +} -describe('pubxai analytics adapter', function() { - beforeEach(function() { +describe('pubxai analytics adapter', () => { + beforeEach(() => { sinon.stub(events, 'getEvents').returns([]); + getGlobal().refreshUserIds?.() }); - afterEach(function() { + afterEach(() => { events.getEvents.restore(); }); - describe('track', function() { + describe('track', () => { + const pubxId = '6c415fc0-8b0e-4cf5-be73-01526a4db625'; let initOptions = { samplingRate: '1', - pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625' + pubxId: pubxId, }; - let location = utils.getWindowLocation(); - let storage = window.top['sessionStorage']; + let originalVS; + + let location = getWindowLocation(); + + const replaceProperty = (obj, params) => { + let strObj = JSON.stringify(obj); + params.forEach(({ field, updated, replaced }) => { + strObj = strObj.replace( + new RegExp('"' + field + '":' + replaced, 'g'), + '"' + field + '":' + updated + ); + }); + return JSON.parse(strObj); + }; let prebidEvent = { - 'auctionInit': { - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'timestamp': 1603865707180, - 'auctionStatus': 'inProgress', - 'adUnits': [{ - 'code': '/19968336/header-bid-tag-1', - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } - }, - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 + auctionInit: { + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + timestamp: 1603865707180, + auctionStatus: 'inProgress', + adUnits: [ + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' - } - }], - 'sizes': [ - [ - 300, - 250 - ] - ], - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' - }], - 'adUnitCodes': [ - '/19968336/header-bid-tag-1' + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + }, + ], + sizes: [[300, 250]], + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + }, ], - 'bidderRequests': [{ - 'bidderCode': 'appnexus', - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderRequestId': '184cbc05bb90ba', - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' + adUnitCodes: ['/19968336/header-bid-tag-1'], + bidderRequests: [ + { + bidderCode: 'appnexus', + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + bidderRequestId: '184cbc05bb90ba', + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + adUnitCode: '/19968336/header-bid-tag-1', + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + sizes: [[300, 250]], + bidId: '248f9a4489835e', + bidderRequestId: '184cbc05bb90ba', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }, + ], + ortb2: { + device: { + ext: { + cdep: true, + }, + }, }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } + auctionStart: 1603865707180, + timeout: 1000, + refererInfo: { + referer: 'http://local-pnh.net:8080/stream/', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['http://local-pnh.net:8080/stream/'], + canonicalUrl: null, }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', - 'sizes': [ - [ - 300, - 250 - ] - ], - 'bidId': '248f9a4489835e', - 'bidderRequestId': '184cbc05bb90ba', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }], - 'auctionStart': 1603865707180, - 'timeout': 1000, - 'refererInfo': { - 'referer': 'http://local-pnh.net:8080/stream/', - 'reachedTop': true, - 'isAmp': false, - 'numIframes': 0, - 'stack': [ - 'http://local-pnh.net:8080/stream/' - ], - 'canonicalUrl': null + start: 1603865707182, }, - 'start': 1603865707182 - }], - 'noBids': [], - 'bidsReceived': [], - 'winningBids': [], - 'timeout': 1000, - 'config': { - 'samplingRate': '1', - 'pubxId': '6c415fc0-8b0e-4cf5-be73-01526a4db625' - } + ], + noBids: [], + bidsReceived: [], + winningBids: [], + timeout: 1000, + config: { + samplingRate: '1', + pubxId: pubxId, + }, }, - 'bidRequested': { - 'bidderCode': 'appnexus', - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderRequestId': '184cbc05bb90ba', - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' + bidRequested: { + bidderCode: 'appnexus', + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + bidderRequestId: '184cbc05bb90ba', + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + adUnitCode: '/19968336/header-bid-tag-1', + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + sizes: [[300, 250]], + bidId: '248f9a4489835e', + bidderRequestId: '184cbc05bb90ba', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } + ], + ortb2: { + device: { + ext: { + cdep: true, + }, }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', - 'sizes': [ - [ - 300, - 250 - ] - ], - 'bidId': '248f9a4489835e', - 'bidderRequestId': '184cbc05bb90ba', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }], - 'auctionStart': 1603865707180, - 'timeout': 1000, - 'refererInfo': { - 'referer': 'http://local-pnh.net:8080/stream/', - 'reachedTop': true, - 'isAmp': false, - 'numIframes': 0, - 'stack': [ - 'http://local-pnh.net:8080/stream/' - ], - 'canonicalUrl': null }, - 'start': 1603865707182 + auctionStart: 1603865707180, + timeout: 1000, + refererInfo: { + referer: 'http://local-pnh.net:8080/stream/', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['http://local-pnh.net:8080/stream/'], + canonicalUrl: null, + }, + start: 1603865707182, }, - 'bidTimeout': [], - 'bidResponse': { - 'bidderCode': 'appnexus', - 'width': 300, - 'height': 250, - 'statusMessage': 'Bid available', - 'adId': '32780c4bc382cb', - 'requestId': '248f9a4489835e', - 'mediaType': 'banner', - 'source': 'client', - 'cpm': 0.5, - 'creativeId': 96846035, - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 300, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'appnexus': { - 'buyerMemberId': 9325 + bidTimeout: [], + bidResponse: { + bidderCode: 'appnexus', + width: 300, + height: 250, + statusMessage: 'Bid available', + adId: '32780c4bc382cb', + requestId: '248f9a4489835e', + mediaType: 'banner', + source: 'client', + cpm: 0.5, + creativeId: 96846035, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: '/19968336/header-bid-tag-1', + appnexus: { + buyerMemberId: 9325, }, - 'meta': { - 'advertiserId': 2529885 + meta: { + advertiserId: 2529885, }, - 'ad': '', - 'originalCpm': 0.5, - 'originalCurrency': 'USD', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + ad: '', + originalCpm: 0.5, + originalCurrency: 'USD', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'responseTimestamp': 1616654313071, - 'requestTimestamp': 1616654312804, - 'bidder': 'appnexus', - 'timeToRespond': 267, - 'pbLg': '0.50', - 'pbMg': '0.50', - 'pbHg': '0.50', - 'pbAg': '0.50', - 'pbDg': '0.50', - 'pbCg': '0.50', - 'size': '300x250', - 'adserverTargeting': { - 'hb_bidder': 'appnexus', - 'hb_adid': '32780c4bc382cb', - 'hb_pb': '0.50', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner' + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + responseTimestamp: 1616654313071, + requestTimestamp: 1616654312804, + bidder: 'appnexus', + timeToRespond: 267, + pbLg: '0.50', + pbMg: '0.50', + pbHg: '0.50', + pbAg: '0.50', + pbDg: '0.50', + pbCg: '0.50', + size: '300x250', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '32780c4bc382cb', + hb_pb: '0.50', + hb_size: '300x250', + hb_source: 'client', + hb_format: 'banner', }, }, - 'auctionEnd': { - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'timestamp': 1616654312804, - 'auctionEnd': 1616654313090, - 'auctionStatus': 'completed', - 'adUnits': [{ - 'code': '/19968336/header-bid-tag-1', - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } + auctionEnd: { + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + timestamp: 1616654312804, + auctionEnd: 1616654313090, + auctionStatus: 'completed', + adUnits: [ + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + }, + ], + sizes: [[300, 250]], + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', }, - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 + ], + adUnitCodes: ['/19968336/header-bid-tag-1'], + bidderRequests: [ + { + bidderCode: 'appnexus', + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + bidderRequestId: '184cbc05bb90ba', + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + adUnitCode: '/19968336/header-bid-tag-1', + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + sizes: [[300, 250]], + bidId: '248f9a4489835e', + bidderRequestId: '184cbc05bb90ba', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }, + ], + ortb2: { + device: { + ext: { + cdep: true, + }, + }, + }, + auctionStart: 1603865707180, + timeout: 1000, + refererInfo: { + referer: 'http://local-pnh.net:8080/stream/', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['http://local-pnh.net:8080/stream/'], + canonicalUrl: null, }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' - } - }], - 'sizes': [ - [ - 300, - 250 - ] - ], - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' - }], - 'adUnitCodes': [ - '/19968336/header-bid-tag-1' + start: 1603865707182, + }, ], - 'bidderRequests': [{ - 'bidderCode': 'appnexus', - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderRequestId': '184cbc05bb90ba', - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 + noBids: [], + bidsReceived: [ + { + bidderCode: 'appnexus', + width: 300, + height: 250, + statusMessage: 'Bid available', + adId: '32780c4bc382cb', + requestId: '248f9a4489835e', + mediaType: 'banner', + source: 'client', + cpm: 0.5, + creativeId: 96846035, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: '/19968336/header-bid-tag-1', + appnexus: { + buyerMemberId: 9325, }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' + meta: { + advertiserId: 2529885, }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } + ad: '', + originalCpm: 0.5, + originalCurrency: 'USD', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', + }, }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', - 'sizes': [ - [ - 300, - 250 - ] - ], - 'bidId': '248f9a4489835e', - 'bidderRequestId': '184cbc05bb90ba', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }], - 'auctionStart': 1603865707180, - 'timeout': 1000, - 'refererInfo': { - 'referer': 'http://local-pnh.net:8080/stream/', - 'reachedTop': true, - 'isAmp': false, - 'numIframes': 0, - 'stack': [ - 'http://local-pnh.net:8080/stream/' - ], - 'canonicalUrl': null - }, - 'start': 1603865707182 - }], - 'noBids': [], - 'bidsReceived': [{ - 'bidderCode': 'appnexus', - 'width': 300, - 'height': 250, - 'statusMessage': 'Bid available', - 'adId': '32780c4bc382cb', - 'requestId': '248f9a4489835e', - 'mediaType': 'banner', - 'source': 'client', - 'cpm': 0.5, - 'creativeId': 96846035, - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 300, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'appnexus': { - 'buyerMemberId': 9325 - }, - 'meta': { - 'advertiserId': 2529885 - }, - 'ad': '', - 'originalCpm': 0.5, - 'originalCurrency': 'USD', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + responseTimestamp: 1616654313071, + requestTimestamp: 1616654312804, + bidder: 'appnexus', + timeToRespond: 267, + pbLg: '0.50', + pbMg: '0.50', + pbHg: '0.50', + pbAg: '0.50', + pbDg: '0.50', + pbCg: '0.50', + size: '300x250', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '32780c4bc382cb', + hb_pb: '0.50', + hb_size: '300x250', + hb_source: 'client', + hb_format: 'banner', }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'responseTimestamp': 1616654313071, - 'requestTimestamp': 1616654312804, - 'bidder': 'appnexus', - 'timeToRespond': 267, - 'pbLg': '0.50', - 'pbMg': '0.50', - 'pbHg': '0.50', - 'pbAg': '0.50', - 'pbDg': '0.50', - 'pbCg': '0.50', - 'size': '300x250', - 'adserverTargeting': { - 'hb_bidder': 'appnexus', - 'hb_adid': '32780c4bc382cb', - 'hb_pb': '0.50', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner' + status: 'rendered', + params: [ + { + placementId: 13144370, + }, + ], }, - 'status': 'rendered', - 'params': [{ - 'placementId': 13144370 - }] - }], - 'winningBids': [], - 'timeout': 1000 + ], + winningBids: [], + timeout: 1000, }, - 'bidWon': { - 'bidderCode': 'appnexus', - 'width': 300, - 'height': 250, - 'statusMessage': 'Bid available', - 'adId': '32780c4bc382cb', - 'requestId': '248f9a4489835e', - 'mediaType': 'banner', - 'source': 'client', - 'cpm': 0.5, - 'creativeId': 96846035, - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 300, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'appnexus': { - 'buyerMemberId': 9325 + bidWon: { + bidderCode: 'appnexus', + width: 300, + height: 250, + statusMessage: 'Bid available', + adId: '32780c4bc382cb', + requestId: '248f9a4489835e', + mediaType: 'banner', + source: 'client', + cpm: 0.5, + creativeId: 96846035, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: '/19968336/header-bid-tag-1', + appnexus: { + buyerMemberId: 9325, }, - 'meta': { - 'advertiserId': 2529885 + meta: { + advertiserId: 2529885, }, - 'ad': '', - 'originalCpm': 0.5, - 'originalCurrency': 'USD', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + ad: '', + originalCpm: 0.5, + originalCurrency: 'USD', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'responseTimestamp': 1616654313071, - 'requestTimestamp': 1616654312804, - 'bidder': 'appnexus', - 'timeToRespond': 267, - 'pbLg': '0.50', - 'pbMg': '0.50', - 'pbHg': '0.50', - 'pbAg': '0.50', - 'pbDg': '0.50', - 'pbCg': '0.50', - 'size': '300x250', - 'adserverTargeting': { - 'hb_bidder': 'appnexus', - 'hb_adid': '32780c4bc382cb', - 'hb_pb': '0.50', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner' + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + responseTimestamp: 1616654313071, + requestTimestamp: 1616654312804, + bidder: 'appnexus', + timeToRespond: 267, + pbLg: '0.50', + pbMg: '0.50', + pbHg: '0.50', + pbAg: '0.50', + pbDg: '0.50', + pbCg: '0.50', + size: '300x250', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '32780c4bc382cb', + hb_pb: '0.50', + hb_size: '300x250', + hb_source: 'client', + hb_format: 'banner', }, - 'status': 'rendered', - 'params': [{ - 'placementId': 13144370 - }] - }, - 'pageDetail': { - 'host': location.host, - 'path': location.pathname, - 'search': location.search + status: 'rendered', + params: [ + { + placementId: 13144370, + }, + ], }, - 'pmcDetail': { - 'bidDensity': storage.getItem('pbx:dpbid'), - 'maxBid': storage.getItem('pbx:mxbid'), - 'auctionId': storage.getItem('pbx:aucid') - } }; let expectedAfterBid = { - 'bids': [{ - 'bidderCode': 'appnexus', - 'bidId': '248f9a4489835e', - 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'sizes': '300x250', - 'renderStatus': 2, - 'requestTimestamp': 1616654312804, - 'creativeId': 96846035, - 'currency': 'USD', - 'cpm': 0.5, - 'netRevenue': true, - 'mediaType': 'banner', - 'statusMessage': 'Bid available', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + bids: [ + { + bidderCode: 'appnexus', + bidId: '248f9a4489835e', + adUnitCode: '/19968336/header-bid-tag-1', + gptSlotCode: + getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || + null, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + sizes: '300x250', + bidType: 2, + requestTimestamp: 1616654312804, + creativeId: 96846035, + currency: 'USD', + cpm: 0.5, + netRevenue: true, + mediaType: 'banner', + statusMessage: 'Bid available', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', + }, }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } + placementId: null, + timeToRespond: 267, + source: 'client', + responseTimestamp: 1616654313071, }, - 'timeToRespond': 267, - 'responseTimestamp': 1616654313071 - }], - 'pageDetail': { - 'host': location.host, - 'path': location.pathname, - 'search': location.search, - 'adUnits': [ - '/19968336/header-bid-tag-1' - ] + ], + auctionDetail: { + adUnitCodes: ['/19968336/header-bid-tag-1'], + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + refreshRank: 0, + timestamp: 1616654312804, }, - 'floorDetail': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false + pageDetail: { + host: location.host, + path: location.pathname, + search: location.search, }, - 'deviceDetail': { - 'platform': navigator.platform, - 'deviceType': getDeviceType(), - 'deviceOS': getOS(), - 'browser': getBrowser() + floorDetail: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, }, - 'pmcDetail': { - 'bidDensity': storage.getItem('pbx:dpbid'), - 'maxBid': storage.getItem('pbx:mxbid'), - 'auctionId': storage.getItem('pbx:aucid') + deviceDetail: { + platform: navigator.platform, + deviceType: getDeviceType(), + deviceOS: getOS(), + browser: getBrowser(), + cdep: true, + }, + userDetail: { + userIdTypes: Object.keys(getGlobal().getUserIds?.() || {}), + }, + consentDetail: { + consentTypes: Object.keys(getGlobal().getConsentMetadata?.() || {}), + }, + pmacDetail: {}, + extraData: {}, + initOptions: { + ...initOptions, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', }, - 'initOptions': initOptions }; let expectedAfterBidWon = { - 'winningBid': { - 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderCode': 'appnexus', - 'bidId': '248f9a4489835e', - 'cpm': 0.5, - 'creativeId': 96846035, - 'currency': 'USD', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + winningBid: { + adUnitCode: '/19968336/header-bid-tag-1', + gptSlotCode: + getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || + null, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + bidderCode: 'appnexus', + bidId: '248f9a4489835e', + cpm: 0.5, + creativeId: 96846035, + currency: 'USD', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } }, - 'floorProvider': 'PubXFloorProvider', - 'floorFetchStatus': 'success', - 'floorLocation': 'fetch', - 'floorModelVersion': 'test model 1.0', - 'floorSkipRate': 0, - 'isFloorSkipped': false, - 'isWinningBid': true, - 'mediaType': 'banner', - 'netRevenue': true, - 'placementId': 13144370, - 'renderedSize': '300x250', - 'renderStatus': 4, - 'responseTimestamp': 1616654313071, - 'requestTimestamp': 1616654312804, - 'status': 'rendered', - 'statusMessage': 'Bid available', - 'timeToRespond': 267 + adServerData: {}, + floorProvider: 'PubXFloorProvider', + floorFetchStatus: 'success', + floorLocation: 'fetch', + floorModelVersion: 'test model 1.0', + floorSkipRate: 0, + isFloorSkipped: false, + isWinningBid: true, + mediaType: 'banner', + netRevenue: true, + placementId: 13144370, + renderedSize: '300x250', + sizes: '300x250', + bidType: 4, + responseTimestamp: 1616654313071, + requestTimestamp: 1616654312804, + status: 'rendered', + statusMessage: 'Bid available', + timeToRespond: 267, + source: 'client', + }, + auctionDetail: { + adUnitCodes: ['/19968336/header-bid-tag-1'], + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + refreshRank: 0, + timestamp: 1616654312804, + }, + pageDetail: { + host: location.host, + path: location.pathname, + search: location.search, }, - 'pageDetail': { - 'host': location.host, - 'path': location.pathname, - 'search': location.search + floorDetail: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, }, - 'deviceDetail': { - 'platform': navigator.platform, - 'deviceType': getDeviceType(), - 'deviceOS': getOS(), - 'browser': getBrowser() + deviceDetail: { + platform: navigator.platform, + deviceType: getDeviceType(), + deviceOS: getOS(), + browser: getBrowser(), + cdep: true, }, - 'initOptions': initOptions - } + userDetail: { + userIdTypes: Object.keys(getGlobal().getUserIds?.() || {}), + }, + consentDetail: { + consentTypes: Object.keys(getGlobal().getConsentMetadata?.() || {}), + }, + pmacDetail: {}, + extraData: {}, + initOptions: { + ...initOptions, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + }, + }; adapterManager.registerAnalyticsAdapter({ code: 'pubxai', - adapter: pubxaiAnalyticsAdapter + adapter: pubxaiAnalyticsAdapter, }); - beforeEach(function() { + beforeEach(() => { adapterManager.enableAnalytics({ provider: 'pubxai', - options: initOptions + options: initOptions, + }); + sinon.stub(navigator, 'sendBeacon').returns(true); + originalVS = document.visibilityState; + document['__defineGetter__']('visibilityState', function () { + return 'hidden'; }); }); - afterEach(function() { + afterEach(() => { pubxaiAnalyticsAdapter.disableAnalytics(); + navigator.sendBeacon.restore(); + delete auctionCache['bc3806e4-873e-453c-8ae5-204f35e923b4']; + delete auctionCache['auction2']; + document['__defineGetter__']('visibilityState', function () { + return originalVS; + }); }); - it('builds and sends auction data', function() { + it('builds and sends auction data', async () => { // Step 1: Send auction init event events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); @@ -685,20 +745,434 @@ describe('pubxai analytics adapter', function() { // Step 5: Send auction end event events.emit(EVENTS.AUCTION_END, prebidEvent['auctionEnd']); - expect(server.requests.length).to.equal(1); + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); - let realAfterBid = JSON.parse(server.requests[0].requestBody); - - expect(realAfterBid).to.deep.equal(expectedAfterBid); + expect(navigator.sendBeacon.callCount).to.equal(0); // Step 6: Send auction bid won event events.emit(EVENTS.BID_WON, prebidEvent['bidWon']); - expect(server.requests.length).to.equal(2); + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + + expect(navigator.sendBeacon.callCount).to.equal(2); + + for (const [index, arg] of navigator.sendBeacon.args.entries()) { + const [expectedUrl, expectedData] = arg; + const parsedUrl = new URL(expectedUrl); + expect(parsedUrl.pathname).to.equal( + ['/analytics/bidwon', '/analytics/auction'][index] + ); + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v2.1.0', + prebidVersion: '$prebid.version$', + pubxId: pubxId, + }); + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ + [expectedAfterBidWon, expectedAfterBid][index], + ]); + } + }); + + it('auction data with only rejected bids', async () => { + // Step 1: Send auction init event + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event + events.emit(EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send bid rejected (afaict the only expected reason would be a bid being too low) + events.emit(EVENTS.BID_REJECTED, prebidEvent['bidResponse']); + + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + + // Step 4: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(0); + + // Step 5: Send auction end event + events.emit(EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + + // Step 6: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(1); + + // Step 7: check the pathname of the calls is correct (sent only to the auction endpoint) + const [expectedUrl, expectedData] = navigator.sendBeacon.args[0]; + const parsedUrl = new URL(expectedUrl); + expect(parsedUrl.pathname).to.equal('/analytics/auction'); + + // Step 8: check that the meta information in the call is correct + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v2.1.0', + prebidVersion: '$prebid.version$', + pubxId: pubxId, + }); + + // Step 9: check that the data sent in the request is correct + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ + { + ...expectedAfterBid, + bids: [{ + ...expectedAfterBid.bids[0], + bidType: 1 + }] + } + ]); + }); + + it('auction data with only timed out bids', async () => { + // Step 1: Send auction init event + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event + events.emit(EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send bid rejected (afaict the only expected reason would be a bid being too low) + events.emit(EVENTS.BID_TIMEOUT, [prebidEvent['bidResponse']]); + + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); - let winEventData = JSON.parse(server.requests[1].requestBody); + // Step 4: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(0); - expect(winEventData).to.deep.equal(expectedAfterBidWon); + // Step 5: Send auction end event + events.emit(EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + + // Step 6: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(1); + + // Step 7: check the pathname of the calls is correct (sent only to the auction endpoint) + const [expectedUrl, expectedData] = navigator.sendBeacon.args[0]; + const parsedUrl = new URL(expectedUrl); + expect(parsedUrl.pathname).to.equal('/analytics/auction'); + + // Step 8: check that the meta information in the call is correct + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v2.1.0', + prebidVersion: '$prebid.version$', + pubxId: pubxId, + }); + + // Step 9: check that the data sent in the request is correct + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ + { + ...expectedAfterBid, + bids: [{ + ...expectedAfterBid.bids[0], + bidType: 3 + }] + } + ]); + }); + + it('auction with no bids', async () => { + // Step 1: Send auction init event + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event + events.emit(EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + + // Step 3: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(0); + + // Step 4: Send auction end event + events.emit(EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + + // Step 5: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(1); + + // Step 6: check the pathname of the calls is correct (sent only to the auction endpoint) + const [expectedUrl, expectedData] = navigator.sendBeacon.args[0]; + const parsedUrl = new URL(expectedUrl); + expect(parsedUrl.pathname).to.equal('/analytics/auction'); + + // Step 7: check that the meta information in the call is correct + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v2.1.0', + prebidVersion: '$prebid.version$', + pubxId: pubxId, + }); + + // Step 8: check that the data sent in the request is correct + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ + { + ...expectedAfterBid, + bids: [], + }, + ]); + }); + + it('2 concurrent auctions', async () => { + // Step 1: Send auction init event for auction 1 + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event for auction 1 + events.emit(EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send auction init event for auction 2 + events.emit( + EVENTS.AUCTION_INIT, + replaceProperty(prebidEvent['auctionInit'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 4: Send bid requested event for auction 2 + events.emit( + EVENTS.BID_REQUESTED, + replaceProperty(prebidEvent['bidRequested'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 5: Send bid response event for auction 1 + events.emit(EVENTS.BID_RESPONSE, prebidEvent['bidResponse']); + + // Step 6: Send bid time out event for auction 1 + events.emit(EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + + // Step 7: Send bid response event for auction 2 + events.emit( + EVENTS.BID_RESPONSE, + replaceProperty(prebidEvent['bidResponse'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 8: Send auction end event for auction 1 + events.emit(EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + + // Step 9: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(0); + + // Step 10: Send auction bid won event for auction 1 + events.emit(EVENTS.BID_WON, prebidEvent['bidWon']); + + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + + // Step 11: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(2); + + // Step 12: Send auction end event for auction 2 + events.emit( + EVENTS.AUCTION_END, + replaceProperty(prebidEvent['auctionEnd'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + + // Step 13: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(2); + + // Step 14: Send auction bid won event for auction 2 + events.emit( + EVENTS.BID_WON, + replaceProperty(prebidEvent['bidWon'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + + // Step 15: check the calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(4); + for (const [index, arg] of navigator.sendBeacon.args.entries()) { + const [expectedUrl, expectedData] = arg; + const parsedUrl = new URL(expectedUrl); + const auctionIdMapFn = index < 2 ? (i, _) => i : replaceProperty; + expect(parsedUrl.pathname).to.equal( + ['/analytics/bidwon', '/analytics/auction'][index % 2] + ); + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v2.1.0', + prebidVersion: '$prebid.version$', + pubxId: pubxId, + }); + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ + auctionIdMapFn([expectedAfterBidWon, expectedAfterBid][index % 2], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + { + field: 'refreshRank', + updated: '1', + replaced: '0', + }, + ]), + ]); + } + }); + + it('2 concurrent auctions with batch sending', async () => { + // Step 1: Send auction init event for auction 1 + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event for auction 1 + events.emit(EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send auction init event for auction 2 + events.emit( + EVENTS.AUCTION_INIT, + replaceProperty(prebidEvent['auctionInit'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 4: Send bid requested event for auction 2 + events.emit( + EVENTS.BID_REQUESTED, + replaceProperty(prebidEvent['bidRequested'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 5: Send bid response event for auction 1 + events.emit(EVENTS.BID_RESPONSE, prebidEvent['bidResponse']); + + // Step 6: Send bid time out event for auction 1 + events.emit(EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + + // Step 7: Send bid response event for auction 2 + events.emit( + EVENTS.BID_RESPONSE, + replaceProperty(prebidEvent['bidResponse'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 8: Send auction end event for auction 1 + events.emit(EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + // Step 9: Send auction bid won event for auction 1 + events.emit(EVENTS.BID_WON, prebidEvent['bidWon']); + + // Step 10: Send auction end event for auction 2 + events.emit( + EVENTS.AUCTION_END, + replaceProperty(prebidEvent['auctionEnd'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 11: Send auction bid won event for auction 2 + events.emit( + EVENTS.BID_WON, + replaceProperty(prebidEvent['bidWon'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 12: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(0); + + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + + // Step 13: check the calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(2); + for (const [index, arg] of navigator.sendBeacon.args.entries()) { + const [expectedUrl, expectedData] = arg; + const parsedUrl = new URL(expectedUrl); + expect(parsedUrl.pathname).to.equal( + ['/analytics/bidwon', '/analytics/auction'][index] + ); + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v2.1.0', + prebidVersion: '$prebid.version$', + pubxId: pubxId, + }); + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ + [expectedAfterBidWon, expectedAfterBid][index], + replaceProperty([expectedAfterBidWon, expectedAfterBid][index], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + { + field: 'refreshRank', + updated: '1', + replaced: '0', + }, + ]), + ]); + } }); }); }); diff --git a/test/spec/modules/pubxaiRtdProvider_spec.js b/test/spec/modules/pubxaiRtdProvider_spec.js index b645b830246..6ffa4952992 100644 --- a/test/spec/modules/pubxaiRtdProvider_spec.js +++ b/test/spec/modules/pubxaiRtdProvider_spec.js @@ -1,6 +1,7 @@ import * as priceFloors from '../../../modules/priceFloors'; import { FLOORS_END_POINT, + storage, FLOORS_EVENT_HANDLE, FloorsApiStatus, beforeInit, @@ -45,6 +46,7 @@ const resetGlobals = () => { window.__pubxFloorsConfig__ = undefined; window.__pubxFloorsApiStatus__ = undefined; window.__pubxFloorRulesPromise__ = null; + localStorage.removeItem('pubx:dynamicFloors'); }; const fakeServer = ( @@ -119,7 +121,7 @@ describe('pubxaiRtdProvider', () => { stub.restore(); }); it('createFloorsDataForAuction called once before and once after __pubxFloorRulesPromise__. Also getBidRequestData executed only once', async () => { - pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => {}); + pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => { }); assert(priceFloors.createFloorsDataForAuction.calledOnce); await window.__pubxFloorRulesPromise__; assert(priceFloors.createFloorsDataForAuction.calledTwice); @@ -129,7 +131,7 @@ describe('pubxaiRtdProvider', () => { reqBidsConfigObj.auctionId ) ); - pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => {}); + pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => { }); await window.__pubxFloorRulesPromise__; assert(priceFloors.createFloorsDataForAuction.calledTwice); }); @@ -137,6 +139,16 @@ describe('pubxaiRtdProvider', () => { describe('fetchFloorRules', () => { const providerConfig = getConfig(); const floorsResponse = getFloorsResponse(); + let storageStub; + + beforeEach(() => { + storageStub = sinon.stub(storage, 'getDataFromSessionStorage'); + }); + + afterEach(() => { + storageStub.restore(); + }); + it('success with floors response', (done) => { const promise = fetchFloorRules(providerConfig); fakeServer(floorsResponse); @@ -145,6 +157,7 @@ describe('pubxaiRtdProvider', () => { done(); }); }); + it('success with no floors response', (done) => { const promise = fetchFloorRules(providerConfig); fakeServer(undefined); @@ -153,6 +166,7 @@ describe('pubxaiRtdProvider', () => { done(); }); }); + it('API call error', (done) => { const promise = fetchFloorRules(providerConfig); fakeServer(undefined, undefined, 404); @@ -167,6 +181,7 @@ describe('pubxaiRtdProvider', () => { done(); }); }); + it('Wrong API response', (done) => { const promise = fetchFloorRules(providerConfig); fakeServer('floorsResponse'); @@ -181,6 +196,25 @@ describe('pubxaiRtdProvider', () => { done(); }); }); + + it('success with local data response', (done) => { + const localFloorsResponse = getFloorsResponse(); + storageStub.withArgs('pubx:dynamicFloors').returns(JSON.stringify(localFloorsResponse)); + const promise = fetchFloorRules({ params: {} }); + promise.then((res) => { + expect(res).to.deep.equal(localFloorsResponse); + done(); + }); + }); + + it('no local data response', (done) => { + storageStub.withArgs('pubx:dynamicFloors').returns(null); + const promise = fetchFloorRules({ params: {} }); + promise.then((res) => { + expect(res).to.deep.equal(null); + done(); + }); + }); }); describe('setPriceFloors', () => { const providerConfig = getConfig(); @@ -383,9 +417,7 @@ describe('pubxaiRtdProvider', () => { expect(FLOORS_END_POINT).to.equal('https://floor.pbxai.com/'); }); it('standard case', () => { - expect(getUrl(provider)).to.equal( - `https://floor.pbxai.com/?pubxId=12345&page=${window.location.href}` - ); + expect(getUrl(provider)).to.equal(null); }); it('custom url provided', () => { provider.params.endpoint = 'https://custom.floor.com/'; diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index 8db7e909771..b6192c1acaf 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -1,8 +1,12 @@ /* eslint dot-notation:0, quote-props:0 */ import {expect} from 'chai'; import {spec} from 'modules/pulsepointBidAdapter.js'; -import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {addFPDToBidderRequest} from '../../helpers/fpd.js'; import {deepClone} from '../../../src/utils'; +import 'modules/consentManagementTcf'; +import 'modules/consentManagementUsp'; +import 'modules/userId/index'; +import 'modules/schain'; describe('PulsePoint Adapter Tests', function () { const slotConfigs = [{ @@ -155,8 +159,8 @@ describe('PulsePoint Adapter Tests', function () { } }; - it('Verify build request', function () { - const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + it('Verify build request', async function () { + const request = spec.buildRequests(slotConfigs, await addFPDToBidderRequest(bidderRequest)); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -179,8 +183,8 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.imp[1].banner.format).to.deep.eq([{'w': 728, 'h': 90}]); }); - it('Verify parse response', function () { - const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + it('Verify parse response', async function () { + const request = spec.buildRequests(slotConfigs, await addFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; const ortbResponse = { seatbid: [{ @@ -221,8 +225,8 @@ describe('PulsePoint Adapter Tests', function () { }); if (FEATURES.NATIVE) { - it('Verify Native request', function () { - const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + it('Verify Native request', async function () { + const request = spec.buildRequests(nativeSlotConfig, await addFPDToBidderRequest(bidderRequest)); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -257,8 +261,8 @@ describe('PulsePoint Adapter Tests', function () { expect(nativeRequest.assets[2].data.type).to.equal(1); }); - it('Verify Native response', function () { - const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + it('Verify Native response', async function () { + const request = spec.buildRequests(nativeSlotConfig, await addFPDToBidderRequest(bidderRequest)); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -358,33 +362,33 @@ describe('PulsePoint Adapter Tests', function () { expect(options[0].url).to.equal('https://bh.contextweb.com/visitormatch/prebid'); }); - it('Verify GDPR', function () { + it('Verify GDPR', async function () { const bidderRequestGdpr = { gdprConsent: { gdprApplies: true, - consentString: 'serialized_gpdr_data' + consentString: 'serialized_gdpr_data' } }; - const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(Object.assign({}, bidderRequest, bidderRequestGdpr))); + const request = spec.buildRequests(slotConfigs, await addFPDToBidderRequest(Object.assign({}, bidderRequest, bidderRequestGdpr))); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; // user object expect(ortbRequest.user).to.not.equal(null); expect(ortbRequest.user.ext).to.not.equal(null); - expect(ortbRequest.user.ext.consent).to.equal('serialized_gpdr_data'); + expect(ortbRequest.user.ext.consent).to.equal('serialized_gdpr_data'); // regs object expect(ortbRequest.regs).to.not.equal(null); expect(ortbRequest.regs.ext).to.not.equal(null); expect(ortbRequest.regs.ext.gdpr).to.equal(1); }); - it('Verify CCPA', function () { + it('Verify CCPA', async function () { const bidderRequestUSPrivacy = { uspConsent: '1YYY' }; const request = spec.buildRequests(slotConfigs, - syncAddFPDToBidderRequest(Object.assign({}, bidderRequest, bidderRequestUSPrivacy))); + await addFPDToBidderRequest(Object.assign({}, bidderRequest, bidderRequestUSPrivacy))); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -395,8 +399,8 @@ describe('PulsePoint Adapter Tests', function () { }); if (FEATURES.VIDEO) { - it('Verify Video request', function () { - const request = spec.buildRequests(videoSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + it('Verify Video request', async function () { + const request = spec.buildRequests(videoSlotConfig, await addFPDToBidderRequest(bidderRequest)); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -440,8 +444,8 @@ describe('PulsePoint Adapter Tests', function () { }); } - it('Verify extra parameters', function () { - let request = spec.buildRequests(additionalParamsConfig, syncAddFPDToBidderRequest(bidderRequest)); + it('Verify extra parameters', async function () { + let request = spec.buildRequests(additionalParamsConfig, await addFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.imp).to.have.lengthOf(1); @@ -463,8 +467,8 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.imp[0].ext).to.be.undefined; }); - it('Verify schain parameters', function () { - const request = spec.buildRequests(schainParamsSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + it('Verify schain parameters', async function () { + const request = spec.buildRequests(schainParamsSlotConfig, await addFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.source).to.not.equal(null); @@ -482,24 +486,35 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.source.ext.schain.nodes[0].domain).to.equal('publisher.com'); }); - it('Verify common id parameters', function () { + it('Verify common id parameters', async function () { const bidRequests = deepClone(slotConfigs); - bidRequests[0].userIdAsEids = [{ - source: 'pubcid.org', - uids: [{ - id: 'userid_pubcid' - }] - }, { - source: 'adserver.org', - uids: [{ - id: 'userid_ttd', - ext: { - rtiPartner: 'TDID' + const eids = [ + { + source: 'pubcid.org', + uids: [{ + id: 'userid_pubcid' + }] + }, { + source: 'adserver.org', + uids: [{ + id: 'userid_ttd', + ext: { + rtiPartner: 'TDID' + } + }] + } + ]; + const br = { + ...bidderRequest, + ortb2: { + user: { + ext: { + eids + } } - }] + } } - ]; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(br)); expect(request).to.be.not.null; expect(request.data).to.be.not.null; const ortbRequest = request.data; @@ -507,10 +522,10 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.user).to.not.be.undefined; expect(ortbRequest.user.ext).to.not.be.undefined; expect(ortbRequest.user.ext.eids).to.not.be.undefined; - expect(ortbRequest.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); + expect(ortbRequest.user.ext.eids).to.deep.equal(eids); }); - it('Verify user level first party data', function () { + it('Verify user level first party data', async function () { const bidderRequest = { refererInfo: { page: 'https://publisher.com/home', @@ -518,7 +533,7 @@ describe('PulsePoint Adapter Tests', function () { }, gdprConsent: { gdprApplies: true, - consentString: 'serialized_gpdr_data' + consentString: 'serialized_gdpr_data' }, ortb2: { user: { @@ -533,7 +548,7 @@ describe('PulsePoint Adapter Tests', function () { } } }; - let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let request = spec.buildRequests(slotConfigs, await addFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.user).to.not.equal(null); @@ -545,12 +560,12 @@ describe('PulsePoint Adapter Tests', function () { registered: true, interests: ['cars'] }, - consent: 'serialized_gpdr_data' + consent: 'serialized_gdpr_data' } }); }); - it('Verify site level first party data', function () { + it('Verify site level first party data', async function () { const bidderRequest = { ortb2: { site: { @@ -571,7 +586,7 @@ describe('PulsePoint Adapter Tests', function () { } } }; - let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let request = spec.buildRequests(slotConfigs, await addFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.site).to.not.equal(null); @@ -645,7 +660,7 @@ describe('PulsePoint Adapter Tests', function () { expect(mkRequest(Object.assign({}, { timeout: 6000 }, bidderRequest)).tmax).to.equal(6000) }); - it('Verify deals', function () { + it('Verify deals', async function () { const bidRequests = deepClone(slotConfigs); const deals = [{ id: 'DEAL_ONE', @@ -655,7 +670,7 @@ describe('PulsePoint Adapter Tests', function () { bidfloor: 2.2 }]; bidRequests[0].params.deals = deals; - const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const request = spec.buildRequests(bidRequests, await addFPDToBidderRequest(bidderRequest)); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; diff --git a/test/spec/modules/pxyzBidAdapter_spec.js b/test/spec/modules/pxyzBidAdapter_spec.js index 36e7a1e9ad6..87dc5ff0783 100644 --- a/test/spec/modules/pxyzBidAdapter_spec.js +++ b/test/spec/modules/pxyzBidAdapter_spec.js @@ -39,12 +39,12 @@ describe('pxyzBidAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placementId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/qortexRtdProvider_spec.js b/test/spec/modules/qortexRtdProvider_spec.js index c9f92e8af67..b6625e297d5 100644 --- a/test/spec/modules/qortexRtdProvider_spec.js +++ b/test/spec/modules/qortexRtdProvider_spec.js @@ -1,84 +1,129 @@ -import * as utils from 'src/utils'; -import * as ajax from 'src/ajax.js'; +import * as utils from 'src/utils.js'; import * as events from 'src/events.js'; import { EVENTS } from '../../../src/constants.js'; import {loadExternalScript} from 'src/adloader.js'; import { qortexSubmodule as module, - getContext, addContextToRequests, setContextData, + loadScriptTag, initializeModuleData, - loadScriptTag + setGroupConfigData, + requestContextData, + windowPostMessageReceived } from '../../../modules/qortexRtdProvider'; import {server} from '../../mocks/xhr.js'; import { cloneDeep } from 'lodash'; describe('qortexRtdProvider', () => { let logWarnSpy; + let logMessageSpy; let ortb2Stub; const defaultApiHost = 'https://demand.qortex.ai'; const defaultGroupId = 'test'; - const validBidderArray = ['qortex', 'test']; const validTagConfig = { videoContainer: 'my-video-container' } - const validModuleConfig = { - params: { - groupId: defaultGroupId, - apiUrl: defaultApiHost, - bidders: validBidderArray - } - }, - emptyModuleConfig = { - params: {} + params: { + groupId: defaultGroupId, + apiUrl: defaultApiHost, + bidders: validBidderArray, + enableBidEnrichment: true } - + } + const bidEnrichmentDisabledModuleConfig = { + params: { + groupId: defaultGroupId, + apiUrl: defaultApiHost, + bidders: validBidderArray + } + } + const invalidApiUrlModuleConfig = { + params: { + groupId: defaultGroupId, + apiUrl: 'test123', + bidders: validBidderArray + } + } + const emptyModuleConfig = { + params: {} + } const validImpressionEvent = { - detail: { - uid: 'uid123', - type: 'qx-impression' - } - }, - validImpressionEvent2 = { - detail: { - uid: 'uid1234', - type: 'qx-impression' - } - }, - missingIdImpressionEvent = { - detail: { - type: 'qx-impression' - } - }, - invalidTypeQortexEvent = { - detail: { - type: 'invalid-type' - } + detail: { + uid: 'uid123', + type: 'qx-impression' } - + } + const validImpressionEvent2 = { + detail: { + uid: 'uid1234', + type: 'qx-impression' + } + } + const missingIdImpressionEvent = { + detail: { + type: 'qx-impression' + } + } + const QortexPostMessageInitialized = { + target: 'QORTEX-PREBIDJS-RTD-MODULE', + message: 'CX-BID-ENRICH-INITIALIZED', + params: {groupConfig: {data: true}} + } + const QortexPostMessageContext = { + target: 'QORTEX-PREBIDJS-RTD-MODULE', + message: 'DISPATCH-CONTEXT', + params: {context: {data: true}} + } + const invalidTypeQortexEvent = { + detail: { + type: 'invalid-type' + } + } const responseHeaders = { 'content-type': 'application/json', 'access-control-allow-origin': '*' }; - - const responseObj = { - content: { - id: '123456', - episode: 15, - title: 'test episode', - series: 'test show', - season: '1', - url: 'https://example.com/file.mp4' + const contextResponseObj = { + site: { + content: { + id: '123456', + episode: 15, + title: 'test episode', + series: 'test show', + season: '1', + url: 'https://example.com/file.mp4' + } } - }; - - const apiResponse = JSON.stringify(responseObj); - + } + const contextResponse = JSON.stringify(contextResponseObj); + const validGroupConfigResponseObj = { + groupId: defaultGroupId, + active: true, + prebidBidEnrichment: true, + prebidBidEnrichmentPercentage: 100, + prebidReportingPercentage: 100 + } + const validGroupConfigResponse = JSON.stringify(validGroupConfigResponseObj); + const inactiveGroupConfigResponseObj = { + groupId: defaultGroupId, + active: false, + PrebidBidEnrichment: true, + PrebidReportingPercentage: 100 + } + const inactiveGroupConfigResponse = JSON.stringify(inactiveGroupConfigResponseObj); + const noEnrichmentGroupConfigResponseObj = { + groupId: defaultGroupId, + active: true, + prebidBidEnrichment: true, + prebidBidEnrichmentPercentage: 0, + prebidReportingPercentage: 100 + } const reqBidsConfig = { + auctionId: '1234', adUnits: [{ bids: [ { bidder: 'qortex' } @@ -93,17 +138,20 @@ describe('qortexRtdProvider', () => { beforeEach(() => { ortb2Stub = sinon.stub(reqBidsConfig, 'ortb2Fragments').value({bidder: {}, global: {}}) logWarnSpy = sinon.spy(utils, 'logWarn'); + logMessageSpy = sinon.spy(utils, 'logMessage'); }) afterEach(() => { logWarnSpy.restore(); + logMessageSpy.restore(); ortb2Stub.restore(); setContextData(null); }) describe('init', () => { - it('returns true for valid config object', () => { - expect(module.init(validModuleConfig)).to.be.true; + it('will not initialize bid enrichment if it is disabled', () => { + module.init(bidEnrichmentDisabledModuleConfig); + expect(logWarnSpy.calledWith('Bid Enrichment Function has been disabled in module configuration')).to.be.true; }) it('returns false and logs error for missing groupId', () => { @@ -168,21 +216,21 @@ describe('qortexRtdProvider', () => { dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); expect(billableEvents.length).to.be.equal(1); - expect(logWarnSpy.calledWith('received invalid billable event due to duplicate uid: qx-impression')).to.be.ok; + expect(logWarnSpy.calledWith('Received invalid billable event due to duplicate uid: qx-impression')).to.be.ok; }) it('will not allow events with missing uid', () => { loadScriptTag(config); dispatchEvent(new CustomEvent('qortex-rtd', missingIdImpressionEvent)); expect(billableEvents.length).to.be.equal(0); - expect(logWarnSpy.calledWith('received invalid billable event due to missing uid: qx-impression')).to.be.ok; + expect(logWarnSpy.calledWith('Received invalid billable event due to missing uid: qx-impression')).to.be.ok; }) it('will not allow events with unavailable type', () => { loadScriptTag(config); dispatchEvent(new CustomEvent('qortex-rtd', invalidTypeQortexEvent)); expect(billableEvents.length).to.be.equal(0); - expect(logWarnSpy.calledWith('received invalid billable event: invalid-type')).to.be.ok; + expect(logWarnSpy.calledWith('Received invalid billable event: invalid-type')).to.be.ok; }) }) @@ -191,7 +239,9 @@ describe('qortexRtdProvider', () => { beforeEach(() => { initializeModuleData(validModuleConfig); + setGroupConfigData(validGroupConfigResponseObj); callbackSpy = sinon.spy(); + server.reset(); }) afterEach(() => { @@ -203,80 +253,86 @@ describe('qortexRtdProvider', () => { const reqBidsConfigNoBids = { adUnits: [] }; module.getBidRequestData(reqBidsConfigNoBids, callbackSpy); expect(callbackSpy.calledOnce).to.be.true; - expect(logWarnSpy.calledWith('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfigNoBids))).to.be.ok; + expect(logWarnSpy.calledOnce).to.be.true; }) - it('will call callback if getContext does not throw', () => { + it('will not request context if prebid disable toggle is true', (done) => { + initializeModuleData(bidEnrichmentDisabledModuleConfig); const cb = function () { - expect(logWarnSpy.calledOnce).to.be.false; + expect(server.requests.length).to.be.eql(0); + expect(logWarnSpy.called).to.be.true; + expect(logWarnSpy.calledWith('Bid enrichment disabled at prebid config')).to.be.true; done(); } module.getBidRequestData(reqBidsConfig, cb); - server.requests[0].respond(200, responseHeaders, apiResponse); }) - it('will catch and log error and fire callback', (done) => { - const a = sinon.stub(ajax, 'ajax').throws(new Error('test')); + it('will request to add context when ad units present and enabled', (done) => { const cb = function () { - expect(logWarnSpy.calledWith('test')).to.be.eql(true); + setContextData(null); + expect(server.requests.length).to.be.eql(0); + expect(logWarnSpy.called).to.be.true; + expect(logWarnSpy.calledWith('No context data received at this time')).to.be.true; done(); } module.getBidRequestData(reqBidsConfig, cb); - a.restore(); }) }) - describe('getContext', () => { + describe('onAuctionEndEvent', () => { beforeEach(() => { initializeModuleData(validModuleConfig); + setGroupConfigData(validGroupConfigResponseObj); }) afterEach(() => { initializeModuleData(emptyModuleConfig); + setGroupConfigData(null); }) - it('returns a promise', (done) => { - const result = getContext(); - expect(result).to.be.a('promise'); - done(); + it('Properly sends analytics event with valid config', () => { + const testData = {auctionId: reqBidsConfig.auctionId, data: 'data'}; + module.onAuctionEndEvent(testData); }) + }) - it('uses request url generated from initialize function in config and resolves to content object data', (done) => { - let requestUrl = `${validModuleConfig.params.apiUrl}/api/v1/analyze/${validModuleConfig.params.groupId}/prebid`; - const ctx = getContext() - expect(server.requests.length).to.be.eql(1); - expect(server.requests[0].url).to.be.eql(requestUrl); - server.requests[0].respond(200, responseHeaders, apiResponse); - ctx.then(response => { - expect(response).to.be.eql(responseObj.content); - done(); - }); + describe('requestContextData', () => { + before(() => { + setContextData({data: true}); }) - it('will return existing context data instead of ajax call if the source was not updated', (done) => { - setContextData(responseObj.content); - const ctx = getContext(); - expect(server.requests.length).to.be.eql(0); - ctx.then(response => { - expect(response).to.be.eql(responseObj.content); - done(); - }); + after(() => { + setContextData(null); }) - it('returns null for non erroring api responses other than 200', (done) => { - const nullContentResponse = { content: null } - const ctx = getContext() - server.requests[0].respond(200, responseHeaders, JSON.stringify(nullContentResponse)) - ctx.then(response => { - expect(response).to.be.null; - expect(server.requests.length).to.be.eql(1); - expect(logWarnSpy.called).to.be.false; - done(); - }); + it('Will log properly when context data already available', () => { + requestContextData(); + expect(logMessageSpy.calledWith('Context data already retrieved.')).to.be.true; }) }) - describe(' addContextToRequests', () => { + describe('addContextToRequests', () => { + let testReqBids; + beforeEach(() => { + setGroupConfigData(validGroupConfigResponseObj); + testReqBids = { + auctionId: '1234', + adUnits: [{ + bids: [ + { bidder: 'qortex' } + ] + }], + ortb2Fragments: { + bidder: {}, + global: {} + } + } + }) + + afterEach(() => { + setGroupConfigData(null); + }) + it('logs error if no data was retrieved from get context call', () => { initializeModuleData(validModuleConfig); addContextToRequests(reqBidsConfig); @@ -286,34 +342,34 @@ describe('qortexRtdProvider', () => { expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); }) - it('adds site.content only to global ortb2 when bidders array is omitted', () => { + it('adds context only to global ortb2 when bidders array is omitted', () => { const omittedBidderArrayConfig = cloneDeep(validModuleConfig); delete omittedBidderArrayConfig.params.bidders; initializeModuleData(omittedBidderArrayConfig); - setContextData(responseObj.content); + setContextData(contextResponseObj); addContextToRequests(reqBidsConfig); expect(reqBidsConfig.ortb2Fragments.global).to.have.property('site'); expect(reqBidsConfig.ortb2Fragments.global.site).to.have.property('content'); - expect(reqBidsConfig.ortb2Fragments.global.site.content).to.be.eql(responseObj.content); + expect(reqBidsConfig.ortb2Fragments.global.site.content).to.be.eql(contextResponseObj.site.content); expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); }) - it('adds site.content only to bidder ortb2 when bidders array is included', () => { + it('adds only to bidder ortb2 when bidders array is included', () => { initializeModuleData(validModuleConfig); - setContextData(responseObj.content); + setContextData(contextResponseObj); addContextToRequests(reqBidsConfig); const qortexOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['qortex'] expect(qortexOrtb2Fragment).to.not.be.null; expect(qortexOrtb2Fragment).to.have.property('site'); expect(qortexOrtb2Fragment.site).to.have.property('content'); - expect(qortexOrtb2Fragment.site.content).to.be.eql(responseObj.content); + expect(qortexOrtb2Fragment.site.content).to.be.eql(contextResponseObj.site.content); const testOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['test'] expect(testOrtb2Fragment).to.not.be.null; expect(testOrtb2Fragment).to.have.property('site'); expect(testOrtb2Fragment.site).to.have.property('content'); - expect(testOrtb2Fragment.site.content).to.be.eql(responseObj.content); + expect(testOrtb2Fragment.site.content).to.be.eql(contextResponseObj.site.content); expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); }) @@ -322,7 +378,7 @@ describe('qortexRtdProvider', () => { const invalidBidderArrayConfig = cloneDeep(validModuleConfig); invalidBidderArrayConfig.params.bidders = []; initializeModuleData(invalidBidderArrayConfig); - setContextData(responseObj.content) + setContextData(contextResponseObj) addContextToRequests(reqBidsConfig); expect(logWarnSpy.calledWith('Config contains an empty bidders array, unable to determine which bids to enrich')).to.be.ok; @@ -330,4 +386,25 @@ describe('qortexRtdProvider', () => { expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); }) }) + + describe('initializeBidEnrichment', () => { + beforeEach(() => { + initializeModuleData(validModuleConfig); + setGroupConfigData(validGroupConfigResponseObj); + setContextData(null); + }) + + afterEach(() => { + setGroupConfigData(null); + setContextData(null); + }) + + it('processes incoming qortex component "initialize" message', () => { + windowPostMessageReceived({data: QortexPostMessageInitialized}) + }) + + it('processes incoming qortex component "context" message', () => { + windowPostMessageReceived({data: QortexPostMessageContext}) + }) + }) }) diff --git a/test/spec/modules/qtBidAdapter_spec.js b/test/spec/modules/qtBidAdapter_spec.js new file mode 100644 index 00000000000..9319df0f660 --- /dev/null +++ b/test/spec/modules/qtBidAdapter_spec.js @@ -0,0 +1,518 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/qtBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'qt'; + +describe('QTBidAdapter', 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 valid URL', function () { + expect(serverRequest.url).to.equal('https://endpoint1.qt.io/pbjs'); + }); + + it('Returns general data valid', function () { + let 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 + } + ]; + + let 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); + let 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); + let 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] + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) + }); + + 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 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let 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 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let 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' + }] + }; + + let 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' + }] + }; + let 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', + }] + }; + let 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' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.qt.io/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.qt.io/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.qt.io/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index d10fea829bc..fdde8d290f4 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -181,7 +181,6 @@ describe('Quantcast adapter', function () { maxbitrate: 10, // optional playbackmethod: [1], // optional delivery: [1], // optional - placement: 1, // optional api: [2, 3] // optional }, { context: 'instream', @@ -205,7 +204,6 @@ describe('Quantcast adapter', function () { maxbitrate: 10, playbackmethod: [1], delivery: [1], - placement: 1, api: [2, 3], w: 600, h: 300 @@ -242,7 +240,6 @@ describe('Quantcast adapter', function () { maxbitrate: 10, // optional playbackmethod: [1], // optional delivery: [1], // optional - placement: 1, // optional api: [2, 3], // optional context: 'instream', playerSize: [600, 300] @@ -265,7 +262,6 @@ describe('Quantcast adapter', function () { maxbitrate: 10, playbackmethod: [1], delivery: [1], - placement: 1, api: [2, 3], w: 600, h: 300 diff --git a/test/spec/modules/quantcastIdSystem_spec.js b/test/spec/modules/quantcastIdSystem_spec.js index e9d44dd6124..157c00e7567 100644 --- a/test/spec/modules/quantcastIdSystem_spec.js +++ b/test/spec/modules/quantcastIdSystem_spec.js @@ -1,6 +1,9 @@ import { quantcastIdSubmodule, storage, firePixel, hasCCPAConsent, hasGDPRConsent, checkTCFv2 } from 'modules/quantcastIdSystem.js'; import * as utils from 'src/utils.js'; import {coppaDataHandler} from 'src/adapterManager'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {expect} from 'chai/index.mjs'; describe('QuantcastId module', function () { beforeEach(function() { @@ -380,4 +383,23 @@ describe('Quantcast GDPR consent check', function() { } })).to.equal(false); }); + describe('eids', () => { + before(() => { + attachIdSystem(quantcastIdSubmodule); + }); + it('quantcastId', function() { + const userId = { + quantcastId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'quantcast.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + }) }); diff --git a/test/spec/modules/r2b2AnalytiscAdapter_spec.js b/test/spec/modules/r2b2AnalytiscAdapter_spec.js new file mode 100644 index 00000000000..5658821e95e --- /dev/null +++ b/test/spec/modules/r2b2AnalytiscAdapter_spec.js @@ -0,0 +1,1009 @@ +import r2b2Analytics from '../../../modules/r2b2AnalyticsAdapter'; +import {resetAnalyticAdapter} from '../../../modules/r2b2AnalyticsAdapter'; +import { expect } from 'chai'; +import {EVENTS, AD_RENDER_FAILED_REASON, REJECTION_REASON} from 'src/constants.js'; +import * as pbEvents from 'src/events.js'; +import * as ajax from 'src/ajax.js'; +import * as utils from 'src/utils'; +import {getGlobal} from 'src/prebidGlobal'; +import * as prebidGlobal from 'src/prebidGlobal'; +let adapterManager = require('src/adapterManager').default; + +const { NO_BID, AUCTION_INIT, BID_REQUESTED, BID_TIMEOUT, BID_RESPONSE, BID_REJECTED, BIDDER_DONE, + AUCTION_END, BID_WON, SET_TARGETING, STALE_RENDER, AD_RENDER_SUCCEEDED, AD_RENDER_FAILED, BID_VIEWABLE +} = EVENTS; + +const BANNER_SETTING_1 = { 'sizes': [[300, 300], [300, 250]] }; +const BANNER_SETTING_2 = { 'sizes': [[320, 150], [320, 50]] }; + +const AD_UNIT_1_CODE = 'prebid_300x300'; +const AD_UNIT_2_CODE = 'prebid_320x150'; +const R2B2_PID_1 = 'test.cz/s2s/300x300/mobile'; +const R2B2_PID_2 = 'test.cz/s2s/320x150/mobile'; +const AD_UNIT_1_TID = '0b3464bb-d80a-490e-8367-a65201a37ba3' +const AD_UNIT_2_TID = 'c8c3643c-9de0-43ea-bcd6-cc0072ec9b45'; +const AD_UNIT_2_AD_ID = '22c828c62d44da5'; +const AD_UNIT_1 = { + 'code': AD_UNIT_1_CODE, + 'mediaTypes': { + 'banner': BANNER_SETTING_1 + }, + 'bids': [{ + 'bidder': 'r2b2', + 'params': {'pid': R2B2_PID_1} + }, { + 'bidder': 'adf', + 'params': {'mid': 1799592} + }], + 'sizes': BANNER_SETTING_1.sizes, + 'transactionId': AD_UNIT_1_TID, + 'ortb2Imp': { + 'ext': { + 'tid': AD_UNIT_1_TID + } + } +} +const AD_UNIT_2 = { + 'code': AD_UNIT_2_CODE, + 'mediaTypes': { + 'banner': BANNER_SETTING_2 + }, + 'bids': [{ + 'bidder': 'r2b2', + 'params': {'pid': R2B2_PID_2} + }, { + 'bidder': 'stroeerCore', + 'params': { 'sid': '9532ef8d-e630-45a9-88f6-3eb3eb265d58' } + }], + 'sizes': BANNER_SETTING_2.sizes, + 'transactionId': AD_UNIT_2_TID, + 'ortb2Imp': { + 'ext': { + 'tid': AD_UNIT_2_TID + } + } +}; +const AUCTION_ID = '5b912b08-ce23-463c-a6cf-1792f7344430'; +const R2B2_BIDDER_REQUEST = { + 'bidderCode': 'r2b2', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1e5fae5d0ee471', + 'bids': [ + { + 'bidder': 'r2b2', + 'params': {'pid': R2B2_PID_1}, + 'mediaTypes': { 'banner': BANNER_SETTING_1 }, + 'adUnitCode': AD_UNIT_1_CODE, + 'transactionId': '0b3464bb-d80a-490e-8367-a65201a37ba3', + 'sizes': BANNER_SETTING_1.sizes, + 'bidId': '27434062b8cc94', + 'bidderRequestId': '1e5fae5d0ee471', + 'auctionId': AUCTION_ID, + }, + { + 'bidder': 'r2b2', + 'params': {'pid': R2B2_PID_2}, + 'mediaTypes': { 'banner': BANNER_SETTING_2 }, + 'adUnitCode': AD_UNIT_2_CODE, + 'transactionId': 'c8c3643c-9de0-43ea-bcd6-cc0072ec9b45', + 'sizes': BANNER_SETTING_2.sizes, + 'bidId': '3c296eca6b08f4', + 'bidderRequestId': '1e5fae5d0ee471', + 'auctionId': AUCTION_ID, + } + ], + 'auctionStart': 1727160493004, + 'timeout': 10000, + 'start': 1727160493009 +}; +const ADFORM_BIDDER_REQUEST = { + 'bidderCode': 'adf', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '49241b449c60b4', + 'bids': [{ + 'bidder': 'adf', + 'params': { + 'mid': 1799592 + }, + 'mediaTypes': { 'banner': BANNER_SETTING_1 }, + 'adUnitCode': AD_UNIT_1_CODE, + 'transactionId': '0b3464bb-d80a-490e-8367-a65201a37ba3', + 'sizes': BANNER_SETTING_1.sizes, + 'bidId': '54ef5ac3c45b93', + 'bidderRequestId': '49241b449c60b4', + 'auctionId': AUCTION_ID, + }], + 'auctionStart': 1727160493004, + 'timeout': 10000, + 'start': 1727160493016 +} +const STROEER_BIDDER_REQUEST = { + 'bidderCode': 'stroeerCore', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '13f374632545075', + 'bids': [ + { + 'bidder': 'stroeerCore', + 'params': { + 'sid': '9532ef8d-e630-45a9-88f6-3eb3eb265d58' + }, + 'mediaTypes': { 'banner': BANNER_SETTING_2 }, + 'adUnitCode': AD_UNIT_2_CODE, + 'transactionId': '0b3464bb-d80a-490e-8367-a65201a37ba3', + 'sizes': BANNER_SETTING_2.sizes, + 'bidId': '14fc718193b4da3', + 'bidderRequestId': '13f374632545075', + 'auctionId': AUCTION_ID, + }, + ], + 'auctionStart': 1727160493004, + 'timeout': 10000, + 'start': 1727160493023 +} +const R2B2_AD_UNIT_2_BID = { + 'bidderCode': 'r2b2', + 'width': 300, + 'height': 100, + 'statusMessage': 'Bid available', + 'adId': '22c828c62d44da5', + 'requestId': '3c296eca6b08f4', + 'transactionId': 'c8c3643c-9de0-43ea-bcd6-cc0072ec9b45', + 'auctionId': AUCTION_ID, + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 1.5, + 'creativeId': '76190558', + 'ttl': 360, + 'netRevenue': true, + 'currency': 'USD', + 'ad': '
Test creative
', + 'adapterCode': 'r2b2', + 'originalCpm': 1.5, + 'originalCurrency': 'USD', + 'meta': {}, + 'responseTimestamp': 1727160493863, + 'requestTimestamp': 1727160493009, + 'bidder': 'r2b2', + 'adUnitCode': AD_UNIT_2_CODE, + 'timeToRespond': 854, + 'size': '300x100', + 'adserverTargeting': { + 'hb_bidder': 'r2b2', + 'hb_adid': AD_UNIT_2_AD_ID, + 'hb_pb': '0.20', + 'hb_size': '300x100', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': '', + 'hb_crid': '76190558' + }, + 'latestTargetedAuctionId': AUCTION_ID, + 'status': 1 +} + +const MOCK = { + AUCTION_INIT: { + adUnitCodes: [AD_UNIT_1_CODE, AD_UNIT_2_CODE], + adUnits: [AD_UNIT_1, AD_UNIT_2], + bidderRequests: [R2B2_BIDDER_REQUEST, ADFORM_BIDDER_REQUEST, STROEER_BIDDER_REQUEST], + auctionId: AUCTION_ID, + }, + BID_REQUESTED: R2B2_BIDDER_REQUEST, + BID_RESPONSE: R2B2_AD_UNIT_2_BID, + BIDDER_DONE: R2B2_BIDDER_REQUEST, + AUCTION_END: { + auctionId: AUCTION_ID, + adUnitCodes: [AD_UNIT_1_CODE, AD_UNIT_2_CODE], + adUnits: [AD_UNIT_1, AD_UNIT_2], + bidderRequests: [R2B2_BIDDER_REQUEST, ADFORM_BIDDER_REQUEST, STROEER_BIDDER_REQUEST], + bidsReceived: [R2B2_AD_UNIT_2_BID], + bidsRejected: [], + noBids: [], + auctionEnd: 1727160493104 + }, + BID_WON: R2B2_AD_UNIT_2_BID, + SET_TARGETING: { + [AD_UNIT_2_CODE]: R2B2_AD_UNIT_2_BID.adserverTargeting + }, + NO_BID: { + bidder: 'r2b2', + params: { pid: R2B2_PID_1 }, + mediaTypes: { banner: BANNER_SETTING_1 }, + adUnitCode: AD_UNIT_1_CODE, + transactionId: 'a0b9d621-6b74-47ce-b7e0-cee5f8e3c124', + adUnitId: 'b87edd48-9572-431d-a508-e7f956332cec', + sizes: BANNER_SETTING_1.sizes, + bidId: '121b6373a78e56b', + bidderRequestId: '104126936185f0b', + auctionId: AUCTION_ID, + src: 'client', + }, + BID_TIMEOUT: [ + { + bidder: 'r2b2', + mediaTypes: { 'banner': BANNER_SETTING_1 }, + adUnitCode: AD_UNIT_1_CODE, + transactionId: '5629f772-9eae-49fa-a749-119f4d6295f9', + adUnitId: 'fb3536c6-7bcd-41a2-b96a-cb1764a06675', + sizes: BANNER_SETTING_1.sizes, + bidId: '25522556ba65bb72', + bidderRequestId: '2544c8d7e5b5aba4', + auctionId: AUCTION_ID, + timeout: 1000 + }, + { + bidder: 'r2b2', + mediaTypes: { 'banner': BANNER_SETTING_1 }, + adUnitCode: AD_UNIT_1_CODE, + transactionId: '5629f772-9eae-49fa-a749-119f4d6295f9', + adUnitId: 'fb3536c6-7bcd-41a2-b96a-cb1764a06675', + sizes: BANNER_SETTING_1.sizes, + bidId: '25522556ba65bb72', + bidderRequestId: '2544c8d7e5b5aba4', + auctionId: AUCTION_ID, + timeout: 1000 + } + ], + AD_RENDER_SUCCEEDED: { + 'doc': { + 'location': { + 'href': 'http://localhost:63342/test/prebid.html', + 'protocol': 'http:', + 'host': 'localhost:63342', + 'hostname': 'localhost', + 'port': '63342', + 'pathname': '/test/prebid.html', + 'hash': '', + 'origin': 'http://localhost:63342', + 'ancestorOrigins': { + '0': 'http://localhost:63342' + } + } + }, + 'bid': R2B2_AD_UNIT_2_BID, + 'adId': R2B2_AD_UNIT_2_BID.adId + }, + AD_RENDER_FAILED: { + bidId: '3c296eca6b08f4', + reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + message: 'message', + bid: R2B2_AD_UNIT_2_BID + }, + STALE_RENDER: R2B2_AD_UNIT_2_BID, + BID_VIEWABLE: R2B2_AD_UNIT_2_BID +} +function fireEvents(events) { + return events.map((ev, i) => { + ev = Array.isArray(ev) ? ev : [ev, {i: i}]; + pbEvents.emit.apply(null, ev) + return ev; + }); +} + +function expectEvents(events, sandbox) { + events = fireEvents(events); + return { + to: { + beTrackedBy(trackFn) { + events.forEach(([eventType, args]) => { + sandbox.assert.calledWithMatch(trackFn, sandbox.match({eventType, args})); + }); + }, + beBundledTo(bundleFn) { + events.forEach(([eventType, args]) => { + sandbox.assert.calledWithMatch(bundleFn, sandbox.match.any, eventType, sandbox.match(args)) + }); + }, + }, + }; +} + +function validateAndExtractEvents(ajaxStub) { + expect(ajaxStub.calledOnce).to.equal(true); + let eventArgs = ajaxStub.firstCall.args[2]; + expect(typeof eventArgs).to.be.equal('string'); + expect(eventArgs.indexOf('events=')).to.be.equal(0); + let eventsString = eventArgs.substring(7); + let events = tryParseJSON(eventsString); + expect(events).to.not.be.undefined; + + return events; +} + +function getQueryData(url, decode = false) { + const queryArgs = url.split('?')[1].split('&'); + return queryArgs.reduce((data, arg) => { + let [key, val] = arg.split('='); + if (decode) { + val = decodeURIComponent(val); + } + if (data[key] !== undefined) { + if (!Array.isArray(data[key])) { + data[key] = [data[key]]; + } + data[key].push(val); + } else { + data[key] = val; + } + return data; + }, {}); +} + +function getPrebidEvents(events) { + return events && events.prebid && events.prebid.e; +} +function getPrebidEventsByName(events, name) { + let prebidEvents = getPrebidEvents(events); + if (!prebidEvents) return []; + + let result = []; + for (let i = 0; i < prebidEvents.length; i++) { + let event = prebidEvents[i]; + if (event.e === name) { + result.push(event); + } + } + + return result; +} + +function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + } +} +describe('r2b2 Analytics', function () { + let sandbox; + let clock; + let ajaxStub; + let getGlobalStub; + let enableAnalytics; + + before(() => { + enableAnalytics = r2b2Analytics.enableAnalytics; + }) + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + sandbox.stub(pbEvents, 'getEvents').returns([]); + getGlobalStub = sandbox.stub(prebidGlobal, 'getGlobal').returns({ + getHighestCpmBids: () => [R2B2_AD_UNIT_2_BID] + }); + ajaxStub = sandbox.stub(ajax, 'ajax'); + + adapterManager.registerAnalyticsAdapter({ + code: 'r2b2', + adapter: r2b2Analytics + }); + + r2b2Analytics.enableAnalytics = enableAnalytics; + }); + + afterEach(() => { + resetAnalyticAdapter(); + sandbox.restore(); + getGlobalStub.restore(); + ajaxStub.restore(); + r2b2Analytics.disableAnalytics(); + }); + + describe('config', () => { + it('missing domain', () => { + let logWarnStub = sandbox.stub(utils, 'logWarn'); + + adapterManager.enableAnalytics({ + provider: 'r2b2', + options: {} + }); + + expect(logWarnStub.calledOnce).to.be.true; + expect(logWarnStub.firstCall.args[0]).to.be.equal('R2B2 Analytics: Mandatory parameter \'domain\' not configured, analytics disabled'); + logWarnStub.restore(); + }); + + it('all params error reporting', () => { + adapterManager.enableAnalytics({ + provider: 'r2b2', + options: { + domain: 'test.cz', + configId: 11, + configVer: 7, + server: 'delivery.local', + } + }); + + fireEvents([ + [BID_RESPONSE, MOCK.BID_RESPONSE], + ]); + + expect(ajaxStub.calledOnce).to.be.true; + expect(typeof ajaxStub.firstCall.args[0]).to.be.equal('string'); + let query = getQueryData(ajaxStub.firstCall.args[0], true); + expect(query['d']).to.be.equal('test.cz'); + expect(query['conf']).to.be.equal('11'); + expect(query['conf_ver']).to.be.equal('7'); + }); + + it('all params events reporting', (done) => { + adapterManager.enableAnalytics({ + provider: 'r2b2', + options: { + domain: 'test.cz', + configId: 11, + configVer: 7, + server: 'delivery.local', + } + }); + + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_RESPONSE, MOCK.BID_RESPONSE], + ]); + + setTimeout(() => { + expect(ajaxStub.calledOnce).to.be.true; + expect(typeof ajaxStub.firstCall.args[0]).to.be.equal('string'); + let query = getQueryData(ajaxStub.firstCall.args[0], true); + expect(query['hbDomain']).to.be.equal('test.cz'); + expect(query['conf']).to.be.equal('11'); + expect(query['conf_ver']).to.be.equal('7'); + done(); + }, 500); + + clock.tick(500); + }); + }); + + describe('events', () => { + beforeEach(() => { + adapterManager.enableAnalytics({ + provider: 'r2b2', + options: { + domain: 'test.com', + } + }); + }); + + it('should catch all events', function () { + sandbox.spy(r2b2Analytics, 'track'); + + expectEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_REQUESTED, MOCK.BID_REQUESTED], + [BID_RESPONSE, MOCK.BID_RESPONSE], + [AUCTION_END, MOCK.AUCTION_END], + [SET_TARGETING, MOCK.SET_TARGETING], + [BID_WON, MOCK.BID_WON], + ], sandbox).to.beTrackedBy(r2b2Analytics.track); + }); + + it('should send ajax after delay', (done) => { + fireEvents([[AUCTION_INIT, MOCK.AUCTION_INIT]]); + setTimeout(() => { + expect(ajaxStub.calledOnce).to.equal(true); + done(); + }, 500); + + clock.tick(500); + }) + + it('auction init content', (done) => { + fireEvents([[AUCTION_INIT, MOCK.AUCTION_INIT]]); + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let initEvents = getPrebidEventsByName(events, 'init'); + expect(initEvents.length).to.be.equal(1); + let initEvent = initEvents[0]; + expect(initEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + u: { + [AD_UNIT_1_CODE]: ['r2b2', 'adf'], + [AD_UNIT_2_CODE]: ['r2b2', 'stroeerCore'] + }, + o: 1 + }) + + done(); + }, 500); + + clock.tick(500); + }) + + it('auction multiple init', (done) => { + let auction_init = MOCK.AUCTION_INIT; + let auction_init_2 = utils.deepClone(MOCK.AUCTION_INIT); + auction_init_2.auctionId = 'different_auction_id'; + + fireEvents([[AUCTION_INIT, auction_init], [AUCTION_INIT, auction_init_2]]); + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let initEvents = getPrebidEventsByName(events, 'init'); + expect(initEvents.length).to.be.equal(2); + done(); + }, 500); + + clock.tick(500); + }); + + it('bid requested content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_REQUESTED, MOCK.BID_REQUESTED], + [BID_REQUESTED, ADFORM_BIDDER_REQUEST], + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let bidRequestedEvents = getPrebidEventsByName(events, 'request'); + expect(bidRequestedEvents.length).to.be.equal(2); + let r2b2BidRequest = bidRequestedEvents[0]; + let adformBidRequest = bidRequestedEvents[1]; + expect(r2b2BidRequest.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: { + [AD_UNIT_1_CODE]: 1, + [AD_UNIT_2_CODE]: 1 + } + }); + expect(adformBidRequest.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'adf', + u: {[AD_UNIT_1_CODE]: 1} + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('no bid content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [NO_BID, MOCK.NO_BID] + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let noBidEvents = getPrebidEventsByName(events, 'noBid'); + expect(noBidEvents.length).to.be.equal(1); + let noBidEvent = noBidEvents[0]; + expect(noBidEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_1_CODE + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('bid timeout content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_TIMEOUT, MOCK.BID_TIMEOUT] + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let timeoutEvents = getPrebidEventsByName(events, 'timeout'); + expect(timeoutEvents.length).to.be.equal(1); + let timeoutEvent = timeoutEvents[0]; + expect(timeoutEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: { + r2b2: {[AD_UNIT_1_CODE]: 2} + } + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('bidder done content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BIDDER_DONE, MOCK.BIDDER_DONE] + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let bidderDoneEvents = getPrebidEventsByName(events, 'bidderDone'); + expect(bidderDoneEvents.length).to.be.equal(1); + let bidderDoneEvent = bidderDoneEvents[0]; + expect(bidderDoneEvent.d).to.be.deep.equal({ ai: AUCTION_ID, b: 'r2b2' }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('auction end content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [AUCTION_END, MOCK.AUCTION_END] + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let auctionEndEvents = getPrebidEventsByName(events, 'auction'); + expect(auctionEndEvents.length).to.be.equal(1); + let auctionEnd = auctionEndEvents[0]; + expect(auctionEnd.d).to.be.deep.equal({ + ai: AUCTION_ID, + wins: [{ + b: 'r2b2', + u: AD_UNIT_2_CODE, + p: 1.5, + c: 'USD', + sz: '300x100', + bi: R2B2_AD_UNIT_2_BID.requestId, + }], + u: {[AD_UNIT_2_CODE]: {b: {r2b2: 1}}}, + o: 1, + bc: 1, + nbc: 0, + rjc: 0, + brc: 4 + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('auction end empty auction', (done) => { + let noBidderRequestsEnd = utils.deepClone(MOCK.AUCTION_END); + noBidderRequestsEnd.bidderRequests = []; + + fireEvents([ + [AUCTION_END, noBidderRequestsEnd] + ]); + + setTimeout(() => { + expect(ajaxStub.calledOnce).to.be.false; + + done(); + }, 500); + + clock.tick(500); + }); + + it('bid response content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_RESPONSE, MOCK.BID_RESPONSE], + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let bidResponseEvents = getPrebidEventsByName(events, 'response'); + expect(bidResponseEvents.length).to.be.equal(1); + let bidResponseEvent = bidResponseEvents[0]; + expect(bidResponseEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_2_CODE, + p: 1.5, + op: 1.5, + c: 'USD', + oc: 'USD', + sz: '300x100', + st: 1, + rt: 854, + bi: R2B2_AD_UNIT_2_BID.requestId, + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('bid rejected content', (done) => { + let rejectedBid = utils.deepClone(R2B2_AD_UNIT_2_BID); + rejectedBid.rejectionReason = REJECTION_REASON.FLOOR_NOT_MET; + + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_REJECTED, rejectedBid], + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let rejectedBidsEvents = getPrebidEventsByName(events, 'reject'); + expect(rejectedBidsEvents.length).to.be.equal(1); + let rejectedBidEvent = rejectedBidsEvents[0]; + expect(rejectedBidEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_2_CODE, + p: 1.5, + c: 'USD', + r: REJECTION_REASON.FLOOR_NOT_MET, + bi: R2B2_AD_UNIT_2_BID.requestId, + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('bid won content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_WON, MOCK.BID_WON], + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let bidWonEvents = getPrebidEventsByName(events, 'bidWon'); + expect(bidWonEvents.length).to.be.equal(1); + let bidWonEvent = bidWonEvents[0]; + expect(bidWonEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_2_CODE, + p: 1.5, + op: 1.5, + c: 'USD', + oc: 'USD', + sz: '300x100', + mt: 'banner', + at: { + b: 'r2b2', + sz: '300x100', + pb: '0.20', + fmt: 'banner' + }, + o: 1, + bi: R2B2_AD_UNIT_2_BID.requestId, + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('bid won content no targeting', (done) => { + let bidWonWithoutTargeting = utils.deepClone(MOCK.BID_WON); + bidWonWithoutTargeting.adserverTargeting = {}; + + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_WON, bidWonWithoutTargeting], + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let bidWonEvents = getPrebidEventsByName(events, 'bidWon'); + expect(bidWonEvents.length).to.be.equal(1); + let bidWonEvent = bidWonEvents[0]; + expect(bidWonEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_2_CODE, + p: 1.5, + op: 1.5, + c: 'USD', + oc: 'USD', + sz: '300x100', + mt: 'banner', + at: { + b: '', + sz: '', + pb: '', + fmt: '' + }, + o: 1, + bi: R2B2_AD_UNIT_2_BID.requestId, + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('targeting content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_RESPONSE, MOCK.BID_RESPONSE], + [SET_TARGETING, MOCK.SET_TARGETING] + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let setTargetingEvents = getPrebidEventsByName(events, 'targeting'); + expect(setTargetingEvents.length).to.be.equal(1); + expect(setTargetingEvents[0].d).to.be.deep.equal({ + ai: AUCTION_ID, + u: { + [AD_UNIT_2_CODE]: { + b: 'r2b2', + sz: '300x100', + pb: '0.20', + fmt: 'banner' + } + } + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('ad render succeeded content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_RESPONSE, MOCK.BID_RESPONSE], + [AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED], + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let setTargetingEvents = getPrebidEventsByName(events, 'render'); + expect(setTargetingEvents.length).to.be.equal(1); + let setTargeting = setTargetingEvents[0]; + expect(setTargeting.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_2_CODE, + p: 1.5, + c: 'USD', + sz: '300x100', + mt: 'banner', + bi: R2B2_AD_UNIT_2_BID.requestId, + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('ad render failed content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_RESPONSE, MOCK.BID_RESPONSE], + [AD_RENDER_FAILED, MOCK.AD_RENDER_FAILED], + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let renderFailedEvents = getPrebidEventsByName(events, 'renderFail'); + expect(renderFailedEvents.length).to.be.equal(1); + let renderFailed = renderFailedEvents[0]; + expect(renderFailed.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_2_CODE, + p: 1.5, + c: 'USD', + r: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + bi: R2B2_AD_UNIT_2_BID.requestId, + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('stale render content', (done) => { + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [STALE_RENDER, MOCK.STALE_RENDER], + ]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let staleRenderEvents = getPrebidEventsByName(events, 'staleRender'); + expect(staleRenderEvents.length).to.be.equal(1); + let staleRenderEvent = staleRenderEvents[0]; + expect(staleRenderEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_2_CODE, + p: 1.5, + c: 'USD', + bi: R2B2_AD_UNIT_2_BID.requestId, + }); + + done(); + }, 500); + + clock.tick(500); + }); + + it('bid viewable content', (done) => { + let dateStub = sandbox.stub(Date, 'now'); + dateStub.returns(100); + + fireEvents([ + [AUCTION_INIT, MOCK.AUCTION_INIT], + [BID_RESPONSE, MOCK.BID_RESPONSE], + [AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED] + ]); + + dateStub.returns(150); + + fireEvents([[BID_VIEWABLE, MOCK.BID_VIEWABLE]]); + + setTimeout(() => { + let events = validateAndExtractEvents(ajaxStub); + let bidViewableEvents = getPrebidEventsByName(events, 'view'); + expect(bidViewableEvents.length).to.be.equal(1); + let bidViewableEvent = bidViewableEvents[0]; + expect(bidViewableEvent.d).to.be.deep.equal({ + ai: AUCTION_ID, + b: 'r2b2', + u: AD_UNIT_2_CODE, + rt: 50, + bi: R2B2_AD_UNIT_2_BID.requestId + }); + + done(); + }, 500); + + clock.tick(500); + dateStub.restore(); + }); + + it('no auction data error', (done) => { + fireEvents([ + [BID_RESPONSE, MOCK.BID_RESPONSE], + ]); + + setTimeout(() => { + expect(ajaxStub.calledOnce).to.be.true; + expect(typeof ajaxStub.firstCall.args[0]).to.be.equal('string'); + let query = getQueryData(ajaxStub.firstCall.args[0], true); + expect(typeof query.m).to.be.equal('string'); + expect(query.m.indexOf('No auction data when creating event')).to.not.be.equal(-1); + + done(); + }, 500); + + clock.tick(500); + }); + + it('empty auction', (done) => { + let emptyAuctionInit = utils.deepClone(MOCK.AUCTION_INIT); + emptyAuctionInit.bidderRequests = undefined; + let emptyAuctionEnd = utils.deepClone(MOCK.AUCTION_END); + emptyAuctionEnd.bidderRequests = []; + + fireEvents([ + [AUCTION_INIT, emptyAuctionInit], + [AUCTION_END, emptyAuctionEnd], + ]) + + setTimeout(() => { + expect(ajaxStub.calledOnce).to.be.true; + let events = validateAndExtractEvents(ajaxStub); + let initEvents = getPrebidEventsByName(events, 'init'); + let auctionEndEvents = getPrebidEventsByName(events, 'auction'); + + expect(initEvents.length).to.be.equal(1); + expect(auctionEndEvents.length).to.be.equal(0); + + done(); + }, 500); + + clock.tick(500); + }); + }); +}); diff --git a/test/spec/modules/r2b2BidAdapter_spec.js b/test/spec/modules/r2b2BidAdapter_spec.js index b94b400a71d..63850b78c40 100644 --- a/test/spec/modules/r2b2BidAdapter_spec.js +++ b/test/spec/modules/r2b2BidAdapter_spec.js @@ -420,7 +420,7 @@ describe('R2B2 adapter', function () { ], }, ]; - bids[0].userIdAsEids = eidsArray; + bidderRequest.ortb2 = {user: {ext: {eids: eidsArray}}} let requests = spec.buildRequests(bids, bidderRequest); let request = requests[0]; let eids = request.data.user.ext.eids; diff --git a/test/spec/modules/radsBidAdapter_spec.js b/test/spec/modules/radsBidAdapter_spec.js index 3ad7ada2ae7..4a64e2922f1 100644 --- a/test/spec/modules/radsBidAdapter_spec.js +++ b/test/spec/modules/radsBidAdapter_spec.js @@ -32,12 +32,12 @@ describe('radsAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'someIncorrectParam': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/rakutenBidAdapter_spec.js b/test/spec/modules/rakutenBidAdapter_spec.js index 15b22afbe29..2a9fcb9f83b 100644 --- a/test/spec/modules/rakutenBidAdapter_spec.js +++ b/test/spec/modules/rakutenBidAdapter_spec.js @@ -40,10 +40,10 @@ describe('rakutenBidAdapter', function() { }); it('should return false when required params are not passed', () => { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false) + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false) }) }); diff --git a/test/spec/modules/raynRtdProvider_spec.js b/test/spec/modules/raynRtdProvider_spec.js index 69ea316e8b5..3920d090550 100644 --- a/test/spec/modules/raynRtdProvider_spec.js +++ b/test/spec/modules/raynRtdProvider_spec.js @@ -152,6 +152,7 @@ describe('rayn RTD Submodule', function () { 2: ['71', '313'], 4: ['33', '145', '712'] }; + TEST_SEGMENTS['103015'] = ['agdv23', 'avscg3']; const bidderOrtb2 = {}; const bidders = RTD_CONFIG.dataProviders[0].params.bidders; @@ -174,6 +175,9 @@ describe('rayn RTD Submodule', function () { TEST_SEGMENTS['4']['3'].forEach((id) => { expect(ortb2.user.data[0].segment.find(segment => segment.id === id)).to.exist; }); + TEST_SEGMENTS['103015'].forEach((id) => { + expect(ortb2.user.data[1].segment.find(segment => segment.id === id)).to.exist; + }); }); }); }); @@ -229,6 +233,27 @@ describe('rayn RTD Submodule', function () { logMessageSpy.restore(); }); + it('should update reqBidsConfigObj and execute callback using persona segment from localStorage', function () { + const callbackSpy = sinon.spy(); + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const testSegments = { + 103015: ['agdv23', 'avscg3'] + }; + + getDataFromLocalStorageStub + .withArgs(raynRTD.RAYN_LOCAL_STORAGE_KEY) + .returns(JSON.stringify(testSegments)); + + const reqBidsConfigObj = { ortb2Fragments: { bidder: {} } }; + + raynRTD.raynSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, RTD_CONFIG.dataProviders[0]); + + expect(callbackSpy.calledOnce).to.be.true; + expect(logMessageSpy.lastCall.lastArg).to.equal(`Segtax data from localStorage: ${JSON.stringify(testSegments)}`); + + logMessageSpy.restore(); + }); + it('should update reqBidsConfigObj and execute callback using segments from raynJS', function () { const callbackSpy = sinon.spy(); const logMessageSpy = sinon.spy(utils, 'logMessage'); diff --git a/test/spec/modules/rediadsBidAdapter_spec.js b/test/spec/modules/rediadsBidAdapter_spec.js new file mode 100644 index 00000000000..5b23a14728e --- /dev/null +++ b/test/spec/modules/rediadsBidAdapter_spec.js @@ -0,0 +1,144 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/rediadsBidAdapter'; + +describe('rediads Bid Adapter', function () { + const BIDDER_CODE = 'rediads'; + const STAGING_ENDPOINT_URL = + 'https://stagingbidding.rediads.com/openrtb2/auction'; + + const bidRequest = { + bidder: BIDDER_CODE, + params: { + account_id: '12345', + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90], + ], + }, + }, + adUnitCode: 'adunit-code', + bidId: '2ab03f1234', + auctionId: '123456789', + }; + + const bidderRequest = { + bidderCode: BIDDER_CODE, + refererInfo: { + referer: 'http://example.com', + }, + }; + + const resetHash = (originalHash) => { + // Reset the hash, ensuring no trailing # + if (originalHash) { + location.hash = originalHash; + } else { + history.replaceState(null, '', location.pathname + location.search); + } + }; + + describe('isBidRequestValid', function () { + it('should return true for valid bid requests', function () { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false if account_id is missing', function () { + const invalidBid = { ...bidRequest, params: {} }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('should build a valid request with correct data', function () { + const requests = spec.buildRequests([bidRequest], bidderRequest); + expect(requests).to.be.an('array').that.is.not.empty; + + const request = requests[0]; + expect(request.method).to.equal('POST'); + expect(request.url).that.is.not.empty; + + const sitePublisherId = request?.data?.site?.publisher?.id; + const appPublisherId = request?.data?.app?.publisher?.id; + const accountId = request?.data?.ext?.rediads?.params?.account_id; + expect(sitePublisherId || appPublisherId || accountId).to.be.ok; + }); + + it('should include test flag if testBidsRequested is true', function () { + const originalHash = location.hash; + location.hash = '#rediads-test-bids'; + + const requests = spec.buildRequests([bidRequest], bidderRequest); + expect(requests[0].data.test).to.equal(1); + + resetHash(originalHash); + }); + + it('should set staging environtment if stagingEnvRequested is true', function () { + const originalHash = location.hash; + location.hash = '#rediads-staging'; + + const requests = spec.buildRequests([bidRequest], bidderRequest); + expect(requests[0].url).to.equal(STAGING_ENDPOINT_URL); + + resetHash(originalHash); + }); + }); + + describe('interpretResponse', function () { + it('should interpret and return valid bid responses for banner bid', function () { + const serverResponse = { + body: { + seatbid: [ + { + bid: [ + { + price: 1.23, + impid: '2ab03f1234', + adm: '
Ad
', + crid: 'creative123', + w: 123, + h: 321, + }, + ], + }, + ], + }, + }; + const requestObj = spec.buildRequests([bidRequest], bidderRequest); + const bids = spec.interpretResponse(serverResponse, requestObj[0]); + expect(bids).to.be.an('array').that.is.not.empty; + + const bid = bids[0]; + expect(bid).to.include({ + requestId: '2ab03f1234', + cpm: 1.23, + creativeId: 'creative123', + width: 123, + height: 321, + ad: '
Ad
', + }); + expect(bid.mediaType).to.equal('banner'); + }); + + it('should return an empty array for invalid responses', function () { + const invalidResponse = { body: {} }; + const updatedBidRequest = { ...bidRequest, params: undefined }; + const requestObj = spec.buildRequests([updatedBidRequest], bidderRequest); + const bids = spec.interpretResponse(invalidResponse, requestObj[0]); + expect(bids).to.be.an('array').that.is.empty; + }); + }); + + describe('Miscellaneous', function () { + it('should support multiple media types', function () { + expect(spec.supportedMediaTypes).to.include.members([ + 'banner', + 'native', + 'video', + ]); + }); + }); +}); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 4a07c84a494..5cb47eb8f51 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -115,6 +115,11 @@ describe('RelaidoAdapter', function () { }] } }; + window.RelaidoPlayer = { + renderAd: function() { + return null; + } + }; }); afterEach(() => { @@ -569,4 +574,20 @@ describe('RelaidoAdapter', function () { expect(query.ref).to.include(window.location.href); }); }); + + describe('spec.outstreamRender', function () { + it('Should to pass a Bid to renderAd', function () { + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + const response = bidResponses[0]; + sinon.spy(window.RelaidoPlayer, 'renderAd'); + response.renderer.render(response); + const renderCall = window.RelaidoPlayer.renderAd.getCall(0); + const arg = renderCall.args[0]; + expect(arg.width).to.equal(640); + expect(arg.height).to.equal(360); + expect(arg.vastXml).to.equal(''); + expect(arg.mediaType).to.equal(VIDEO); + expect(arg.placementId).to.equal(100000); + }); + }); }); diff --git a/test/spec/modules/relevatehealthBidAdapter_spec.js b/test/spec/modules/relevatehealthBidAdapter_spec.js new file mode 100644 index 00000000000..ef974bc3ac1 --- /dev/null +++ b/test/spec/modules/relevatehealthBidAdapter_spec.js @@ -0,0 +1,239 @@ +import { + expect +} from 'chai'; +import { + spec +} from '../../../modules/relevatehealthBidAdapter.js'; +import * as utils from '../../../src/utils.js'; + +describe('relevatehealth adapter', function() { + let request; + let bannerResponse, invalidResponse; + + beforeEach(function() { + request = [{ + bidder: 'relevatehealth', + mediaTypes: { + banner: { + sizes: [ + [160, 600] + ] + } + }, + params: { + placement_id: 110011, + user_id: '11211', + width: 160, + height: 600, + domain: '', + bid_floor: 0.5 + } + }]; + bannerResponse = { + 'body': { + 'id': 'a48b79e7-7104-4213-99f3-55f3234f2e54', + 'seatbid': [{ + 'bid': [{ + 'id': '3d7dd6dc-7665-4cdc-96a4-5ea192df32b8', + 'impid': '285b9c53b2c662', + 'price': 20.68, + 'adid': '3389', + 'adm': "", + 'adomain': ['google.com'], + 'iurl': 'https://rtb.relevate.health/prebid/relevate', + 'cid': '1431/3389', + 'crid': '3389', + 'w': 160, + 'h': 600, + 'cat': ['IAB1-15'] + }], + 'seat': '00001', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_1276' + } + }; + invalidResponse = { + 'body': { + 'id': 'a48b79e7-7104-4213-99f3-55f3234f2e54', + 'seatbid': [{ + 'bid': [{ + 'id': '3d7dd6dc-7665-4cdc-96a4-5ea192df32b8', + 'impid': '285b9c53b2c662', + 'price': 20.68, + 'adid': '3389', + 'adm': 'invalid response', + 'adomain': ['google.com'], + 'iurl': 'https://rtb.relevate.health/prebid/relevate', + 'cid': '1431/3389', + 'crid': '3389', + 'w': 160, + 'h': 600, + 'cat': ['IAB1-15'] + }], + 'seat': '00001', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_1276' + } + }; + }); + + describe('validations', function() { + it('isBidValid : placement_id and user_id are passed', function() { + let bid = { + bidder: 'relevatehealth', + params: { + placement_id: 110011, + user_id: '11211' + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(true); + }); + it('isBidValid : placement_id and user_id are not passed', function() { + let bid = { + bidder: 'relevatehealth', + params: { + width: 160, + height: 600, + domain: '', + bid_floor: 0.5 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + it('isBidValid : placement_id is passed but user_id is not passed', function() { + let bid = { + bidder: 'relevatehealth', + params: { + placement_id: 110011, + width: 160, + height: 600, + domain: '', + bid_floor: 0.5 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + it('isBidValid : user_id is passed but placement_id is not passed', function() { + let bid = { + bidder: 'relevatehealth', + params: { + width: 160, + height: 600, + domain: '', + bid_floor: 0.5, + user_id: '11211' + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + }); + describe('Validate Request', function() { + it('Immutable bid request validate', function() { + let _Request = utils.deepClone(request), + bidRequest = spec.buildRequests(request); + expect(request).to.deep.equal(_Request); + }); + it('Validate bidder connection', function() { + let _Request = spec.buildRequests(request); + expect(_Request.url).to.equal('https://rtb.relevate.health/prebid/relevate'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function() { + let _Request = spec.buildRequests(request); + let data = JSON.parse(_Request.data); + expect(data[0].imp[0].id).to.equal(request[0].bidId); + expect(data[0].placementId).to.equal(110011); + }); + it('Validate bid request : ad size', function() { + let _Request = spec.buildRequests(request); + let data = JSON.parse(_Request.data); + expect(data[0].imp[0].banner).to.be.a('object'); + expect(data[0].imp[0].banner.w).to.equal(160); + expect(data[0].imp[0].banner.h).to.equal(600); + }); + it('Validate bid request : user object', function() { + let _Request = spec.buildRequests(request); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function() { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(request, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].us_privacy).to.equal('1NYN'); + }); + }); + describe('Validate response ', function() { + it('Validate bid response : valid bid response', function() { + let bResponse = spec.interpretResponse(bannerResponse, request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(bannerResponse.body.seatbid[0].bid[0].impid); + expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('banner'); + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['google.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId); + }); + it('Invalid bid response check ', function() { + let bRequest = spec.buildRequests(request); + let response = spec.interpretResponse(invalidResponse, bRequest); + expect(response[0].ad).to.equal('invalid response'); + }); + }); + describe('GPP and coppa', function() { + it('Request params check with GPP Consent', function() { + let bidderReq = { + gppConsent: { + gppString: 'gpp-string-test', + applicableSections: [5] + } + }; + let _Request = spec.buildRequests(request, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].gpp).to.equal('gpp-string-test'); + expect(data[0].gpp_sid[0]).to.equal(5); + }); + it('Request params check with GPP Consent read from ortb2', function() { + let bidderReq = { + ortb2: { + regs: { + gpp: 'gpp-test-string', + gpp_sid: [5] + } + } + }; + let _Request = spec.buildRequests(request, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].gpp).to.equal('gpp-test-string'); + expect(data[0].gpp_sid[0]).to.equal(5); + }); + it(' Bid request should have coppa flag if its true', () => { + let bidderReq = { + ortb2: { + regs: { + coppa: 1 + } + } + }; + let _Request = spec.buildRequests(request, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].coppa).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/responsiveAdsBidAdapter_spec.js b/test/spec/modules/responsiveAdsBidAdapter_spec.js new file mode 100644 index 00000000000..83202ad0916 --- /dev/null +++ b/test/spec/modules/responsiveAdsBidAdapter_spec.js @@ -0,0 +1,145 @@ +import { expect } from 'chai'; +import { spec } from 'modules/responsiveAdsBidAdapter.js'; +import * as utils from 'src/utils.js'; + +describe('responsiveAdsBidAdapter', function() { + let bidRequests; + let bidderRequest; + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + sandbox.stub(utils, 'isSafeFrameWindow').returns(false); + sandbox.stub(utils, 'canAccessWindowTop').returns(true); + bidRequests = [{ + bidder: 'responsiveads', + params: { + placementId: '1', + }, + adUnitCode: '/3434399/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bidId: '123', + auctionId: '456', + bidderRequestId: '789', + transactionId: '123' + }]; + + bidderRequest = { + timeout: 3000, + } + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('Check if bid is valid', function() { + it('Should accept valid bid', function() { + const validBid = { + bidder: 'responsiveads', + params: {}, + }; + + const isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + + it('Should not reject bid if missing placementId', function() { + const validBid = { + bidder: 'responsiveads', + params: {} + }; + + const isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + }); + + describe('Build requests', function () { + it('Should not bit on safeframe', function() { + utils.isSafeFrameWindow.restore(); + sandbox.stub(utils, 'isSafeFrameWindow').returns(true); + + const requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests).to.be.null; + }); + + it('Should not bit if cant access window top', function () { + utils.canAccessWindowTop.restore(); + sandbox.stub(utils, 'canAccessWindowTop').returns(false); + + const requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests).to.be.null; + }); + + it('Should use POST and have URL', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.method).to.exist; + expect(request.method).to.equal('POST'); + expect(request.url).to.exist; + }); + + it('Should add adapter version', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.ext.prebid.adapterVersion).to.exist; + }); + }); + + describe('Handling responses', function() { + it('Should return complete bid response', function() { + const serverResponse = { + body: { + id: 'response-id', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: '123', + impid: '123', + price: 0.5, + adm: ``, + nurl: 'https://example.com/win', + crid: '662d13e12e0c567af92d0918', + w: 300, + h: 250, + mediaType: 'banner', + adomain: ['responsiveads.com'], + attr: [1], + cat: ['IAB1'] + } + ] + } + ] + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].requestId).to.equal('123'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal(300); + expect(bids[0].height).to.equal(250); + expect(bids[0].ad).to.have.length.above(1); + expect(bids[0].meta.advertiserDomains).to.deep.equal(['responsiveads.com']); + }); + + it('should return empty bid response', function () { + const emptyServerResponse = { + body: [] + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(emptyServerResponse, request); + + expect(bids).to.be.lengthOf(0); + }); + }); +}); diff --git a/test/spec/modules/retailspotBidAdapter_spec.js b/test/spec/modules/retailspotBidAdapter_spec.js index 39cddb323b8..c5cb001c1ba 100644 --- a/test/spec/modules/retailspotBidAdapter_spec.js +++ b/test/spec/modules/retailspotBidAdapter_spec.js @@ -246,7 +246,7 @@ describe('RetailSpot Adapter', function () { ]; const adapter = newBidder(spec); - const DEV_URL = 'http://localhost:8090/'; + const DEV_URL = 'http://localhost:3030/'; describe('inherited functions', function () { it('exists and is a function', function () { @@ -286,19 +286,19 @@ describe('RetailSpot Adapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.size; + let invalidBid = Object.assign({}, bid); + delete invalidBid.sizes; - expect(!!spec.isBidRequestValid(bid)).to.equal(false); + expect(!!spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placement': 0 }; - expect(!!spec.isBidRequestValid(bid)).to.equal(false); + expect(!!spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -333,7 +333,7 @@ describe('RetailSpot Adapter', function () { const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); const payload = JSON.parse(request.data); - expect(request.url).to.contain('https://ssp.retail-spot.io/prebid'); + expect(request.url).to.contain('https://hbapi.retailspotads.com/'); expect(request.method).to.equal('POST'); expect(payload).to.deep.equal(bidderRequest); @@ -344,7 +344,7 @@ describe('RetailSpot Adapter', function () { const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); const payload = JSON.parse(request.data); - expect(request.url).to.contain('https://ssp.retail-spot.io/prebid'); + expect(request.url).to.contain('https://hbapi.retailspotads.com/'); expect(request.method).to.equal('POST'); expect(payload).to.deep.equal(bidderRequest); @@ -355,7 +355,7 @@ describe('RetailSpot Adapter', function () { const request = spec.buildRequests(bidRequestMultiPlacements, bidderRequest); const payload = JSON.parse(request.data); - expect(request.url).to.contain('https://ssp.retail-spot.io/prebid'); + expect(request.url).to.contain('https://hbapi.retailspotads.com/'); expect(request.method).to.equal('POST'); expect(payload).to.deep.equal(bidderRequest); diff --git a/test/spec/modules/rewardedInterestIdSystem_spec.js b/test/spec/modules/rewardedInterestIdSystem_spec.js new file mode 100644 index 00000000000..b6ce1e03f76 --- /dev/null +++ b/test/spec/modules/rewardedInterestIdSystem_spec.js @@ -0,0 +1,190 @@ +import sinon from 'sinon'; +import {expect} from 'chai'; +import * as utils from 'src/utils.js'; +import {attachIdSystem} from 'modules/userId'; +import {createEidsArray} from 'modules/userId/eids'; +import { + MODULE_NAME, + SOURCE, + getRewardedInterestApi, + watchRewardedInterestApi, + getRewardedInterestId, + apiNotAvailable, + rewardedInterestIdSubmodule +} from 'modules/rewardedInterestIdSystem.js'; + +describe('rewardedInterestIdSystem', () => { + const mockUserId = 'rewarded_interest_id'; + const mockApi = { + getApiVersion: () => '1.0', + getIdentityToken: () => Promise.resolve(mockUserId) + }; + const errorApiNotFound = `${MODULE_NAME} module: Rewarded Interest API not found`; + const errorIdFetch = `${MODULE_NAME} module: ID fetch encountered an error`; + let mockReadySate = 'complete'; + let logErrorSpy; + let callbackSpy; + + before(() => { + Object.defineProperty(document, 'readyState', { + get() { + return mockReadySate; + }, + }); + }); + + beforeEach(() => { + logErrorSpy = sinon.spy(utils, 'logError'); + callbackSpy = sinon.spy(); + }); + + afterEach(() => { + mockReadySate = 'complete'; + delete window.__riApi; + logErrorSpy.restore(); + }); + + describe('getRewardedInterestApi', () => { + it('should return Rewarded Interest Api if exists', () => { + expect(getRewardedInterestApi()).to.be.undefined; + window.__riApi = {}; + expect(getRewardedInterestApi()).to.be.undefined; + window.__riApi.getIdentityToken = mockApi.getIdentityToken; + expect(getRewardedInterestApi()).to.deep.equal(window.__riApi); + }); + }); + + describe('watchRewardedInterestApi', () => { + it('should execute callback when __riApi is set', () => { + watchRewardedInterestApi(callbackSpy); + expect(window.__riApi).to.be.undefined; + window.__riApi = mockApi; + expect(callbackSpy.calledOnceWithExactly(mockApi)).to.be.true; + expect(getRewardedInterestApi()).to.deep.equal(mockApi); + }); + }); + + describe('getRewardedInterestId', () => { + it('should get id from API and pass it to callback', async () => { + await getRewardedInterestId(mockApi, callbackSpy); + expect(callbackSpy.calledOnceWithExactly(mockUserId)).to.be.true; + }); + }); + + describe('apiNotAvailable', () => { + it('should call callback without ID and log error', () => { + apiNotAvailable(callbackSpy); + expect(callbackSpy.calledOnceWithExactly()).to.be.true; + expect(logErrorSpy.calledOnceWithExactly(errorApiNotFound)).to.be.true; + }); + }); + + describe('rewardedInterestIdSubmodule.name', () => { + it('should expose the name of the submodule', () => { + expect(rewardedInterestIdSubmodule).to.be.an.instanceof(Object); + expect(rewardedInterestIdSubmodule.name).to.equal(MODULE_NAME); + }); + }); + + describe('rewardedInterestIdSubmodule.decode', () => { + it('should wrap the given value inside an object literal', () => { + expect(rewardedInterestIdSubmodule.decode(mockUserId)).to.deep.equal({ [MODULE_NAME]: mockUserId }); + expect(rewardedInterestIdSubmodule.decode('')).to.be.undefined; + expect(rewardedInterestIdSubmodule.decode(null)).to.be.undefined; + }); + }); + + describe('rewardedInterestIdSubmodule.getId', () => { + it('should return object with callback property', () => { + const idResponse = rewardedInterestIdSubmodule.getId(); + expect(idResponse).to.be.an.instanceof(Object); + expect(idResponse).to.have.property('callback'); + expect(idResponse.callback).to.be.a('function'); + }); + + it('API not found, window loaded', async () => { + const idResponse = rewardedInterestIdSubmodule.getId(); + idResponse.callback(callbackSpy); + await Promise.resolve(); + expect(callbackSpy.calledOnceWithExactly()).to.be.true; + expect(logErrorSpy.calledOnceWithExactly(errorApiNotFound)).to.be.true; + }); + + it('API not found, window not loaded', async () => { + mockReadySate = 'loading'; + const idResponse = rewardedInterestIdSubmodule.getId(); + idResponse.callback(callbackSpy); + window.dispatchEvent(new Event('load')); + expect(callbackSpy.calledOnceWithExactly()).to.be.true; + expect(logErrorSpy.calledOnceWithExactly(errorApiNotFound)).to.be.true; + }); + + it('API is set before getId, getIdentityToken return error', async () => { + const error = Error(); + window.__riApi = {getIdentityToken: () => Promise.reject(error)}; + const idResponse = rewardedInterestIdSubmodule.getId(); + idResponse.callback(callbackSpy); + await window.__riApi.getIdentityToken().catch(() => {}); + expect(callbackSpy.calledOnceWithExactly()).to.be.true; + expect(logErrorSpy.calledOnceWithExactly(errorIdFetch, error)).to.be.true; + }); + + it('API is set after getId, getIdentityToken return error', async () => { + const error = Error(); + mockReadySate = 'loading'; + const idResponse = rewardedInterestIdSubmodule.getId(); + idResponse.callback(callbackSpy); + window.__riApi = {getIdentityToken: () => Promise.reject(error)}; + await window.__riApi.getIdentityToken().catch(() => {}); + expect(callbackSpy.calledOnceWithExactly()).to.be.true; + expect(logErrorSpy.calledOnceWithExactly(errorIdFetch, error)).to.be.true; + }); + + it('API is set before getId, getIdentityToken return user ID', async () => { + window.__riApi = mockApi; + const idResponse = rewardedInterestIdSubmodule.getId(); + idResponse.callback(callbackSpy); + await mockApi.getIdentityToken(); + expect(callbackSpy.calledOnceWithExactly(mockUserId)).to.be.true; + }); + + it('API is set after getId, getIdentityToken return user ID', async () => { + mockReadySate = 'loading'; + const idResponse = rewardedInterestIdSubmodule.getId(); + idResponse.callback(callbackSpy); + window.__riApi = mockApi; + window.dispatchEvent(new Event('load')); + await window.__riApi.getIdentityToken().catch(() => {}); + expect(callbackSpy.calledOnceWithExactly(mockUserId)).to.be.true; + }); + }); + + describe('rewardedInterestIdSubmodule.eids', () => { + it('should expose the eids of the submodule', () => { + expect(rewardedInterestIdSubmodule).to.have.property('eids'); + expect(rewardedInterestIdSubmodule.eids).to.be.a('object'); + expect(rewardedInterestIdSubmodule.eids).to.deep.equal({ + [MODULE_NAME]: { + source: SOURCE, + atype: 3, + }, + }); + }); + + it('createEidsArray', () => { + attachIdSystem(rewardedInterestIdSubmodule); + const eids = createEidsArray({ + [MODULE_NAME]: mockUserId + }); + expect(eids).to.be.a('array'); + expect(eids.length).to.equal(1); + expect(eids[0]).to.deep.equal({ + source: SOURCE, + uids: [{ + id: mockUserId, + atype: 3, + }] + }); + }); + }); +}); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index d2b173f53df..e4fd11d8604 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -35,6 +35,53 @@ describe('Richaudience adapter tests', function () { user: {} }]; + var DEFAULT_PARAMS_NEW_DSA = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + 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', + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: 2, + pubrender: 1, + datatopub: 1, + transparency: [ + { + domain: 'richaudience.com', + dsaparams: [1, 3, 6] + }, + { + domain: 'adpone.com', + dsaparams: [8, 10, 12] + }, + { + domain: 'sunmedia.com', + dsaparams: [14, 16, 18] + } + ] + } + } + } + }, + user: {} + }]; + var DEFAULT_PARAMS_NEW_SIZES_GPID = [{ adUnitCode: 'test-div', bidId: '2c7c8e9c900244', @@ -199,6 +246,66 @@ describe('Richaudience adapter tests', function () { transactionId: '29df2112-348b-4961-8863-1b33684d95e6' }]; + var BID_PARAMS_EIDS = [{ + 'bidder': 'richaudience', + 'params': { + 'pid': 'IHOhChZNuI', + 'supplyType': 'site' + }, + 'userIdAsEids': [], + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + }] + + var id5 = { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'id5-string-cookie', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'id5-pba', + 'abTestingControlGroup': false + } + } + ] + } + + var first_id = { + 'source': 'first-id.fr', + 'uids': [ + { + 'id': 'value read from cookie or local storage', + 'atype': 1, + 'ext': { + 'stype': 'ppuid' + } + } + ] + } + + var three_party_provided = { + 'source': '3rdpartyprovided.com', + 'uids': [ + { + 'id': 'value read from cookie or local storage', + 'atype': 3, + 'ext': { + 'stype': 'dmp' + } + } + ] + } + var BID_RESPONSE = { body: { cpm: 1.50, @@ -211,7 +318,7 @@ describe('Richaudience adapter tests', function () { currency: 'USD', ttl: 300, dealId: 'dealId', - adomain: 'richaudience.com' + adomain: ['richaudience.com'] } }; @@ -227,7 +334,7 @@ describe('Richaudience adapter tests', function () { ttl: 300, vastXML: '', dealId: 'dealId', - adomain: 'richaudience.com' + adomain: ['richaudience.com'] } }; @@ -242,7 +349,7 @@ describe('Richaudience adapter tests', function () { } } - it('Referer undefined', function() { + it('Referer undefined', function () { config.setConfig({ 'currency': {'adServerCurrency': 'USD'} }) @@ -259,7 +366,7 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('referer').and.to.equal(null); }) - it('Verify build request to prebid 3.0 display test', function() { + it('Verify build request to prebid 3.0 display test', function () { const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { gdprConsent: { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', @@ -301,7 +408,7 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('kws').and.to.equal('key1=value1;key2=value2'); }) - it('Verify build request to prebid video inestream', function() { + it('Verify build request to prebid video inestream', function () { const request = spec.buildRequests(DEFAULT_PARAMS_VIDEO_IN, { gdprConsent: { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', @@ -320,7 +427,7 @@ describe('Richaudience adapter tests', function () { expect(requestContent.videoData).to.have.property('format').and.to.equal('instream'); }) - it('Verify build request to prebid video outstream', function() { + it('Verify build request to prebid video outstream', function () { const request = spec.buildRequests(DEFAULT_PARAMS_VIDEO_OUT, { gdprConsent: { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', @@ -420,8 +527,8 @@ describe('Richaudience adapter tests', function () { pd: 'MT1iNTBjY...' // optional, see table below for a link to how to generate this }, storage: { - type: 'html5', // "html5" is the required storage type - name: 'id5id', // "id5id" is the required storage name + type: 'html5', // 'html5' is the required storage type + name: 'id5id', // 'id5id' is the required storage name expires: 90, // storage lasts for 90 days refreshInSeconds: 8 * 3600 // refresh ID every 8 hours to ensure it's fresh } @@ -429,213 +536,55 @@ describe('Richaudience adapter tests', function () { auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules } }); - it('Verify build id5', function () { - var request; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: 1 }; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - var requestContent = JSON.parse(request[0].data); - - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: [] }; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: null }; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: {} }; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = null; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = {}; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - }); - - it('Verify build pubCommonId', function () { - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.pubcid = 'pub_common_user_id'; - - var request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + it('Verify build return empty users', function () { + var request = spec.buildRequests(BID_PARAMS_EIDS, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); - expect(requestContent.user).to.deep.equal([{ - 'userId': 'pub_common_user_id', - 'source': 'pubcommon' - }]); + expect(requestContent.eids).to.deep.equal([]); + }) - var request; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.pubcid = 1; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + it('Verify build return all users', function () { + BID_PARAMS_EIDS[0].userIdAsEids = [id5, three_party_provided, first_id] + var request = spec.buildRequests(BID_PARAMS_EIDS, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.pubcid = []; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.pubcid = null; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.pubcid = {}; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - }); - - it('Verify build criteoId', function () { - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.criteoId = 'criteo-user-id'; + expect(requestContent.eids).to.deep.equal([id5, three_party_provided, first_id]); + }) - var request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + it('Verify build return first-id.fr users', function () { + BID_PARAMS_EIDS[0].userIdAsEids = [first_id] + var request = spec.buildRequests(BID_PARAMS_EIDS, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); + expect(requestContent.eids).to.deep.equal([first_id]); + }) - expect(requestContent.user).to.deep.equal([{ - 'userId': 'criteo-user-id', - 'source': 'criteo.com' - }]); - - var request; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.criteoId = 1; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + it('Verify build return first-id.fr users', function () { + BID_PARAMS_EIDS[0].userIdAsEids = [first_id] + var request = spec.buildRequests(BID_PARAMS_EIDS, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.criteoId = []; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.criteoId = null; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.criteoId = {}; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - }); - - it('Verify build identityLink', function () { - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = 'identity-link-user-id'; + expect(requestContent.eids).to.deep.equal([first_id]); + }) - var request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + it('Verify build return users []', function () { + BID_PARAMS_EIDS[0].userIdAsEids = [] + var request = spec.buildRequests(BID_PARAMS_EIDS, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); + expect(requestContent.eids).to.deep.equal([]); + }) - expect(requestContent.user).to.deep.equal([{ - 'userId': 'identity-link-user-id', - 'source': 'liveramp.com' - }]); - - var request; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = 1; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + it('Verify build return users null', function () { + BID_PARAMS_EIDS[0].userIdAsEids = null + var request = spec.buildRequests(BID_PARAMS_EIDS, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.criteoId = []; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = null; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = {}; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - }); - it('Verify build liveIntentId', function () { - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = 'identity-link-user-id'; - - var request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - var requestContent = JSON.parse(request[0].data) - - expect(requestContent.user).to.deep.equal([{ - 'userId': 'identity-link-user-id', - 'source': 'liveramp.com' - }]); + expect(requestContent.eids).to.deep.equal([]); + }) - var request; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = 1; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + it('Verify build return users {}', function () { + BID_PARAMS_EIDS[0].userIdAsEids = null + var request = spec.buildRequests(BID_PARAMS_EIDS, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.criteoId = []; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = null; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = {}; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - }); - it('Verify build TradeDesk', function () { - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.tdid = 'tdid-user-id'; - - var request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - var requestContent = JSON.parse(request[0].data) - - expect(requestContent.user).to.deep.equal([{ - 'userId': 'tdid-user-id', - 'source': 'adserver.org' - }]); - - request; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = 1; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.criteoId = []; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = null; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.idl_env = {}; - request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - requestContent = JSON.parse(request[0].data); - expect(requestContent.user.eids).to.equal(undefined); - }); + expect(requestContent.eids).to.deep.equal([]); + }) }); it('Verify interprete response', function () { @@ -663,7 +612,7 @@ describe('Richaudience adapter tests', function () { expect(bid.currency).to.equal('USD'); expect(bid.ttl).to.equal(300); expect(bid.dealId).to.equal('dealId'); - expect(bid.meta).to.equal('richaudience.com'); + expect(bid.meta.advertiserDomains[0]).to.equal('richaudience.com'); }); it('no banner media response inestream', function () { @@ -692,7 +641,7 @@ describe('Richaudience adapter tests', function () { expect(bid.currency).to.equal('USD'); expect(bid.ttl).to.equal(300); expect(bid.dealId).to.equal('dealId'); - expect(bid.meta).to.equal('richaudience.com'); + expect(bid.meta.advertiserDomains[0]).to.equal('richaudience.com'); }); it('no banner media response outstream', function () { @@ -749,7 +698,7 @@ describe('Richaudience adapter tests', function () { it('Verifies bidder aliases', function () { expect(spec.aliases).to.have.lengthOf(1); - expect(spec.aliases[0]).to.equal('ra'); + expect(spec.aliases[0]).to.deep.equal({code: 'ra', gvlid: 108}); }); it('Verifies bidder gvlid', function () { @@ -853,7 +802,7 @@ describe('Richaudience adapter tests', function () { })).to.equal(true); }); - it('should pass schain', function() { + it('should pass schain', function () { let schain = { 'ver': '1.0', 'complete': 1, @@ -893,7 +842,22 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('schain').to.deep.equal(schain); }) - it('should pass gpid', function() { + it('should pass DSA', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_DSA, { + 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('dsa').property('dsarequired').and.to.equal(2) + expect(requestContent).to.have.property('dsa').property('pubrender').and.to.equal(1); + expect(requestContent).to.have.property('dsa').property('datatopub').and.to.equal(1); + expect(requestContent.dsa.transparency[0]).to.have.property('domain').and.to.equal('richaudience.com'); + }) + + it('should pass 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', @@ -906,11 +870,11 @@ describe('Richaudience adapter tests', function () { }) describe('onTimeout', function () { - beforeEach(function() { + beforeEach(function () { sinon.stub(utils, 'triggerPixel'); }); - afterEach(function() { + afterEach(function () { utils.triggerPixel.restore(); }); it('onTimeout exist as a function', () => { @@ -928,7 +892,7 @@ describe('Richaudience adapter tests', function () { beforeEach(function () { sandbox = sinon.sandbox.create(); }); - afterEach(function() { + afterEach(function () { sandbox.restore(); }); it('Verifies user syncs iframe include', function () { @@ -940,7 +904,8 @@ describe('Richaudience adapter tests', function () { iframeEnabled: true }, [BID_RESPONSE], { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - gdprApplies: true}, + gdprApplies: true + }, ); expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('iframe'); @@ -980,7 +945,8 @@ describe('Richaudience adapter tests', function () { iframeEnabled: true }, [BID_RESPONSE], { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - gdprApplies: true}, + gdprApplies: true + }, ); expect(syncs).to.have.lengthOf(0); @@ -1088,7 +1054,12 @@ describe('Richaudience adapter tests', function () { it('Verifies user syncs iframe/image include', function () { config.setConfig({ - 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}, image: {bidders: '*', filter: 'include'}}} + 'userSync': { + filterSettings: { + iframe: {bidders: '*', filter: 'include'}, + image: {bidders: '*', filter: 'include'} + } + } }) var syncs = spec.getUserSyncs({ @@ -1096,7 +1067,8 @@ describe('Richaudience adapter tests', function () { pixelEnabled: true }, [BID_RESPONSE], { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - gdprApplies: true}, + gdprApplies: true + }, ); expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('iframe'); @@ -1134,7 +1106,12 @@ describe('Richaudience adapter tests', function () { it('Verifies user syncs iframe/image exclude', function () { config.setConfig({ - 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'exclude'}, image: {bidders: '*', filter: 'exclude'}}} + 'userSync': { + filterSettings: { + iframe: {bidders: '*', filter: 'exclude'}, + image: {bidders: '*', filter: 'exclude'} + } + } }) var syncs = spec.getUserSyncs({ @@ -1142,7 +1119,8 @@ describe('Richaudience adapter tests', function () { pixelEnabled: true }, [BID_RESPONSE], { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - gdprApplies: true}, + gdprApplies: true + }, ); expect(syncs).to.have.lengthOf(0); @@ -1179,7 +1157,12 @@ describe('Richaudience adapter tests', function () { it('Verifies user syncs iframe exclude / image include', function () { config.setConfig({ - 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'exclude'}, image: {bidders: '*', filter: 'include'}}} + 'userSync': { + filterSettings: { + iframe: {bidders: '*', filter: 'exclude'}, + image: {bidders: '*', filter: 'include'} + } + } }) var syncs = spec.getUserSyncs({ @@ -1187,7 +1170,8 @@ describe('Richaudience adapter tests', function () { pixelEnabled: true }, [BID_RESPONSE], { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - gdprApplies: true}, + gdprApplies: true + }, ); expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('image'); @@ -1225,7 +1209,12 @@ describe('Richaudience adapter tests', function () { it('Verifies user syncs iframe include / image exclude', function () { config.setConfig({ - 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}, image: {bidders: '*', filter: 'exclude'}}} + 'userSync': { + filterSettings: { + iframe: {bidders: '*', filter: 'include'}, + image: {bidders: '*', filter: 'exclude'} + } + } }) var syncs = spec.getUserSyncs({ @@ -1233,7 +1222,8 @@ describe('Richaudience adapter tests', function () { pixelEnabled: true }, [BID_RESPONSE], { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - gdprApplies: true}, + gdprApplies: true + }, ); expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('iframe'); @@ -1276,7 +1266,8 @@ describe('Richaudience adapter tests', function () { var syncs = spec.getUserSyncs({iframeEnabled: true}, [BID_RESPONSE], { gppString: 'DBABL~BVVqAAEABgA.QA', - applicableSections: [7]}, + applicableSections: [7] + }, ); expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('iframe'); @@ -1287,17 +1278,22 @@ describe('Richaudience adapter tests', function () { var syncs = spec.getUserSyncs({pixelEnabled: true}, [BID_RESPONSE], { gppString: 'DBABL~BVVqAAEABgA.QA', - applicableSections: [7, 5]}, + applicableSections: [7, 5] + }, ); expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('image'); }); it('Verifies user syncs URL image include with GPP', function () { - const gppConsent = { gppString: 'DBACMYA~CP5P4cAP5P4cAPoABAESAlEAAAAAAAAAAAAAA2QAQA2ADZABADYAAAAA.QA2QAQA2AAAA.IA2QAQA2AAAA~BP5P4cAP5P4cAPoABABGBACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA', applicableSections: [0] }; + const gppConsent = { + gppString: 'DBACMYA~CP5P4cAP5P4cAPoABAESAlEAAAAAAAAAAAAAA2QAQA2ADZABADYAAAAA.QA2QAQA2AAAA.IA2QAQA2AAAA~BP5P4cAP5P4cAPoABABGBACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA', + applicableSections: [0] + }; const result = spec.getUserSyncs({pixelEnabled: true}, undefined, undefined, undefined, gppConsent); expect(result).to.deep.equal([{ - type: 'image', url: `https://sync.richaudience.com/bf7c142f4339da0278e83698a02b0854/?referrer=http%3A%2F%2Fdomain.com&gpp=DBACMYA~CP5P4cAP5P4cAPoABAESAlEAAAAAAAAAAAAAA2QAQA2ADZABADYAAAAA.QA2QAQA2AAAA.IA2QAQA2AAAA~BP5P4cAP5P4cAPoABABGBACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA&gpp_sid=0` + type: 'image', + url: `https://sync.richaudience.com/bf7c142f4339da0278e83698a02b0854/?referrer=http%3A%2F%2Fdomain.com&gpp=DBACMYA~CP5P4cAP5P4cAPoABAESAlEAAAAAAAAAAAAAA2QAQA2ADZABADYAAAAA.QA2QAQA2AAAA.IA2QAQA2AAAA~BP5P4cAP5P4cAPoABABGBACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA&gpp_sid=0` }]); }); }) diff --git a/test/spec/modules/ringieraxelspringerBidAdapter_spec.js b/test/spec/modules/ringieraxelspringerBidAdapter_spec.js index 1ddd62b8102..e679c0b3bde 100644 --- a/test/spec/modules/ringieraxelspringerBidAdapter_spec.js +++ b/test/spec/modules/ringieraxelspringerBidAdapter_spec.js @@ -301,7 +301,7 @@ describe('ringieraxelspringerBidAdapter', function () { } }]; const resp = spec.interpretResponse({body: {gctx: '1234567890'}}, bidRequest); - expect(resp).to.deep.equal({bids: [], fledgeAuctionConfigs: auctionConfigs}); + expect(resp).to.deep.equal({bids: [], paapi: auctionConfigs}); }); }); diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 3cb5cb5c154..a3fef50f825 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -2,8 +2,9 @@ import { expect } from 'chai'; import { spec } from 'modules/riseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; import * as utils from 'src/utils.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native'; const ENDPOINT = 'https://hb.yellowblue.io/hb-multi'; const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-multi-test'; @@ -72,7 +73,6 @@ describe('riseAdapter', function () { 'plcmt': 1 } }, - 'vastXml': '"..."' }, { 'bidder': spec.code, @@ -89,7 +89,59 @@ describe('riseAdapter', function () { 'banner': { } }, - 'ad': '""' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ 300, 250 ] + ] + }, + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream', + 'plcmt': 1 + }, + 'native': { + 'ortb': { + 'assets': [ + { + 'id': 1, + 'required': 1, + 'img': { + 'type': 3, + 'w': 300, + 'h': 200, + } + }, + { + 'id': 2, + 'required': 1, + 'title': { + 'len': 80 + } + }, + { + 'id': 3, + 'required': 1, + 'data': { + 'type': 1 + } + } + ] + } + }, + }, } ]; @@ -111,6 +163,7 @@ describe('riseAdapter', function () { const bidderRequest = { bidderCode: 'rise', + ortb2: {device: {}}, } const placementId = '12345678'; const api = [1, 2]; @@ -174,10 +227,10 @@ describe('riseAdapter', function () { }); it('should send the correct mimes array', function () { - bidRequests[1].mediaTypes.banner.mimes = mimes; + bidRequests[0].mediaTypes.video.mimes = mimes; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bids[1].mimes).to.be.an('array'); - expect(request.data.bids[1].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); + expect(request.data.bids[0].mimes).to.be.an('array'); + expect(request.data.bids[0].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); }); it('should send the correct protocols array', function () { @@ -193,12 +246,21 @@ describe('riseAdapter', function () { expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) expect(request.data.bids[1].sizes).to.be.an('array'); expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + expect(request.data.bids[2].sizes).to.be.an('array'); + expect(request.data.bids[2].sizes).to.eql(bidRequests[2].sizes) + }); + + it('should send nativeOrtbRequest in native bid request', function () { + decorateAdUnitsWithNativeParams(bidRequests) + const request = spec.buildRequests(bidRequests, bidderRequest); + assert.deepEqual(request.data.bids[2].nativeOrtbRequest, bidRequests[2].mediaTypes.native.ortb) }); it('should send the correct media type', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.bids[0].mediaType).to.equal(VIDEO) expect(request.data.bids[1].mediaType).to.equal(BANNER) + expect(request.data.bids[2].mediaType.split(',')).to.include.members([VIDEO, NATIVE, BANNER]) }); it('should respect syncEnabled option', function() { @@ -434,6 +496,31 @@ describe('riseAdapter', function () { expect(request.data.bids[0].sua).to.not.exist; }); + it('should send ORTB2 device data in bid request', function() { + const ortb2 = { + device: { + 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'}, + }, + }; + + const request = spec.buildRequests(bidRequests, { + ...bidderRequest, + ortb2, + }); + + expect(request.data.params.device).to.deep.equal(ortb2.device); + }); + describe('COPPA Param', function() { it('should set coppa equal 0 in bid request if coppa is set to false', function() { const request = spec.buildRequests(bidRequests, bidderRequest); @@ -466,6 +553,8 @@ describe('riseAdapter', function () { height: 480, requestId: '21e12606d47ba7', adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', mediaType: VIDEO }, { @@ -475,7 +564,31 @@ describe('riseAdapter', function () { height: 250, requestId: '21e12606d47ba7', adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', mediaType: BANNER + }, + { + cpm: 12.5, + width: 300, + height: 200, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + native: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg' + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } }] }; @@ -486,7 +599,7 @@ describe('riseAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -501,10 +614,10 @@ describe('riseAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -515,10 +628,42 @@ describe('riseAdapter', function () { ad: '""' }; + const expectedNativeResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 300, + height: 200, + ttl: TTL, + creativeId: 'creative-id', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + meta: { + mediaType: NATIVE, + advertiserDomains: ['abc.com'] + }, + native: { + ortb: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg', + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } + }, + }; + it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); + expect(result[2]).to.deep.equal(expectedNativeResponse); }); it('video type should have vastXml key', function () { @@ -530,6 +675,11 @@ describe('riseAdapter', function () { const result = spec.interpretResponse({ body: response }); expect(result[1].ad).to.equal(expectedBannerResponse.ad) }); + + it('native type should have native key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[2].native).to.eql(expectedNativeResponse.native) + }); }) describe('getUserSyncs', function() { diff --git a/test/spec/modules/robustaBidAdapter_spec.js b/test/spec/modules/robustaBidAdapter_spec.js new file mode 100644 index 00000000000..811a0d1b351 --- /dev/null +++ b/test/spec/modules/robustaBidAdapter_spec.js @@ -0,0 +1,150 @@ +import { spec } from 'modules/robustaBidAdapter.js'; +import { config } from 'src/config.js'; +import { deepClone } from 'src/utils.js'; + +describe('robustaBidAdapter', function () { + const validBidRequest = { + bidId: 'bid123', + params: { + lineItemId: '12345' + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }; + + const validBidderRequest = { + bidderCode: 'robusta', + auctionId: 'auction123', + bidderRequestId: 'req123', + timeout: 3000, + gdprConsent: { + consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + gdprApplies: true + } + }; + + const validServerResponse = { + body: { + id: 'auction123', + seatbid: [{ + bid: [{ + mtype: 1, + id: 'bid123', + impid: 'bid123', + price: 0.5, + adm: '
ad
', + w: 300, + h: 250, + crid: 'creative123' + }] + }], + cur: 'USD' + } + }; + + describe('isBidRequestValid', function () { + it('should return true when lineItemId is present', function () { + expect(spec.isBidRequestValid(validBidRequest)).to.be.true; + }); + + it('should return false when lineItemId is missing', function () { + const bid = deepClone(validBidRequest); + delete bid.params.lineItemId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + it('should create request with correct structure', function () { + const requests = spec.buildRequests([validBidRequest], validBidderRequest); + + expect(requests).to.have.lengthOf(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.equal('//pbjs.baristartb.com/api/prebid'); + expect(requests[0].options.withCredentials).to.be.false; + }); + + it('should use custom rtbDomain if configured', function () { + config.setBidderConfig({ bidders: ['robusta'], config: { rtbDomain: 'custom.domain.com' } }); + const requests = config.runWithBidder(spec.code, () => spec.buildRequests([validBidRequest], validBidderRequest)); + + expect(requests[0].url).to.equal('//custom.domain.com/api/prebid'); + config.resetConfig(); + }); + + it('should include bid params in imp.ext.params', function () { + const requests = spec.buildRequests([validBidRequest], validBidderRequest); + const imp = requests[0].data.imp[0]; + + expect(imp.ext.params).to.deep.equal(validBidRequest.params); + }); + }); + + describe('interpretResponse', function () { + it('should return valid bid response', function () { + const request = spec.buildRequests([validBidRequest], validBidderRequest)[0]; + const result = spec.interpretResponse(validServerResponse, request); + + expect(result.bids).to.be.an('array'); + expect(result.bids).to.have.lengthOf(1); + expect(result.bids[0]).to.include({ + requestId: 'bid123', + cpm: 0.5, + width: 300, + height: 250, + ad: '
ad
', + creativeId: 'creative123', + netRevenue: true, + ttl: 30, + currency: 'USD' + }); + }); + + it('should return empty bids array if no valid bids', function () { + const emptyResponse = { body: { id: 'auction123', seatbid: [] } }; + const request = spec.buildRequests([validBidRequest], validBidderRequest)[0]; + const result = spec.interpretResponse(emptyResponse, request); + + expect(result.bids).to.be.an('array'); + expect(result.bids).to.have.lengthOf(0); + }); + }); + + describe('getUserSyncs', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA' + }; + + it('should return iframe sync when iframeEnabled', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], gdprConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.include('//sync.baristartb.com/api/sync?'); + expect(syncs[0].url).to.include('gdpr=1'); + expect(syncs[0].url).to.include('gdpr_consent=BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA'); + }); + + it('should return pixel sync when pixelEnabled', function () { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [], gdprConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + }); + + it('should use custom syncDomain if configured', function () { + config.setBidderConfig({ bidders: ['robusta'], config: { syncDomain: 'custom.sync.com' } }); + const syncs = config.runWithBidder(spec.code, () => spec.getUserSyncs({ iframeEnabled: true }, [], gdprConsent)); + expect(syncs[0].url).to.include('//custom.sync.com/api/sync?'); + config.resetConfig(); + }); + + it('should handle missing gdprConsent', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs[0].url).to.not.include('gdpr'); + expect(syncs[0].url).to.not.include('gdpr_consent'); + }); + }); +}); diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index 77b746b9b69..dded1fe15a0 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -1,8 +1,9 @@ import { expect } from 'chai'; -import { OPENRTB, spec } from 'modules/rtbhouseBidAdapter.js'; +import { spec } from 'modules/rtbhouseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; import { mergeDeep } from '../../../src/utils'; +import { OPENRTB } from '../../../libraries/precisoUtils/bidNativeUtils'; describe('RTBHouseAdapter', () => { const adapter = newBidder(spec); @@ -43,12 +44,12 @@ describe('RTBHouseAdapter', () => { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'someIncorrectParam': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -460,7 +461,7 @@ describe('RTBHouseAdapter', () => { let bidRequest = Object.assign([], bidRequests); delete bidRequest[0].params.test; config.setConfig({ fledgeConfig: true }); - const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: true }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, paapi: { enabled: true } }); expect(request.url).to.equal('https://prebid-eu.creativecdn.com/bidder/prebidfledge/bids'); expect(request.method).to.equal('POST'); }); @@ -470,7 +471,7 @@ describe('RTBHouseAdapter', () => { delete bidRequest[0].params.test; config.setConfig({ fledgeConfig: false }); - const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: true }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, paapi: {enabled: true} }); const data = JSON.parse(request.data); expect(data.ext).to.exist.and.to.be.a('object'); expect(data.ext.fledge_config).to.exist.and.to.be.a('object'); @@ -480,7 +481,7 @@ describe('RTBHouseAdapter', () => { expect(data.ext.fledge_config.sellerTimeout).to.equal(500); }); - it('sets a fledgeConfig object values when available from config', function () { + it('sets request.ext.fledge_config object values when available from fledgeConfig', function () { let bidRequest = Object.assign([], bidRequests); delete bidRequest[0].params.test; @@ -490,7 +491,7 @@ describe('RTBHouseAdapter', () => { decisionLogicUrl: 'https://sellers.domain/decision.url' } }); - const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: true }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, paapi: {enabled: true} }); const data = JSON.parse(request.data); expect(data.ext).to.exist.and.to.be.a('object'); expect(data.ext.fledge_config).to.exist.and.to.be.a('object'); @@ -500,13 +501,56 @@ describe('RTBHouseAdapter', () => { expect(data.ext.fledge_config.sellerTimeout).to.not.exist; }); + it('sets request.ext.fledge_config object values when available from paapiConfig', function () { + let bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + + config.setConfig({ + paapiConfig: { + seller: 'https://sellers.domain', + decisionLogicUrl: 'https://sellers.domain/decision.url' + } + }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, paapi: {enabled: true} }); + const data = JSON.parse(request.data); + expect(data.ext).to.exist.and.to.be.a('object'); + expect(data.ext.fledge_config).to.exist.and.to.be.a('object'); + expect(data.ext.fledge_config).to.contain.keys('seller', 'decisionLogicUrl'); + expect(data.ext.fledge_config.seller).to.equal('https://sellers.domain'); + expect(data.ext.fledge_config.decisionLogicUrl).to.equal('https://sellers.domain/decision.url'); + expect(data.ext.fledge_config.sellerTimeout).to.not.exist; + }); + + it('sets request.ext.fledge_config object values when available from paapiConfig rather than from fledgeConfig if both exist', function () { + let bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + + config.setConfig({ + paapiConfig: { + seller: 'https://paapiconfig.sellers.domain', + decisionLogicUrl: 'https://paapiconfig.sellers.domain/decision.url' + }, + fledgeConfig: { + seller: 'https://fledgeconfig.sellers.domain', + decisionLogicUrl: 'https://fledgeconfig.sellers.domain/decision.url' + } + }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, paapi: {enabled: true} }); + const data = JSON.parse(request.data); + expect(data.ext).to.exist.and.to.be.a('object'); + expect(data.ext.fledge_config).to.exist.and.to.be.a('object'); + expect(data.ext.fledge_config).to.contain.keys('seller', 'decisionLogicUrl'); + expect(data.ext.fledge_config.seller).to.equal('https://paapiconfig.sellers.domain'); + expect(data.ext.fledge_config.decisionLogicUrl).to.equal('https://paapiconfig.sellers.domain/decision.url'); + }); + it('when FLEDGE is disabled, should not send imp.ext.ae', function () { let bidRequest = Object.assign([], bidRequests); delete bidRequest[0].params.test; bidRequest[0].ortb2Imp = { ext: { ae: 2 } }; - const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: false }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, paapi: {enabled: false} }); let data = JSON.parse(request.data); if (data.imp[0].ext) { expect(data.imp[0].ext).to.not.have.property('ae'); @@ -519,7 +563,7 @@ describe('RTBHouseAdapter', () => { bidRequest[0].ortb2Imp = { ext: { ae: 2 } }; - const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: true }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, paapi: {enabled: true} }); let data = JSON.parse(request.data); expect(data.imp[0].ext.ae).to.equal(2); }); @@ -782,9 +826,39 @@ describe('RTBHouseAdapter', () => { it('should return FLEDGE auction_configs alongside bids', function () { expect(response).to.have.property('bids'); - expect(response).to.have.property('fledgeAuctionConfigs'); - expect(response.fledgeAuctionConfigs.length).to.equal(1); - expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test-bid-id'); + expect(response).to.have.property('paapi'); + expect(response.paapi.length).to.equal(1); + expect(response.paapi[0].bidId).to.equal('test-bid-id'); + }); + }); + + context('when the response contains FLEDGE auction config and bid request has additional signals in paapiConfig', function () { + let bidderRequest; + config.setConfig({ + paapiConfig: { + interestGroupBuyers: ['https://buyer1.com'], + perBuyerSignals: { + 'https://buyer1.com': { signal: 1 } + }, + customSignal: 1 + } + }); + let response = spec.interpretResponse({body: fledgeResponse}, {bidderRequest}); + + it('should have 2 buyers in interestGroupBuyers', function () { + expect(response.paapi[0].config.interestGroupBuyers.length).to.equal(2); + expect(response.paapi[0].config.interestGroupBuyers).to.have.members(['https://buyer1.com', 'https://buyer-domain.com']); + }); + + it('should have 2 perBuyerSignals with proper values', function () { + expect(response.paapi[0].config.perBuyerSignals).to.contain.keys('https://buyer1.com', 'https://buyer-domain.com'); + expect(response.paapi[0].config.perBuyerSignals['https://buyer1.com']).to.deep.equal({ signal: 1 }); + expect(response.paapi[0].config.perBuyerSignals['https://buyer-domain.com']).to.deep.equal({}); + }); + + it('should contain any custom signal passed via paapiConfig', function () { + expect(response.paapi[0].config).to.contain.keys('customSignal'); + expect(response.paapi[0].config.customSignal).to.equal(1); }); }); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 55e8909f6c8..8b36256f6a6 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -6,21 +6,20 @@ import { resetUserSync, classifiedAsVideo, resetRubiConf, + resetImpIdMap, converter } from 'modules/rubiconBidAdapter.js'; -import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {find} from 'src/polyfill.js'; -import {createEidsArray} from 'modules/userId/eids.js'; import 'modules/schain.js'; -import 'modules/consentManagement.js'; +import 'modules/consentManagementTcf.js'; import 'modules/consentManagementUsp.js'; import 'modules/userId/index.js'; import 'modules/priceFloors.js'; import 'modules/multibid/index.js'; import adapterManager from 'src/adapterManager.js'; -import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {addFPDToBidderRequest} from '../../helpers/fpd.js'; import { deepClone } from '../../../src/utils.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid @@ -272,7 +271,7 @@ describe('the rubicon adapter', function () { }], criteoId: '1111', }; - bid.userIdAsEids = [ + const eids = [ { 'source': 'liveintent.com', 'uids': [ @@ -347,6 +346,7 @@ describe('the rubicon adapter', function () { ] } ]; + bidderRequest.ortb2 = {user: {ext: {eids}}}; return bidderRequest; } @@ -485,6 +485,7 @@ describe('the rubicon adapter', function () { utils.logError.restore(); config.resetConfig(); resetRubiConf(); + resetImpIdMap(); delete $$PREBID_GLOBAL$$.installedModules; }); @@ -515,7 +516,7 @@ describe('the rubicon adapter', function () { duplicate.bids[0].params.floor = 0.01; let [request] = spec.buildRequests(duplicate.bids, duplicate); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); @@ -547,9 +548,9 @@ describe('the rubicon adapter', function () { Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; if (value instanceof RegExp) { - expect(data[key]).to.match(value); + expect(data.get(key)).to.match(value); } else { - expect(data[key]).to.equal(value); + expect(data.get(key)).to.equal(value); } }); }); @@ -569,38 +570,38 @@ describe('the rubicon adapter', function () { }) ).to.be.true; - let data = parseQuery(request.data); - expect(data.rp_hard_floor).to.be.undefined; + let data = new URLSearchParams(request.data); + expect(data.get('rp_hard_floor')).to.be.null; // not an object should work and not send getFloorResponse = undefined; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - data = parseQuery(request.data); - expect(data.rp_hard_floor).to.be.undefined; + data = new URLSearchParams(request.data); + expect(data.get('rp_hard_floor')).to.be.null; // make it respond with a non USD floor should not send it getFloorResponse = {currency: 'EUR', floor: 1.0}; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - data = parseQuery(request.data); - expect(data.rp_hard_floor).to.be.undefined; + data = new URLSearchParams(request.data); + expect(data.get('rp_hard_floor')).to.be.null; // make it respond with a non USD floor should not send it getFloorResponse = {currency: 'EUR'}; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - data = parseQuery(request.data); - expect(data.rp_hard_floor).to.be.undefined; + data = new URLSearchParams(request.data); + expect(data.get('rp_hard_floor')).to.be.null; // make it respond with USD floor and string floor getFloorResponse = {currency: 'USD', floor: '1.23'}; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - data = parseQuery(request.data); - expect(data.rp_hard_floor).to.equal('1.23'); + data = new URLSearchParams(request.data); + expect(data.get('rp_hard_floor')).to.equal('1.23'); // make it respond with USD floor and num floor getFloorResponse = {currency: 'USD', floor: 1.23}; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - data = parseQuery(request.data); - expect(data.rp_hard_floor).to.equal('1.23'); + data = new URLSearchParams(request.data); + expect(data.get('rp_hard_floor')).to.equal('1.23'); }); it('should send rp_maxbids to AE if rubicon multibid config exists', function () { @@ -608,9 +609,9 @@ describe('the rubicon adapter', function () { multibidRequest.bidLimit = 5; let [request] = spec.buildRequests(multibidRequest.bids, multibidRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['rp_maxbids']).to.equal('5'); + expect(data.get('rp_maxbids')).to.equal('5'); }); it('should not send p_pos to AE if not params.position specified', function () { @@ -618,10 +619,10 @@ describe('the rubicon adapter', function () { delete noposRequest.bids[0].params.position; let [request] = spec.buildRequests(noposRequest.bids, noposRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['site_id']).to.equal('70608'); - expect(data['p_pos']).to.equal(undefined); + expect(data.get('site_id')).to.equal('70608'); + expect(data.get('p_pos')).to.equal(null); }); it('should not send p_pos to AE if not mediaTypes.banner.pos is invalid', function () { @@ -634,10 +635,10 @@ describe('the rubicon adapter', function () { delete bidRequest.bids[0].params.position; let [request] = spec.buildRequests(bidRequest.bids, bidRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['site_id']).to.equal('70608'); - expect(data['p_pos']).to.equal(undefined); + expect(data.get('site_id')).to.equal('70608'); + expect(data.get('p_pos')).to.equal(null); }); it('should send p_pos to AE if mediaTypes.banner.pos is valid', function () { @@ -650,10 +651,10 @@ describe('the rubicon adapter', function () { delete bidRequest.bids[0].params.position; let [request] = spec.buildRequests(bidRequest.bids, bidRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['site_id']).to.equal('70608'); - expect(data['p_pos']).to.equal('atf'); + expect(data.get('site_id')).to.equal('70608'); + expect(data.get('p_pos')).to.equal('atf'); }); it('should not send p_pos to AE if not params.position is invalid', function () { @@ -661,10 +662,10 @@ describe('the rubicon adapter', function () { badposRequest.bids[0].params.position = 'bad'; let [request] = spec.buildRequests(badposRequest.bids, badposRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['site_id']).to.equal('70608'); - expect(data['p_pos']).to.equal(undefined); + expect(data.get('site_id')).to.equal('70608'); + expect(data.get('p_pos')).to.equal(null); }); it('should correctly send p_pos in sra fashion', function() { @@ -693,9 +694,9 @@ describe('the rubicon adapter', function () { sraPosRequest.bids.push(bidCopy3); let [request] = spec.buildRequests(sraPosRequest.bids, sraPosRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['p_pos']).to.equal('atf;;btf;;'); + expect(data.get('p_pos')).to.equal('atf;;btf;;'); }); it('should correctly send cdep signal when requested', () => { @@ -703,9 +704,31 @@ describe('the rubicon adapter', function () { badposRequest.bids[0].ortb2 = {device: {ext: {cdep: 3}}}; let [request] = spec.buildRequests(badposRequest.bids, badposRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['o_cdep']).to.equal('3'); + expect(data.get('o_cdep')).to.equal('3'); + }); + + it('should correctly send ip signal when ortb2.device.ip is provided', () => { + const ipRequest = utils.deepClone(bidderRequest); + ipRequest.bids[0].ortb2 = { device: { ip: '123.45.67.89' } }; + + let [request] = spec.buildRequests(ipRequest.bids, ipRequest); + let data = new URLSearchParams(request.data); + + // Verify if 'ip' is correctly added to the request data + expect(data.get('ip')).to.equal('123.45.67.89'); + }); + + it('should correctly send ipv6 signal when ortb2.device.ipv6 is provided', () => { + const ipv6Request = utils.deepClone(bidderRequest); + ipv6Request.bids[0].ortb2 = { device: { ipv6: '2001:db8::ff00:42:8329' } }; + + let [request] = spec.buildRequests(ipv6Request.bids, ipv6Request); + let data = new URLSearchParams(request.data); + + // Verify if 'ipv6' is correctly added to the request data + expect(data.get('ipv6')).to.equal('2001:db8::ff00:42:8329'); }); it('ad engine query params should be ordered correctly', function () { @@ -741,15 +764,15 @@ describe('the rubicon adapter', function () { 'tg_i.rating': '5-star', 'tg_i.prodtype': 'tech,mobile', 'rf': 'localhost', - 'p_geo.latitude': undefined, - 'p_geo.longitude': undefined + 'p_geo.latitude': null, + 'p_geo.longitude': null }; sandbox.stub(Math, 'random').callsFake(() => 0.1); delete bidderRequest.bids[0].params.latLong; let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); @@ -757,15 +780,15 @@ describe('the rubicon adapter', function () { Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; if (value instanceof RegExp) { - expect(data[key]).to.match(value); + expect(data.get(key)).to.match(value); } else { - expect(data[key]).to.equal(value); + expect(data.get(key)).to.equal(value); } }); bidderRequest.bids[0].params.latLong = []; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - data = parseQuery(request.data); + data = new URLSearchParams(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); @@ -773,9 +796,9 @@ describe('the rubicon adapter', function () { Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; if (value instanceof RegExp) { - expect(data[key]).to.match(value); + expect(data.get(key)).to.match(value); } else { - expect(data[key]).to.equal(value); + expect(data.get(key)).to.equal(value); } }); }); @@ -795,24 +818,24 @@ describe('the rubicon adapter', function () { delete bidderRequest.bids[0].params.referrer; let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(parseQuery(request.data).rf).to.exist; - expect(parseQuery(request.data).rf).to.equal('https://www.prebid.org'); + expect(new URLSearchParams(request.data).get('rf')).to.exist; + expect(new URLSearchParams(request.data).get('rf')).to.equal('https://www.prebid.org'); }); it('page_url should use params.referrer, bidderRequest.refererInfo in that order', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(parseQuery(request.data).rf).to.equal('localhost'); + expect(new URLSearchParams(request.data).get('rf')).to.equal('localhost'); delete bidderRequest.bids[0].params.referrer; let refererInfo = {page: 'https://www.prebid.org'}; bidderRequest = Object.assign({refererInfo}, bidderRequest); [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(parseQuery(request.data).rf).to.equal('https://www.prebid.org'); + expect(new URLSearchParams(request.data).get('rf')).to.equal('https://www.prebid.org'); bidderRequest.refererInfo.page = 'http://www.prebid.org'; bidderRequest.bids[0].params.secure = true; [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(parseQuery(request.data).rf).to.equal('https://www.prebid.org'); + expect(new URLSearchParams(request.data).get('rf')).to.equal('https://www.prebid.org'); }); it('should use rubicon sizes if present (including non-mappable sizes)', function () { @@ -820,10 +843,10 @@ describe('the rubicon adapter', function () { sizesBidderRequest.bids[0].params.sizes = [55, 57, 59, 801]; let [request] = spec.buildRequests(sizesBidderRequest.bids, sizesBidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['size_id']).to.equal('55'); - expect(data['alt_size_ids']).to.equal('57,59,801'); + expect(data.get('size_id')).to.equal('55'); + expect(data.get('alt_size_ids')).to.equal('57,59,801'); }); it('should not validate bid request if no valid sizes', function () { @@ -849,48 +872,48 @@ describe('the rubicon adapter', function () { floorBidderRequest.bids[0].params.floor = 2; let [request] = spec.buildRequests(floorBidderRequest.bids, floorBidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['rp_floor']).to.equal('2'); + expect(data.get('rp_floor')).to.equal('2'); }); describe('GDPR consent config', function () { it('should send "gdpr" and "gdpr_consent", when gdprConsent defines consentString and gdprApplies', function () { const bidderRequest = createGdprBidderRequest(true); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['gdpr']).to.equal('1'); - expect(data['gdpr_consent']).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + expect(data.get('gdpr')).to.equal('1'); + expect(data.get('gdpr_consent')).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); }); it('should send only "gdpr_consent", when gdprConsent defines only consentString', function () { const bidderRequest = createGdprBidderRequest(); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['gdpr_consent']).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); - expect(data['gdpr']).to.equal(undefined); + expect(data.get('gdpr_consent')).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + expect(data.get('gdpr')).to.equal(null); }); it('should not send GDPR params if gdprConsent is not defined', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['gdpr']).to.equal(undefined); - expect(data['gdpr_consent']).to.equal(undefined); + expect(data.get('gdpr')).to.equal(null); + expect(data.get('gdpr_consent')).to.equal(null); }); it('should set "gdpr" value as 1 or 0, using "gdprApplies" value of either true/false', function () { let bidderRequest = createGdprBidderRequest(true); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - expect(data['gdpr']).to.equal('1'); + let data = new URLSearchParams(request.data); + expect(data.get('gdpr')).to.equal('1'); bidderRequest = createGdprBidderRequest(false); [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - data = parseQuery(request.data); - expect(data['gdpr']).to.equal('0'); + data = new URLSearchParams(request.data); + expect(data.get('gdpr')).to.equal('0'); }); }); @@ -898,16 +921,16 @@ describe('the rubicon adapter', function () { it('should send us_privacy if bidderRequest has a value for uspConsent', function () { addUspToBidderRequest(bidderRequest); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['us_privacy']).to.equal('1NYN'); + expect(data.get('us_privacy')).to.equal('1NYN'); }); it('should not send us_privacy if bidderRequest has no uspConsent value', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['us_privacy']).to.equal(undefined); + expect(data.get('us_privacy')).to.equal(null); }); }); @@ -918,19 +941,19 @@ describe('the rubicon adapter', function () { applicableSections: 2 }; let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); delete bidderRequest.gppConsent; - expect(data['gpp']).to.equal('consent'); - expect(data['gpp_sid']).to.equal('2'); + expect(data.get('gpp')).to.equal('consent'); + expect(data.get('gpp_sid')).to.equal('2'); }); it('should not send gpp information if bidderRequest does not have a value for gppConsent', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['gpp']).to.equal(undefined); - expect(data['gpp_sid']).to.equal(undefined); + expect(data.get('gpp')).to.equal(null); + expect(data.get('gpp_sid')).to.equal(null); }); }); @@ -953,13 +976,14 @@ describe('the rubicon adapter', function () { // get the built request let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); // make sure that no tg_v or tg_i keys are present in the request - let matchingExp = RegExp('^tg_(i|v)\..*$') - Object.keys(data).forEach(key => { + let matchingExp = RegExp('^tg_(i|v)\..*$'); + // Display the keys + for (const key of data.keys()) { expect(key).to.not.match(matchingExp); - }); + } }); it('should contain valid params when some are undefined', function () { @@ -985,17 +1009,17 @@ describe('the rubicon adapter', function () { // get the built request let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); // make sure none of the undefined keys are in query undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); + expect(data.get(key)).to.equal(null); }); // make sure the expected and defined ones do show up still Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; - expect(data[key]).to.equal(value); + expect(data.get(key)).to.equal(value); }); }); @@ -1079,12 +1103,12 @@ describe('the rubicon adapter', function () { // get the built request let [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2})), bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); // make sure that tg_v, tg_i, and kw values are correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; - expect(data[key]).to.deep.equal(value); + expect(data.get(key)).to.deep.equal(value); }); }); }); @@ -1163,13 +1187,13 @@ describe('the rubicon adapter', function () { expect(bidRequestItem.params.siteId).to.equal(array[0].params.siteId); }); - const data = parseQuery(item.data); + const data = new URLSearchParams(item.data); Object.keys(expectedQuery).forEach(key => { - expect(data).to.have.property(key); + expect(data.get(key)).to.be.exist; // extract semicolon delineated values - const params = data[key].split(';'); + const params = data.get(key).split(';'); // skip value test for site and zone ids if (key !== 'site_id' && key !== 'zone_id') { @@ -1218,10 +1242,10 @@ describe('the rubicon adapter', function () { // check that slots param value matches expect(serverRequests[0].data.indexOf('&slots=10&') !== -1).to.equal(true); // check that zone_id has 10 values (since all zone_ids are unique all should exist in get param) - data = parseQuery(serverRequests[0].data); - expect(data).to.be.a('object'); - expect(data).to.have.property('zone_id'); - expect(data.zone_id.split(';')).to.have.lengthOf(10); + data = new URLSearchParams(serverRequests[0].data); + expect(typeof data).to.equal('object'); + expect(data.get('zone_id')).to.be.exist; + expect(data.get('zone_id').split(';')).to.have.lengthOf(10); // TEST '100' BIDS, add 90 to the previously added 10 for (let i = 0; i < 90; i++) { @@ -1259,10 +1283,10 @@ describe('the rubicon adapter', function () { expect(serverRequests).that.is.an('array').of.length(1); // get the built query - let data = parseQuery(serverRequests[0].data); + let data = new URLSearchParams(serverRequests[0].data); // num slots should be 4 - expect(data.slots).to.equal('4'); + expect(data.get('slots')).to.equal('4'); }); it('should not group bid requests if singleRequest does not equal true', function () { @@ -1329,173 +1353,60 @@ describe('the rubicon adapter', function () { }); describe('user id config', function () { - it('should send tpid_tdid when userIdAsEids contains unifiedId', function () { - const clonedBid = utils.deepClone(bidderRequest.bids[0]); - clonedBid.userId = { - tdid: 'abcd-efgh-ijkl-mnop-1234' - }; - clonedBid.userIdAsEids = [ - { - 'source': 'adserver.org', - 'uids': [ - { - 'id': 'abcd-efgh-ijkl-mnop-1234', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - } - ] - } - ]; - let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); - - expect(data['tpid_tdid']).to.equal('abcd-efgh-ijkl-mnop-1234'); - expect(data['eid_adserver.org']).to.equal('abcd-efgh-ijkl-mnop-1234'); - }); - - describe('LiveIntent support', function () { - it('should send tpid_liveintent.com when userIdAsEids contains liveintentId', function () { - const clonedBid = utils.deepClone(bidderRequest.bids[0]); - clonedBid.userId = { - lipb: { - lipbid: '0000-1111-2222-3333', - segments: ['segA', 'segB'] - } - }; - clonedBid.userIdAsEids = [ - { - 'source': 'liveintent.com', - 'uids': [ - { - 'id': '0000-1111-2222-3333', - 'atype': 3 - } - ], - 'ext': { - 'segments': [ - 'segA', - 'segB' - ] - } - } - ]; - let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); - - expect(data['tpid_liveintent.com']).to.equal('0000-1111-2222-3333'); - expect(data['eid_liveintent.com']).to.equal('0000-1111-2222-3333'); - expect(data['tg_v.LIseg']).to.equal('segA,segB'); - }); - - it('should send tg_v.LIseg when userIdAsEids contains liveintentId with ext.segments as array', function () { - const clonedBid = utils.deepClone(bidderRequest.bids[0]); - clonedBid.userId = { - lipb: { - lipbid: '1111-2222-3333-4444', - segments: ['segD', 'segE'] - } - }; - clonedBid.userIdAsEids = [ - { - 'source': 'liveintent.com', - 'uids': [ - { - 'id': '1111-2222-3333-4444', - 'atype': 3 - } - ], - 'ext': { - 'segments': [ - 'segD', - 'segE' - ] - } - } - ] - let [request] = spec.buildRequests([clonedBid], bidderRequest); - const unescapedData = unescape(request.data); - - expect(unescapedData.indexOf('&tpid_liveintent.com=1111-2222-3333-4444&') !== -1).to.equal(true); - expect(unescapedData.indexOf('&tg_v.LIseg=segD,segE&') !== -1).to.equal(true); - }); - }); - - describe('LiveRamp support', function () { - it('should send x_liverampidl when userIdAsEids contains liverampId', function () { - const clonedBid = utils.deepClone(bidderRequest.bids[0]); - clonedBid.userId = { - idl_env: '1111-2222-3333-4444' - }; - clonedBid.userIdAsEids = [ - { - 'source': 'liveramp.com', - 'uids': [ - { - 'id': '1111-2222-3333-4444', - 'atype': 3 - } - ] - } - ] - let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); - - expect(data['x_liverampidl']).to.equal('1111-2222-3333-4444'); - }); - }); - describe('pubcid support', function () { - it('should send eid_pubcid.org when userIdAsEids contains pubcid', function () { + it('should send eid_pubcid.org when ortb2.user.ext.eids contains pubcid', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { pubcid: '1111' }; - clonedBid.userIdAsEids = [ - { - 'source': 'pubcid.org', - 'uids': [ - { - 'id': '1111', - 'atype': 1 - } - ] + clonedBid.ortb2 = { + user: { + ext: { + eids: [{ + 'source': 'pubcid.org', + 'uids': [{ + 'id': '1111', + 'atype': 1 + }] + }] + } } - ] + }; let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['eid_pubcid.org']).to.equal('1111^1'); + expect(data.get('eid_pubcid.org')).to.equal('1111^1^^^^^'); }); }); describe('Criteo support', function () { - it('should send eid_criteo.com when userIdAsEids contains criteo', function () { + it('should send eid_criteo.com when ortb2.user.ext.eids contains criteo', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { criteoId: '1111' }; - clonedBid.userIdAsEids = [ - { - 'source': 'criteo.com', - 'uids': [ - { - 'id': '1111', - 'atype': 1 - } - ] + clonedBid.ortb2 = { + user: { + ext: { + eids: [{ + 'source': 'criteo.com', + 'uids': [{ + 'id': '1111', + 'atype': 1 + }] + }] + } } - ] + }; let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['eid_criteo.com']).to.equal('1111^1'); + expect(data.get('eid_criteo.com')).to.equal('1111^1^^^^^'); }); }); describe('pubProvidedId support', function () { - it('should send pubProvidedId when userIdAsEids contains pubProvidedId ids', function () { + it('should send pubProvidedId when ortb2.user.ext.eids contains pubProvidedId ids', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { pubProvidedId: [{ @@ -1513,36 +1424,36 @@ describe('the rubicon adapter', function () { }] }] }; - clonedBid.userIdAsEids = [ - { - 'source': 'example.com', - 'uids': [ - { - 'id': '11111', - 'ext': { - 'stype': 'ppuid' - } - } - ] - }, - { - 'source': 'id-partner.com', - 'uids': [ + clonedBid.ortb2 = { + user: { + ext: { + eids: [{ + 'source': 'example.com', + 'uids': [{ + 'id': '11111', + 'ext': { + 'stype': 'ppuid' + } + }] + }, { - 'id': '222222' - } - ] + 'source': 'id-partner.com', + 'uids': [{ + 'id': '222222' + }] + }] + } } - ]; + }; let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['ppuid']).to.equal('11111'); + expect(data.get('ppuid')).to.equal('11111'); }); }); describe('ID5 support', function () { - it('should send ID5 id when userIdAsEids contains ID5', function () { + it('should send ID5 id when ortb2.user.ext.eids contains ID5', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { id5id: { @@ -1552,24 +1463,26 @@ describe('the rubicon adapter', function () { } } }; - clonedBid.userIdAsEids = [ - { - 'source': 'id5-sync.com', - 'uids': [ - { - 'id': '11111', - 'atype': 1, - 'ext': { - 'linkType': '22222' - } - } - ] + clonedBid.ortb2 = { + user: { + ext: { + eids: [{ + 'source': 'id5-sync.com', + 'uids': [{ + 'id': '11111', + 'atype': 1, + 'ext': { + 'linkType': '22222' + } + }] + }] + } } - ]; + }; let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['eid_id5-sync.com']).to.equal('11111^1^22222'); + expect(data.get('eid_id5-sync.com')).to.equal('11111^1^^^^^'); }); }); @@ -1577,36 +1490,115 @@ describe('the rubicon adapter', function () { it('should send user id with generic format', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js - clonedBid.userIdAsEids = [{ - source: 'catchall', - uids: [{ - id: '11111', - atype: 2 - }] - }] + clonedBid.ortb2 = { + user: { + ext: { + eids: [{ + 'source': 'catchall', + 'uids': [{ + 'id': '11111', + 'atype': 2 + }] + }] + } + } + }; let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['eid_catchall']).to.equal('11111^2'); + expect(data.get('eid_catchall')).to.equal('11111^2^^^^^'); }); it('should send rubiconproject special case', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js - clonedBid.userIdAsEids = [{ - source: 'rubiconproject.com', - uids: [{ - id: 'some-cool-id', - atype: 3 - }] - }] + clonedBid.ortb2 = { + user: { + ext: { + eids: [{ + source: 'rubiconproject.com', + uids: [{ + id: 'some-cool-id', + atype: 3 + }] + }] + } + } + }; let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['eid_rubiconproject.com']).to.equal('some-cool-id'); + expect(data.get('eid_rubiconproject.com')).to.equal('some-cool-id^3^^^^^'); }); - }); + describe('Full eidValue format validation', function () { + it('should send complete eidValue in the format uid^atype^third^inserter^matcher^mm^rtipartner', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + // Simulating a full EID object with multiple fields + clonedBid.ortb2 = { + user: { + ext: { + eids: [{ + source: 'example.com', + uids: [{ + id: '11111', // UID + atype: 2, // atype + ext: { + rtipartner: 'rtipartner123', // rtipartner + stype: 'ppuid' // stype + } + }], + inserter: 'inserter123', // inserter + matcher: 'matcher123', // matcher + mm: 'mm123' // mm + }] + } + } + }; + + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = new URLSearchParams(request.data); + + // Expected format: uid^atype^third^inserter^matcher^mm^rtipartner + const expectedEidValue = '11111^2^^inserter123^matcher123^mm123^rtipartner123'; + // Check if the generated EID value matches the expected format + expect(data.get('eid_example.com')).to.equal(expectedEidValue); + }); + it('should generate eidValue with all attributes including rtiPartner', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + // Simulating a full EID object with multiple fields + clonedBid.ortb2 = { + user: { + ext: { + eids: [{ + source: 'example.com', + uids: [{ + id: '11111', // UID + atype: 2, // atype + ext: { + rtiPartner: 'rtipartner123', // rtiPartner (note the different capitalization) + stype: 'ppuid' // stype + } + }], + inserter: 'inserter123', // inserter + matcher: 'matcher123', // matcher + mm: 'mm123' // mm + }] + } + } + }; + + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = new URLSearchParams(request.data); + + // Expected format: uid^atype^third^inserter^matcher^mm^rtipartner + const expectedEidValue = '11111^2^^inserter123^matcher123^mm123^rtipartner123'; + + // Check if the generated EID value matches the expected format + expect(data.get('eid_example.com')).to.equal(expectedEidValue); + }); + }); + }); describe('Config user.id support', function () { it('should send ppuid when config defines user.id', function () { config.setConfig({user: {id: '123'}}); @@ -1615,9 +1607,9 @@ describe('the rubicon adapter', function () { pubcid: '1111' }; let [request] = spec.buildRequests([clonedBid], bidderRequest); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); - expect(data['ppuid']).to.equal('123'); + expect(data.get('ppuid')).to.equal('123'); }); }); }); @@ -1632,20 +1624,20 @@ describe('the rubicon adapter', function () { it('should not send \"tg_i.pbadslot’\" if \"ortb2Imp.ext.data\" object is not valid', function () { const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.not.have.property('tg_i.pbadslot’'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.pbadslot’')).to.be.null; }); it('should not send \"tg_i.pbadslot’\" if \"ortb2Imp.ext.data.pbadslot\" is undefined', function () { bidderRequest.bids[0].ortb2Imp = {}; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.not.have.property('tg_i.pbadslot’'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.pbadslot’')).to.be.null; }); it('should not send \"tg_i.pbadslot’\" if \"ortb2Imp.ext.data.pbadslot\" value is an empty string', function () { @@ -1658,10 +1650,10 @@ describe('the rubicon adapter', function () { }; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.not.have.property('tg_i.pbadslot'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.pbadslot')).to.be.null; }); it('should send \"tg_i.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" value is a valid string', function () { @@ -1674,11 +1666,11 @@ describe('the rubicon adapter', function () { } const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.have.property('tg_i.pbadslot'); - expect(data['tg_i.pbadslot']).to.equal('abc'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.pbadslot')).to.be.exist; + expect(data.get('tg_i.pbadslot')).to.equal('abc'); }); it('should send \"tg_i.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" value is a valid string', function () { @@ -1691,11 +1683,11 @@ describe('the rubicon adapter', function () { } const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.have.property('tg_i.pbadslot'); - expect(data['tg_i.pbadslot']).to.equal('/a/b/c'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.pbadslot')).to.be.exist; + expect(data.get('tg_i.pbadslot')).to.equal('/a/b/c'); }); it('should send gpid as p_gpid if valid', function () { @@ -1706,11 +1698,11 @@ describe('the rubicon adapter', function () { } const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.have.property('p_gpid'); - expect(data['p_gpid']).to.equal('/1233/sports&div1'); + expect(typeof data).to.equal('object'); + expect(data.get('p_gpid')).to.be.exist; + expect(data.get('p_gpid')).to.equal('/1233/sports&div1'); }); describe('Pass DSA signals', function() { @@ -1735,32 +1727,90 @@ describe('the rubicon adapter', function () { } } } + it('should send valid dsaparams but filter out invalid ones', function () { + const ortb2Clone = JSON.parse(JSON.stringify(ortb2)); + ortb2Clone.regs.ext.dsa.transparency = [ + { + domain: 'testdomain.com', + dsaparams: [1], + }, + { + domain: '', + dsaparams: [2], + } + ]; + + const expectedTransparency = 'testdomain.com~1'; + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({ ...b, ortb2: ortb2Clone })), bidderRequest); + const data = new URLSearchParams(request.data); + + expect(data.get('dsatransparency')).to.equal(expectedTransparency); + }) + it('should send dsaparams if \"ortb2.regs.ext.dsa.transparancy[0].params\"', function() { + const ortb2Clone = JSON.parse(JSON.stringify(ortb2)); + + ortb2Clone.regs.ext.dsa.transparency = [{ + domain: 'testdomain.com', + dsaparams: [1], + }]; + + const expectedTransparency = 'testdomain.com~1'; + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2: ortb2Clone})), bidderRequest); + const data = new URLSearchParams(request.data); + + expect(data.get('dsatransparency')).to.equal(expectedTransparency); + }) + it('should pass an empty transparency param if \"ortb2.regs.ext.dsa.transparency[0].params\" is empty', function() { + const ortb2Clone = JSON.parse(JSON.stringify(ortb2)); + + ortb2Clone.regs.ext.dsa.transparency = [{ + domain: 'testdomain.com', + params: [], + }]; + + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2: ortb2Clone})), bidderRequest); + const data = new URLSearchParams(request.data); + expect(data.get('dsatransparency')).to.be.null + }) + it('should send an empty transparency if \"ortb2.regs.ext.dsa.transparency[0].domain\" is empty', function() { + const ortb2Clone = JSON.parse(JSON.stringify(ortb2)); + + ortb2Clone.regs.ext.dsa.transparency = [{ + domain: '', + dsaparams: [1], + }]; + + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2: ortb2Clone})), bidderRequest); + const data = new URLSearchParams(request.data); + + expect(data.get('dsatransparency')).to.be.null + }) it('should send dsa signals if \"ortb2.regs.ext.dsa\"', function() { const expectedTransparency = 'testdomain.com~1~~testdomain2.com~1_2' const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2})), bidderRequest) - const data = parseQuery(request.data); - - expect(data).to.be.an('Object'); - expect(data).to.have.property('dsarequired'); - expect(data).to.have.property('dsapubrender'); - expect(data).to.have.property('dsadatatopubs'); - expect(data).to.have.property('dsatransparency'); - - expect(data['dsarequired']).to.equal(ortb2.regs.ext.dsa.dsarequired.toString()); - expect(data['dsapubrender']).to.equal(ortb2.regs.ext.dsa.pubrender.toString()); - expect(data['dsadatatopubs']).to.equal(ortb2.regs.ext.dsa.datatopub.toString()); - expect(data['dsatransparency']).to.equal(expectedTransparency) + const data = new URLSearchParams(request.data); + + expect(typeof data).to.equal('object'); + expect(data.get('dsarequired')).to.be.exist; + expect(data.get('dsapubrender')).to.be.exist; + expect(data.get('dsadatatopubs')).to.be.exist; + expect(data.get('dsatransparency')).to.be.exist; + + expect(data.get('dsarequired')).to.equal(ortb2.regs.ext.dsa.dsarequired.toString()); + expect(data.get('dsapubrender')).to.equal(ortb2.regs.ext.dsa.pubrender.toString()); + expect(data.get('dsadatatopubs')).to.equal(ortb2.regs.ext.dsa.datatopub.toString()); + expect(data.get('dsatransparency')).to.equal(expectedTransparency); }) it('should return one transparency param', function() { const expectedTransparency = 'testdomain.com~1'; const ortb2Clone = deepClone(ortb2); ortb2Clone.regs.ext.dsa.transparency.pop() const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2: ortb2Clone})), bidderRequest) - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.have.property('dsatransparency'); - expect(data['dsatransparency']).to.equal(expectedTransparency); + expect(typeof data).to.equal('object'); + expect(data.get('dsatransparency')).to.be.exist; + expect(data.get('dsatransparency')).to.equal(expectedTransparency); }) }) @@ -1779,12 +1829,12 @@ describe('the rubicon adapter', function () { } const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data['p_gpid']).to.equal('/1233/sports&div1'); - expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); - expect(data['tg_i.pbadslot']).to.equal('pb_slot'); + expect(typeof data).to.equal('object'); + expect(data.get('p_gpid')).to.equal('/1233/sports&div1'); + expect(data.get('tg_i.dfp_ad_unit_code')).to.be.null; + expect(data.get('tg_i.pbadslot')).to.equal('pb_slot'); }); }); @@ -1798,20 +1848,20 @@ describe('the rubicon adapter', function () { it('should not send \"tg_i.dfp_ad_unit_code’\" if \"ortb2Imp.ext.data\" object is not valid', function () { const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.not.have.property('tg_i.dfp_ad_unit_code’'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.dfp_ad_unit_code’')).to.be.null; }); it('should not send \"tg_i.dfp_ad_unit_code’\" if \"ortb2Imp.ext.data.adServer.adslot\" is undefined', function () { bidderRequest.bids[0].ortb2Imp = {}; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.not.have.property('tg_i.dfp_ad_unit_code’'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.dfp_ad_unit_code’')).to.be.null; }); it('should not send \"tg_i.dfp_ad_unit_code’\" if \"ortb2Imp.ext.data.adServer.adslot\" value is an empty string', function () { @@ -1826,10 +1876,10 @@ describe('the rubicon adapter', function () { }; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.dfp_ad_unit_code')).to.be.null; }); it('should send NOT \"tg_i.dfp_ad_unit_code\" if \"ortb2Imp.ext.data.adServer.adslot\" value is a valid string but not gam', function () { @@ -1845,10 +1895,10 @@ describe('the rubicon adapter', function () { } const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.dfp_ad_unit_code')).to.but.null; }); it('should send \"tg_i.dfp_ad_unit_code\" if \"ortb2Imp.ext.data.adServer.adslot\" value is a valid string and name is gam', function () { @@ -1864,11 +1914,11 @@ describe('the rubicon adapter', function () { }; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const data = parseQuery(request.data); + const data = new URLSearchParams(request.data); - expect(data).to.be.an('Object'); - expect(data).to.have.property('tg_i.dfp_ad_unit_code'); - expect(data['tg_i.dfp_ad_unit_code']).to.equal('/a/b/c'); + expect(typeof data).to.equal('object'); + expect(data.get('tg_i.dfp_ad_unit_code')).to.be.exist; + expect(data.get('tg_i.dfp_ad_unit_code')).to.equal('/a/b/c'); }); }); @@ -1920,7 +1970,7 @@ describe('the rubicon adapter', function () { architecture: 'x86' } }); - it('should send m_ch_* params if ortb2.device.sua object is there', function () { + it('should send m_ch_* params if ortb2.device.sua object is there with igh entropy', function () { let bidRequestSua = utils.deepClone(bidderRequest); bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; @@ -1937,11 +1987,11 @@ describe('the rubicon adapter', function () { // Build Fastlane call let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); // Loop through expected values and if they do not match push an error const errors = Object.entries(expectedValues).reduce((accum, [key, val]) => { - if (data[key] !== val) accum.push(`${key} - expect: ${val} - got: ${data[key]}`) + if (data.get(key) !== val) accum.push(`${key} - expect: ${val} - got: ${data[key]}`) return accum; }, []); @@ -1971,38 +2021,92 @@ describe('the rubicon adapter', function () { // Build Fastlane request let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); - let data = parseQuery(request.data); + let data = new URLSearchParams(request.data); // should show new names - expect(data.m_ch_model).to.equal('Suface Duo'); - expect(data.m_ch_mobile).to.equal('?1'); + expect(data.get('m_ch_model')).to.equal('Suface Duo'); + expect(data.get('m_ch_mobile')).to.equal('?1'); // should still send platform - expect(data.m_ch_platform).to.equal('macOS'); + expect(data.get('m_ch_platform')).to.equal('macOS'); // platform version not sent - expect(data).to.not.haveOwnProperty('m_ch_platform_ver'); + expect(data.get('m_ch_platform_ver')).to.be.null; // both ua and full_ver not sent because browsers not array - expect(data).to.not.haveOwnProperty('m_ch_ua'); - expect(data).to.not.haveOwnProperty('m_ch_full_ver'); + expect(data.get('m_ch_ua')).to.be.null; + expect(data.get('m_ch_full_ver')).to.be.null; // arch not sent - expect(data).to.not.haveOwnProperty('m_ch_arch'); + expect(data.get('m_ch_arch')).to.be.null; + }); + it('should not send high entropy if not present when it is low entropy client hints', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + bidRequestSua.bids[0].ortb2 = { device: { sua: { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Not A(Brand', + 'version': [ + '8' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '132' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '132' + ] + } + ], + 'mobile': 0 + } } }; + + // How should fastlane query be constructed with default SUA + let expectedValues = { + m_ch_ua: `"Not A(Brand"|v="8","Chromium"|v="132","Google Chrome"|v="132"`, + m_ch_mobile: '?0', + m_ch_platform: 'macOS', + } + + // Build Fastlane call + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = new URLSearchParams(request.data); + + // Loop through expected values and if they do not match push an error + const errors = Object.entries(expectedValues).reduce((accum, [key, val]) => { + if (data.get(key) !== val) accum.push(`${key} - expect: ${val} - got: ${data[key]}`) + return accum; + }, []); + + // should be no errors + expect(errors).to.deep.equal([]); + + // make sure high entropy keys are not present + let highEntropyHints = ['m_ch_full_ver', 'm_ch_arch', 'm_ch_bitness', 'm_ch_platform_ver']; + highEntropyHints.forEach((hint) => { expect(data.get(hint)).to.be.null; }); }); }); }); if (FEATURES.VIDEO) { describe('for video requests', function () { - it('should make a well-formed video request', function () { + it('should make a well-formed video request', async function () { const bidderRequest = createVideoBidderRequest(); sandbox.stub(Date, 'now').callsFake(() => bidderRequest.auctionStart + 100 ); - let [request] = spec.buildRequests(bidderRequest.bids, syncAddFPDToBidderRequest(bidderRequest)); + let [request] = spec.buildRequests(bidderRequest.bids, await addFPDToBidderRequest(bidderRequest)); let post = request.data; expect(post).to.have.property('imp'); @@ -2486,7 +2590,7 @@ describe('the rubicon adapter', function () { expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); }); - it('should include coppa flag in video bid request', () => { + it('should include coppa flag in video bid request', async () => { const bidderRequest = createVideoBidderRequest(); sandbox.stub(Date, 'now').callsFake(() => @@ -2499,7 +2603,7 @@ describe('the rubicon adapter', function () { }; return config[key]; }); - const [request] = spec.buildRequests(bidderRequest.bids, syncAddFPDToBidderRequest(bidderRequest)); + const [request] = spec.buildRequests(bidderRequest.bids, await addFPDToBidderRequest(bidderRequest)); expect(request.data.regs.coppa).to.equal(1); }); @@ -2629,7 +2733,7 @@ describe('the rubicon adapter', function () { expect(request.data.ext.prebid.bidders.rubicon.integration).to.equal('testType'); }); - it('should pass the user.id provided in the config', function () { + it('should pass the user.id provided in the config', async function () { config.setConfig({user: {id: '123'}}); const bidderRequest = createVideoBidderRequest(); @@ -2637,7 +2741,7 @@ describe('the rubicon adapter', function () { bidderRequest.auctionStart + 100 ); - let [request] = spec.buildRequests(bidderRequest.bids, syncAddFPDToBidderRequest(bidderRequest)); + let [request] = spec.buildRequests(bidderRequest.bids, await addFPDToBidderRequest(bidderRequest)); let post = request.data; expect(post).to.have.property('imp') @@ -2810,6 +2914,59 @@ describe('the rubicon adapter', function () { expect(slotParams['tg_i.tax10']).is.equal('2,3'); expect(slotParams['tg_v.tax404']).is.equal(undefined); }); + + it('should support IAB segtax 7 in site segments', () => { + const localBidderRequest = Object.assign({}, bidderRequest); + localBidderRequest.refererInfo = {domain: 'bob'}; + config.setConfig({ + rubicon: { + sendUserSegtax: [4], + sendSiteSegtax: [1, 2, 5, 6, 7] + } + }); + localBidderRequest.ortb2.site = { + content: { + data: [{ + ext: { + segtax: '7' + }, + segment: [{id: 8}, {id: 9}] + }] + } + }; + const slotParams = spec.createSlotParams(bidderRequest.bids[0], localBidderRequest); + expect(slotParams['tg_i.tax7']).to.equal('8,9'); + }); + + it('should add p_site.mobile if mobile is a number in ortb2.site', function () { + // Set up a bidRequest with mobile property as a number + const localBidderRequest = Object.assign({}, bidderRequest); + localBidderRequest.bids[0].ortb2 = { + site: { + mobile: 1 // Valid mobile value (number) + } + }; + + // Call the function + const slotParams = spec.createSlotParams(localBidderRequest.bids[0], localBidderRequest); + // Check that p_site.mobile was added to the slotParams with the correct value + expect(slotParams['p_site.mobile']).to.equal(1); + }); + it('should not add p_site.mobile if mobile is not a number in ortb2.site', function () { + // Set up a bidRequest with mobile property as a string (invalid value) + const localBidderRequest = Object.assign({}, bidderRequest); + localBidderRequest.bids[0].ortb2 = { + site: { + mobile: 'not-a-number' // Invalid mobile value (string) + } + }; + + // Call the function + const slotParams = spec.createSlotParams(localBidderRequest.bids[0], localBidderRequest); + + // Check that p_site.mobile is not added to the slotParams + expect(slotParams['p_site.mobile']).to.be.undefined; + }); }); describe('classifiedAsVideo', function () { @@ -3001,6 +3158,21 @@ describe('the rubicon adapter', function () { expect(other).to.be.empty; }); }); + + describe('with duplicate adUnitCodes', () => { + it('should increment PBS request imp[].id starting at 2', () => { + const nativeBidderRequest = addNativeToBidRequest(bidderRequest, {twin: true}); + const request = converter.toORTB({bidderRequest: nativeBidderRequest, bidRequests: nativeBidderRequest.bids}); + for (let i = 0; i < nativeBidderRequest.bids.length; i++) { + var adUnitCode = nativeBidderRequest.bids[i].adUnitCode; + if (i === 0) { + expect(request.imp[i].id).to.equal(adUnitCode); + } else { + expect(request.imp[i].id).to.equal(adUnitCode + (i + 1)); + } + } + }); + }); }); } }); @@ -3637,14 +3809,14 @@ describe('the rubicon adapter', function () { }] }; - let {bids, fledgeAuctionConfigs} = spec.interpretResponse({body: response}, { + let {bids, paapi} = spec.interpretResponse({body: response}, { bidRequest: bidderRequest.bids[0] }); expect(bids).to.be.lengthOf(1); - expect(fledgeAuctionConfigs[0].bidId).to.equal('5432'); - expect(fledgeAuctionConfigs[0].config.random).to.equal('value'); - expect(fledgeAuctionConfigs[1].bidId).to.equal('6789'); + 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 () { @@ -3708,6 +3880,71 @@ describe('the rubicon adapter', function () { expect(bids[0].cpm).to.be.equal(0); }); + it('should use ads.emulated_format if defined for bid.meta.mediaType', function () { + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [ + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', + 'size_id': '15', + 'ad_id': '6', + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.811, + 'emulated_format': 'video', + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '15_tier_all_test' + ] + } + ] + }, + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374d', + 'size_id': '43', + 'ad_id': '7', + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.911, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '43_tier_all_test' + ] + } + ] + } + ] + }; + let bids = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + expect(bids[0].meta.mediaType).to.equal('banner'); + expect(bids[1].meta.mediaType).to.equal('video'); + }); + describe('singleRequest enabled', function () { it('handles bidRequest of type Array and returns associated adUnits', function () { const overrideMap = []; @@ -3929,6 +4166,16 @@ describe('the rubicon adapter', function () { let bids = spec.interpretResponse({body: response}, {data: request}); expect(bids).to.have.nested.property('[0].native'); }); + it('should set 0 to bids width and height if `w` and `h` in response object not defined', () => { + const nativeBidderRequest = addNativeToBidRequest(bidderRequest); + const request = converter.toORTB({bidderRequest: nativeBidderRequest, bidRequests: nativeBidderRequest.bids}); + let response = getNativeResponse({impid: request.imp[0].id}); + delete response.seatbid[0].bid[0].w; + delete response.seatbid[0].bid[0].h + let bids = spec.interpretResponse({body: response}, {data: request}); + expect(bids[0].width).to.equal(0); + expect(bids[0].height).to.equal(0); + }); }); } @@ -4059,6 +4306,7 @@ describe('the rubicon adapter', function () { const bid = bids[0]; bid.adUnitCode = 'outstream_video1_placement'; const adUnit = document.createElement('div'); + const adUnitSelector = `#${bid.adUnitCode}` adUnit.id = bid.adUnitCode; document.body.appendChild(adUnit); @@ -4072,7 +4320,7 @@ describe('the rubicon adapter', function () { label: undefined, placement: { align: 'left', - attachTo: adUnit, + attachTo: adUnitSelector, position: 'append', }, vastUrl: 'https://test.com/vast.xml', @@ -4128,6 +4376,7 @@ describe('the rubicon adapter', function () { const bid = bids[0]; bid.adUnitCode = 'outstream_video1_placement'; const adUnit = document.createElement('div'); + const adUnitSelector = `#${bid.adUnitCode}` adUnit.id = bid.adUnitCode; document.body.appendChild(adUnit); @@ -4141,7 +4390,7 @@ describe('the rubicon adapter', function () { label: undefined, placement: { align: 'left', - attachTo: adUnit, + attachTo: adUnitSelector, position: 'append', }, vastUrl: 'https://test.com/vast.xml', @@ -4157,7 +4406,7 @@ describe('the rubicon adapter', function () { it('should use the integration type provided in the config instead of the default', () => { config.setConfig({rubicon: {int_type: 'testType'}}); const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(parseQuery(request.data).tk_flint).to.equal('testType_v$prebid.version$'); + expect(new URLSearchParams(request.data).get('tk_flint')).to.equal('testType_v$prebid.version$'); }); }); }); @@ -4380,7 +4629,7 @@ describe('the rubicon adapter', function () { it('should properly serialize schain object with correct delimiters', () => { const results = spec.buildRequests(bidRequests.bids, bidRequests); const numNodes = schainConfig.nodes.length; - const schain = parseQuery(results[0].data).rp_schain; + const schain = new URLSearchParams(results[0].data).get('rp_schain'); // each node serialization should start with an ! expect(schain.match(/!/g).length).to.equal(numNodes); @@ -4391,21 +4640,21 @@ describe('the rubicon adapter', function () { it('should send the proper version for the schain', () => { const results = spec.buildRequests(bidRequests.bids, bidRequests); - const schain = parseQuery(results[0].data).rp_schain.split('!'); + const schain = new URLSearchParams(results[0].data).get('rp_schain').split('!'); const version = schain.shift().split(',')[0]; expect(version).to.equal(bidRequests.bids[0].schain.ver); }); it('should send the correct value for complete in schain', () => { const results = spec.buildRequests(bidRequests.bids, bidRequests); - const schain = parseQuery(results[0].data).rp_schain.split('!'); + const schain = new URLSearchParams(results[0].data).get('rp_schain').split('!'); const complete = schain.shift().split(',')[1]; expect(complete).to.equal(String(bidRequests.bids[0].schain.complete)); }); it('should send available params in the right order', () => { const results = spec.buildRequests(bidRequests.bids, bidRequests); - const schain = parseQuery(results[0].data).rp_schain.split('!'); + const schain = new URLSearchParams(results[0].data).get('rp_schain').split('!'); schain.shift(); schain.forEach((serializeNode, nodeIndex) => { @@ -4476,7 +4725,7 @@ describe('the rubicon adapter', function () { }); }); -function addNativeToBidRequest(bidderRequest) { +function addNativeToBidRequest(bidderRequest, options = {twin: false}) { const nativeOrtbRequest = { assets: [{ id: 0, @@ -4505,27 +4754,30 @@ function addNativeToBidRequest(bidderRequest) { bidderRequest.refererInfo = { page: 'localhost' } - bidderRequest.bids[0] = { - bidder: 'rubicon', - params: { - accountId: '14062', - siteId: '70608', - zoneId: '335918', - }, - adUnitCode: '/19968336/header-bid-tag-0', - code: 'div-1', - bidId: '2ffb201a808da7', - bidderRequestId: '178e34bad3658f', - auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', - transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', - mediaTypes: { - native: { - ortb: { - ...nativeOrtbRequest + const numBids = !options.twin ? 1 : 2; + for (let i = 0; i < numBids; i++) { + bidderRequest.bids[i] = { + bidder: 'rubicon', + params: { + accountId: '14062', + siteId: '70608', + zoneId: '335918', + }, + adUnitCode: '/19968336/header-bid-tag-0', + code: 'div-1', + bidId: '2ffb201a808da7', + bidderRequestId: '178e34bad3658f', + auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + mediaTypes: { + native: { + ortb: { + ...nativeOrtbRequest + } } - } - }, - nativeOrtbRequest + }, + nativeOrtbRequest + } } return bidderRequest; } diff --git a/test/spec/modules/seedingAllianceAdapter_spec.js b/test/spec/modules/seedingAllianceAdapter_spec.js index 03548cf923a..e3ff85e9c83 100755 --- a/test/spec/modules/seedingAllianceAdapter_spec.js +++ b/test/spec/modules/seedingAllianceAdapter_spec.js @@ -1,8 +1,7 @@ // jshint esversion: 6, es3: false, node: true import {assert, expect} from 'chai'; +import {getStorageManager} from 'src/storageManager.js'; import {spec} from 'modules/seedingAllianceBidAdapter.js'; -import { NATIVE } from 'src/mediaTypes.js'; -import { config } from 'src/config.js'; describe('SeedingAlliance adapter', function () { let serverResponse, bidRequest, bidResponses; @@ -13,6 +12,14 @@ describe('SeedingAlliance adapter', function () { } }; + let validBidRequests = [{ + bidId: 'bidId', + params: {}, + mediaType: { + native: {} + } + }]; + describe('isBidRequestValid', function () { it('should return true when required params found', function () { assert(spec.isBidRequestValid(bid)); @@ -26,11 +33,6 @@ describe('SeedingAlliance adapter', function () { describe('buildRequests', function () { it('should send request with correct structure', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {} - }]; - let request = spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }); assert.equal(request.method, 'POST'); @@ -39,64 +41,96 @@ describe('SeedingAlliance adapter', function () { it('should have default request structure', function () { let keys = 'site,cur,imp,regs'.split(','); - let validBidRequests = [{ - bidId: 'bidId', - params: {} - }]; let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); let data = Object.keys(request); - assert.deepEqual(keys, data); + assert.includeDeepMembers(data, keys); }); it('Verify the site url', function () { let siteUrl = 'https://www.yourdomain.tld/your-directory/'; - let validBidRequests = [{ - bidId: 'bidId', - params: { - url: siteUrl - } - }]; + validBidRequests[0].params.url = siteUrl; let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); assert.equal(request.site.page, siteUrl); }); + }); - it('Verify native asset ids', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - nativeParams: { - body: { - required: true, - len: 350 - }, - image: { - required: true - }, - title: { - required: true - }, - sponsoredBy: { - required: true - }, - cta: { - required: true - }, - icon: { - required: true - } + describe('check user ID functionality', function () { + let storage = getStorageManager({ bidderCode: 'seedingAlliance' }); + let localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + let getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + const bidRequests = [{ + bidId: 'bidId', + params: {} + }]; + const bidderRequest = { + refererInfo: { referer: 'page' }, + gdprConsent: 'CP0j9IAP0j9IAAGABCENAYEgAP_gAAAAAAYgIxBVBCpNDWFAMHBVAJIgCYAU1sARIAQAABCAAyAFAAOA8IAA0QECEAQAAAACAAAAgVABAAAAAABEAACAAAAEAQFkAAQQgAAIAAAAAAEQQgBQAAgAAAAAEAAIgAABAwQAkACQIYLEBUCAhIAgCgAAAIgBgICAAgMACEAYAAAAAAIAAIBAAgIEMIAAAAECAQAAAFhIEoACAAKgAcgA-AEAAMgAaABEACYAG8APwAhIBDAESAJYATQAw4B9gH6ARQAjQBKQC5gF6AMUAbQA3ACdgFDgLzAYMAw0BmYDVwGsgOCAcmA8cCEMELQQuCAAgGQgQMHQKAAKgAcgA-AEAAMgAaABEACYAG8AP0AhgCJAEsAJoAYYA0YB9gH6ARQAiwBIgCUgFzAL0AYoA2gBuAEXgJkATsAocBeYDBgGGgMqAZYAzMBpoDVwHFgOTAeOBC0cAHAAQABcAKACEAF0AMEAZCQgFABMADeARQAlIBcwDFAG0AeOBCgCFpAAGAAgBggEMyUAwABAAHAAPgBEACZAIYAiQB-AFzAMUAi8BeYEISQAMAC4DLAIZlIEAAFQAOQAfACAAGQANAAiABMACkAH6AQwBEgDRgH4AfoBFgCRAEpALmAYoA2gBuAEXgJ2AUOAvMBhoDLAGsgOCAcmA8cCEIELQIZlAAoAFwB9gLoAYIBAwtADAL0AzMB44AAA.f_wAAAAAAAAA' + } + let request; + + before(function () { + storage.removeDataFromLocalStorage('nativendo_id'); + const localStorageData = { + nativendo_id: '123' + }; + + getDataFromLocalStorageStub.callsFake(function (key) { + return localStorageData[key]; + }); + }); + + it('should return an empty array if local storage is not enabled', function () { + localStorageIsEnabledStub.returns(false); + $$PREBID_GLOBAL$$.bidderSettings = { + seedingAlliance: { + storageAllowed: false } - }]; + }; + + request = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); + expect(request.user.ext.eids).to.be.an('array').that.is.empty; + }); + + it('should return an empty array if local storage is enabled but storageAllowed is false', function () { + $$PREBID_GLOBAL$$.bidderSettings = { + seedingAlliance: { + storageAllowed: false + } + }; + localStorageIsEnabledStub.returns(true); + + request = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); + expect(request.user.ext.eids).to.be.an('array').that.is.empty; + }); + + it('should return a non empty array if local storage is enabled and storageAllowed is true', function () { + $$PREBID_GLOBAL$$.bidderSettings = { + seedingAlliance: { + storageAllowed: true + } + }; + localStorageIsEnabledStub.returns(true); + + request = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); + expect(request.user.ext.eids).to.be.an('array').that.is.not.empty; + }); + + it('should return an array containing the nativendoUserEid', function () { + $$PREBID_GLOBAL$$.bidderSettings = { + seedingAlliance: { + storageAllowed: true + } + }; + localStorageIsEnabledStub.returns(true); + + let nativendoUserEid = { source: 'nativendo.de', uids: [{ id: '123', atype: 1 }] }; + storage.setDataInLocalStorage('nativendo_id', '123'); - let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data).imp[0].native.request.assets; + request = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); - assert.equal(assets[0].id, 1); - assert.equal(assets[1].id, 3); - assert.equal(assets[2].id, 0); - assert.equal(assets[3].id, 2); - assert.equal(assets[4].id, 4); - assert.equal(assets[5].id, 5); + expect(request.user.ext.eids).to.deep.include(nativendoUserEid); }); }); @@ -104,23 +138,24 @@ describe('SeedingAlliance adapter', function () { const goodNativeResponse = { body: { cur: 'EUR', - id: '4b516b80-886e-4ec0-82ae-9209e6d625fb', + id: 'bidid1', seatbid: [ { seat: 'seedingAlliance', bid: [{ - adm: { - native: { - assets: [ - {id: 0, title: {text: 'this is a title'}} - ], - imptrackers: ['https://domain.for/imp/tracker?price=${AUCTION_PRICE}'], - link: { - clicktrackers: ['https://domain.for/imp/tracker?price=${AUCTION_PRICE}'], - url: 'https://domain.for/ad/' - } - } - }, + adm: JSON.stringify({ + native: { + assets: [ + {id: 0, title: {text: 'this is a title'}}, + {id: 1, img: {url: 'https://domain.for/img.jpg'}}, + ], + imptrackers: ['https://domain.for/imp/tracker?price=${AUCTION_PRICE}'], + link: { + clicktrackers: ['https://domain.for/imp/tracker?price=${AUCTION_PRICE}'], + url: 'https://domain.for/ad/' + } + } + }), impid: 1, price: 0.55 }] @@ -132,7 +167,7 @@ describe('SeedingAlliance adapter', function () { const goodBannerResponse = { body: { cur: 'EUR', - id: 'b4516b80-886e-4ec0-82ae-9209e6d625fb', + id: 'bidid1', seatbid: [ { seat: 'seedingAlliance', @@ -150,18 +185,18 @@ describe('SeedingAlliance adapter', function () { const badResponse = { body: { cur: 'EUR', - id: '4b516b80-886e-4ec0-82ae-9209e6d625fb', + id: 'bidid1', seatbid: [] }}; const bidNativeRequest = { data: {}, - bidRequests: [{bidId: 'bidId1', nativeParams: {title: {required: true, len: 800}}}] + bidRequests: [{bidId: '1', nativeParams: {title: {required: true, len: 800}, image: {required: true, sizes: [300, 250]}}}] }; const bidBannerRequest = { data: {}, - bidRequests: [{bidId: 'bidId1', sizes: [300, 250]}] + bidRequests: [{bidId: '1', sizes: [300, 250]}] }; it('should return null if body is missing or empty', function () { diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index 2012c78d239..db19d71f23f 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -3,6 +3,7 @@ import { getTimeoutUrl, spec } from 'modules/seedtagBidAdapter.js'; import * as utils from 'src/utils.js'; import * as mockGpt from 'test/spec/integration/faker/googletag.js'; import { config } from '../../../src/config.js'; +import { BIDFLOOR_CURRENCY } from '../../../modules/seedtagBidAdapter.js'; const PUBLISHER_ID = '0000-0000-01'; const ADUNIT_ID = '000000'; @@ -37,6 +38,7 @@ function getSlotConfigs(mediaTypes, params) { ortb2Imp: { ext: { tid: 'd704d006-0d6e-4a09-ad6c-179e7e758096', + gpid: 'some-gpid' } }, adUnitCode: adUnitCode, @@ -47,15 +49,13 @@ function createInStreamSlotConfig(mediaType) { return getSlotConfigs(mediaType, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'inStream', }); } -const createBannerSlotConfig = (placement, mediatypes) => { +const createBannerSlotConfig = (mediatypes) => { return getSlotConfigs(mediatypes || { banner: {} }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement, }); }; @@ -70,49 +70,69 @@ describe('Seedtag Adapter', function () { describe('isBidRequestValid method', function () { describe('returns true', function () { describe('when banner slot config has all mandatory params', () => { - const placements = ['inBanner', 'inImage', 'inScreen', 'inArticle']; - placements.forEach((placement) => { - it(placement + 'should be valid', function () { + it('should be valid', function () { + const isBidRequestValid = spec.isBidRequestValid( + createBannerSlotConfig() + ); + expect(isBidRequestValid).to.equal(true); + }); + + it('should be valid when has display and video mediatypes, and video context is outstream', + function () { const isBidRequestValid = spec.isBidRequestValid( - createBannerSlotConfig(placement) + createBannerSlotConfig({ + banner: {}, + video: { + context: 'outstream', + playerSize: [[600, 200]], + }, + }) ); expect(isBidRequestValid).to.equal(true); - }); - - it( - placement + - ' should be valid when has display and video mediatypes, and video context is outstream', - function () { - const isBidRequestValid = spec.isBidRequestValid( - createBannerSlotConfig(placement, { - banner: {}, - video: { - context: 'outstream', - playerSize: [[600, 200]], - }, - }) - ); - expect(isBidRequestValid).to.equal(true); - } - ); + } + ); - it( - placement + - " shouldn't be valid when has display and video mediatypes, and video context is instream", - function () { - const isBidRequestValid = spec.isBidRequestValid( - createBannerSlotConfig(placement, { - banner: {}, - video: { - context: 'instream', - playerSize: [[600, 200]], - }, - }) - ); - expect(isBidRequestValid).to.equal(false); - } - ); - }); + it('should be valid when has only video mediatypes, and video context is outstream', + function () { + const isBidRequestValid = spec.isBidRequestValid( + createBannerSlotConfig({ + video: { + context: 'outstream', + playerSize: [[600, 200]], + }, + }) + ); + expect(isBidRequestValid).to.equal(true); + } + ); + it('should be valid when has display and video mediatypes, and video context is instream', + function () { + const isBidRequestValid = spec.isBidRequestValid( + createBannerSlotConfig({ + banner: {}, + video: { + context: 'instream', + playerSize: [[600, 200]], + }, + }) + ); + expect(isBidRequestValid).to.equal(false); + } + ); + it("shouldn't be valid when has display and video mediatypes, and video context is instream", + function () { + const isBidRequestValid = spec.isBidRequestValid( + createBannerSlotConfig({ + banner: {}, + video: { + context: 'instream', + playerSize: [[600, 200]], + }, + }) + ); + expect(isBidRequestValid).to.equal(false); + } + ); }); describe('when video slot has all mandatory params', function () { it('should return true, when video context is instream', function () { @@ -125,7 +145,7 @@ describe('Seedtag Adapter', function () { const isBidRequestValid = spec.isBidRequestValid(slotConfig); expect(isBidRequestValid).to.equal(true); }); - it('should return true, when video context is instream and mediatype is video and banner', function () { + it('should return false, when video context is instream and mediatype is video and banner', function () { const slotConfig = createInStreamSlotConfig({ video: { context: 'instream', @@ -134,33 +154,6 @@ describe('Seedtag Adapter', function () { banner: {}, }); const isBidRequestValid = spec.isBidRequestValid(slotConfig); - expect(isBidRequestValid).to.equal(true); - }); - it('should return false, when video context is instream, but placement is not inStream', function () { - const slotConfig = getSlotConfigs( - { - video: { - context: 'instream', - playerSize: [[600, 200]], - }, - }, - { - publisherId: PUBLISHER_ID, - adUnitId: ADUNIT_ID, - placement: 'inBanner', - } - ); - const isBidRequestValid = spec.isBidRequestValid(slotConfig); - expect(isBidRequestValid).to.equal(false); - }); - it('should return false, when video context is outstream', function () { - const slotConfig = createInStreamSlotConfig({ - video: { - context: 'outstream', - playerSize: [[600, 200]], - }, - }); - const isBidRequestValid = spec.isBidRequestValid(slotConfig); expect(isBidRequestValid).to.equal(false); }); }); @@ -174,7 +167,6 @@ describe('Seedtag Adapter', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ adUnitId: ADUNIT_ID, - placement: 'inBanner', }) ); expect(isBidRequestValid).to.equal(false); @@ -183,26 +175,6 @@ describe('Seedtag Adapter', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, - placement: 'inBanner', - }) - ); - expect(isBidRequestValid).to.equal(false); - }); - it('does not have the placement.', function () { - const isBidRequestValid = spec.isBidRequestValid( - createSlotConfig({ - publisherId: PUBLISHER_ID, - adUnitId: ADUNIT_ID, - }) - ); - expect(isBidRequestValid).to.equal(false); - }); - it('does not have a the correct placement.', function () { - const isBidRequestValid = spec.isBidRequestValid( - createSlotConfig({ - publisherId: PUBLISHER_ID, - adUnitId: ADUNIT_ID, - placement: 'another_thing', }) ); expect(isBidRequestValid).to.equal(false); @@ -222,17 +194,7 @@ describe('Seedtag Adapter', function () { ); expect(isBidRequestValid).to.equal(false); }); - it('is outstream ', function () { - const isBidRequestValid = spec.isBidRequestValid( - createInStreamSlotConfig({ - video: { - context: 'outstream', - playerSize: [[600, 200]], - }, - }) - ); - expect(isBidRequestValid).to.equal(false); - }); + describe('order does not matter', function () { it('when video is not the first slot', function () { const isBidRequestValid = spec.isBidRequestValid( @@ -252,6 +214,7 @@ describe('Seedtag Adapter', function () { }); describe('buildRequests method', function () { + const bidFloor = 0.60 const bidderRequest = { refererInfo: { page: 'referer' }, timeout: 1000, @@ -259,12 +222,10 @@ describe('Seedtag Adapter', function () { const mandatoryDisplayParams = { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'inBanner', }; const mandatoryVideoParams = { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'inStream', }; const validBidRequests = [ getSlotConfigs({ banner: {} }, mandatoryDisplayParams), @@ -279,6 +240,11 @@ describe('Seedtag Adapter', function () { mandatoryVideoParams ), ]; + validBidRequests[0].getFloor = () => ({ + currency: BIDFLOOR_CURRENCY, + floor: bidFloor + }) + it('Url params should be correct ', function () { const request = spec.buildRequests(validBidRequests, bidderRequest); expect(request.method).to.equal('POST'); @@ -299,6 +265,7 @@ describe('Seedtag Adapter', function () { expect(data.ttfb).to.be.greaterThanOrEqual(0); expect(data.bidRequests[0].adUnitCode).to.equal(adUnitCode); + expect(data.bidRequests[0].gpid).to.equal('some-gpid'); }); describe('GDPR params', function () { @@ -424,6 +391,15 @@ describe('Seedtag Adapter', function () { expect(bannerBid).to.not.have.property('geom') } }) + + it('should have bidfloor parameter if available', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + const bidRequests = data.bidRequests; + + expect(bidRequests[0].bidFloor).to.be.equal(bidFloor) + expect(bidRequests[1]).not.to.have.property('bidFloor') + }) }); describe('COPPA param', function () { @@ -590,6 +566,124 @@ describe('Seedtag Adapter', function () { expect(data.user.eids).to.deep.equal(userIdAsEids); }) }); + + describe('Blocking params', function () { + it('should add bcat param to payload when bidderRequest has ortb2 bcat info', function () { + const blockedCategories = ['IAB1', 'IAB2'] + var ortb2 = { + bcat: blockedCategories + } + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.bcat).to.deep.equal(blockedCategories); + }); + + it('should add badv param to payload when bidderRequest has ortb2 badv info', function () { + const blockedAdvertisers = ['blocked.com'] + var ortb2 = { + badv: blockedAdvertisers + } + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.badv).to.deep.equal(blockedAdvertisers); + }); + + it('should not add bcat and badv params to payload when bidderRequest does not have ortb2 badv and bcat info', function () { + var ortb2 = {} + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.bcat).to.be.undefined; + expect(data.badv).to.be.undefined; + }); + }); + + describe('Site params', function () { + it('should add cat param to payload when bidderRequest has ortb2 site cat info', function () { + const siteCategories = ['1217', 'bsr004', '692'] + var ortb2 = { + site: { + cat: siteCategories + } + } + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.site.cat).to.deep.equal(siteCategories); + }); + + it('should add pagecat param to payload when bidderRequest has ortb2 site pagecat info', function () { + const pageCategories = ['1217', 'bsr004', '692'] + var ortb2 = { + site: { + pagecat: pageCategories + } + } + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.site.pagecat).to.deep.equal(pageCategories); + }); + + it('should add cattac param to payload when bidderRequest has ortb2 site cattax info', function () { + const taxonomy = 6 + var ortb2 = { + site: { + cattax: taxonomy + } + } + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.site.cattax).to.equal(taxonomy); + }); + + it('should not add site params to payload when bidderRequest does not have ortb2 site info', function () { + var ortb2 = {} + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.site.cattax).to.be.undefined; + expect(data.site.cat).to.be.undefined; + expect(data.site.pagecat).to.be.undefined; + }); + }); + + describe('device.sua param', function () { + it('should add device.sua param to payload when bidderRequest has ortb2 device.sua info', function () { + const sua = 1 + var ortb2 = { + device: { + sua: sua + } + } + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.sua).to.equal(sua); + }); + + it('should not add device.sua param to payload when bidderRequest does not have ortb2 device.sua info', function () { + var ortb2 = { + device: {} + } + bidderRequest['ortb2'] = ortb2 + + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.sua).to.be.undefined; + }); + }); }) describe('interpret response method', function () { it('should return a void array, when the server response are not correct.', function () { diff --git a/test/spec/modules/semantiqRtdProvider_spec.js b/test/spec/modules/semantiqRtdProvider_spec.js new file mode 100644 index 00000000000..d9fd0098273 --- /dev/null +++ b/test/spec/modules/semantiqRtdProvider_spec.js @@ -0,0 +1,441 @@ +import { convertSemantiqKeywordToOrtb, getOrtbKeywords, semantiqRtdSubmodule, storage } from '../../../modules/semantiqRtdProvider'; +import { expect } from 'chai'; +import { server } from '../../mocks/xhr.js'; +import * as utils from '../../../src/utils.js'; + +describe('semantiqRtdProvider', () => { + let clock; + let getDataFromLocalStorageStub; + let getWindowLocationStub; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage').returns(null); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns(new URL('https://example.com/article')); + }); + + afterEach(() => { + clock.restore(); + getDataFromLocalStorageStub.restore(); + getWindowLocationStub.restore(); + }); + + describe('init', () => { + it('returns true on initialization', () => { + const initResult = semantiqRtdSubmodule.init({}); + expect(initResult).to.be.true; + }); + }); + + describe('pageImpression event', () => { + it('dispatches an event on initialization', () => { + getWindowLocationStub.returns(new URL('https://example.com/article')); + + semantiqRtdSubmodule.init({ params: { companyId: 5 } }); + + const body = JSON.parse(server.requests[0].requestBody); + + expect(server.requests[0].url).to.be.equal('https://api.adnz.co/api/ws-clickstream-collector/submit'); + expect(server.requests[0].method).to.be.equal('POST'); + + expect(body.company_id).to.be.equal(5); + expect(body.event_id).not.to.be.empty; + expect(body.event_timestamp).not.to.be.empty; + 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'); + }); + + it('uses the correct company ID', () => { + semantiqRtdSubmodule.init({ params: { companyId: 555 } }); + semantiqRtdSubmodule.init({ params: { companyId: [111, 222, 333] } }); + + const body1 = JSON.parse(server.requests[0].requestBody); + const body2 = JSON.parse(server.requests[1].requestBody); + + expect(body1.company_id).to.be.equal(555); + expect(body2.company_id).to.be.equal(111); + }); + + it('uses cached page impression ID if present', () => { + window.audienzz = { collectorPageImpressionId: 'cached-guid' }; + semantiqRtdSubmodule.init({ params: { companyId: 5 } }); + + const body = JSON.parse(server.requests[0].requestBody); + + expect(body.page_impression_id).to.be.equal('cached-guid'); + }); + }); + + describe('convertSemantiqKeywordToOrtb', () => { + it('converts SemantIQ keywords properly', () => { + expect(convertSemantiqKeywordToOrtb('foo', 'bar')).to.be.equal('foo=bar'); + expect(convertSemantiqKeywordToOrtb('foo', ['bar', 'baz'])).to.be.equal('foo=bar,foo=baz'); + }); + + it('returns an empty string if keyword value is empty', () => { + expect(convertSemantiqKeywordToOrtb('foo', '')).to.be.equal(''); + expect(convertSemantiqKeywordToOrtb('foo', [])).to.be.equal(''); + }); + }); + + describe('getOrtbKeywords', () => { + it('returns an empty string if no keywords are provided', () => { + expect(getOrtbKeywords({})).to.be.equal(''); + }); + + it('converts keywords to ORTB format', () => { + expect(getOrtbKeywords({ foo: 'bar', fizz: ['buzz', 'quz'] })).to.be.equal('foo=bar,fizz=buzz,fizz=quz'); + }); + + it('ignores keywords with no value', () => { + expect(getOrtbKeywords({ foo: 'bar', fizz: ['buzz', 'quz'], baz: '', xyz: [], quz: undefined, buzz: null })).to.be.equal('foo=bar,fizz=buzz,fizz=quz'); + }); + }); + + describe('getBidRequestData', () => { + it('requests data with correct parameters', async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + }; + + semantiqRtdSubmodule.getBidRequestData( + reqBidsConfigObj, + () => undefined, + { params: {} }, + {} + ); + + server.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({}) + ); + + const requestUrl = new URL(server.requests[0].url); + + expect(requestUrl.host).to.be.equal('api.adnz.co'); + expect(requestUrl.searchParams.get('url')).to.be.equal('https://example.com/article'); + expect(requestUrl.searchParams.get('tenantIds')).to.be.equal('1'); + }); + + it('allows to specify company ID as a parameter', async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + }; + + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => semantiqRtdSubmodule.getBidRequestData( + reqBidsConfigObj, + () => { + onDoneSpy(); + resolve(); + }, + { params: { companyId: 13 } }, + {} + )); + + server.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({}) + ); + + await promise; + + const requestUrl = new URL(server.requests[0].url); + + expect(requestUrl.searchParams.get('tenantIds')).to.be.equal('13'); + }); + + it('allows to specify multiple company IDs as a parameter', async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + }; + + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => semantiqRtdSubmodule.getBidRequestData( + reqBidsConfigObj, + () => { + onDoneSpy(); + resolve(); + }, + { params: { companyId: [13, 23] } }, + {} + )); + + server.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({}) + ); + + await promise; + + const requestUrl = new URL(server.requests[0].url); + + expect(requestUrl.searchParams.get('tenantIds')).to.be.equal('13,23'); + }); + + it('gets keywords from the cache if the data is present in the storage', async () => { + getDataFromLocalStorageStub.returns(JSON.stringify({ url: 'https://example.com/article', keywords: { sentiment: 'negative', ctx_segment: ['C001', 'C002'] } })); + + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: { + global: {}, + } + }; + + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => semantiqRtdSubmodule.getBidRequestData( + reqBidsConfigObj, + () => { + onDoneSpy(); + resolve(); + }, + { params: {} }, + {} + )); + + await promise; + + expect(onDoneSpy.calledOnce).to.be.true; + expect(server.requests).to.have.lengthOf(0); + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.equal({ site: { keywords: 'sentiment=negative,ctx_segment=C001,ctx_segment=C002' } }); + }); + + it('requests keywords from the server if the URL of the page is different from the cached one', async () => { + getDataFromLocalStorageStub.returns(JSON.stringify({ url: 'https://example.com/article', keywords: { cached: 'true' } })); + getWindowLocationStub.returns(new URL('https://example.com/another-article')); + + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: { + global: {}, + } + }; + + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => semantiqRtdSubmodule.getBidRequestData( + reqBidsConfigObj, + () => { + onDoneSpy(); + resolve(); + }, + { params: {} }, + {} + )); + + server.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ server: 'true' }) + ); + + await promise; + + expect(onDoneSpy.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.equal({ site: { keywords: 'server=true' } }); + }); + + it('requests keywords from the server if the cached data is missing in the storage', async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: { + global: {}, + }, + }; + + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => semantiqRtdSubmodule.getBidRequestData( + reqBidsConfigObj, + () => { + onDoneSpy(); + resolve(); + }, + { params: {} }, + {} + )); + + server.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ sentiment: 'negative', ctx_segment: ['C001', 'C002'] }) + ); + + await promise; + + expect(onDoneSpy.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.equal({ site: { keywords: 'sentiment=negative,ctx_segment=C001,ctx_segment=C002' } }); + }); + + it('merges ORTB site keywords if they are present', async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: { + global: { + site: { + keywords: 'iab_category=politics', + } + }, + }, + }; + + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => semantiqRtdSubmodule.getBidRequestData( + reqBidsConfigObj, + () => { + onDoneSpy(); + resolve(); + }, + { params: {} }, + {} + )); + + server.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ sentiment: 'negative', ctx_segment: ['C001', 'C002'] }) + ); + + await promise; + + expect(onDoneSpy.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.equal({ site: { keywords: 'iab_category=politics,sentiment=negative,ctx_segment=C001,ctx_segment=C002' } }); + }); + + it("won't modify ortb2 if if no ad units are provided", async () => { + const reqBidsConfigObj = { + adUnits: [], + ortb2Fragments: {} + }; + + const onDoneSpy = sinon.spy(); + + semantiqRtdSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, { params: {} }, {}); + + expect(onDoneSpy.calledOnce).to.be.true; + expect(reqBidsConfigObj).to.deep.equal({ + adUnits: [], + ortb2Fragments: {} + }); + }); + + it("won't modify ortb2 if response is broken", async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: {} + }; + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => { + semantiqRtdSubmodule.getBidRequestData(reqBidsConfigObj, () => { + onDoneSpy(); + resolve(); + }, { params: {} }, {}); + }); + + server.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + '{' + ); + + await promise; + + expect(onDoneSpy.calledOnce).to.be.true; + expect(reqBidsConfigObj).to.deep.equal({ + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: {} + }); + }); + + it("won't modify ortb2 if response status is not 200", async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: {} + }; + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => { + semantiqRtdSubmodule.getBidRequestData(reqBidsConfigObj, () => { + onDoneSpy(); + resolve(); + }, { params: {} }, {}); + }); + + server.requests[0].respond( + 204, + { 'Content-Type': 'application/json' }, + ); + + await promise; + + expect(onDoneSpy.calledOnce).to.be.true; + expect(reqBidsConfigObj).to.deep.equal({ + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: {} + }); + }); + + it("won't modify ortb2 if an error occurs during the request", async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: {} + }; + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => { + semantiqRtdSubmodule.getBidRequestData(reqBidsConfigObj, () => { + onDoneSpy(); + resolve(); + }, { params: {} }, {}); + }); + + server.requests[0].respond( + 500, + { 'Content-Type': 'application/json' }, + '{}' + ); + + await promise; + + expect(onDoneSpy.calledOnce).to.be.true; + expect(reqBidsConfigObj).to.deep.equal({ + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: {}, + }); + }); + + it("won't modify ortb2 if response time hits timeout", async () => { + const reqBidsConfigObj = { + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: {}, + }; + const onDoneSpy = sinon.spy(); + + const promise = new Promise((resolve) => semantiqRtdSubmodule.getBidRequestData(reqBidsConfigObj, () => { + onDoneSpy(); + resolve(); + }, { params: { timeout: 500 } }, {})); + + clock.tick(510); + + await promise; + + expect(onDoneSpy.calledOnce).to.be.true; + expect(reqBidsConfigObj).to.deep.equal({ + adUnits: [{ bids: [{ bidder: 'appnexus' }] }], + ortb2Fragments: {}, + }); + }); + }); +}); diff --git a/test/spec/modules/setupadBidAdapter_spec.js b/test/spec/modules/setupadBidAdapter_spec.js index d4ff73d005f..3a184c50922 100644 --- a/test/spec/modules/setupadBidAdapter_spec.js +++ b/test/spec/modules/setupadBidAdapter_spec.js @@ -1,4 +1,4 @@ -import { spec } from 'modules/setupadBidAdapter.js'; +import { spec, biddersCreativeIds } from 'modules/setupadBidAdapter.js'; describe('SetupadAdapter', function () { const userIdAsEids = [ @@ -42,9 +42,104 @@ describe('SetupadAdapter', function () { }, userIdAsEids, }, + { + adUnitCode: 'test-div-2', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'rubicon', + bidderRequestId: '15246a574e859f', + uspConsent: 'usp-context-string', + gdprConsent: { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + gdprApplies: true, + }, + params: { + placement_id: '123', + account_id: 'test-account-id', + }, + sizes: [[300, 250]], + ortb2: { + device: { + w: 1500, + h: 1000, + }, + site: { + domain: 'test.com', + page: 'http://test.com', + }, + }, + userIdAsEids, + }, ]; const bidderRequest = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + auctionStart: 1579746300522, + bidderCode: 'setupad', + bidderRequestId: '15246a574e859f', + bids: [ + { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'rubicon', + bidderRequestId: '15246a574e859f', + uspConsent: 'usp-context-string', + gdprConsent: { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + gdprApplies: true, + }, + params: { + placement_id: '123', + account_id: 'test-account-id', + }, + sizes: [[300, 250]], + ortb2: { + device: { + w: 1500, + h: 1000, + }, + site: { + domain: 'test.com', + page: 'http://test.com', + }, + }, + userIdAsEids, + }, + { + adUnitCode: 'test-div-2', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'rubicon', + bidderRequestId: '15246a574e859f', + uspConsent: 'usp-context-string', + gdprConsent: { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + gdprApplies: true, + }, + params: { + placement_id: '123', + account_id: 'test-account-id', + }, + sizes: [[300, 250]], + ortb2: { + device: { + w: 1500, + h: 1000, + }, + site: { + domain: 'test.com', + page: 'http://test.com', + }, + }, + userIdAsEids, + }, + ], + gdprConsent: { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + vendorData: {}, + gdprApplies: true, + }, ortb2: { device: { w: 1500, @@ -52,39 +147,27 @@ describe('SetupadAdapter', function () { }, }, refererInfo: { + canonicalUrl: null, domain: 'test.com', page: 'http://test.com', - ref: '', + referer: null, }, }; const serverResponse = { body: { - id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', seatbid: [ { - bid: [ - { - id: 'test-bid-id', - price: 0.8, - adm: 'this is an ad', - adid: 'test-ad-id', - adomain: ['test.addomain.com'], - w: 300, - h: 250, - }, - ], - seat: 'testBidder', + bid: [{ crid: 123 }, { crid: 1234 }], + seat: 'pubmatic', }, - ], - cur: 'USD', - ext: { - sync: { - image: ['urlA?gdpr={{.GDPR}}'], - iframe: ['urlB'], + { + bid: [{ crid: 12345 }], + seat: 'setupad', }, - }, + ], }, + testCase: 1, }; describe('isBidRequestValid', function () { @@ -92,11 +175,26 @@ describe('SetupadAdapter', function () { bidder: 'setupad', params: { placement_id: '123', + account_id: '123', }, }; + it('should return true when required params found', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); + + it('should return false when placement_id is missing', function () { + const bidWithoutPlacementId = { ...bid }; + delete bidWithoutPlacementId.params.placement_id; + expect(spec.isBidRequestValid(bidWithoutPlacementId)).to.equal(false); + }); + + it('should return false when account_id is missing', function () { + const bidWithoutAccountId = { ...bid }; + delete bidWithoutAccountId.params.account_id; + expect(spec.isBidRequestValid(bidWithoutAccountId)).to.equal(false); + }); + it('should return false when required params are not passed', function () { delete bid.params.placement_id; expect(spec.isBidRequestValid(bid)).to.equal(false); @@ -104,77 +202,25 @@ describe('SetupadAdapter', function () { }); describe('buildRequests', function () { - it('check request params with GDPR and USP', function () { - const request = spec.buildRequests(bidRequests, bidRequests[0]); - expect(JSON.parse(request[0].data).user.ext.consent).to.equal( - 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA' - ); - expect(JSON.parse(request[0].data).regs.ext.gdpr).to.equal(1); - expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('usp-context-string'); - }); - - it('check request params without GDPR', function () { - let bidRequestsWithoutGDPR = Object.assign({}, bidRequests[0]); - delete bidRequestsWithoutGDPR.gdprConsent; - const request = spec.buildRequests([bidRequestsWithoutGDPR], bidRequestsWithoutGDPR); - expect(JSON.parse(request[0].data).regs.ext.gdpr).to.be.undefined; - expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('usp-context-string'); + it('should return correct storedrequest id for bids if placement_id is provided', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].ext.prebid.storedrequest.id).to.equal('123'); }); it('should return correct storedrequest id if account_id is provided', function () { - const request = spec.buildRequests(bidRequests, bidRequests[0]); - expect(JSON.parse(request[0].data).ext.prebid.storedrequest.id).to.equal('test-account-id'); - }); - - it('should return correct storedrequest id if account_id is not provided', function () { - let bidRequestsWithoutAccountId = Object.assign({}, bidRequests[0]); - delete bidRequestsWithoutAccountId.params.account_id; - const request = spec.buildRequests( - [bidRequestsWithoutAccountId], - bidRequestsWithoutAccountId - ); - expect(JSON.parse(request[0].data).ext.prebid.storedrequest.id).to.equal('default'); - }); - - it('validate generated params', function () { - const request = spec.buildRequests(bidRequests); - expect(request[0].bidId).to.equal('22c4871113f461'); - expect(JSON.parse(request[0].data).id).to.equal('15246a574e859f'); - }); - - it('check if correct site object was added', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - const siteObj = JSON.parse(request[0].data).site; - - expect(siteObj.domain).to.equal('test.com'); - expect(siteObj.page).to.equal('http://test.com'); - expect(siteObj.ref).to.equal(''); + expect(request.data.ext.prebid.storedrequest.id).to.equal('test-account-id'); }); - it('check if correct device object was added', function () { + it('should return setupad custom adapter param', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - const deviceObj = JSON.parse(request[0].data).device; - - expect(deviceObj.w).to.equal(1500); - expect(deviceObj.h).to.equal(1000); - }); - - it('check if imp object was added', function () { - const request = spec.buildRequests(bidRequests); - expect(JSON.parse(request[0].data).imp).to.be.an('array'); - }); - - it('should send "user.ext.eids" in the request for Prebid.js supported modules only', function () { - const request = spec.buildRequests(bidRequests); - expect(JSON.parse(request[0].data).user.ext.eids).to.deep.equal(userIdAsEids); + expect(request.data.setupad).to.equal('adapter'); }); - it('should send an undefined "user.ext.eids" in the request if userId module is unsupported', function () { - let bidRequestsUnsupportedUserIdModule = Object.assign({}, bidRequests[0]); - delete bidRequestsUnsupportedUserIdModule.userIdAsEids; - const request = spec.buildRequests(bidRequestsUnsupportedUserIdModule); - - expect(JSON.parse(request[0].data).user.ext.eids).to.be.undefined; + // Change this to 1 whenever TEST_REQUEST = 1. This is allowed only for testing requests locally + it('should return correct test attribute value from global value', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.test).to.equal(0); }); }); @@ -241,25 +287,21 @@ describe('SetupadAdapter', function () { describe('interpretResponse', function () { it('should return empty array if error during parsing', () => { const wrongServerResponse = 'wrong data'; - let request = spec.buildRequests(bidRequests, bidRequests[0]); + let request = spec.buildRequests(bidRequests, bidderRequest); let result = spec.interpretResponse(wrongServerResponse, request); expect(result).to.be.instanceof(Array); expect(result.length).to.equal(0); }); - it('should get correct bid response', function () { - const result = spec.interpretResponse(serverResponse, bidRequests[0]); - expect(result).to.be.an('array').with.lengthOf(1); - expect(result[0].requestId).to.equal('22c4871113f461'); - expect(result[0].cpm).to.equal(0.8); - expect(result[0].width).to.equal(300); - expect(result[0].height).to.equal(250); - expect(result[0].creativeId).to.equal('test-bid-id'); - expect(result[0].currency).to.equal('USD'); - expect(result[0].netRevenue).to.equal(true); - expect(result[0].ttl).to.equal(360); - expect(result[0].ad).to.equal('this is an ad'); + it('should update biddersCreativeIds correctly', function () { + spec.interpretResponse(serverResponse, bidderRequest); + + expect(biddersCreativeIds).to.deep.equal({ + 123: 'pubmatic', + 1234: 'pubmatic', + 12345: 'setupad', + }); }); }); diff --git a/test/spec/modules/sharedIdSystem_spec.js b/test/spec/modules/sharedIdSystem_spec.js index fcfbe5f7c3f..c0fd351150b 100644 --- a/test/spec/modules/sharedIdSystem_spec.js +++ b/test/spec/modules/sharedIdSystem_spec.js @@ -1,8 +1,11 @@ import {sharedIdSystemSubmodule, storage} from 'modules/sharedIdSystem.js'; -import {coppaDataHandler} from 'src/adapterManager'; +import {config} from 'src/config.js'; import sinon from 'sinon'; import * as utils from 'src/utils.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {attachIdSystem, init} from '../../../modules/userId/index.js'; +import {getGlobal} from '../../../src/prebidGlobal.js'; let expect = require('chai').expect; @@ -21,14 +24,11 @@ describe('SharedId System', function () { describe('SharedId System getId()', function () { const callbackSpy = sinon.spy(); - let coppaDataHandlerDataStub let sandbox; beforeEach(function () { sandbox = sinon.sandbox.create(); - coppaDataHandlerDataStub = sandbox.stub(coppaDataHandler, 'getCoppa'); sandbox.stub(utils, 'hasDeviceAccess').returns(true); - coppaDataHandlerDataStub.returns(''); callbackSpy.resetHistory(); }); @@ -51,22 +51,18 @@ describe('SharedId System', function () { expect(callbackSpy.lastCall.lastArg).to.equal(UUID); }); it('should abort if coppa is set', function () { - coppaDataHandlerDataStub.returns('true'); - const result = sharedIdSystemSubmodule.getId({}); + const result = sharedIdSystemSubmodule.getId({}, {coppa: true}); expect(result).to.be.undefined; }); }); describe('SharedId System extendId()', function () { const callbackSpy = sinon.spy(); - let coppaDataHandlerDataStub; let sandbox; beforeEach(function () { sandbox = sinon.sandbox.create(); - coppaDataHandlerDataStub = sandbox.stub(coppaDataHandler, 'getCoppa'); sandbox.stub(utils, 'hasDeviceAccess').returns(true); callbackSpy.resetHistory(); - coppaDataHandlerDataStub.returns(''); }); afterEach(function () { sandbox.restore(); @@ -86,9 +82,47 @@ describe('SharedId System', function () { expect(pubcommId).to.equal('TestId'); }); it('should abort if coppa is set', function () { - coppaDataHandlerDataStub.returns('true'); - const result = sharedIdSystemSubmodule.extendId({params: {extend: true}}, undefined, 'TestId'); + const result = sharedIdSystemSubmodule.extendId({params: {extend: true}}, {coppa: true}, 'TestId'); expect(result).to.be.undefined; }); }); + describe('eid', () => { + before(() => { + attachIdSystem(sharedIdSystemSubmodule); + }); + afterEach(() => { + config.resetConfig(); + }); + it('pubCommonId', function() { + const userId = { + pubcid: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubcid.org', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + + it('should set inserter, if provided in config', async () => { + config.setConfig({ + userSync: { + userIds: [{ + name: 'sharedId', + params: { + inserter: 'mock-inserter' + }, + value: {pubcid: 'mock-id'} + }] + } + }); + await getGlobal().getUserIdsAsync(); + const eids = getGlobal().getUserIdsAsEids(); + sinon.assert.match(eids[0], { + source: 'pubcid.org', + inserter: 'mock-inserter' + }) + }) + }) }); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index ab099d87429..0e0bc7fd14c 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -4,6 +4,7 @@ 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'; const spec = newBidder(sharethroughAdapterSpec).getSpec(); @@ -38,19 +39,8 @@ describe('sharethrough adapter spec', function () { expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - it('should return false if req has wrong bidder code', function () { - const invalidBidRequest = { - bidder: 'notSharethrough', - params: { - pkey: 'abc123', - }, - }; - expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); - }); - it('should return true if req is correct', function () { const validBidRequest = { - bidder: 'sharethrough', params: { pkey: 'abc123', }, @@ -85,6 +75,7 @@ describe('sharethrough adapter spec', function () { mediaTypes: { banner: { pos: 1, + battr: [6, 7], }, }, ortb2Imp: { @@ -226,7 +217,7 @@ describe('sharethrough adapter spec', function () { video: { pos: 3, skip: 1, - linearity: 0, + linearity: 1, minduration: 10, maxduration: 30, playbackmethod: [1], @@ -238,9 +229,12 @@ describe('sharethrough adapter spec', function () { skipmin: 10, skipafter: 20, delivery: 1, + battr: [13, 14], companiontype: 'companion type', companionad: 'companion ad', context: 'instream', + placement: 1, + plcmt: 1, }, }, getFloor: () => ({ currency: 'USD', floor: 42 }), @@ -346,6 +340,41 @@ describe('sharethrough adapter spec', function () { expect(openRtbReq.user.ext.eids).to.deep.equal([]); }); + + it('should add ORTB2 device data to the request', () => { + const bidderRequestWithOrtb2Device = { + ...bidderRequest, + ...{ + ortb2: { + device: { + 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', + }, + }, + }, + }; + + const [request] = spec.buildRequests(bidRequests, bidderRequestWithOrtb2Device); + + expect(request.data.device.w).to.equal(bidderRequestWithOrtb2Device.ortb2.device.w); + expect(request.data.device.h).to.equal(bidderRequestWithOrtb2Device.ortb2.device.h); + expect(request.data.device.dnt).to.equal(bidderRequestWithOrtb2Device.ortb2.device.dnt); + expect(request.data.device.ua).to.equal(bidderRequestWithOrtb2Device.ortb2.device.ua); + expect(request.data.device.language).to.equal(bidderRequestWithOrtb2Device.ortb2.device.language); + expect(request.data.device.devicetype).to.equal(bidderRequestWithOrtb2Device.ortb2.device.devicetype); + expect(request.data.device.make).to.equal(bidderRequestWithOrtb2Device.ortb2.device.make); + expect(request.data.device.model).to.equal(bidderRequestWithOrtb2Device.ortb2.device.model); + expect(request.data.device.os).to.equal(bidderRequestWithOrtb2Device.ortb2.device.os); + expect(request.data.device.osv).to.equal(bidderRequestWithOrtb2Device.ortb2.device.osv); + }); }); describe('no referer provided', () => { @@ -524,8 +553,58 @@ describe('sharethrough adapter spec', function () { ]); }); + it('should correctly harvest battr values for banner if present in mediaTypes.banner of impression and battr is not defined in ortb2Imp.banner', () => { + // assemble + const EXPECTED_BATTR_VALUES = [6, 7]; + + // act + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + + // assert + expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); + }); + + it('should not include battr values for banner if NOT present in mediaTypes.banner of impression and battr is not defined in ortb2Imp.banner', () => { + // assemble + delete bidRequests[0].mediaTypes.banner.battr; + + // act + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; + + // assert + expect(builtRequest.data.imp[0].banner.battr).to.be.undefined; + }); + + it('should prefer battr values from mediaTypes.banner over ortb2Imp.banner', () => { + // assemble + deepSetValue(bidRequests[0], 'ortb2Imp.banner.battr', [1, 2, 3]); + const EXPECTED_BATTR_VALUES = [6, 7]; // values from mediaTypes.banner + + // act + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + + // assert + expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); + }); + + it('should use battr values from ortb2Imp.banner if mediaTypes.banner.battr is not present', () => { + // assemble + delete bidRequests[0].mediaTypes.banner.battr; + const EXPECTED_BATTR_VALUES = [1, 2, 3]; + deepSetValue(bidRequests[0], 'ortb2Imp.banner.battr', EXPECTED_BATTR_VALUES); + + // act + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + + // assert + expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); + }); + it('should default to pos 0 if not provided', () => { - delete bidRequests[0].mediaTypes; + delete bidRequests[0].mediaTypes.banner.pos; const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; const bannerImp = builtRequest.data.imp[0].banner; @@ -541,7 +620,7 @@ describe('sharethrough adapter spec', function () { expect(videoImp.pos).to.equal(3); expect(videoImp.topframe).to.equal(1); expect(videoImp.skip).to.equal(1); - expect(videoImp.linearity).to.equal(0); + expect(videoImp.linearity).to.equal(1); expect(videoImp.minduration).to.equal(10); expect(videoImp.maxduration).to.equal(30); expect(videoImp.playbackmethod).to.deep.equal([1]); @@ -555,91 +634,71 @@ describe('sharethrough adapter spec', function () { expect(videoImp.skipafter).to.equal(20); expect(videoImp.placement).to.equal(1); expect(videoImp.delivery).to.equal(1); + expect(videoImp.battr).to.deep.equal([13, 14]); expect(videoImp.companiontype).to.equal('companion type'); expect(videoImp.companionad).to.equal('companion ad'); }); - it('should set defaults if no value provided', () => { + it('should set defaults in some circumstances if no value provided', () => { delete bidRequests[1].mediaTypes.video.pos; - delete bidRequests[1].mediaTypes.video.skip; - delete bidRequests[1].mediaTypes.video.linearity; - delete bidRequests[1].mediaTypes.video.minduration; - delete bidRequests[1].mediaTypes.video.maxduration; - delete bidRequests[1].mediaTypes.video.playbackmethod; - delete bidRequests[1].mediaTypes.video.api; - delete bidRequests[1].mediaTypes.video.mimes; - delete bidRequests[1].mediaTypes.video.protocols; delete bidRequests[1].mediaTypes.video.playerSize; - delete bidRequests[1].mediaTypes.video.startdelay; - delete bidRequests[1].mediaTypes.video.skipmin; - delete bidRequests[1].mediaTypes.video.skipafter; - delete bidRequests[1].mediaTypes.video.placement; - delete bidRequests[1].mediaTypes.video.delivery; - delete bidRequests[1].mediaTypes.video.companiontype; - delete bidRequests[1].mediaTypes.video.companionad; const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; const videoImp = builtRequest.data.imp[0].video; expect(videoImp.pos).to.equal(0); - expect(videoImp.skip).to.equal(0); - expect(videoImp.linearity).to.equal(1); - expect(videoImp.minduration).to.equal(5); - expect(videoImp.maxduration).to.equal(60); - expect(videoImp.playbackmethod).to.deep.equal([2]); - expect(videoImp.api).to.deep.equal([2]); - expect(videoImp.mimes).to.deep.equal(['video/mp4']); - expect(videoImp.protocols).to.deep.equal([2, 3, 5, 6, 7, 8]); expect(videoImp.w).to.equal(640); expect(videoImp.h).to.equal(360); - expect(videoImp.startdelay).to.equal(0); - expect(videoImp.skipmin).to.equal(0); - expect(videoImp.skipafter).to.equal(0); - expect(videoImp.placement).to.equal(1); - expect(videoImp.delivery).to.be.undefined; - expect(videoImp.companiontype).to.be.undefined; - expect(videoImp.companionad).to.be.undefined; }); - describe('outstream', () => { - it('should use placement value if provided', () => { - bidRequests[1].mediaTypes.video.context = 'outstream'; - bidRequests[1].mediaTypes.video.placement = 3; - - const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; - const videoImp = builtRequest.data.imp[0].video; + it('should not set values in some circumstances when non-valid values are supplied', () => { + // arrange + bidRequests[1].mediaTypes.video.api = 1; // non-array value, will not be used + bidRequests[1].mediaTypes.video.battr = undefined; // non-array value, will not be used + bidRequests[1].mediaTypes.video.mimes = 'video/3gpp'; // non-array value, will not be used + bidRequests[1].mediaTypes.video.playbackmethod = null; // non-array value, will not be used + bidRequests[1].mediaTypes.video.protocols = []; // empty array, will not be used - expect(videoImp.placement).to.equal(3); - }); + // act + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; + const videoImp = builtRequest.data.imp[0].video; - it('should default placement to 4 if not provided', () => { - bidRequests[1].mediaTypes.video.context = 'outstream'; + // assert + expect(videoImp.api).to.be.undefined; + expect(videoImp.battr).to.be.undefined; + expect(videoImp.mimes).to.be.undefined; + expect(videoImp.playbackmethod).to.be.undefined; + expect(videoImp.protocols).to.be.undefined; + }); - const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; - const videoImp = builtRequest.data.imp[0].video; + 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' + ] - expect(videoImp.placement).to.equal(4); + // act + propertiesToConsider.forEach(propertyToConsider => { + delete bidRequests[1].mediaTypes.video[propertyToConsider]; }); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; + const videoImp = builtRequest.data.imp[0].video; - it('should not override "placement" value if "plcmt" prop is present', () => { - // ASSEMBLE - const ARBITRARY_PLACEMENT_VALUE = 99; - const ARBITRARY_PLCMT_VALUE = 100; - - bidRequests[1].mediaTypes.video.context = 'instream'; - bidRequests[1].mediaTypes.video.placement = ARBITRARY_PLACEMENT_VALUE; + // assert + propertiesToConsider.forEach(propertyToConsider => { + expect(videoImp[propertyToConsider]).to.be.undefined; + }); + }); - // adding "plcmt" property - this should prevent "placement" prop - // from getting overridden to 1 - bidRequests[1].mediaTypes.video['plcmt'] = ARBITRARY_PLCMT_VALUE; + describe('outstream', () => { + it('should use placement value if provided', () => { + bidRequests[1].mediaTypes.video.context = 'outstream'; + bidRequests[1].mediaTypes.video.placement = 3; - // ACT const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; const videoImp = builtRequest.data.imp[0].video; - // ASSERT - expect(videoImp.placement).to.equal(ARBITRARY_PLACEMENT_VALUE); - expect(videoImp.plcmt).to.equal(ARBITRARY_PLCMT_VALUE); + expect(videoImp.placement).to.equal(3); }); }); }); @@ -755,7 +814,7 @@ describe('sharethrough adapter spec', function () { const EXPECTED_AE_VALUE = 1; // ACT - bidderRequest['fledgeEnabled'] = true; + bidderRequest.paapi = {enabled: true}; const builtRequests = spec.buildRequests(bidRequests, bidderRequest); const ACTUAL_AE_VALUE = builtRequests[0].data.imp[0].ext.ae; diff --git a/test/spec/modules/shinezBidAdapter_spec.js b/test/spec/modules/shinezBidAdapter_spec.js index 4e6c2d3420e..d4ad99359bb 100644 --- a/test/spec/modules/shinezBidAdapter_spec.js +++ b/test/spec/modules/shinezBidAdapter_spec.js @@ -2,8 +2,9 @@ import { expect } from 'chai'; import { spec } from 'modules/shinezBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; import * as utils from 'src/utils.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native'; const ENDPOINT = 'https://hb.sweetgum.io/hb-sz-multi'; const TEST_ENDPOINT = 'https://hb.sweetgum.io/hb-multi-sz-test'; @@ -61,7 +62,6 @@ describe('shinezAdapter', function () { 'context': 'instream' } }, - 'vastXml': '"..."' }, { 'bidder': spec.code, @@ -77,7 +77,59 @@ describe('shinezAdapter', function () { 'banner': { } }, - 'ad': '""' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ 300, 250 ] + ] + }, + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream', + 'plcmt': 1 + }, + 'native': { + 'ortb': { + 'assets': [ + { + 'id': 1, + 'required': 1, + 'img': { + 'type': 3, + 'w': 300, + 'h': 200, + } + }, + { + 'id': 2, + 'required': 1, + 'title': { + 'len': 80 + } + }, + { + 'id': 3, + 'required': 1, + 'data': { + 'type': 1 + } + } + ] + } + }, + }, } ]; @@ -130,12 +182,21 @@ describe('shinezAdapter', function () { expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) expect(request.data.bids[1].sizes).to.be.an('array'); expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + expect(request.data.bids[2].sizes).to.be.an('array'); + expect(request.data.bids[2].sizes).to.eql(bidRequests[2].sizes) + }); + + it('should send nativeOrtbRequest in native bid request', function () { + decorateAdUnitsWithNativeParams(bidRequests) + const request = spec.buildRequests(bidRequests, bidderRequest); + assert.deepEqual(request.data.bids[2].nativeOrtbRequest, bidRequests[2].mediaTypes.native.ortb) }); it('should send the correct media type', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.bids[0].mediaType).to.equal(VIDEO) expect(request.data.bids[1].mediaType).to.equal(BANNER) + expect(request.data.bids[2].mediaType.split(',')).to.include.members([VIDEO, NATIVE, BANNER]) }); it('should respect syncEnabled option', function() { @@ -305,6 +366,8 @@ describe('shinezAdapter', function () { height: 480, requestId: '21e12606d47ba7', adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', mediaType: VIDEO }, { @@ -314,7 +377,31 @@ describe('shinezAdapter', function () { height: 250, requestId: '21e12606d47ba7', adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', mediaType: BANNER + }, + { + cpm: 12.5, + width: 300, + height: 200, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + creativeId: 'creative-id', + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + native: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg' + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } }] }; @@ -325,7 +412,7 @@ describe('shinezAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -340,10 +427,10 @@ describe('shinezAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -354,10 +441,42 @@ describe('shinezAdapter', function () { ad: '""' }; + const expectedNativeResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 300, + height: 200, + ttl: TTL, + creativeId: 'creative-id', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + meta: { + mediaType: NATIVE, + advertiserDomains: ['abc.com'] + }, + native: { + ortb: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg', + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } + }, + }; + it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); + expect(result[2]).to.deep.equal(expectedNativeResponse); }); it('video type should have vastXml key', function () { @@ -369,6 +488,11 @@ describe('shinezAdapter', function () { const result = spec.interpretResponse({ body: response }); expect(result[1].ad).to.equal(expectedBannerResponse.ad) }); + + it('native type should have native key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[2].native).to.eql(expectedNativeResponse.native) + }); }) describe('getUserSyncs', function() { diff --git a/test/spec/modules/shinezRtbBidAdapter_spec.js b/test/spec/modules/shinezRtbBidAdapter_spec.js index 3965cd69c5f..883646d5c27 100644 --- a/test/spec/modules/shinezRtbBidAdapter_spec.js +++ b/test/spec/modules/shinezRtbBidAdapter_spec.js @@ -2,6 +2,9 @@ import {expect} from 'chai'; import { spec as adapter, createDomain, + storage +} from 'modules/shinezRtbBidAdapter'; +import { hashCode, extractPID, extractCID, @@ -10,15 +13,14 @@ import { setStorageItem, tryParseJSON, getUniqueDealId, -} from 'modules/shinezRtbBidAdapter'; -import * as utils from 'src/utils.js'; +} from '../../../libraries/vidazooUtils/bidderUtils.js'; +import {parseUrl, deepClone} from 'src/utils.js'; import {version} from 'package.json'; import {useFakeTimers} from 'sinon'; import {BANNER, VIDEO} from '../../../src/mediaTypes'; import {config} from '../../../src/config'; -import {deepAccess} from 'src/utils.js'; -export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId', 'digitrustid']; +export const TEST_ID_SYSTEMS = ['criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'pubcid', 'tdid', 'pubProvidedId']; const SUB_DOMAIN = 'exchange'; @@ -92,6 +94,36 @@ const VIDEO_BID = { } } +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': '' + }, + 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'}, +}; + const BIDDER_REQUEST = { 'gdprConsent': { 'consentString': 'consent_string', @@ -107,28 +139,17 @@ const BIDDER_REQUEST = { 'ref': 'https://www.somereferrer.com' }, 'ortb2': { + 'site': { + 'content': { + 'language': 'en' + } + }, 'regs': { 'gpp': 'gpp_string', - 'gpp_sid': [7] + 'gpp_sid': [7], + 'coppa': 0 }, - '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': '' - } - } + 'device': ORTB2_DEVICE, } }; @@ -181,7 +202,7 @@ const REQUEST = { function getTopWindowQueryParams() { try { - const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); return parsedUrl.search; } catch (e) { return ''; @@ -309,6 +330,7 @@ describe('ShinezRtbBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + device: ORTB2_DEVICE, uniqueDealId: `${hashUrl}_${Date.now().toString()}`, uqs: getTopWindowQueryParams(), mediaTypes: { @@ -328,7 +350,14 @@ describe('ShinezRtbBidAdapter', function () { startdelay: 0 } }, - gpid: '0123456789' + gpid: '0123456789', + cat: [], + contentLang: 'en', + contentData: [], + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 } }); }); @@ -373,6 +402,7 @@ describe('ShinezRtbBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + device: ORTB2_DEVICE, url: 'https%3A%2F%2Fwww.greatsite.com', referrer: 'https://www.somereferrer.com', cb: 1000, @@ -390,6 +420,13 @@ describe('ShinezRtbBidAdapter', function () { uqs: getTopWindowQueryParams(), 'ext.param1': 'loremipsum', 'ext.param2': 'dolorsitamet', + cat: [], + contentLang: 'en', + contentData: [], + isStorageAllowed: true, + pagecat: [], + userData: [], + coppa: 0 } }); }); @@ -405,7 +442,7 @@ describe('ShinezRtbBidAdapter', function () { expect(result).to.deep.equal([{ type: 'iframe', - url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' }]); }); @@ -413,7 +450,7 @@ describe('ShinezRtbBidAdapter', function () { const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ type: 'iframe', - url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' }]); }); @@ -421,10 +458,21 @@ describe('ShinezRtbBidAdapter', function () { const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ - 'url': 'https://sync.sweetgum.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'url': 'https://sync.sweetgum.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', 'type': 'image' }]); - }) + }); + + it('should have valid user sync with coppa on response', function () { + config.setConfig({ + coppa: 1 + }); + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1' + }]); + }); }); describe('interpret response', function () { @@ -463,7 +511,7 @@ describe('ShinezRtbBidAdapter', function () { }); it('should get meta from response metaData', function () { - const serverResponse = utils.deepClone(SERVER_RESPONSE); + const serverResponse = deepClone(SERVER_RESPONSE); serverResponse.body.results[0].metaData = { advertiserDomains: ['sweetgum.io'], agencyName: 'Agency Name', @@ -496,7 +544,7 @@ describe('ShinezRtbBidAdapter', function () { }); it('should take default TTL', function () { - const serverResponse = utils.deepClone(SERVER_RESPONSE); + const serverResponse = deepClone(SERVER_RESPONSE); delete serverResponse.body.results[0].exp; const responses = adapter.interpretResponse(serverResponse, REQUEST); expect(responses).to.have.length(1); @@ -507,16 +555,12 @@ describe('ShinezRtbBidAdapter', function () { describe('user id system', function () { TEST_ID_SYSTEMS.forEach((idSystemProvider) => { const id = Date.now().toString(); - const bid = utils.deepClone(BID); + const bid = deepClone(BID); const userId = (function () { switch (idSystemProvider) { - case 'digitrustid': - return {data: {id}}; case 'lipb': return {lipbid: id}; - case 'parrableId': - return {eid: id}; case 'id5id': return {uid: id}; default: @@ -569,13 +613,13 @@ describe('ShinezRtbBidAdapter', function () { const key = 'myKey'; let uniqueDealId; beforeEach(() => { - uniqueDealId = getUniqueDealId(key, 0); + uniqueDealId = getUniqueDealId(storage, key, 0); }) it('should get current unique deal id', function (done) { // waiting some time so `now` will become past setTimeout(() => { - const current = getUniqueDealId(key); + const current = getUniqueDealId(storage, key); expect(current).to.be.equal(uniqueDealId); done(); }, 200); @@ -583,7 +627,7 @@ describe('ShinezRtbBidAdapter', function () { it('should get new unique deal id on expiration', function (done) { setTimeout(() => { - const current = getUniqueDealId(key, 100); + const current = getUniqueDealId(storage, key, 100); expect(current).to.not.be.equal(uniqueDealId); done(); }, 200) @@ -607,8 +651,8 @@ describe('ShinezRtbBidAdapter', function () { shouldAdvanceTime: true, now }); - setStorageItem('myKey', 2020); - const {value, created} = getStorageItem('myKey'); + 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'); @@ -619,7 +663,7 @@ describe('ShinezRtbBidAdapter', function () { it('should get external stored value', function () { const value = 'superman' window.localStorage.setItem('myExternalKey', value); - const item = getStorageItem('myExternalKey'); + const item = getStorageItem(storage, 'myExternalKey'); expect(item).to.be.equal(value); }); diff --git a/test/spec/modules/showheroes-bsBidAdapter_spec.js b/test/spec/modules/showheroes-bsBidAdapter_spec.js index 30e95b04ccf..07211ca37cc 100644 --- a/test/spec/modules/showheroes-bsBidAdapter_spec.js +++ b/test/spec/modules/showheroes-bsBidAdapter_spec.js @@ -1,98 +1,67 @@ -import {expect} from 'chai' -import {spec} from 'modules/showheroes-bsBidAdapter.js' -import {newBidder} from 'src/adapters/bidderFactory.js' -import {VIDEO, BANNER} from 'src/mediaTypes.js' +import { expect } from 'chai' +import { spec } from 'modules/showheroes-bsBidAdapter.js' +import { addFPDToBidderRequest } from '../../helpers/fpd.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagementTcf.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; +import { VIDEO } from 'src/mediaTypes.js' const bidderRequest = { refererInfo: { - canonicalUrl: 'https://example.com' + page: 'https://example.com/home', + ref: 'https://referrer' } } const adomain = ['showheroes.com']; const gdpr = { - 'gdprConsent': { - 'apiVersion': 2, - 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', - 'gdprApplies': true + gdprConsent: { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, } } const uspConsent = '1---'; const schain = { - 'schain': { - 'validation': 'strict', - 'config': { - 'ver': '1.0', - 'complete': 1, - 'nodes': [ + schain: { + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [ { - 'asi': 'some.com', - 'sid': '00001', - 'hp': 1 + asi: 'some.com', + sid: '00001', + hp: 1 } ] } } } -const bidRequestCommonParams = { - 'bidder': 'showheroes-bs', - 'params': { - 'playerId': '47427aa0-f11a-4d24-abca-1295a46a46cd', - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[640, 480]], - 'bidId': '38b373e1e31c18', - 'bidderRequestId': '12e3ade2543ba6', - 'auctionId': '43aa080090a47f', -} - const bidRequestCommonParamsV2 = { - 'bidder': 'showheroes-bs', - 'params': { - 'unitId': 'AACBWAcof-611K4U', + bidder: 'showheroes-bs', + params: { + unitId: 'AACBWAcof-611K4U', }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[640, 480]], - 'bidId': '38b373e1e31c18', - 'bidderRequestId': '12e3ade2543ba6', - 'auctionId': '43aa080090a47f', -} - -const bidRequestVideo = { - ...bidRequestCommonParams, - ...{ - 'mediaTypes': { - 'video': { - 'playerSize': [640, 480], - 'context': 'instream', - } - } - } -} - -const bidRequestOutstream = { - ...bidRequestCommonParams, - ...{ - 'mediaTypes': { - 'video': { - 'playerSize': [640, 480], - 'context': 'outstream', - } - } - } + adUnitCode: 'adunit-code-1', + bidId: '38b373e1e31c18', + bidderRequestId: '12e3ade2543ba6', + auctionId: '43aa080090a47f', } const bidRequestVideoV2 = { ...bidRequestCommonParamsV2, ...{ - 'mediaTypes': { - 'video': { - 'playerSize': [640, 480], - 'context': 'instream', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', } } } @@ -101,541 +70,188 @@ const bidRequestVideoV2 = { const bidRequestOutstreamV2 = { ...bidRequestCommonParamsV2, ...{ - 'mediaTypes': { - 'video': { - 'playerSize': [640, 480], - 'context': 'outstream' - } - } - } -} - -const bidRequestVideoVpaid = { - ...bidRequestCommonParams, - ...{ - 'params': { - 'playerId': '47427aa0-f11a-4d24-abca-1295a46a46cd', - 'vpaidMode': true, - }, - 'mediaTypes': { - 'video': { - 'playerSize': [640, 480], - 'context': 'instream', - } - } - } -} - -const bidRequestBanner = { - ...bidRequestCommonParams, - ...{ - 'mediaTypes': { - 'banner': { - 'sizes': [[640, 360]] - } - } - } -} - -const bidRequestBannerMultiSizes = { - ...bidRequestCommonParams, - ...{ - 'mediaTypes': { - 'banner': { - 'sizes': [[640, 360], [480, 320]] - } + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream' + }, } } } -const bidRequestVideoAndBanner = { - ...bidRequestCommonParams, - 'mediaTypes': { - ...bidRequestBanner.mediaTypes, - ...bidRequestVideo.mediaTypes - } -} - -describe('shBidAdapter', 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 () { - it('should return true when required params found', function () { - const requestV1 = { - 'params': { - 'playerId': '47427aa0-f11a-4d24-abca-1295a46a46cd', - } - } - expect(spec.isBidRequestValid(requestV1)).to.equal(true) - - const requestV2 = { - 'params': { - 'unitId': 'AACBTwsZVANd9NlB', - } - } - expect(spec.isBidRequestValid(requestV2)).to.equal(true) - }) - - it('should return false when required params are not passed', function () { - const request = { - 'params': {} - } - expect(spec.isBidRequestValid(request)).to.equal(false) - }) - }) - - describe('buildRequests', function () { - it('sends bid request to ENDPOINT via POST', function () { - const request = spec.buildRequests([bidRequestVideo], bidderRequest) - expect(request.method).to.equal('POST') - - const requestV2 = spec.buildRequests([bidRequestVideoV2], bidderRequest) - expect(requestV2.method).to.equal('POST') - }) - - it('check sizes formats', function () { - const request = spec.buildRequests([{ - 'params': {}, - 'mediaTypes': { - 'banner': { - 'sizes': [[320, 240]] - } - }, - }], bidderRequest) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload.size).to.have.property('width', 320); - expect(payload.size).to.have.property('height', 240); - - const request2 = spec.buildRequests([{ - 'params': {}, - 'mediaTypes': { - 'video': { - 'playerSize': [640, 360] - } - }, - }], bidderRequest) - const payload2 = request2.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload2.size).to.have.property('width', 640); - expect(payload2.size).to.have.property('height', 360); - }) - - it('should get size from mediaTypes when sizes property is empty', function () { - const request = spec.buildRequests([{ - 'params': {}, - 'mediaTypes': { - 'video': { - 'playerSize': [640, 480] - } - }, - 'sizes': [], - }], bidderRequest) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload.size).to.have.property('width', 640); - expect(payload.size).to.have.property('height', 480); - - const request2 = spec.buildRequests([{ - 'params': {}, - 'mediaTypes': { - 'banner': { - 'sizes': [[320, 240]] - } - }, - 'sizes': [], - }], bidderRequest) - const payload2 = request2.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload2.size).to.have.property('width', 320); - expect(payload2.size).to.have.property('height', 240); - }) - - it('should attach valid params to the payload when type is video', function () { - const request = spec.buildRequests([bidRequestVideo], bidderRequest) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); - expect(payload).to.have.property('mediaType', VIDEO); - expect(payload).to.have.property('type', 2); - }) - - it('should attach valid params to the payload when type is video & vpaid mode on', function () { - const request = spec.buildRequests([bidRequestVideoVpaid], bidderRequest) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); - expect(payload).to.have.property('mediaType', VIDEO); - expect(payload).to.have.property('type', 1); - }) - - it('should attach valid params to the payload when type is banner', function () { - const request = spec.buildRequests([bidRequestBanner], bidderRequest) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); - expect(payload).to.have.property('mediaType', BANNER); - expect(payload).to.have.property('type', 5); - }) - - it('should attach valid params to the payload when type is banner (multi sizes)', function () { - const request = spec.buildRequests([bidRequestBannerMultiSizes], bidderRequest) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); - expect(payload).to.have.property('mediaType', BANNER); - expect(payload).to.have.property('type', 5); - expect(payload).to.have.nested.property('size.width', 640); - expect(payload).to.have.nested.property('size.height', 360); - const payload2 = request.data.requests[1]; - expect(payload2).to.be.an('object'); - expect(payload2).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); - expect(payload2).to.have.property('mediaType', BANNER); - expect(payload2).to.have.property('type', 5); - expect(payload2).to.have.nested.property('size.width', 480); - expect(payload2).to.have.nested.property('size.height', 320); - }) - - it('should attach valid params to the payload when type is banner and video', function () { - const request = spec.buildRequests([bidRequestVideoAndBanner], bidderRequest) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); - expect(payload).to.have.property('mediaType', VIDEO); - expect(payload).to.have.property('type', 2); - const payload2 = request.data.requests[1]; - expect(payload2).to.be.an('object'); - expect(payload2).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); - expect(payload2).to.have.property('mediaType', BANNER); - expect(payload2).to.have.property('type', 5); - }) - - it('should attach valid params to the payload when type is video (instream V2)', function () { - const request = spec.buildRequests([bidRequestVideoV2], bidderRequest) - const payload = request.data.bidRequests[0]; - expect(payload).to.be.an('object'); - expect(payload).to.have.property('unitId', 'AACBWAcof-611K4U'); - expect(payload.mediaTypes).to.eql({ - [VIDEO]: { - 'context': 'instream' - } - }); - }) - - it('should attach valid params to the payload when type is video (outstream V2)', function () { - const request = spec.buildRequests([bidRequestOutstreamV2], bidderRequest) - const payload = request.data.bidRequests[0]; - expect(payload).to.be.an('object'); - expect(payload).to.have.property('unitId', 'AACBWAcof-611K4U'); - expect(payload.mediaTypes).to.eql({ - [VIDEO]: { - 'context': 'outstream' - } - }); - }) - - it('passes gdpr & uspConsent if present', function () { - const request = spec.buildRequests([bidRequestVideo], { - ...bidderRequest, - ...gdpr, - uspConsent, - }) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload.gdprConsent).to.eql(gdpr.gdprConsent) - expect(payload.uspConsent).to.eql(uspConsent) - }) - - it('passes gdpr & usp if present (V2)', function () { - const request = spec.buildRequests([bidRequestVideoV2], { - ...bidderRequest, - ...gdpr, - uspConsent, - }) - const context = request.data.context; - expect(context).to.be.an('object'); - expect(context.gdprConsent).to.eql(gdpr.gdprConsent) - expect(context.uspConsent).to.eql(uspConsent) - }) - - it('passes schain object if present', function() { - const request = spec.buildRequests([{ - ...bidRequestVideo, - ...schain - }], bidderRequest) - const payload = request.data.requests[0]; - expect(payload).to.be.an('object'); - expect(payload.schain).to.eql(schain.schain); - }) - - it('passes schain object if present (V2)', function() { - const request = spec.buildRequests([{ - ...bidRequestVideoV2, - ...schain - }], bidderRequest) - const context = request.data.context; - expect(context).to.be.an('object'); - expect(context.schain).to.eql(schain.schain); - }) - }) - - describe('interpretResponse', function () { - it('handles nobid responses', function () { - expect(spec.interpretResponse({body: {}}, {data: {meta: {}}}).length).to.equal(0) - expect(spec.interpretResponse({body: []}, {data: {meta: {}}}).length).to.equal(0) - }) - - const vastTag = 'https://test.com/commercial/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6' - const vastXml = '' - - const basicResponse = { - 'cpm': 5, - 'currency': 'EUR', - 'mediaType': VIDEO, - 'context': 'instream', - 'bidId': '38b373e1e31c18', - 'size': {'width': 640, 'height': 480}, - 'vastTag': 'https:\/\/test.com\/commercial\/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6', - 'vastXml': vastXml, - 'adomain': adomain, - }; - - const responseVideo = { - 'bids': [{ - ...basicResponse, - }], +describe('shBidAdapter', () => { + it('validates request', () => { + const bid = { + params: { + testKey: 'testValue', + }, }; - - const responseVideoOutstream = { - 'bids': [{ - ...basicResponse, - 'context': 'outstream', - }], + expect(spec.isBidRequestValid(bid)).to.eql(false); + bid.params = { + unitId: 'test_unit', }; + expect(spec.isBidRequestValid(bid)).to.eql(true); + }); - const responseBanner = { - 'bids': [{ - ...basicResponse, - 'mediaType': BANNER, - }], + it('passes gdpr, usp, schain, floor in ortb request', async () => { + const bidRequest = Object.assign({}, bidRequestVideoV2) + const fullRequest = { + bids: [bidRequestVideoV2], + ...bidderRequest, + ...gdpr, + ...schain, + ...{uspConsent: uspConsent}, }; + bidRequest.schain = schain.schain.config; + const getFloorResponse = {currency: 'EUR', floor: 3}; + bidRequest.getFloor = () => getFloorResponse; + const request = spec.buildRequests([bidRequest], await addFPDToBidderRequest(fullRequest)); + const payload = request.data; + expect(payload.regs.ext.gdpr).to.eql(Number(gdpr.gdprConsent.gdprApplies)); + expect(payload.regs.ext.us_privacy).to.eql(uspConsent); + expect(payload.user.ext.consent).to.eql(gdpr.gdprConsent.consentString); + expect(payload.source.ext.schain).to.eql(bidRequest.schain); + expect(payload.test).to.eql(0); + expect(payload.imp[0].bidfloor).eql(3); + expect(payload.imp[0].bidfloorcur).eql('EUR'); + expect(payload.imp[0].displaymanager).eql('Prebid.js'); + expect(payload.site.page).to.eql('https://example.com/home'); + }); - const basicResponseV2 = { - 'requestId': '38b373e1e31c18', - 'adUnitCode': 'adunit-code-1', - 'cpm': 1, - 'currency': 'EUR', - 'width': 640, - 'height': 480, - 'advertiserDomain': [], - 'callbacks': { - 'won': ['https://test.com/track/?ver=15&session_id=01ecd03ce381505ccdeb88e555b05001&category=request_session&type=event&request_session_id=01ecd03ce381505ccdeb88e555b05001&label=prebid_won&reason=ok'] - }, - 'mediaType': 'video', - 'adomain': adomain, - }; + describe('interpretResponse', function () { + const vastXml = '' + const callback_won = 'https://test.com/track/?ver=15&session_id=01ecd03ce381505ccdeb88e555b05001&category=request_session&type=event&request_session_id=01ecd03ce381505ccdeb88e555b05001&label=prebid_won&reason=ok' + const basicResponse = { + cur: 'EUR', + seatbid: [{ + bid: [{ + price: 1, + w: 640, + h: 480, + adm: vastXml, + impid: '38b373e1e31c18', + crid: 'c_38b373e1e31c18', + adomain: adomain, + ext: { + callbacks: { + won: [callback_won], + }, + extra: 'test', + }, + }], + seat: 'showheroes', + }] + } const vastUrl = 'https://test.com/vast/?zid=AACBWAcof-611K4U&u=https://example.org/?foo=bar&gdpr=0&cs=XXXXXXXXXXXXXXXXXXXX&sid=01ecd03ce381505ccdeb88e555b05001&width=300&height=200&prebidmode=1' - const responseVideoV2 = { - 'bidResponses': [{ - ...basicResponseV2, - 'context': 'instream', - 'vastUrl': vastUrl, - }], - }; - - const responseVideoOutstreamV2 = { - 'bidResponses': [{ - ...basicResponseV2, - 'context': 'outstream', - 'ad': '', - }], - }; - - it('should get correct bid response when type is video', function () { - const request = spec.buildRequests([bidRequestVideo], bidderRequest) - const expectedResponse = [ - { - 'cpm': 5, - 'creativeId': 'c_38b373e1e31c18', - 'adUnitCode': 'adunit-code-1', - 'currency': 'EUR', - 'width': 640, - 'height': 480, - 'mediaType': 'video', - 'netRevenue': true, - 'vastUrl': vastTag, - 'vastXml': vastXml, - 'requestId': '38b373e1e31c18', - 'ttl': 300, - 'adResponse': { - 'content': vastXml - }, - 'meta': { - 'advertiserDomains': adomain + if (FEATURES.VIDEO) { + it('should get correct bid response when type is video (V2)', function () { + const request = spec.buildRequests([bidRequestVideoV2], bidderRequest) + const expectedResponse = [ + { + cpm: 1, + creativeId: 'c_38b373e1e31c18', + creative_id: 'c_38b373e1e31c18', + currency: 'EUR', + width: 640, + height: 480, + playerHeight: 480, + playerWidth: 640, + mediaType: 'video', + netRevenue: true, + requestId: '38b373e1e31c18', + ttl: 300, + meta: { + advertiserDomains: adomain + }, + vastXml: vastXml, + callbacks: { + won: [callback_won], + }, + extra: 'test', } - } - ] + ] - const result = spec.interpretResponse({'body': responseVideo}, request) - expect(result).to.deep.equal(expectedResponse) - }) + const result = spec.interpretResponse({ 'body': basicResponse }, request) + expect(result).to.deep.equal(expectedResponse) + }) - it('should get correct bid response when type is video (V2)', function () { - const request = spec.buildRequests([bidRequestVideoV2], bidderRequest) - const expectedResponse = [ - { - 'cpm': 1, - 'creativeId': 'c_38b373e1e31c18', - 'adUnitCode': 'adunit-code-1', - 'currency': 'EUR', - 'width': 640, - 'height': 480, - 'mediaType': 'video', - 'netRevenue': true, - 'vastUrl': vastUrl, - 'requestId': '38b373e1e31c18', - 'ttl': 300, - 'meta': { - 'advertiserDomains': adomain + it('should get correct bid response when type is outstream (slot V2)', function () { + window.myRenderer = { + renderAd: function() { + return null; } } - ] - - const result = spec.interpretResponse({'body': responseVideoV2}, request) - expect(result).to.deep.equal(expectedResponse) - }) - - it('should get correct bid response when type is banner', function () { - const request = spec.buildRequests([bidRequestBanner], bidderRequest) - - const result = spec.interpretResponse({'body': responseBanner}, request) - expect(result[0]).to.have.property('mediaType', BANNER); - expect(result[0].ad).to.include('
", + 'cpm': 0.1, + 'ttl': 120, + 'creativeId': '123', + 'netRevenue': true, + 'currency': 'USD', + 'dealId': 'HASH', + 'sid': 1234, + 'meta': { + 'advertiserDomains': [ + 'smartyads.com' + ], + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [ + { + 'name': 'smartyads' + } + ] + } + }, + 'metrics': { + 'requestBids.usp': 0.20000000298023224, + 'requestBids.gdpr': 67.19999999925494, + 'requestBids.fpd': 4.299999997019768, + 'requestBids.validate': 0.29999999701976776, + 'requestBids.makeRequests': 1.800000000745058, + 'requestBids.total': 740.9000000022352, + 'requestBids.callBids': 663, + 'adapter.client.validate': 0, + 'adapters.client.smartyads.validate': 0, + 'adapter.client.buildRequests': 1, + 'adapters.client.smartyads.buildRequests': 1, + 'adapter.client.total': 661.6999999992549, + 'adapters.client.smartyads.total': 661.6999999992549, + 'adapter.client.net': 657.8999999985099, + 'adapters.client.smartyads.net': 657.8999999985099, + 'adapter.client.interpretResponse': 0, + 'adapters.client.smartyads.interpretResponse': 0, + 'addBidResponse.validate': 0.19999999925494194, + 'addBidResponse.categoryTranslation': 0, + 'addBidResponse.dchain': 0.10000000149011612, + 'addBidResponse.dsa': 0, + 'addBidResponse.multibid': 0, + 'addBidResponse.total': 1.5999999977648258, + 'render.pending': 368.80000000074506, + 'render.e2e': 1109.7000000029802 + }, + 'adapterCode': 'smartyads', + 'originalCpm': 0.1, + 'originalCurrency': 'USD', + 'responseTimestamp': 1715350155081, + 'requestTimestamp': 1715350154420, + 'bidder': 'smartyads', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'timeToRespond': 661, + 'pbLg': '0.00', + 'pbMg': '0.10', + 'pbHg': '0.10', + 'pbAg': '0.10', + 'pbDg': '0.10', + 'pbCg': '', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'smartyads', + 'hb_adid': '3c81d46b03abb', + 'hb_pb': '0.10', + 'hb_size': '300x250', + 'hb_deal': 'HASH', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': 'smartyads.com', + 'hb_crid': '123' + }, + 'latestTargetedAuctionId': '5c0d10bf-96cb-4afa-92ac-2ef75470da22', + 'status': 'rendered', + 'params': [ + { + 'sourceid': '983', + 'host': 'prebid', + 'accountid': '18349', + 'traffic': 'banner' + } + ] + } + }; + + after(function () { + smartyadsAnalytics.disableAnalytics(); + }); + + describe('main test', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + sinon.spy(smartyadsAnalytics, 'track'); + }); + + afterEach(function () { + events.getEvents.restore(); + smartyadsAnalytics.disableAnalytics(); + smartyadsAnalytics.track.restore(); + }); + + it('test auctionEnd', function () { + adapterManager.registerAnalyticsAdapter({ + code: 'smartyads', + adapter: smartyadsAnalytics + }); + + adapterManager.enableAnalytics({ + provider: 'smartyads', + }); + + events.emit(EVENTS.AUCTION_END, auctionEnd); + expect(server.requests.length).to.equal(1); + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.have.property('auctionData'); + expect(message).to.have.property('eventType').and.to.equal(EVENTS.AUCTION_END); + expect(message.auctionData).to.have.property('auctionId'); + expect(message.auctionData.bidderRequests).to.have.length(1); + }); + + it('test bidWon', function() { + adapterManager.registerAnalyticsAdapter({ + code: 'smartyads', + adapter: smartyadsAnalytics + }); + + adapterManager.enableAnalytics({ + provider: 'smartyads', + }); + + events.emit(EVENTS.BID_WON, bidWon); + expect(server.requests.length).to.equal(1); + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.have.property('eventType').and.to.equal(EVENTS.BID_WON); + expect(message).to.have.property('bid'); + expect(message.bid).to.have.property('bidder').and.to.equal('smartyads'); + expect(message.bid).to.have.property('cpm').and.to.equal(bidWon.cpm); + }); + + it('test adRender', function() { + adapterManager.registerAnalyticsAdapter({ + code: 'smartyads', + adapter: smartyadsAnalytics + }); + + adapterManager.enableAnalytics({ + provider: 'smartyads', + }); + + events.emit(EVENTS.AD_RENDER_SUCCEEDED, renderData); + expect(server.requests.length).to.equal(1); + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.have.property('eventType').and.to.equal(EVENTS.AD_RENDER_SUCCEEDED); + expect(message).to.have.property('renderData'); + expect(message.renderData).to.have.property('doc'); + expect(message.renderData).to.have.property('doc'); + expect(message.renderData).to.have.property('bid'); + expect(message.renderData.bid).to.have.property('adId').and.to.equal(renderData.bid.adId); + }); + }); +}); diff --git a/test/spec/modules/smartyadsBidAdapter_spec.js b/test/spec/modules/smartyadsBidAdapter_spec.js index 1b592e142c3..65480ee11e6 100644 --- a/test/spec/modules/smartyadsBidAdapter_spec.js +++ b/test/spec/modules/smartyadsBidAdapter_spec.js @@ -61,12 +61,10 @@ describe('SmartyadsAdapter', function () { it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa', 'eeid', 'ifa'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'host', 'page', 'placements', 'coppa', 'eeid', 'ifa'); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); expect(data.coppa).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'); let placement = data['placements'][0]; @@ -126,8 +124,8 @@ describe('SmartyadsAdapter', function () { expect(dataItem.width).to.equal(300); expect(dataItem.height).to.equal(250); expect(dataItem.ad).to.equal('Test'); - expect(dataItem.meta).to.have.property('advertiserDomains') - expect(dataItem.meta.advertiserDomains).to.deep.equal(['example.com']); + expect(dataItem.meta).to.have.property('advertiserDomains'); + expect(dataItem.meta.advertiserDomains).to.be.an('array'); expect(dataItem.ttl).to.equal(120); expect(dataItem.creativeId).to.equal('2'); expect(dataItem.netRevenue).to.be.true; @@ -152,7 +150,7 @@ describe('SmartyadsAdapter', function () { let dataItem = videoResponses[0]; expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType'); + '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'); @@ -183,7 +181,7 @@ describe('SmartyadsAdapter', function () { expect(nativeResponses).to.be.an('array').that.is.not.empty; let dataItem = nativeResponses[0]; - expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + 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); @@ -263,7 +261,7 @@ describe('SmartyadsAdapter', function () { }); }); describe('getUserSyncs', function () { - const syncUrl = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a&gdpr=0&gdpr_consent=&type=iframe&us_privacy=&gpp='; + const syncUrl = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a/iframe?pbjs=1&coppa=0'; const syncOptions = { iframeEnabled: true }; @@ -277,79 +275,4 @@ describe('SmartyadsAdapter', function () { ]); }); }); - - describe('onBidWon', function () { - it('should exists', function () { - expect(spec.onBidWon).to.exist.and.to.be.a('function'); - }); - - it('should send a valid bid won notice', function () { - const bid = { - 'c': 'o', - 'm': 'prebid', - 'secret_key': 'prebid_js', - 'winTest': '1', - 'postData': [{ - 'bidder': 'smartyads', - 'params': [ - {'host': 'prebid', - 'accountid': '123', - 'sourceid': '12345' - }] - }] - }; - spec.onBidWon(bid); - expect(server.requests.length).to.equal(1); - }); - }); - - describe('onTimeout', function () { - it('should exists', function () { - expect(spec.onTimeout).to.exist.and.to.be.a('function'); - }); - - it('should send a valid bid timeout notice', function () { - const bid = { - 'c': 'o', - 'm': 'prebid', - 'secret_key': 'prebid_js', - 'bidTimeout': '1', - 'postData': [{ - 'bidder': 'smartyads', - 'params': [ - {'host': 'prebid', - 'accountid': '123', - 'sourceid': '12345' - }] - }] - }; - spec.onTimeout(bid); - expect(server.requests.length).to.equal(1); - }); - }); - - describe('onBidderError', function () { - it('should exists', function () { - expect(spec.onBidderError).to.exist.and.to.be.a('function'); - }); - - it('should send a valid bidder error notice', function () { - const bid = { - 'c': 'o', - 'm': 'prebid', - 'secret_key': 'prebid_js', - 'bidderError': '1', - 'postData': [{ - 'bidder': 'smartyads', - 'params': [ - {'host': 'prebid', - 'accountid': '123', - 'sourceid': '12345' - }] - }] - }; - spec.onBidderError(bid); - expect(server.requests.length).to.equal(1); - }); - }); }); diff --git a/test/spec/modules/smartytechBidAdapter_spec.js b/test/spec/modules/smartytechBidAdapter_spec.js index 6b3147859bf..3b6d5d0c5fc 100644 --- a/test/spec/modules/smartytechBidAdapter_spec.js +++ b/test/spec/modules/smartytechBidAdapter_spec.js @@ -204,7 +204,11 @@ function mockResponseData(requestData) { creativeId: `creative-id-${index}`, cpm: Math.floor(Math.random() * 100), currency: `UAH-${rndIndex}`, - mediaType: mediaType + mediaType: mediaType, + meta: { + primaryCatId: 'IAB2-2', + secondaryCatIds: ['IAB2-14', 'IAB2-6'] + } }; }); return { diff --git a/test/spec/modules/smilewantedBidAdapter_spec.js b/test/spec/modules/smilewantedBidAdapter_spec.js index 99c4034610f..1c71c7bee07 100644 --- a/test/spec/modules/smilewantedBidAdapter_spec.js +++ b/test/spec/modules/smilewantedBidAdapter_spec.js @@ -76,6 +76,49 @@ const DISPLAY_REQUEST_WITH_POSITION_TYPE = [{ }, }]; +const SCHAIN = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + }, + { + 'asi': 'exchange2.com', + 'sid': 'abcd', + 'hp': 1, + 'rid': 'bid-request-2', + 'name': 'intermediary', + 'domain': 'intermediary.com' + } + ] +}; + +const DISPLAY_REQUEST_WITH_SCHAIN = [{ + adUnitCode: 'sw_300x250', + bidId: '12345', + sizes: [ + [300, 250], + [300, 200] + ], + bidder: 'smilewanted', + params: { + zoneId: 1, + }, + requestId: 'request_abcd1234', + ortb2Imp: { + ext: { + tid: 'trans_abcd1234', + } + }, + schain: SCHAIN, +}]; + const BID_RESPONSE_DISPLAY = { body: { cpm: 3, @@ -580,8 +623,21 @@ describe('smilewantedBidAdapterTests', function () { expect(requestContent).to.have.property('positionType').and.to.equal('infeed'); }); + it('SmileWanted - Verify if schain is well passed', function () { + const request = spec.buildRequests(DISPLAY_REQUEST_WITH_SCHAIN, {}); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('schain').and.to.equal('1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com,!exchange2.com,abcd,1,bid-request-2,intermediary,intermediary.com,'); + }); + + it('SmileWanted - Verify user sync - empty data', function () { + let syncs = spec.getUserSyncs({iframeEnabled: true}, {}, {}, null); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://csync.smilewanted.com'); + }); + it('SmileWanted - Verify user sync', function () { - var syncs = spec.getUserSyncs({iframeEnabled: true}, {}, { + let syncs = spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: 'foo' }, '1NYN'); expect(syncs).to.have.lengthOf(1); diff --git a/test/spec/modules/smootBidAdapter_spec.js b/test/spec/modules/smootBidAdapter_spec.js new file mode 100644 index 00000000000..f51c054f883 --- /dev/null +++ b/test/spec/modules/smootBidAdapter_spec.js @@ -0,0 +1,599 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/smootBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'smoot'; + +describe('SmootBidAdapter', 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', + }, + 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 () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys( + 'deviceWidth', + 'deviceHeight', + '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, + }, + ]; + + let 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); + let 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); + let 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], + }; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }); + }); + + 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, + }, + }, + ], + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let 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, + }, + }, + ], + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let 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, + }, + }, + ], + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let 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', + }, + ], + }; + + let 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', + }, + ], + }; + let 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', + }, + ], + }; + let 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', + }, + ], + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('Should return array of objects with proper sync config , include GDPR', function () { + const syncData = spec.getUserSyncs( + {}, + {}, + { + consentString: 'ALL', + gdprApplies: true, + }, + {} + ); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object'); + expect(syncData[0].type).to.be.a('string'); + expect(syncData[0].type).to.equal('image'); + expect(syncData[0].url).to.be.a('string'); + expect(syncData[0].url).to.equal( + 'https://usync.smxconv.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0' + ); + }); + it('Should return array of objects with proper sync config , include CCPA', function () { + const syncData = spec.getUserSyncs( + {}, + {}, + {}, + { + consentString: '1---', + } + ); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object'); + expect(syncData[0].type).to.be.a('string'); + expect(syncData[0].type).to.equal('image'); + expect(syncData[0].url).to.be.a('string'); + expect(syncData[0].url).to.equal( + 'https://usync.smxconv.com/image?pbjs=1&ccpa_consent=1---&coppa=0' + ); + }); + it('Should return array of objects with proper sync config , include GPP', function () { + const syncData = spec.getUserSyncs( + {}, + {}, + {}, + {}, + { + gppString: 'abc123', + applicableSections: [8], + } + ); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object'); + expect(syncData[0].type).to.be.a('string'); + expect(syncData[0].type).to.equal('image'); + expect(syncData[0].url).to.be.a('string'); + expect(syncData[0].url).to.equal( + 'https://usync.smxconv.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0' + ); + }); + }); +}); diff --git a/test/spec/modules/snigelBidAdapter_spec.js b/test/spec/modules/snigelBidAdapter_spec.js index 828aec9491c..faeba529abe 100644 --- a/test/spec/modules/snigelBidAdapter_spec.js +++ b/test/spec/modules/snigelBidAdapter_spec.js @@ -36,6 +36,7 @@ const makeBidderRequest = function (overrides) { const DUMMY_USP_CONSENT = '1YYN'; const DUMMY_GDPR_CONSENT_STRING = 'BOSSotLOSSotLAPABAENBc-AAAAgR7_______9______9uz_Gv_v_f__33e8__9v_l_7_-___u_-33d4-_1vX99yfm1-7ftr3tp_86ues2_XqK_9oIiA'; +const DUMMY_GPP_CONSENT_STRING = 'DBABrw~BAAAAAAAAABA.QA~BAAAAABA.QA'; describe('snigelBidAdapter', function () { describe('isBidRequestValid', function () { @@ -181,6 +182,50 @@ describe('snigelBidAdapter', function () { expect(data.placements[2].refresh.count).to.equal(1); expect(data.placements[2].refresh.time).to.be.greaterThanOrEqual(0); }); + + it('should increment auction counter upon every request', function () { + const bidderRequest = makeBidderRequest({}); + + let request = spec.buildRequests([], bidderRequest); + expect(request).to.have.property('data'); + let data = JSON.parse(request.data); + const previousCounter = data.counter; + + request = spec.buildRequests([], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data.counter).to.equal(previousCounter + 1); + }); + + it('should increment placement counter for each placement', function () { + const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); + const topLeaderboard = makeBidRequest({adUnitCode: 'top_leaderboard', params: {placement: 'ros'}}); + const bottomLeaderboard = makeBidRequest({adUnitCode: 'bottom_leaderboard', params: {placement: 'ros'}}); + const sidebar = makeBidRequest({adUnitCode: 'sidebar', params: {placement: 'other'}}); + + let request = spec.buildRequests([topLeaderboard, bottomLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + let data = JSON.parse(request.data); + const previousCounters = {}; + data.placements.forEach((placement) => { + previousCounters[placement.name] = Math.max(previousCounters[placement.name] || 0, placement.counter); + }); + + request = spec.buildRequests([topLeaderboard, bottomLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(3); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(previousCounters).to.have.property(data.placements[0].name); + expect(data.placements[0].counter).to.equal(previousCounters[data.placements[0].name] + 1); + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(previousCounters).to.have.property(data.placements[1].name); + expect(data.placements[1].counter).to.equal(previousCounters[data.placements[1].name] + 2); + expect(data.placements[2].id).to.equal('sidebar'); + expect(previousCounters).to.have.property(data.placements[2].name); + expect(data.placements[2].counter).to.equal(previousCounters[data.placements[2].name] + 1); + }); }); describe('interpretResponse', function () { @@ -266,7 +311,7 @@ describe('snigelBidAdapter', function () { expect(syncs).to.be.undefined; }); - it('should not return any user syncs if GDPR applies and the user did not consent to purpose one', function () { + it("should return an iframe specific to the publisher's property if all conditions are met", function () { const response = { body: { id: BASE_BIDDER_REQUEST.bidderRequestId, @@ -279,19 +324,19 @@ describe('snigelBidAdapter', function () { iframeEnabled: true, }; const gdprConsent = { - gdprApplies: true, - vendorData: { - purpose: { - consents: {1: false}, - }, - }, + gdprApplies: false, }; const syncs = spec.getUserSyncs(syncOptions, [response], gdprConsent); - expect(syncs).to.be.undefined; + expect(syncs).to.be.an('array').and.of.length(1); + const sync = syncs[0]; + expect(sync).to.have.property('type'); + expect(sync.type).to.equal('iframe'); + expect(sync).to.have.property('url'); + expect(sync.url).to.equal('https://somesyncurl?gdpr=0&gdpr_consent=&gpp_sid=&gpp=&us_privacy='); }); - it("should return an iframe specific to the publisher's property if all conditions are met", function () { + it('should pass GDPR applicability and consent string as query parameters', function () { const response = { body: { id: BASE_BIDDER_REQUEST.bidderRequestId, @@ -304,7 +349,13 @@ describe('snigelBidAdapter', function () { iframeEnabled: true, }; const gdprConsent = { - gdprApplies: false, + gdprApplies: true, + consentString: DUMMY_GDPR_CONSENT_STRING, + vendorData: { + purpose: { + consents: {1: true}, + }, + }, }; const syncs = spec.getUserSyncs(syncOptions, [response], gdprConsent); @@ -313,10 +364,12 @@ describe('snigelBidAdapter', function () { expect(sync).to.have.property('type'); expect(sync.type).to.equal('iframe'); expect(sync).to.have.property('url'); - expect(sync.url).to.equal('https://somesyncurl?gdpr=0&gdpr_consent='); + expect(sync.url).to.equal( + `https://somesyncurl?gdpr=1&gdpr_consent=${DUMMY_GDPR_CONSENT_STRING}&gpp_sid=&gpp=&us_privacy=` + ); }); - it('should pass GDPR applicability and consent string as query parameters', function () { + it('should pass GPP section IDs and consent string as query parameters', function () { const response = { body: { id: BASE_BIDDER_REQUEST.bidderRequestId, @@ -328,26 +381,23 @@ describe('snigelBidAdapter', function () { const syncOptions = { iframeEnabled: true, }; - const gdprConsent = { - gdprApplies: true, - consentString: DUMMY_GDPR_CONSENT_STRING, - vendorData: { - purpose: { - consents: {1: true}, - }, - }, + const gppConsent = { + applicableSections: [7, 8], + gppString: DUMMY_GPP_CONSENT_STRING, }; - const syncs = spec.getUserSyncs(syncOptions, [response], gdprConsent); + const syncs = spec.getUserSyncs(syncOptions, [response], undefined, undefined, gppConsent); expect(syncs).to.be.an('array').and.of.length(1); const sync = syncs[0]; expect(sync).to.have.property('type'); expect(sync.type).to.equal('iframe'); expect(sync).to.have.property('url'); - expect(sync.url).to.equal(`https://somesyncurl?gdpr=1&gdpr_consent=${DUMMY_GDPR_CONSENT_STRING}`); + expect(sync.url).to.equal( + `https://somesyncurl?gdpr=0&gdpr_consent=&gpp_sid=7,8&gpp=${DUMMY_GPP_CONSENT_STRING}&us_privacy=` + ); }); - it('should omit session ID if no device access', function() { + it('should omit session ID if no device access', function () { const bidderRequest = makeBidderRequest(); const unregisterRule = registerActivityControl(ACTIVITY_ACCESS_DEVICE, 'denyAccess', () => { return {allow: false, reason: 'no consent'}; @@ -373,9 +423,9 @@ describe('snigelBidAdapter', function () { }, vendor: { consents: {[spec.gvlid]: true}, - } + }, }, - } + }, }); let request = spec.buildRequests([], baseBidderRequest); expect(request).to.have.property('data'); @@ -388,25 +438,14 @@ describe('snigelBidAdapter', function () { data = JSON.parse(request.data); expect(data.gdprConsent).to.be.false; - bidderRequest = {...baseBidderRequest, ...{gdprConsent: {vendorData: {vendor: {consents: {[spec.gvlid]: false}}}}}}; + bidderRequest = { + ...baseBidderRequest, + ...{gdprConsent: {vendorData: {vendor: {consents: {[spec.gvlid]: false}}}}}, + }; request = spec.buildRequests([], bidderRequest); expect(request).to.have.property('data'); data = JSON.parse(request.data); expect(data.gdprConsent).to.be.false; }); - - it('should increment auction counter upon every request', function() { - const bidderRequest = makeBidderRequest({}); - - let request = spec.buildRequests([], bidderRequest); - expect(request).to.have.property('data'); - let data = JSON.parse(request.data); - const previousCounter = data.counter; - - request = spec.buildRequests([], bidderRequest); - expect(request).to.have.property('data'); - data = JSON.parse(request.data); - expect(data.counter).to.equal(previousCounter + 1); - }); }); }); diff --git a/test/spec/modules/sonobiAnalyticsAdapter_spec.js b/test/spec/modules/sonobiAnalyticsAdapter_spec.js deleted file mode 100644 index c34de91dd9f..00000000000 --- a/test/spec/modules/sonobiAnalyticsAdapter_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import sonobiAnalytics, {DEFAULT_EVENT_URL} from 'modules/sonobiAnalyticsAdapter.js'; -import {expect} from 'chai'; -import {server} from 'test/mocks/xhr.js'; -import { EVENTS } from 'src/constants.js'; - -let events = require('src/events'); -let adapterManager = require('src/adapterManager').default; - -describe('Sonobi Prebid Analytic', function () { - var clock; - - describe('enableAnalytics', function () { - beforeEach(function () { - sinon.stub(events, 'getEvents').returns([]); - clock = sinon.useFakeTimers(Date.now()); - }); - - afterEach(function () { - events.getEvents.restore(); - clock.restore(); - }); - - after(function () { - sonobiAnalytics.disableAnalytics(); - }); - - it('should catch all events', function (done) { - const initOptions = { - pubId: 'A3B254F', - siteId: '1234', - delay: 100 - }; - - sonobiAnalytics.enableAnalytics(initOptions) - - const bid = { - bidderCode: 'sonobi_test_bid', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '1234', - auctionId: '13', - responseTimestamp: 1496410856397, - requestTimestamp: 1496410856295, - cpm: 1.13, - bidder: 'sonobi', - adUnitCode: 'dom-sample-id', - timeToRespond: 100, - placementCode: 'placementtest' - }; - - // Step 1: Initialize adapter - adapterManager.enableAnalytics({ - provider: 'sonobi', - options: initOptions - }); - - // Step 2: Send init auction event - events.emit(EVENTS.AUCTION_INIT, { config: initOptions, auctionId: '13', timestamp: Date.now() }); - - expect(sonobiAnalytics.initOptions).to.have.property('pubId', 'A3B254F'); - expect(sonobiAnalytics.initOptions).to.have.property('siteId', '1234'); - expect(sonobiAnalytics.initOptions).to.have.property('delay', 100); - // Step 3: Send bid requested event - events.emit(EVENTS.BID_REQUESTED, { bids: [bid], auctionId: '13' }); - - // Step 4: Send bid response event - events.emit(EVENTS.BID_RESPONSE, bid); - - // Step 5: Send bid won event - events.emit(EVENTS.BID_WON, bid); - - // Step 6: Send bid timeout event - events.emit(EVENTS.BID_TIMEOUT, { auctionId: '13' }); - - // Step 7: Send auction end event - events.emit(EVENTS.AUCTION_END, { auctionId: '13', bidsReceived: [bid] }); - - clock.tick(5000); - const req = server.requests.find(req => req.url.indexOf(DEFAULT_EVENT_URL) !== -1); - expect(JSON.parse(req.requestBody)).to.have.length(3) - done(); - }); - }); -}); diff --git a/test/spec/modules/sonobiBidAdapter_spec.js b/test/spec/modules/sonobiBidAdapter_spec.js index c7f954cfdcf..0b0a00c75b3 100644 --- a/test/spec/modules/sonobiBidAdapter_spec.js +++ b/test/spec/modules/sonobiBidAdapter_spec.js @@ -4,9 +4,18 @@ import { newBidder } from 'src/adapters/bidderFactory.js'; import { userSync } from '../../../src/userSync.js'; import { config } from 'src/config.js'; import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; - +import { parseQS } from '../../../src/utils' describe('SonobiBidAdapter', function () { const adapter = newBidder(spec) + const originalBuildRequests = spec.buildRequests; + spec.buildRequests = (...args) => { + const result = originalBuildRequests(...args); + if (result && result.data) { + result.data = parseQS(result.data); // Translate back into a js object so we can validate it + } + + return result; + } describe('.code', function () { it('should return a bidder code of sonobi', function () { expect(spec.code).to.equal('sonobi') @@ -298,7 +307,42 @@ describe('SonobiBidAdapter', function () { context: 'outstream', playbackmethod: [1, 2, 3], plcmt: 3, - placement: 2 + placement: 2, + protocols: [1, 2, 3, 4, 5], + mimes: ['video/mp4', 'video/mpeg', 'video/x-flv'], + battr: [16, 17], + api: [1, 2, 3], + minduration: 5, + maxduration: 60, + skip: 1, + skipafter: 10, + startdelay: 5, + linearity: 1, + minbitrate: 1, + maxbitrate: 2 + } + } + }, + { + + 'bidder': 'sonobi', + 'params': { + 'keywords': 'sports,news,some_other_keyword', + 'placement_id': '1a2b3c4d5e6f1a2b3c4d', + 'sizes': [[300, 250], [300, 600]], + 'floor': '1.25', + }, + 'adUnitCode': 'adunit-code-42', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1g', + ortb2Imp: { + ext: { + gpid: '/123123/gpt_publisher/adunit-code-42' + } + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] } } }, @@ -342,7 +386,8 @@ describe('SonobiBidAdapter', function () { }]; let keyMakerData = { - '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,pm=1:2:3,p=2,pl=3,', + '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,pm=1:2:3,p=2,pl=3,protocols=1:2:3:4:5,mimes=video/mp4:video/mpeg:video/x-flv,battr=16:17,api=1:2:3,minduration=5,maxduration=60,skip=1,skipafter=10,startdelay=5,linearity=1,minbitrate=1,maxbitrate=2,', + '30b31c1838de1g': '1a2b3c4d5e6f1a2b3c4d|300x250,300x600|f=1.25,gpid=/123123/gpt_publisher/adunit-code-42,c=d,', '30b31c1838de1d': '1a2b3c4d5e6f1a2b3c4e|300x250,300x600|f=0.42,gpid=/123123/gpt_publisher/adunit-code-3,c=d,', '/7780971/sparks_prebid_LB|30b31c1838de1e': '300x250,300x600|gpid=/7780971/sparks_prebid_LB,c=d,', }; @@ -384,21 +429,21 @@ describe('SonobiBidAdapter', function () { } }; const bidRequests = spec.buildRequests(bidRequest, { ...bidderRequests, ortb2 }); - expect(bidRequests.data.fpd).to.equal(JSON.stringify(ortb2)); + expect(bidRequests.data.fpd).to.equal(encodeURIComponent(JSON.stringify(ortb2))); }); it('should populate coppa as 1 if set in config', function () { config.setConfig({ coppa: true }); const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(bidRequests.data.coppa).to.equal(1); + expect(bidRequests.data.coppa).to.equal(encodeURIComponent(1)); }); it('should populate coppa as 0 if set in config', function () { config.setConfig({ coppa: false }); const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(bidRequests.data.coppa).to.equal(0); + expect(bidRequests.data.coppa).to.equal(encodeURIComponent(0)); }); it('should have storageAllowed set to true', function () { @@ -409,13 +454,13 @@ describe('SonobiBidAdapter', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) const bidRequestsPageViewID = spec.buildRequests(bidRequest, bidderRequests) expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json') - expect(bidRequests.method).to.equal('GET') - expect(bidRequests.data.key_maker).to.deep.equal(JSON.stringify(keyMakerData)) + expect(bidRequests.method).to.equal('POST') + expect(decodeURIComponent(bidRequests.data.key_maker)).to.deep.equal(JSON.stringify((keyMakerData))) expect(bidRequests.data.ref).not.to.be.empty expect(bidRequests.data.s).not.to.be.empty expect(bidRequests.data.pv).to.equal(bidRequestsPageViewID.data.pv) - expect(JSON.parse(bidRequests.data.iqid).pcid).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) - expect(JSON.parse(bidRequests.data.iqid).pcidDate).to.match(/^[0-9]{13}$/) + expect(JSON.parse(decodeURIComponent(bidRequests.data.iqid)).pcid).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) + expect(JSON.parse(decodeURIComponent(bidRequests.data.iqid)).pcidDate).to.match(/^[0-9]{13}$/) expect(bidRequests.data.hfa).to.not.exist expect(bidRequests.bidderRequests).to.eql(bidRequest); expect(bidRequests.data.ref).to.equal('overrides_top_window_location'); @@ -425,24 +470,24 @@ describe('SonobiBidAdapter', function () { it('should return a properly formatted request with GDPR applies set to true', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json') - expect(bidRequests.method).to.equal('GET') + expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr).to.equal('true') - expect(bidRequests.data.consent_string).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') + expect(bidRequests.data.consent_string).to.equal(encodeURIComponent('BOJ/P2HOJ/P2HABABMAAAAAZ+A==')) }) it('should return a properly formatted request with referer', function () { bidRequest[0].params.referrer = '' const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.data.ref).to.equal('https://example.com') + expect(bidRequests.data.ref).to.equal(encodeURIComponent('https://example.com')) }) it('should return a properly formatted request with GDPR applies set to false', function () { bidderRequests.gdprConsent.gdprApplies = false; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json') - expect(bidRequests.method).to.equal('GET') + expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr).to.equal('false') - expect(bidRequests.data.consent_string).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') + expect(bidRequests.data.consent_string).to.equal(encodeURIComponent('BOJ/P2HOJ/P2HABABMAAAAAZ+A==')) }) it('should return a properly formatted request with GDPR applies set to false with no consent_string param', function () { let bidderRequests = { @@ -460,7 +505,7 @@ describe('SonobiBidAdapter', function () { }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json') - expect(bidRequests.method).to.equal('GET') + expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr).to.equal('false') expect(bidRequests.data).to.not.include.keys('consent_string') }) @@ -480,7 +525,7 @@ describe('SonobiBidAdapter', function () { }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json') - expect(bidRequests.method).to.equal('GET') + expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr).to.equal('true') expect(bidRequests.data).to.not.include.keys('consent_string') }) @@ -489,7 +534,7 @@ describe('SonobiBidAdapter', function () { bidRequest[1].params.hfa = 'hfakey' const bidRequests = spec.buildRequests(bidRequest, bidderRequests) expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json') - expect(bidRequests.method).to.equal('GET') + expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.ref).not.to.be.empty expect(bidRequests.data.s).not.to.be.empty expect(bidRequests.data.hfa).to.equal('hfakey') @@ -509,18 +554,18 @@ describe('SonobiBidAdapter', function () { it('should set ius as 0 if Sonobi cannot drop iframe pixels', function () { userSync.canBidderRegisterSync.returns(false); const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(bidRequests.data.ius).to.equal(0); + expect(bidRequests.data.ius).to.equal(encodeURIComponent(0)); }); it('should set ius as 1 if Sonobi can drop iframe pixels', function () { userSync.canBidderRegisterSync.returns(true); const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(bidRequests.data.ius).to.equal(1); + expect(bidRequests.data.ius).to.equal(encodeURIComponent(1)); }); it('should return a properly formatted request with schain defined', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(JSON.parse(bidRequests.data.schain)).to.deep.equal(bidRequest[0].schain) + expect(JSON.parse(decodeURIComponent(bidRequests.data.schain))).to.deep.equal(bidRequest[0].schain) }); it('should return a properly formatted request with eids as a JSON-encoded set of eids', function () { @@ -548,10 +593,10 @@ describe('SonobiBidAdapter', function () { ]; const bidRequests = spec.buildRequests(bidRequest, bidderRequests); expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json'); - expect(bidRequests.method).to.equal('GET'); + expect(bidRequests.method).to.equal('POST'); expect(bidRequests.data.ref).not.to.be.empty; expect(bidRequests.data.s).not.to.be.empty; - expect(JSON.parse(bidRequests.data.eids)).to.eql([ + expect(JSON.parse(decodeURIComponent(bidRequests.data.eids))).to.eql([ { 'source': 'pubcid.org', 'uids': [ @@ -569,7 +614,7 @@ describe('SonobiBidAdapter', function () { bidRequest[1].userId = { 'pubcid': 'abcd-efg-0101', 'tdid': 'td-abcd-efg-0101', 'id5id': { 'uid': 'ID5-ZHMOrVeUVTUKgrZ-a2YGxeh5eS_pLzHCQGYOEAiTBQ', 'ext': { 'linkType': 2 } } }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests); expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json'); - expect(bidRequests.method).to.equal('GET'); + expect(bidRequests.method).to.equal('POST'); expect(bidRequests.data.ref).not.to.be.empty; expect(bidRequests.data.s).not.to.be.empty; expect(bidRequests.data.userid).to.be.undefined; @@ -577,7 +622,7 @@ describe('SonobiBidAdapter', function () { it('should return a properly formatted request with keywrods included as a csv of strings', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(bidRequests.data.kw).to.equal('sports,news,some_other_keyword'); + expect(bidRequests.data.kw).to.equal(encodeURIComponent('sports,news,some_other_keyword')); }); it('should return a properly formatted request with us_privacy included', function () { @@ -602,7 +647,7 @@ describe('SonobiBidAdapter', function () { describe('.interpretResponse', function () { const bidRequests = { - 'method': 'GET', + 'method': 'POST', 'url': 'https://apex.go.sonobi.com/trinity.json', 'withCredentials': true, 'data': { diff --git a/test/spec/modules/sovrnAnalyticsAdapter_spec.js b/test/spec/modules/sovrnAnalyticsAdapter_spec.js deleted file mode 100644 index 7945bdc9910..00000000000 --- a/test/spec/modules/sovrnAnalyticsAdapter_spec.js +++ /dev/null @@ -1,530 +0,0 @@ -import sovrnAnalyticsAdapter from '../../../modules/sovrnAnalyticsAdapter.js'; -import {expect} from 'chai'; -import {config} from 'src/config.js'; -import adaptermanager from 'src/adapterManager.js'; -import {server} from 'test/mocks/xhr.js'; -import {expectEvents, fireEvents} from '../../helpers/analytics.js'; -import { EVENTS } from 'src/constants.js'; - -var assert = require('assert'); - -let events = require('src/events'); - -/** - * Emit analytics events - * @param {Array} eventType - array of objects to define the events that will fire - * @param {object} event - key is eventType, value is event - * @param {string} auctionId - the auction id to attached to the events - */ -function emitEvent(eventType, event, auctionId) { - event.auctionId = auctionId; - events.emit(EVENTS[eventType], event); -} - -let auctionStartTimestamp = Date.now(); -let timeout = 3000; -let auctionInit = { - timestamp: auctionStartTimestamp, - timeout: timeout -}; -let bidderCode = 'sovrn'; -let bidderRequestId = '123bri'; -let adUnitCode = 'div'; -let adUnitCode2 = 'div2'; -let bidId = 'bidid'; -let bidId2 = 'bidid2'; -let tId = '7aafa3ee-a80a-46d7-a4a0-cbcba463d97a'; -let tId2 = '99dca3ee-a80a-46d7-a4a0-cbcba463d97e'; -let bidRequested = { - auctionStart: auctionStartTimestamp, - bidderCode: bidderCode, - bidderRequestId: bidderRequestId, - bids: [ - { - adUnitCode: adUnitCode, - bidId: bidId, - bidder: bidderCode, - bidderRequestId: '10340af0c7dc72', - sizes: [[300, 250]], - startTime: auctionStartTimestamp + 100, - transactionId: tId - }, - { - adUnitCode: adUnitCode2, - bidId: bidId2, - bidder: bidderCode, - bidderRequestId: '10340af0c7dc72', - sizes: [[300, 250]], - startTime: auctionStartTimestamp + 100, - transactionId: tId2 - } - ], - doneCbCallCount: 1, - start: auctionStartTimestamp, - timeout: timeout -}; -let bidResponse = { - bidderCode: bidderCode, - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '3870e27a5752fb', - mediaType: 'banner', - source: 'client', - requestId: bidId, - cpm: 0.8584999918937682, - creativeId: 'cridprebidrtb', - dealId: null, - currency: 'USD', - netRevenue: true, - ad: '
divvy mcdiv
', - ttl: 60000, - responseTimestamp: auctionStartTimestamp + 150, - requestTimestamp: auctionStartTimestamp + 100, - bidder: bidderCode, - adUnitCode: adUnitCode, - timeToRespond: 50, - pbLg: '0.50', - pbMg: '0.80', - pbHg: '0.85', - pbAg: '0.85', - pbDg: '0.85', - pbCg: '', - size: '300x250', - adserverTargeting: { - hb_bidder: bidderCode, - hb_adid: '3870e27a5752fb', - hb_pb: '0.85' - }, - status: 'rendered' -}; - -let bidResponse2 = { - bidderCode: bidderCode, - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '9999e27a5752fb', - mediaType: 'banner', - source: 'client', - requestId: bidId2, - cpm: 0.12, - creativeId: 'cridprebidrtb', - dealId: null, - currency: 'USD', - netRevenue: true, - ad: '
divvy mcdiv
', - ttl: 60000, - responseTimestamp: auctionStartTimestamp + 150, - requestTimestamp: auctionStartTimestamp + 100, - bidder: bidderCode, - adUnitCode: adUnitCode2, - timeToRespond: 50, - pbLg: '0.10', - pbMg: '0.10', - pbHg: '0.10', - pbAg: '0.10', - pbDg: '0.10', - pbCg: '', - size: '300x250', - adserverTargeting: { - hb_bidder: bidderCode, - hb_adid: '9999e27a5752fb', - hb_pb: '0.10' - }, - status: 'rendered' -}; -let bidAdjustment = {}; -for (var k in bidResponse) bidAdjustment[k] = bidResponse[k]; -bidAdjustment.cpm = 0.8; -let bidAdjustmentNoMatchingRequest = { - bidderCode: 'not-sovrn', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '1', - mediaType: 'banner', - source: 'client', - requestId: '1', - cpm: 0.10, - creativeId: '', - dealId: null, - currency: 'USD', - netRevenue: true, - ad: '
divvy mcdiv
', - ttl: 60000, - responseTimestamp: auctionStartTimestamp + 150, - requestTimestamp: auctionStartTimestamp + 100, - bidder: 'not-sovrn', - adUnitCode: '', - timeToRespond: 50, - pbLg: '0.00', - pbMg: '0.10', - pbHg: '0.10', - pbAg: '0.10', - pbDg: '0.10', - pbCg: '', - size: '300x250', - adserverTargeting: { - hb_bidder: 'not-sovrn', - hb_adid: '1', - hb_pb: '0.10' - }, -}; -let bidResponseNoMatchingRequest = bidAdjustmentNoMatchingRequest; - -describe('Sovrn Analytics Adapter', function () { - beforeEach(() => { - sinon.stub(events, 'getEvents').returns([]); - }); - afterEach(() => { - events.getEvents.restore(); - }); - - describe('enableAnalytics ', function () { - beforeEach(() => { - sinon.spy(sovrnAnalyticsAdapter, 'track'); - }); - afterEach(() => { - sovrnAnalyticsAdapter.disableAnalytics(); - sovrnAnalyticsAdapter.track.restore(); - }); - - it('should catch all events if affiliate id present', function () { - adaptermanager.enableAnalytics({ - provider: 'sovrn', - options: { - sovrnId: 123 - } - }); - expectEvents().to.beTrackedBy(sovrnAnalyticsAdapter.track); - }); - - it('should catch no events if no affiliate id', function () { - adaptermanager.enableAnalytics({ - provider: 'sovrn', - options: { - } - }); - fireEvents(); - sinon.assert.callCount(sovrnAnalyticsAdapter.track, 0); - }); - }); - - describe('sovrnAnalyticsAdapter ', function() { - beforeEach(() => { - sovrnAnalyticsAdapter.enableAnalytics({ - provider: 'sovrn', - options: { - sovrnId: 123 - } - }); - sinon.spy(sovrnAnalyticsAdapter, 'track'); - }); - afterEach(() => { - sovrnAnalyticsAdapter.disableAnalytics(); - sovrnAnalyticsAdapter.track.restore(); - }); - it('should have correct type', function () { - assert.equal(sovrnAnalyticsAdapter.getAdapterType(), 'endpoint') - }) - }); - - describe('auction data collector ', function() { - beforeEach(() => { - sovrnAnalyticsAdapter.enableAnalytics({ - provider: 'sovrn', - options: { - sovrnId: 123 - } - }); - sinon.spy(sovrnAnalyticsAdapter, 'track'); - }); - afterEach(() => { - sovrnAnalyticsAdapter.disableAnalytics(); - sovrnAnalyticsAdapter.track.restore(); - }); - it('should create auctiondata record from init ', function () { - let auctionId = '123.123.123.123'; - emitEvent('AUCTION_INIT', auctionInit, auctionId); - - let auctionData = sovrnAnalyticsAdapter.getAuctions(); - let currentAuction = auctionData[auctionId]; - assert(currentAuction); - let expectedTimeOutData = { - buffer: config.getConfig('timeoutBuffer'), - bidder: config.getConfig('bidderTimeout'), - }; - expect(currentAuction.auction.timeouts).to.deep.equal(expectedTimeOutData); - assert.equal(currentAuction.auction.payload, 'auction'); - assert.equal(currentAuction.auction.priceGranularity, config.getConfig('priceGranularity')) - assert.equal(currentAuction.auction.auctionId, auctionId); - assert.equal(currentAuction.auction.sovrnId, 123); - }); - it('should create a bidrequest object ', function() { - let auctionId = '234.234.234.234'; - emitEvent('AUCTION_INIT', auctionInit, auctionId); - emitEvent('BID_REQUESTED', bidRequested, auctionId); - - let auctionData = sovrnAnalyticsAdapter.getAuctions(); - let currentAuction = auctionData[auctionId]; - assert(currentAuction); - let requests = currentAuction.auction.requests; - assert(requests); - assert.equal(requests.length, 1); - assert.equal(requests[0].bidderCode, bidderCode); - assert.equal(requests[0].bidderRequestId, bidderRequestId); - assert.equal(requests[0].timeout, timeout); - let bids = requests[0].bids; - assert(bids); - assert.equal(bids.length, 2); - assert.equal(bids[0].bidId, bidId); - assert.equal(bids[0].bidder, bidderCode); - assert.equal(bids[0].transactionId, tId); - assert.equal(bids[0].sizes.length, 1); - assert.equal(bids[0].sizes[0][0], 300); - assert.equal(bids[0].sizes[0][1], 250); - expect(requests[0]).to.not.have.property('doneCbCallCount'); - expect(requests[0]).to.not.have.property('auctionId'); - }); - it('should add results to the bid with response ', function () { - let auctionId = '345.345.345.345'; - emitEvent('AUCTION_INIT', auctionInit, auctionId); - emitEvent('BID_REQUESTED', bidRequested, auctionId); - emitEvent('BID_RESPONSE', bidResponse, auctionId); - - let auctionData = sovrnAnalyticsAdapter.getAuctions(); - let currentAuction = auctionData[auctionId]; - let returnedBid = currentAuction.auction.requests[0].bids[0]; - assert.equal(returnedBid.bidId, bidId); - assert.equal(returnedBid.bidder, bidderCode); - assert.equal(returnedBid.transactionId, tId); - assert.equal(returnedBid.sizes.length, 1); - assert.equal(returnedBid.sizes[0][0], 300); - assert.equal(returnedBid.sizes[0][1], 250); - assert.equal(returnedBid.adserverTargeting.hb_adid, '3870e27a5752fb'); - assert.equal(returnedBid.adserverTargeting.hb_bidder, bidderCode); - assert.equal(returnedBid.adserverTargeting.hb_pb, '0.85'); - assert.equal(returnedBid.cpm, 0.8584999918937682); - }); - it('should add new unsynced bid if no request exists for response ', function () { - let auctionId = '456.456.456.456'; - emitEvent('AUCTION_INIT', auctionInit, auctionId); - emitEvent('BID_REQUESTED', bidRequested, auctionId); - emitEvent('BID_RESPONSE', bidResponseNoMatchingRequest, auctionId); - - let auctionData = sovrnAnalyticsAdapter.getAuctions(); - let currentAuction = auctionData[auctionId]; - let requests = currentAuction.auction.requests; - assert(requests); - assert.equal(requests.length, 1); - let bidRequest = requests[0].bids[0]; - expect(bidRequest).to.not.have.property('adserverTargeting'); - expect(bidRequest).to.not.have.property('cpm'); - expect(currentAuction.auction.unsynced[0]).to.deep.equal(bidResponseNoMatchingRequest); - }); - it('should adjust the bid ', function () { - let auctionId = '567.567.567.567'; - emitEvent('AUCTION_INIT', auctionInit, auctionId); - emitEvent('BID_REQUESTED', bidRequested, auctionId); - emitEvent('BID_ADJUSTMENT', bidResponse, auctionId); - emitEvent('BID_RESPONSE', bidAdjustment, auctionId); - - let auctionData = sovrnAnalyticsAdapter.getAuctions(); - let currentAuction = auctionData[auctionId]; - let returnedBid = currentAuction.auction.requests[0].bids[0]; - assert.equal(returnedBid.cpm, 0.8); - assert.equal(returnedBid.originalValues.cpm, 0.8584999918937682); - }); - }); - describe('auction data send ', function() { - let expectedPostBody = { - sovrnId: 123, - auctionId: '678.678.678.678', - payload: 'auction', - priceGranularity: 'medium', - }; - let expectedRequests = { - bidderCode: 'sovrn', - bidderRequestId: '123bri', - timeout: 3000 - }; - let expectedBids = { - adUnitCode: 'div', - bidId: 'bidid', - bidder: 'sovrn', - bidderRequestId: '10340af0c7dc72', - transactionId: '7aafa3ee-a80a-46d7-a4a0-cbcba463d97a', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '3870e27a5752fb', - mediaType: 'banner', - source: 'client', - cpm: 0.8584999918937682, - creativeId: 'cridprebidrtb', - dealId: null, - currency: 'USD', - netRevenue: true, - ttl: 60000, - timeToRespond: 50, - size: '300x250', - status: 'rendered', - isAuctionWinner: true - }; - let SecondAdUnitExpectedBids = { - adUnitCode: 'div2', - bidId: 'bidid2', - bidder: 'sovrn', - bidderRequestId: '10340af0c7dc72', - transactionId: '99dca3ee-a80a-46d7-a4a0-cbcba463d97e', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '9999e27a5752fb', - mediaType: 'banner', - source: 'client', - cpm: 0.12, - creativeId: 'cridprebidrtb', - dealId: null, - currency: 'USD', - netRevenue: true, - ttl: 60000, - timeToRespond: 50, - size: '300x250', - status: 'rendered', - isAuctionWinner: true - }; - let expectedAdServerTargeting = { - hb_bidder: 'sovrn', - hb_adid: '3870e27a5752fb', - hb_pb: '0.85' - }; - beforeEach(() => { - sovrnAnalyticsAdapter.enableAnalytics({ - provider: 'sovrn', - options: { - sovrnId: 123 - } - }); - sinon.spy(sovrnAnalyticsAdapter, 'track'); - }); - afterEach(() => { - sovrnAnalyticsAdapter.disableAnalytics(); - sovrnAnalyticsAdapter.track.restore(); - }); - it('should send auction data ', function () { - let auctionId = '678.678.678.678'; - emitEvent('AUCTION_INIT', auctionInit, auctionId); - emitEvent('BID_REQUESTED', bidRequested, auctionId); - emitEvent('BID_RESPONSE', bidResponse, auctionId); - emitEvent('BID_RESPONSE', bidResponse2, auctionId) - emitEvent('AUCTION_END', {}, auctionId); - let requestBody = JSON.parse(server.requests[0].requestBody); - let requestsFromRequestBody = requestBody.requests[0]; - let bidsFromRequests = requestsFromRequestBody.bids[0]; - expect(requestBody).to.deep.include(expectedPostBody); - expect(requestBody.timeouts).to.deep.equal({buffer: 400, bidder: 3000}); - expect(requestsFromRequestBody).to.deep.include(expectedRequests); - expect(bidsFromRequests).to.deep.include(expectedBids); - let bidsFromRequests2 = requestsFromRequestBody.bids[1]; - expect(bidsFromRequests2).to.deep.include(SecondAdUnitExpectedBids); - expect(bidsFromRequests.adserverTargeting).to.deep.include(expectedAdServerTargeting); - }); - }); - describe('bid won data send ', function() { - let auctionId = '789.789.789.789'; - let creativeId = 'cridprebidrtb'; - let requestId = 'requestId69'; - let bidWonEvent = { - ad: 'html', - adId: 'adId', - adUnitCode: adUnitCode, - auctionId: auctionId, - bidder: bidderCode, - bidderCode: bidderCode, - cpm: 1.01, - creativeId: creativeId, - currency: 'USD', - height: 250, - mediaType: 'banner', - requestId: requestId, - size: '300x250', - source: 'client', - status: 'rendered', - statusMessage: 'Bid available', - timeToRespond: 421, - ttl: 60, - width: 300 - }; - let expectedBidWonBody = { - sovrnId: 123, - payload: 'winner' - }; - let expectedWinningBid = { - bidderCode: bidderCode, - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: 'adId', - mediaType: 'banner', - source: 'client', - requestId: requestId, - cpm: 1.01, - creativeId: creativeId, - currency: 'USD', - ttl: 60, - auctionId: auctionId, - bidder: bidderCode, - adUnitCode: adUnitCode, - timeToRespond: 421, - size: '300x250', - }; - beforeEach(() => { - sovrnAnalyticsAdapter.enableAnalytics({ - provider: 'sovrn', - options: { - sovrnId: 123 - } - }); - sinon.spy(sovrnAnalyticsAdapter, 'track'); - }); - afterEach(() => { - sovrnAnalyticsAdapter.disableAnalytics(); - sovrnAnalyticsAdapter.track.restore(); - }); - it('should send bid won data ', function () { - emitEvent('AUCTION_INIT', auctionInit, auctionId); - emitEvent('BID_WON', bidWonEvent, auctionId); - let requestBody = JSON.parse(server.requests[0].requestBody); - expect(requestBody).to.deep.include(expectedBidWonBody); - expect(requestBody.winningBid).to.deep.include(expectedWinningBid); - }); - }); - describe('Error Tracking', function() { - beforeEach(() => { - sovrnAnalyticsAdapter.enableAnalytics({ - provider: 'sovrn', - options: { - sovrnId: 123 - } - }); - sinon.spy(sovrnAnalyticsAdapter, 'track'); - }); - afterEach(() => { - sovrnAnalyticsAdapter.disableAnalytics() - sovrnAnalyticsAdapter.track.restore() - }); - it('should send an error message when a bid is received for a closed auction', function() { - let auctionId = '678.678.678.678'; - emitEvent('AUCTION_INIT', auctionInit, auctionId) - emitEvent('BID_REQUESTED', bidRequested, auctionId) - emitEvent('AUCTION_END', {}, auctionId) - server.requests[0].respond(200) - emitEvent('BID_RESPONSE', bidResponse, auctionId) - let requestBody = JSON.parse(server.requests[1].requestBody) - expect(requestBody.payload).to.equal('error') - expect(requestBody.message).to.include('Event Received after Auction Close Auction Id') - }) - }) -}) diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 9c7f433ef96..c684f8691aa 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -243,7 +243,7 @@ describe('sovrnBidAdapter', function() { it('when FLEDGE is enabled, should send ortb2imp.ext.ae', function () { const bidderRequest = { ...baseBidderRequest, - fledgeEnabled: true + paapi: {enabled: true} } const bidRequest = { ...baseBidRequest, @@ -273,7 +273,9 @@ describe('sovrnBidAdapter', function() { it('when FLEDGE is enabled, but env is malformed, should not send ortb2imp.ext.ae', function () { const bidderRequest = { ...baseBidderRequest, - fledgeEnabled: true + paapi: { + enabled: true + } } const bidRequest = { ...baseBidRequest, @@ -419,6 +421,31 @@ describe('sovrnBidAdapter', function() { expect(regs.gpp_sid).to.include(8) }) + it('should add ORTB2 device data to the request', function () { + const bidderRequest = { + ...baseBidderRequest, + ortb2: { + device: { + 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', + }, + }, + }; + + const request = spec.buildRequests([baseBidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.device).to.deep.equal(bidderRequest.ortb2.device); + }); + it('should not send gpp info when gppConsent is not defined', function () { const bidderRequest = { ...baseBidderRequest, @@ -844,26 +871,56 @@ describe('sovrnBidAdapter', function() { }] }], ext: { - fledge_auction_configs: { - 'test_bid_id': { - seller: 'ap.lijit.com', - interestGroupBuyers: ['dsp1.com'], - sellerTimeout: 0, - perBuyerSignals: { - 'dsp1.com': { - bid_macros: 0.1, - disallowed_adv_ids: [ - '8765', - '4321' - ], - } + seller: 'seller.lijit.com', + decisionLogicUrl: 'https://decision.lijit.com', + igbid: [{ + impid: 'test_imp_id', + igbuyer: [{ + igdomain: 'ap.lijit.com', + buyerdata: { + base_bid_micros: 0.1, + use_bid_multiplier: true, + multiplier: '1.3' } - } - } + }, { + igdomain: 'buyer2.com', + buyerdata: {} + }, { + igdomain: 'buyer3.com', + buyerdata: {} + }] + }, { + impid: 'test_imp_id_2', + igbuyer: [{ + igdomain: 'ap2.lijit.com', + buyerdata: { + base_bid_micros: '0.2', + } + }] + }, { + impid: '', + igbuyer: [{ + igdomain: 'ap3.lijit.com', + buyerdata: { + base_bid_micros: '0.3', + } + }] + }, { + impid: 'test_imp_id_3', + igbuyer: [{ + igdomain: '', + buyerdata: { + base_bid_micros: '0.3', + } + }] + }, { + impid: 'test_imp_id_4', + igbuyer: [] + }] } } } - let invalidFledgeResponse = { + let emptyFledgeResponse = { body: { id: '37386aade21a71', seatbid: [{ @@ -879,25 +936,73 @@ describe('sovrnBidAdapter', function() { }] }], ext: { - fledge_auction_configs: { + igbid: { } } } } - it('should return fledge auction configs alongside bids', function () { + let expectedResponse = { + requestId: '263c448586f5a1', + cpm: 0.45882675, + width: 728, + height: 90, + creativeId: 'creativelycreatedcreativecreative', + dealId: null, + currency: 'USD', + netRevenue: true, + mediaType: 'banner', + ttl: 60000, + meta: { advertiserDomains: [] }, + ad: decodeURIComponent(`>`) + } + let expectedFledgeResponse = [ + { + bidId: 'test_imp_id', + config: { + seller: 'seller.lijit.com', + decisionLogicUrl: 'https://decision.lijit.com', + sellerTimeout: undefined, + auctionSignals: {}, + interestGroupBuyers: ['ap.lijit.com', 'buyer2.com', 'buyer3.com'], + perBuyerSignals: { + 'ap.lijit.com': { + base_bid_micros: 0.1, + use_bid_multiplier: true, + multiplier: '1.3' + }, + 'buyer2.com': {}, + 'buyer3.com': {} + } + } + }, + { + bidId: 'test_imp_id_2', + config: { + seller: 'seller.lijit.com', + decisionLogicUrl: 'https://decision.lijit.com', + sellerTimeout: undefined, + auctionSignals: {}, + interestGroupBuyers: ['ap2.lijit.com'], + perBuyerSignals: { + 'ap2.lijit.com': { + base_bid_micros: '0.2', + } + } + } + } + ] + + it('should return valid fledge auction configs alongside bids', function () { const result = spec.interpretResponse(fledgeResponse) expect(result).to.have.property('bids') - expect(result).to.have.property('fledgeAuctionConfigs') - expect(result.fledgeAuctionConfigs.length).to.equal(1) - expect(result.fledgeAuctionConfigs[0].bidId).to.equal('test_bid_id') - expect(result.fledgeAuctionConfigs[0].config).to.not.be.undefined - expect(result.fledgeAuctionConfigs[0].config).to.contain.keys('seller', 'interestGroupBuyers', 'sellerTimeout', 'perBuyerSignals') + expect(result).to.have.property('paapi') + expect(result.paapi.length).to.equal(2) + expect(result.paapi).to.deep.equal(expectedFledgeResponse) }) - it('should ignore invalid fledge auction configs', function () { - const result = spec.interpretResponse(invalidFledgeResponse) - expect(result).to.have.property('bids') - expect(result).to.have.property('fledgeAuctionConfigs') - expect(result.fledgeAuctionConfigs.length).to.equal(0) + it('should ignore empty fledge auction configs array', function () { + const result = spec.interpretResponse(emptyFledgeResponse) + expect(result.length).to.equal(1) + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse)) }) }) diff --git a/test/spec/modules/sparteoBidAdapter_spec.js b/test/spec/modules/sparteoBidAdapter_spec.js index 293f7da30a1..ec90d4c7eeb 100644 --- a/test/spec/modules/sparteoBidAdapter_spec.js +++ b/test/spec/modules/sparteoBidAdapter_spec.js @@ -59,6 +59,7 @@ const VALID_REQUEST_BANNER = { url: REQUEST_URL, data: { 'imp': [{ + 'secure': 1, 'id': '1a2b3c4d', 'banner': { 'format': [{ @@ -71,6 +72,7 @@ const VALID_REQUEST_BANNER = { 'sparteo': { 'params': { 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'adUnitCode': 'id-1234', 'formats': ['corner'] } } @@ -80,7 +82,8 @@ const VALID_REQUEST_BANNER = { 'publisher': { 'ext': { 'params': { - 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'pbjsVersion': '$prebid.version$' } } } @@ -94,6 +97,7 @@ const VALID_REQUEST_VIDEO = { url: REQUEST_URL, data: { 'imp': [{ + 'secure': 1, 'id': '5e6f7g8h', 'video': { 'w': 640, @@ -112,7 +116,8 @@ const VALID_REQUEST_VIDEO = { 'pbadslot': 'video', 'sparteo': { 'params': { - 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'adUnitCode': 'id-5678' } } } @@ -121,7 +126,8 @@ const VALID_REQUEST_VIDEO = { 'publisher': { 'ext': { 'params': { - 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'pbjsVersion': '$prebid.version$' } } } @@ -135,6 +141,7 @@ const VALID_REQUEST = { url: REQUEST_URL, data: { 'imp': [{ + 'secure': 1, 'id': '1a2b3c4d', 'banner': { 'format': [{ @@ -147,11 +154,13 @@ const VALID_REQUEST = { 'sparteo': { 'params': { 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'adUnitCode': 'id-1234', 'formats': ['corner'] } } } }, { + 'secure': 1, 'id': '5e6f7g8h', 'video': { 'w': 640, @@ -170,7 +179,8 @@ const VALID_REQUEST = { 'pbadslot': 'video', 'sparteo': { 'params': { - 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'adUnitCode': 'id-5678' } } } @@ -179,7 +189,8 @@ const VALID_REQUEST = { 'publisher': { 'ext': { 'params': { - 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'pbjsVersion': '$prebid.version$' } } } diff --git a/test/spec/modules/spotxBidAdapter_spec.js b/test/spec/modules/spotxBidAdapter_spec.js deleted file mode 100644 index ec99d0f7142..00000000000 --- a/test/spec/modules/spotxBidAdapter_spec.js +++ /dev/null @@ -1,711 +0,0 @@ -import {expect} from 'chai'; -import {config} from 'src/config.js'; -import {loadExternalScript} from '../../../src/adloader'; -import {isRendererRequired} from '../../../src/Renderer'; -import {spec, GOOGLE_CONSENT} from 'modules/spotxBidAdapter.js'; - -describe('the spotx adapter', function () { - function getValidBidObject() { - return { - bidId: 123, - mediaTypes: { - video: { - playerSize: [['300', '200']] - } - }, - params: { - channel_id: 12345, - } - }; - }; - - describe('isBidRequestValid', function() { - let bid; - - beforeEach(function() { - bid = getValidBidObject(); - }); - - it('should fail validation if the bid isn\'t defined or not an object', function() { - let result = spec.isBidRequestValid(); - - expect(result).to.equal(false); - - result = spec.isBidRequestValid('not an object'); - - expect(result).to.equal(false); - }); - - it('should succeed validation with all the right parameters', function() { - expect(spec.isBidRequestValid(getValidBidObject())).to.equal(true); - }); - - it('should succeed validation with mediaType and outstream_function or outstream_options', function() { - bid.mediaType = 'video'; - bid.params.outstream_function = 'outstream_func'; - - expect(spec.isBidRequestValid(bid)).to.equal(true); - - delete bid.params.outstream_function; - bid.params.outstream_options = { - slot: 'elemID' - }; - - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - - it('should succeed with ad_unit outstream and outstream function set', function() { - bid.params.ad_unit = 'outstream'; - bid.params.outstream_function = function() {}; - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - - it('should succeed with ad_unit outstream, options set for outstream and slot provided', function() { - bid.params.ad_unit = 'outstream'; - bid.params.outstream_options = {slot: 'ad_container_id'}; - expect(spec.isBidRequestValid(bid)).to.equal(true); - }); - - it('should fail without a channel_id', function() { - delete bid.params.channel_id; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail without playerSize', function() { - delete bid.mediaTypes.video.playerSize; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail without video', function() { - delete bid.mediaTypes.video; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail with ad_unit outstream but no options set for outstream', function() { - bid.params.ad_unit = 'outstream'; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - - it('should fail with ad_unit outstream, options set for outstream but no slot provided', function() { - bid.params.ad_unit = 'outstream'; - bid.params.outstream_options = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); - }); - }); - - describe('buildRequests', function() { - let bid, bidRequestObj; - - beforeEach(function() { - bid = getValidBidObject(); - bidRequestObj = { - refererInfo: { - page: 'prebid.js' - } - }; - }); - - it('should build a very basic request', function() { - let request = spec.buildRequests([bid], bidRequestObj)[0]; - expect(request.method).to.equal('POST'); - expect(request.url).to.equal('https://search.spotxchange.com/openrtb/2.3/dados/12345?src_sys=prebid'); - expect(request.bidRequest).to.equal(bidRequestObj); - expect(request.data.id).to.equal(12345); - expect(request.data.ext.wrap_response).to.equal(1); - expect(request.data.imp.id).to.match(/\d+/); - expect(request.data.imp.secure).to.equal(0); - expect(request.data.imp.video).to.deep.equal({ - ext: { - sdk_name: 'Prebid 1+', - versionOrtb: '2.3' - }, - h: '200', - mimes: [ - 'application/javascript', - 'video/mp4', - 'video/webm' - ], - w: '300' - }); - expect(request.data.site).to.deep.equal({ - content: 'content', - id: '', - page: 'prebid.js' - }); - }); - - it('should change request parameters based on options sent', function() { - let request = spec.buildRequests([bid], bidRequestObj)[0]; - expect(request.data.imp.video.ext).to.deep.equal({ - sdk_name: 'Prebid 1+', - versionOrtb: '2.3' - }); - - bid.params = { - channel_id: 54321, - ad_mute: 1, - hide_skin: 1, - ad_volume: 1, - ad_unit: 'incontent', - outstream_options: {foo: 'bar'}, - outstream_function: '987', - custom: {bar: 'foo'}, - start_delay: true, - number_of_ads: 2, - spotx_all_google_consent: 1, - min_duration: 5, - max_duration: 10, - placement_type: 1, - position: 1 - }; - - bid.userIdAsEids = [{ - source: 'adserver.org', - uids: [{id: 'tdid_1', atype: 1, ext: {rtiPartner: 'TDID'}}] - }, - { - source: 'id5-sync.com', - uids: [{id: 'id5id_1', ext: {}}] - }, - { - source: 'uidapi.com', - uids: [{ - id: 'uid_1', - atype: 3 - }] - } - ]; - - bid.crumbs = { - pubcid: 'pubcid_1' - }; - - bid.schain = { - complete: 1, - nodes: [ - { - asi: 'indirectseller.com', - sid: '00001', - hp: 1 - } - ] - } - - request = spec.buildRequests([bid], bidRequestObj)[0]; - expect(request.data.id).to.equal(54321); - expect(request.data.imp.video).to.contain({ - minduration: 5, - maxduration: 10 - }) - expect(request.data.imp.video.ext).to.deep.equal({ - ad_volume: 1, - hide_skin: 1, - ad_unit: 'incontent', - outstream_options: {foo: 'bar'}, - outstream_function: '987', - custom: {bar: 'foo'}, - sdk_name: 'Prebid 1+', - versionOrtb: '2.3', - placement: 1, - pos: 1 - }); - - expect(request.data.imp.video.startdelay).to.equal(1); - expect(request.data.ext).to.deep.equal({ - number_of_ads: 2, - wrap_response: 1 - }); - expect(request.data.user.ext).to.deep.equal({ - consented_providers_settings: GOOGLE_CONSENT, - eids: [{ - source: 'adserver.org', - uids: [{ - id: 'tdid_1', - atype: 1, - ext: { - rtiPartner: 'TDID' - } - }] - }, { - source: 'id5-sync.com', - uids: [{ - id: 'id5id_1', - ext: {} - }] - }, - { - source: 'uidapi.com', - uids: [{ - id: 'uid_1', - atype: 3, - ext: { - rtiPartner: 'UID2' - } - }] - }], - fpc: 'pubcid_1' - }); - - expect(request.data.source).to.deep.equal({ - ext: { - schain: { - complete: 1, - nodes: [ - { - asi: 'indirectseller.com', - sid: '00001', - hp: 1 - } - ] - } - } - }) - }); - - it('should process premarket bids', function() { - let request; - sinon.stub(Date, 'now').returns(1000); - - bid.params.pre_market_bids = [{ - vast_url: 'prebid.js', - deal_id: '123abc', - price: 12, - currency: 'USD' - }]; - - request = spec.buildRequests([bid], bidRequestObj)[0]; - expect(request.data.imp.video.ext.pre_market_bids).to.deep.equal([ - { - 'cur': 'USD', - 'ext': { - 'event_log': [ - {} - ] - }, - 'id': '123abc', - 'seatbid': [ - { - 'bid': [ - { - 'adm': 'prebid.js', - 'dealid': '123abc', - 'impid': 1000, - 'price': 12, - } - ] - } - ] - } - ]); - Date.now.restore(); - }); - - it('should pass GDPR params', function() { - let request; - - bidRequestObj.gdprConsent = { - consentString: 'consent123', - gdprApplies: true - }; - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.regs.ext.gdpr).to.equal(1); - expect(request.data.user.ext.consent).to.equal('consent123'); - }); - - it('should pass CCPA us_privacy string', function() { - let request; - - bidRequestObj.uspConsent = '1YYY' - - request = spec.buildRequests([bid], bidRequestObj)[0]; - expect(request.data.regs.ext.us_privacy).to.equal('1YYY'); - }); - - it('should pass both GDPR params and CCPA us_privacy', function() { - let request; - - bidRequestObj.gdprConsent = { - consentString: 'consent123', - gdprApplies: true - }; - bidRequestObj.uspConsent = '1YYY' - - request = spec.buildRequests([bid], bidRequestObj)[0]; - expect(request.data.regs.ext.gdpr).to.equal(1); - expect(request.data.user.ext.consent).to.equal('consent123'); - expect(request.data.regs.ext.us_privacy).to.equal('1YYY'); - }); - - it('should pass min and max duration params', function() { - let request; - - bid.params.min_duration = 3 - bid.params.max_duration = 15 - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.imp.video.minduration).to.equal(3); - expect(request.data.imp.video.maxduration).to.equal(15); - }); - - it('should pass placement_type and position params', function() { - let request; - - bid.params.placement_type = 2 - bid.params.position = 5 - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.imp.video.ext.placement).to.equal(2); - expect(request.data.imp.video.ext.pos).to.equal(5); - }); - - it('should pass page param and override refererInfo.referer', function() { - let request; - - bid.params.page = 'https://example.com'; - - let origGetConfig = config.getConfig; - sinon.stub(config, 'getConfig').callsFake(function (key) { - if (key === 'pageUrl') { - return 'https://www.spotx.tv'; - } - return origGetConfig.apply(config, arguments); - }); - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.site.page).to.equal('https://example.com'); - config.getConfig.restore(); - }); - - it('should use refererInfo.referer if no page is passed', function() { - let request; - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.site.page).to.equal('prebid.js'); - }); - - it('should set ext.wrap_response to 0 when cache url is set and ignoreBidderCacheKey is true', function() { - let request; - - let origGetConfig = config.getConfig; - sinon.stub(config, 'getConfig').callsFake(function (key) { - if (key === 'cache') { - return { - url: 'prebidCacheLocation', - ignoreBidderCacheKey: true - }; - } - if (key === 'cache.url') { - return 'prebidCacheLocation'; - } - if (key === 'cache.ignoreBidderCacheKey') { - return true; - } - return origGetConfig.apply(config, arguments); - }); - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.ext.wrap_response).to.equal(0); - config.getConfig.restore(); - }); - - it('should pass price floor in USD from the floors module if available', function () { - let request; - - bid.getFloor = function () { - return { currency: 'USD', floor: 3 }; - } - - bid.params.price_floor = 2; - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.imp.bidfloor).to.equal(3); - }); - - it('should not pass price floor if price floors module gives a non-USD currency', function () { - let request; - - bid.getFloor = function () { - return { currency: 'EUR', floor: 3 }; - } - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.imp.bidfloor).to.be.undefined; - }); - - it('if floors module is not available, should pass price floor from price_floor param if available', function () { - let request; - - bid.params.price_floor = 2; - - request = spec.buildRequests([bid], bidRequestObj)[0]; - - expect(request.data.imp.bidfloor).to.equal(2); - }); - }); - - describe('interpretResponse', function() { - let serverResponse, bidderRequestObj; - - beforeEach(function() { - bidderRequestObj = { - bidRequest: { - bids: [{ - mediaTypes: { - video: { - playerSize: [['400', '300']] - } - }, - bidId: 123, - params: { - ad_unit: 'outstream', - player_width: 400, - player_height: 300, - content_page_url: 'prebid.js', - ad_mute: 1, - outstream_options: {foo: 'bar'}, - outstream_function: 'function' - } - }, { - mediaTypes: { - video: { - playerSize: [['200', '100']] - } - }, - bidId: 124, - params: { - player_width: 200, - player_height: 100, - content_page_url: 'prebid.js', - ad_mute: 1, - outstream_options: {foo: 'bar'}, - outstream_function: 'function' - } - }] - } - }; - - serverResponse = { - body: { - id: 12345, - seatbid: [{ - bid: [{ - impid: 123, - cur: 'USD', - price: 12, - adomain: ['abc.com'], - crid: 321, - w: 400, - h: 300, - ext: { - cache_key: 'cache123', - slot: 'slot123' - } - }, { - impid: 124, - cur: 'USD', - price: 13, - adomain: ['def.com'], - w: 200, - h: 100, - ext: { - cache_key: 'cache124', - slot: 'slot124' - } - }] - }] - } - }; - }); - - it('should return an array of bid responses', function() { - let responses = spec.interpretResponse(serverResponse, bidderRequestObj); - expect(responses).to.be.an('array').with.length(2); - expect(responses[0].cache_key).to.equal('cache123'); - expect(responses[0].channel_id).to.equal(12345); - expect(responses[0].meta.advertiserDomains[0]).to.equal('abc.com'); - expect(responses[0].cpm).to.equal(12); - expect(responses[0].creativeId).to.equal(321); - expect(responses[0].currency).to.equal('USD'); - expect(responses[0].height).to.equal(300); - expect(responses[0].mediaType).to.equal('video'); - expect(responses[0].netRevenue).to.equal(true); - expect(responses[0].requestId).to.equal(123); - expect(responses[0].ttl).to.equal(360); - expect(responses[0].vastUrl).to.equal('https://search.spotxchange.com/ad/vast.html?key=cache123'); - expect(responses[0].videoCacheKey).to.equal('cache123'); - expect(responses[0].width).to.equal(400); - expect(responses[1].cache_key).to.equal('cache124'); - expect(responses[1].channel_id).to.equal(12345); - expect(responses[1].cpm).to.equal(13); - expect(responses[1].meta.advertiserDomains[0]).to.equal('def.com'); - expect(responses[1].creativeId).to.equal(''); - expect(responses[1].currency).to.equal('USD'); - expect(responses[1].height).to.equal(100); - expect(responses[1].mediaType).to.equal('video'); - expect(responses[1].netRevenue).to.equal(true); - expect(responses[1].requestId).to.equal(124); - expect(responses[1].ttl).to.equal(360); - expect(responses[1].vastUrl).to.equal('https://search.spotxchange.com/ad/vast.html?key=cache124'); - expect(responses[1].videoCacheKey).to.equal('cache124'); - expect(responses[1].width).to.equal(200); - }); - - it('should set the renderer attached to the bid to render immediately', function () { - var renderer = spec.interpretResponse(serverResponse, bidderRequestObj)[0].renderer, - hasRun = false; - expect(renderer._render).to.be.a('function'); - renderer._render = () => { - hasRun = true; - } - renderer.render(); - expect(hasRun).to.equal(true); - }); - - it('should include the url property on the renderer for Prebid Core checks', function () { - var renderer = spec.interpretResponse(serverResponse, bidderRequestObj)[0].renderer; - expect(isRendererRequired(renderer)).to.be.true; - }); - }); - - describe('outstreamRender', function() { - let serverResponse, bidderRequestObj; - - beforeEach(function() { - sinon.stub(window.document, 'getElementById').returns({ - clientWidth: 200, - appendChild: sinon.stub().callsFake(function(script) {}) - }); - sinon.stub(window.document, 'createElement').returns({ - setAttribute: function () {} - }); - bidderRequestObj = { - bidRequest: { - bids: [{ - mediaTypes: { - video: { - playerSize: [['400', '300']] - } - }, - bidId: 123, - params: { - ad_unit: 'outstream', - player_width: 400, - player_height: 300, - content_page_url: 'prebid.js', - outstream_options: { - ad_mute: 1, - foo: 'bar', - slot: 'slot123', - playersize_auto_adapt: true, - custom_override: { - digitrust_opt_out: 1, - vast_url: 'bad_vast' - } - }, - } - }] - } - }; - - serverResponse = { - body: { - id: 12345, - seatbid: [{ - bid: [{ - impid: 123, - cur: 'USD', - price: 12, - crid: 321, - w: 400, - h: 300, - ext: { - cache_key: 'cache123', - slot: 'slot123' - } - }] - }] - } - }; - }); - afterEach(function () { - window.document.getElementById.restore(); - window.document.createElement.restore(); - }); - - it('should attempt to insert the EASI script', function() { - window.document.getElementById.restore(); - sinon.stub(window.document, 'getElementById').returns({ - appendChild: sinon.stub().callsFake(function(script) {}), - }); - let responses = spec.interpretResponse(serverResponse, bidderRequestObj); - let attrs; - - responses[0].renderer.render(responses[0]); - expect(loadExternalScript.called).to.be.true; - attrs = valuesToString(loadExternalScript.args[0][4]); - - expect(attrs['data-spotx_channel_id']).to.equal('12345'); - expect(attrs['data-spotx_vast_url']).to.equal('https://search.spotxchange.com/ad/vast.html?key=cache123'); - expect(attrs['data-spotx_ad_unit']).to.equal('incontent'); - expect(attrs['data-spotx_collapse']).to.equal('0'); - expect(attrs['data-spotx_autoplay']).to.equal('1'); - expect(attrs['data-spotx_blocked_autoplay_override_mode']).to.equal('1'); - expect(attrs['data-spotx_video_slot_can_autoplay']).to.equal('1'); - expect(attrs['data-spotx_digitrust_opt_out']).to.equal('1'); - expect(attrs['data-spotx_content_width']).to.equal('400'); - expect(attrs['data-spotx_content_height']).to.equal('300'); - expect(attrs['data-spotx_ad_mute']).to.equal('1'); - }); - - it('should append into an iframe', function() { - bidderRequestObj.bidRequest.bids[0].params.outstream_options.in_iframe = 'iframeId'; - window.document.getElementById.restore(); - sinon.stub(window.document, 'getElementById').returns({ - nodeName: 'IFRAME', - clientWidth: 200, - appendChild: sinon.stub().callsFake(function(script) {}), - contentDocument: {nodeName: 'IFRAME'} - }); - - let responses = spec.interpretResponse(serverResponse, bidderRequestObj); - responses[0].renderer.render(responses[0]); - expect(loadExternalScript.called).to.be.true; - expect(loadExternalScript.args[0][3].nodeName).to.equal('IFRAME'); - }); - - it('should adjust width and height to match slot clientWidth if playersize_auto_adapt is used', function() { - let responses = spec.interpretResponse(serverResponse, bidderRequestObj); - - responses[0].renderer.render(responses[0]); - expect(loadExternalScript.args[0][4]['data-spotx_content_width']).to.equal('200'); - expect(loadExternalScript.args[0][4]['data-spotx_content_height']).to.equal('150'); - }); - - it('should use a default 4/3 ratio if playersize_auto_adapt is used and response does not contain width or height', function() { - delete serverResponse.body.seatbid[0].bid[0].w; - delete serverResponse.body.seatbid[0].bid[0].h; - let responses = spec.interpretResponse(serverResponse, bidderRequestObj); - - responses[0].renderer.render(responses[0]); - expect(loadExternalScript.args[0][4]['data-spotx_content_width']).to.equal('200'); - expect(loadExternalScript.args[0][4]['data-spotx_content_height']).to.equal('150'); - }); - }); -}); - -function valuesToString(obj) { - let newObj = {}; - for (let prop in obj) { - newObj[prop] = '' + obj[prop]; - } - return newObj; -} diff --git a/test/spec/modules/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index 2f5fe104eb1..ceaad85faac 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -4,7 +4,8 @@ import * as utils from 'src/utils.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; -const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; +const SYNC_URL_IFRAME = 'https://ssp.wp.pl/bidder/usersync'; +const SYNC_URL_IMAGE = 'https://ssp.wp.pl/v1/sync/pixel'; describe('SSPBC adapter', function () { function prepareTestData() { @@ -303,7 +304,7 @@ describe('SSPBC adapter', function () { 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', - 'adm': 'AD CODE 1', + 'adm': 'AD_CODE1', 'adomain': ['adomain.pl'], 'cid': 'BZ4gAg21T5nNtxlUCDSW', 'crid': 'lxHWkB7OnZeso3QiN1N4', @@ -319,7 +320,7 @@ describe('SSPBC adapter', function () { 'siteid': '8816', 'slotid': '005', 'price': 2, - 'adm': 'AD CODE 2', + 'adm': 'AD_CODE2', 'cid': '57744', 'crid': '858252', 'w': 300, @@ -343,7 +344,64 @@ describe('SSPBC adapter', function () { 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', - 'adm': 'AD CODE 1', + 'adm': 'AD_CODE', + 'adomain': ['adomain.pl'], + 'cid': 'BZ4gAg21T5nNtxlUCDSW', + 'crid': 'lxHWkB7OnZeso3QiN1N4', + 'w': 728, + 'h': 90, + }], + 'seat': 'dsp1', + 'group': 0 + }], + 'cur': 'PLN' + } + }; + + const serverResponsePaapi = { + 'body': { + 'id': bidderRequestId, + 'seatbid': [{ + 'bid': [{ + 'id': '3347324c-6889-46d2-a800-ae78a5214c06', + 'impid': '003', + 'siteid': '8816', + 'slotid': '003', + 'price': 1, + 'adid': 'lxHWkB7OnZeso3QiN1N4', + 'nurl': '', + 'adm': 'AD_CODE', + 'adomain': ['adomain.pl'], + 'cid': 'BZ4gAg21T5nNtxlUCDSW', + 'crid': 'lxHWkB7OnZeso3QiN1N4', + 'w': 728, + 'h': 90, + }], + 'seat': 'dsp1', + 'group': 0 + }], + 'cur': 'PLN', + 'ext': { + 'paapi': [ + { 'config_data': 'config value' }, + ] + }, + } + }; + + const serverResponseIncorrect = { + 'body': { + 'id': bidderRequestId, + 'seatbid': [{ + 'bid': [{ + 'id': '3347324c-6889-46d2-a800-ae78a5214c06', + 'impid': '003', + 'siteid': '8816', + 'slotid': '003', + 'price': 1, + 'adid': 'lxHWkB7OnZeso3QiN1N4', + 'nurl': '', + 'adm': 'THIS_IS_NOT_AN_AD', 'adomain': ['adomain.pl'], 'cid': 'BZ4gAg21T5nNtxlUCDSW', 'crid': 'lxHWkB7OnZeso3QiN1N4', @@ -366,7 +424,7 @@ describe('SSPBC adapter', function () { 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', - 'adm': 'AD CODE 1', + 'adm': 'AD_CODE', 'adomain': ['adomain.pl'], 'cid': 'BZ4gAg21T5nNtxlUCDSW', 'crid': 'lxHWkB7OnZeso3QiN1N4', @@ -458,6 +516,8 @@ describe('SSPBC adapter', function () { serverResponse, serverResponseOneCode, serverResponseSingle, + serverResponseIncorrect, + serverResponsePaapi, serverResponseVideo, serverResponseNative, emptyResponse @@ -590,10 +650,29 @@ describe('SSPBC adapter', function () { expect(extAssets1).to.have.property('pbsize').that.equals('750x200_1') expect(extAssets2).to.have.property('pbsize').that.equals('750x200_1') }); + + it('should send supply chain data', function () { + const supplyChain = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'first-seller.com', + sid: '00001', + hp: 1 + }, + ] + } + const bidWithSupplyChain = Object.assign(bids[0], { schain: supplyChain }); + const requestWithSupplyChain = spec.buildRequests([bidWithSupplyChain], bidRequest); + const payloadWithSupplyChain = requestWithSupplyChain ? JSON.parse(requestWithSupplyChain.data) : { site: false, imp: false }; + + expect(payloadWithSupplyChain.source).to.have.property('schain').that.has.keys('ver', 'complete', 'nodes'); + }); }); describe('interpretResponse', function () { - const { bid_OneCode, bid_video, bid_native, bids, emptyResponse, serverResponse, serverResponseOneCode, serverResponseSingle, serverResponseVideo, serverResponseNative, bidRequest, bidRequestOneCode, bidRequestSingle, bidRequestVideo, bidRequestNative } = prepareTestData(); + const { bid_OneCode, bid_video, bid_native, bids, emptyResponse, serverResponse, serverResponseOneCode, serverResponseSingle, serverResponseIncorrect, serverResponsePaapi, serverResponseVideo, serverResponseNative, bidRequest, bidRequestOneCode, bidRequestSingle, bidRequestVideo, bidRequestNative } = prepareTestData(); const request = spec.buildRequests(bids, bidRequest); const requestSingle = spec.buildRequests([bids[0]], bidRequestSingle); const requestOneCode = spec.buildRequests([bid_OneCode], bidRequestOneCode); @@ -632,15 +711,11 @@ describe('SSPBC adapter', function () { expect(resultPartial.length).to.equal(1); }); - it('banner ad code should contain required variables', function () { + it('should not alter HTML from response', function () { let resultSingle = spec.interpretResponse(serverResponseSingle, requestSingle); let adcode = resultSingle[0].ad; - expect(adcode).to.be.a('string'); - expect(adcode).to.contain('window.rekid'); - expect(adcode).to.contain('window.mcad'); - expect(adcode).to.contain('window.tcString'); - expect(adcode).to.contain('window.page'); - expect(adcode).to.contain('window.requestPVID'); + + expect(adcode).to.be.equal(serverResponseSingle.body.seatbid[0].bid[0].adm); }); it('should create a correct video bid', function () { @@ -666,6 +741,19 @@ describe('SSPBC adapter', function () { expect(nativeBid).to.have.keys('cpm', 'creativeId', 'currency', 'width', 'height', 'meta', 'mediaType', 'netRevenue', 'requestId', 'ttl', 'native', 'vurls'); expect(nativeBid.native).to.have.keys('image', 'icon', 'title', 'sponsoredBy', 'body', 'clickUrl', 'impressionTrackers', 'javascriptTrackers', 'clickTrackers'); }); + + it('should reject responses that are not HTML, VATS/VPAID or native', function () { + let resultIncorrect = spec.interpretResponse(serverResponseIncorrect, requestSingle); + + expect(resultIncorrect.length).to.equal(0); + }); + + it('should response with fledge auction configs', function () { + const { bids, fledgeAuctionConfigs } = spec.interpretResponse(serverResponsePaapi, requestSingle); + + expect(bids.length).to.equal(1); + expect(fledgeAuctionConfigs.length).to.equal(1); + }); }); describe('getUserSyncs', function () { @@ -673,13 +761,18 @@ describe('SSPBC adapter', function () { let syncResultImage = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }); let syncResultNone = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: false }); - it('should provide correct url, if frame sync is allowed', function () { + it('should provide correct iframe url, if frame sync is allowed', function () { expect(syncResultAll).to.have.length(1); - expect(syncResultAll[0].url).to.have.string(SYNC_URL); + expect(syncResultAll[0].url).to.have.string(SYNC_URL_IFRAME); + }); + + it('should provide correct image url, if image sync is allowed', function () { + expect(syncResultImage).to.have.length(1); + expect(syncResultImage[0].url).to.have.string(SYNC_URL_IMAGE); }); - it('should send no syncs, if frame sync is not allowed', function () { - expect(syncResultImage).to.have.length(0); + it('should send no syncs, if no sync is allowed', function () { + expect(syncResultNone).to.have.length(0); expect(syncResultNone).to.have.length(0); }); }); @@ -703,6 +796,25 @@ describe('SSPBC adapter', function () { }); }); + describe('onBidBillable', function () { + it('should generate no notification if bid is undefined', function () { + let notificationPayload = spec.onBidBillable(); + expect(notificationPayload).to.be.undefined; + }); + + it('should generate notification with event name and request/adUnit data, if correct bid is provided. Should also contain site/slot data as arrays.', function () { + const { bids } = prepareTestData(); + let bid = bids[0]; + + let notificationPayload = spec.onBidBillable(bid); + expect(notificationPayload).to.have.property('event').that.equals('bidBillable'); + expect(notificationPayload).to.have.property('requestId').that.equals(bid.bidderRequestId); + expect(notificationPayload).to.have.property('tagid').that.deep.equals([bid.adUnitCode]); + expect(notificationPayload).to.have.property('siteId').that.is.an('array'); + expect(notificationPayload).to.have.property('slotId').that.is.an('array'); + }); + }); + describe('onTimeout', function () { it('should generate no notification if timeout data is undefined / has no bids', function () { let notificationPayloadUndefined = spec.onTimeout(); diff --git a/test/spec/modules/ssp_genieeBidAdapter_spec.js b/test/spec/modules/ssp_genieeBidAdapter_spec.js new file mode 100644 index 00000000000..76f4775344d --- /dev/null +++ b/test/spec/modules/ssp_genieeBidAdapter_spec.js @@ -0,0 +1,543 @@ +import { expect } from 'chai'; +import { + spec, + BANNER_ENDPOINT, + buildExtuidQuery, +} from 'modules/ssp_genieeBidAdapter.js'; +import { config } from 'src/config.js'; + +describe('ssp_genieeBidAdapter', function () { + const ZONE_ID = 1234567; + const AD_UNIT_CODE = 'adunit-code'; + const BANNER_BID = { + bidder: spec.code, + params: { + zoneId: ZONE_ID, + invalidImpBeacon: false, + }, + adUnitCode: AD_UNIT_CODE, + sizes: [[300, 250]], + bidId: 'bidId12345', + bidderRequestId: 'bidderRequestId12345', + auctionId: 'auctionId12345', + }; + + function getGeparamsDefinedBid(bid, params) { + const newBid = { ...bid }; + newBid.params.geparams = params; + return newBid; + } + + function hasParamsNotBlankStringTestGeparams(param, query) { + it(`should set the ${query} query to geparams.${param} when geparams.${param} is neither undefined nor null nor a blank string`, function () { + window.geparams[param] = undefined; + let request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property(`"${query}:`); + + window.geparams[param] = null; + request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property(`"${query}:`); + + window.geparams[param] = ''; + request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property(`"${query}:`); + + const value = 'hoge'; + window.geparams[param] = value; + request = spec.buildRequests([BANNER_BID]); + expect(JSON.stringify(request[0].data)).to.have.string(`"${query}":"${value}"`); + }); + } + + function hasParamsNotBlankStringTestGecuparams(param, query) { + it(`should set the ${query} query to gecuparams.${param} when gecuparams.${param} is neither undefined nor null nor a blank string`, function () { + window.gecuparams = {}; + window.gecuparams[param] = undefined; + let request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property(`"${query}:`); + + window.gecuparams[param] = null; + request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property(`"${query}:`); + + window.gecuparams[param] = ''; + request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property(`"${query}:`); + + const value = 'hoge'; + window.gecuparams[param] = value; + request = spec.buildRequests([BANNER_BID]); + expect(JSON.stringify(request[0].data)).to.have.string(`"${query}":"${value}"`); + }); + } + + beforeEach(function () { + document.documentElement.innerHTML = ''; + const adTagParent = document.createElement('div'); + adTagParent.id = AD_UNIT_CODE; + document.body.appendChild(adTagParent); + }); + + 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 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; + }); + + it('should return false when params.zoneId does not exist', function () { + expect(spec.isBidRequestValid({ ...BANNER_BID, params: {} })).to.be.false; + }); + + 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('buildRequests', function () { + it('should changes the endpoint with banner ads or naive ads', function () { + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].url).to.equal(BANNER_ENDPOINT); + }); + + it('should return a ServerRequest where the bid is a bid for validBidRequests', function () { + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].bid).to.equal(BANNER_BID); + }); + + describe('QueryStringParameters', function () { + it('should sets the value of the zoneid query to bid.params.zoneId', function () { + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].data.zoneid).to.deep.equal(BANNER_BID.params.zoneId); + }); + + it('should sets the values for loc and referer queries when bidderRequest.refererInfo.referer has a value', function () { + const referer = 'https://example.com/'; + const request = spec.buildRequests([BANNER_BID], { + refererInfo: { legacy: { referer: referer }, ref: referer }, + }); + expect(request[0].data.loc).to.deep.equal(referer); + expect(request[0].data.referer).to.deep.equal(referer); + }); + + it('should makes the values of loc query and referer query geparams value when bidderRequest.refererInfo.referer is a falsy value', function () { + const loc = 'https://www.google.com/'; + const referer = 'https://example.com/'; + window.geparams = { + loc: 'https://www.google.com/', + ref: 'https://example.com/', + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, { loc: loc, ref: referer }), + ]); + expect(request[0].data.loc).to.deep.equal(encodeURIComponent(loc)); + expect(request[0].data.referer).to.deep.equal(encodeURIComponent(referer)); + }); + + it('should sets the value of the ct0 query to geparams.ct0', function () { + const ct0 = 'hoge'; + window.geparams = { + ct0: 'hoge', + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, { ct0: ct0 }), + ]); + expect(request[0].data.ct0).to.deep.equal(ct0); + }); + + it('should replaces currency with JPY if there is no currency provided', function () { + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].data.cur).to.deep.equal('JPY'); + }); + + it('should makes currency the value of params.currency when params.currency exists', function () { + const request = spec.buildRequests([ + { + ...BANNER_BID, + params: { ...BANNER_BID.params, currency: 'JPY' }, + }, + { + ...BANNER_BID, + params: { ...BANNER_BID.params, currency: 'USD' }, + }, + ]); + expect(request[0].data.cur).to.deep.equal('JPY'); + expect(request[1].data.cur).to.deep.equal('USD'); + }); + + it('should makes invalidImpBeacon the value of params.invalidImpBeacon when params.invalidImpBeacon exists (in current version, this parameter is not necessary and ib is always `0`)', function () { + const request = spec.buildRequests([ + { + ...BANNER_BID, + params: { ...BANNER_BID.params, invalidImpBeacon: true }, + }, + { + ...BANNER_BID, + params: { ...BANNER_BID.params, invalidImpBeacon: false }, + }, + { + ...BANNER_BID, + params: { ...BANNER_BID.params }, + }, + ]); + expect(request[0].data.ib).to.deep.equal(0); + expect(request[1].data.ib).to.deep.equal(0); + expect(request[2].data.ib).to.deep.equal(0); + }); + + it('should not sets the value of the adtk query when geparams.lat does not exist', function () { + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property('adtk'); + }); + + it('should sets the value of the adtk query to 0 when geparams.lat is truthy value', function () { + window.geparams = { + lat: 1, + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, { lat: 1 }), + ]); + expect(request[0].data.adtk).to.deep.equal('0'); + }); + + it('should sets the value of the adtk query to 1 when geparams.lat is falsy value', function () { + window.geparams = { + lat: 0, + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, { lat: 0 }), + ]); + expect(request[0].data.adtk).to.deep.equal('1'); + }); + + it('should sets the value of the idfa query to geparams.idfa', function () { + const idfa = 'hoge'; + window.geparams = { + idfa: 'hoge', + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, { idfa: idfa }), + ]); + expect(request[0].data.idfa).to.deep.equal(idfa); + }); + + it('should set the sw query to screen.height and the sh query to screen.width when screen.width is greater than screen.height', function () { + const width = 1440; + const height = 900; + const stub = sinon.stub(window, 'screen').get(function () { + return { width: width, height: height }; + }); + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].data.sw).to.deep.equal(height); + expect(request[0].data.sh).to.deep.equal(width); + stub.restore(); + }); + + it('should set the sw query to screen.width and the sh query to screen.height when screen.width is not greater than screen.height', function () { + const width = 411; + const height = 731; + const stub = sinon.stub(window, 'screen').get(function () { + return { width: width, height: height }; + }); + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].data.sw).to.deep.equal(width); + expect(request[0].data.sh).to.deep.equal(height); + stub.restore(); + }); + + hasParamsNotBlankStringTestGeparams('zip', 'zip'); + hasParamsNotBlankStringTestGeparams('country', 'country'); + hasParamsNotBlankStringTestGeparams('city', 'city'); + hasParamsNotBlankStringTestGeparams('long', 'long'); + hasParamsNotBlankStringTestGeparams('lati', 'lati'); + + it('should set the custom query to geparams.custom', function () { + const params = { + custom: { + c1: undefined, + c2: null, + c3: '', + c4: 'hoge', + }, + }; + window.geparams = { + custom: { + c1: undefined, + c2: null, + c3: '', + c4: 'hoge', + }, + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, params), + ]); + expect(request[0].data).to.not.have.property('custom_c1'); + expect(request[0].data).to.not.have.property('custom_c2'); + expect(request[0].data).to.not.have.property('custom_c3'); + expect(request[0].data.custom_c4).to.have.string( + `${params.custom.c4}` + ); + }); + + hasParamsNotBlankStringTestGecuparams('ver', 'gc_ver'); + hasParamsNotBlankStringTestGecuparams('minor', 'gc_minor'); + hasParamsNotBlankStringTestGecuparams('value', 'gc_value'); + + it('should sets the value of the gfuid query to geparams.gfuid', function () { + const gfuid = 'hoge'; + window.geparams = { + gfuid: 'hoge', + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, { gfuid: gfuid }), + ]); + expect(request[0].data).to.not.have.property('gfuid'); + }); + + it('should sets the value of the adt query to geparams.adt', function () { + const adt = 'hoge'; + window.geparams = { + adt: 'hoge', + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, { adt: adt }), + ]); + expect(request[0].data).to.not.have.property('adt'); + }); + + it('should adds a query for naive ads and no query for banner ads', function () { + // const query = '&tkf=1&ad_track=1&apiv=1.1.0'; + const query_apiv = '1.1.0'; + const query_tkf = '1'; + const query_ad_track = '1'; + const request = spec.buildRequests([BANNER_BID]); + expect(String(request[0].data.apiv)).to.not.have.string(query_apiv); + expect(String(request[0].data.tkf)).to.not.have.string(query_tkf); + expect(String(request[0].data.ad_track)).to.not.have.string(query_ad_track); + }); + + it('should sets the value of the apid query to geparams.bundle when media type is banner', function () { + const bundle = 'hoge'; + window.geparams = { + bundle: 'hoge', + }; + const request = spec.buildRequests([ + getGeparamsDefinedBid(BANNER_BID, { bundle: bundle }), + ]); + expect(request[0].data.apid).to.deep.equal(bundle); + }); + + it('should include only imuid in extuid query when only imuid exists', function () { + const imuid = 'b.a4ad1d3eeb51e600'; + const request = spec.buildRequests([{...BANNER_BID, userId: {imuid}}]); + expect(request[0].data.extuid).to.deep.equal(`im:${imuid}`); + }); + + it('should include only id5id in extuid query when only id5id exists', function () { + const id5id = 'id5id'; + const request = spec.buildRequests([{...BANNER_BID, userId: {id5id: {uid: id5id}}}]); + expect(request[0].data.extuid).to.deep.equal(`id5:${id5id}`); + }); + + it('should include id5id and imuid in extuid query when id5id and imuid exists', function () { + const imuid = 'b.a4ad1d3eeb51e600'; + const id5id = 'id5id'; + const request = spec.buildRequests([{...BANNER_BID, userId: {id5id: {uid: id5id}, imuid: imuid}}]); + expect(request[0].data.extuid).to.deep.equal(`id5:${id5id}\tim:${imuid}`); + }); + + it('should not include the extuid query when both id5 and imuid are missing', function () { + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property('extuid'); + }); + + describe('buildExtuidQuery', function() { + it('should return tab-separated string when both id5 and imuId exist', function() { + const result = buildExtuidQuery({ id5: 'test_id5', imuId: 'test_imu' }); + expect(result).to.equal('id5:test_id5\tim:test_imu'); + }); + + it('should return only id5 when imuId is missing', function() { + const result = buildExtuidQuery({ id5: 'test_id5', imuId: null }); + expect(result).to.equal('id5:test_id5'); + }); + + it('should return only imuId when id5 is missing', function() { + const result = buildExtuidQuery({ id5: null, imuId: 'test_imu' }); + expect(result).to.equal('im:test_imu'); + }); + + it('should return null when both id5 and imuId are missing', function() { + const result = buildExtuidQuery({ id5: null, imuId: null }); + expect(result).to.be.null; + }); + }); + + it('should include gpid when ortb2Imp.ext.gpid exists', function () { + const gpid = '/123/abc'; + const bidWithGpid = { + ...BANNER_BID, + ortb2Imp: { + ext: { + gpid: gpid + } + } + }; + const request = spec.buildRequests([bidWithGpid]); + expect(String(request[0].data.gpid)).to.have.string(gpid); + }); + + it('should include gpid when ortb2Imp.ext.data.pbadslot exists', function () { + const pbadslot = '/123/abc'; + const bidWithPbadslot = { + ...BANNER_BID, + ortb2Imp: { + ext: { + data: { + pbadslot: pbadslot + } + } + } + }; + const request = spec.buildRequests([bidWithPbadslot]); + expect(String(request[0].data.gpid)).to.have.string(pbadslot); + }); + + it('should prioritize ortb2Imp.ext.gpid over ortb2Imp.ext.data.pbadslot', function () { + const gpid = '/123/abc'; + const pbadslot = '/456/def'; + const bidWithBoth = { + ...BANNER_BID, + ortb2Imp: { + ext: { + gpid: gpid, + data: { + pbadslot: pbadslot + } + } + } + }; + const request = spec.buildRequests([bidWithBoth]); + expect(String(request[0].data.gpid)).to.have.string(gpid); + }); + + it('should not include gpid when neither ortb2Imp.ext.gpid nor ortb2Imp.ext.data.pbadslot exists', function () { + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property('gpid'); + }); + + it('should include gpid when ortb2Imp.ext.gpid exists', function () { + const gpid = '/123/abc'; + const bidWithGpid = { + ...BANNER_BID, + ortb2Imp: { + ext: { + gpid: gpid + } + } + }; + const request = spec.buildRequests([bidWithGpid]); + expect(String(request[0].data.gpid)).to.have.string(gpid); + }); + + it('should include gpid when ortb2Imp.ext.data.pbadslot exists', function () { + const pbadslot = '/123/abc'; + const bidWithPbadslot = { + ...BANNER_BID, + ortb2Imp: { + ext: { + data: { + pbadslot: pbadslot + } + } + } + }; + const request = spec.buildRequests([bidWithPbadslot]); + expect(String(request[0].data.gpid)).to.have.string(pbadslot); + }); + + it('should prioritize ortb2Imp.ext.gpid over ortb2Imp.ext.data.pbadslot', function () { + const gpid = '/123/abc'; + const pbadslot = '/456/def'; + const bidWithBoth = { + ...BANNER_BID, + ortb2Imp: { + ext: { + gpid: gpid, + data: { + pbadslot: pbadslot + } + } + } + }; + const request = spec.buildRequests([bidWithBoth]); + expect(String(request[0].data.gpid)).to.have.string(gpid); + }); + + it('should not include gpid when neither ortb2Imp.ext.gpid nor ortb2Imp.ext.data.pbadslot exists', function () { + const request = spec.buildRequests([BANNER_BID]); + expect(request[0].data).to.not.have.property('gpid'); + }); + }); + }); + + describe('interpretResponse', function () { + const response = {}; + response[ZONE_ID] = { + creativeId: '', + cur: 'JPY', + price: 0.092, + width: 300, + height: 250, + requestid: '2e42361a6172bf', + adm: '', + }; + const expected = { + requestId: response[ZONE_ID].requestid, + cpm: response[ZONE_ID].price, + creativeId: response[ZONE_ID].creativeId, + netRevenue: true, + currency: 'JPY', + ttl: 700, + width: response[ZONE_ID].width, + height: response[ZONE_ID].height, + }; + + it('should sets the response correctly when it comes to banner ads', function () { + const expectedBanner = { + ...expected, + ad: + '
' + + response[ZONE_ID].adm + + '
', + mediaType: 'banner', + }; + const request = spec.buildRequests([BANNER_BID])[0]; + const result = spec.interpretResponse({ body: response }, request); + expect(result[0]).to.deep.equal(expectedBanner); + }); + }); +}); diff --git a/test/spec/modules/stackadaptBidAdapter_spec.js b/test/spec/modules/stackadaptBidAdapter_spec.js new file mode 100644 index 00000000000..ea86adf28ca --- /dev/null +++ b/test/spec/modules/stackadaptBidAdapter_spec.js @@ -0,0 +1,1380 @@ +import { expect } from 'chai'; +import { spec } from 'modules/stackadaptBidAdapter'; +import { deepClone, mergeDeep, deepSetValue } from 'src/utils.js'; +import { config } from 'src/config'; + +describe('stackadaptBidAdapter', function () { + describe('intepretResponse() mediatypes - complete', () => { + const defaultBidRequest = { + 'bidderRequestId': '2856b3d7c2c8e93e', + 'bidder': 'stackadapt', + 'params': { + 'publisherId': 473298, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [336, 280], + [320, 100] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'aa837ec1-ba90-3821-jduq-1cc083921a9a', + 'sizes': [ + [336, 280], + [320, 100] + ], + 'bidId': '001', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + 'ortb2': {} + }; + + const ortbResponse = { + 'body': { + 'id': '2856b3d7c2c8e93e', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1', + 'impid': '001', + 'price': 6.97, + 'adid': '5739901', + 'adm': '', + 'adomain': ['mobility.com'], + 'crid': '5739901', + 'w': 336, + 'h': 280, + } + ], + 'seat': 'StackAdapt' + } + ], + 'cur': 'USD' + }, + 'headers': {} + } + + it('should return empty', () => { + const req = spec.buildRequests([defaultBidRequest], { + bids: [defaultBidRequest] + }) + const result = spec.interpretResponse(null, { + data: req.data + }) + + expect(result.length).to.eq(0); + }); + + it('should set mediaType from bid request mediaTypes', () => { + const req = spec.buildRequests([defaultBidRequest], { + id: '832j6c82-893j-21j9-8392-4wd9d82pl739', + bidderRequestId: '2856b3d7c2c8e93e', + bids: [defaultBidRequest] + }) + const result = spec.interpretResponse(ortbResponse, { + data: req.data + }) + + expect(result.length).to.eq(1); + expect(result[0].mediaType).to.eq('banner') + }); + + it('should set mediaType from present video adm', () => { + const bidRequest = mergeDeep(defaultBidRequest, { + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + }, + video: { + playerSize: [640, 480] + } + } + }) + const bannerResponse = deepClone(ortbResponse); + const ortbReq = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }) + deepSetValue(bannerResponse, 'body.seatbid.0.bid.0.adm', ''); + const result = spec.interpretResponse(bannerResponse, { + data: ortbReq.data + }) + + expect(result.length).to.eq(1); + expect(result[0].mediaType).to.eq('video') + }); + + it('should set mediaType from missing adm', () => { + const bidRequest = mergeDeep(defaultBidRequest, { + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + }, + video: { + playerSize: [640, 480] + } + } + }) + const ortbReq = spec.buildRequests([bidRequest], { + bids: [bidRequest] + }) + const result = spec.interpretResponse(ortbResponse, { + data: ortbReq.data + }) + + expect(result.length).to.eq(1); + expect(result[0].mediaType).to.eq('banner') + }); + }) + + describe('interpretResponse() empty', function () { + it('should handle empty response', function () { + let result = spec.interpretResponse({}); + expect(result.length).to.equal(0); + }); + + it('should handle empty seatbid response', function () { + let response = { + body: { + 'id': '9p1a65c0oc85a62', + 'seatbid': [] + } + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + }); + + describe('interpretResponse() single-display - complete', function () { + const ortbResponse = { + body: { + 'id': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'bidid': '173283728930905039521896', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1', + 'impid': '5', + 'crid': '1609382', + 'price': 6.97, + 'adm': '', + 'cat': [ + 'IAB1', + 'IAB2' + ], + 'h': 50, + 'w': 320, + 'dealid': '189321890321', + 'adomain': ['mobility.com'], + 'ext': { + 'creative_id': '8493266', + 'bid_type': 'cpm', + 'crtype': 'display' + } + } + ], + 'seat': 'StackAdapt' + } + ], + 'cur': 'USD', + } + }; + + const bidderRequest = { + 'id': '832j6c82-893j-21j9-8392-4wd9d82pl739', + 'bidder': 'stackadapt', + 'params': { + 'publisherId': 473298, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 320, + 50 + ] + ] + } + }, + 'sizes': [ + [ + 320, + 50 + ] + ], + 'bidId': '5', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + 'ortb2': {} + }; + + const expectedBid = { + 'requestId': '5', + 'seatBidId': '1', + 'cpm': 6.97, + 'width': 320, + 'height': 50, + 'creativeId': '1609382', + 'creative_id': '1609382', + 'dealId': '189321890321', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['mobility.com'], + 'primaryCatId': 'IAB1', + 'secondaryCatIds': [ + 'IAB2' + ] + } + }; + + it('should match bid response', function () { + const ortbRequest = spec.buildRequests([bidderRequest], { + bids: [bidderRequest] + }) + + let result = spec.interpretResponse(ortbResponse, {data: ortbRequest.data}); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + }); + + describe('interpretResponse() multi-display - complete', function () { + const ortbResponse = { + 'body': { + 'id': 'r4r90kj7-2816-392j-1d41-31y998t21d2d', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1', + 'impid': '001', + 'price': 3.50, + 'adm': '', + 'cid': '4521903', + 'crid': '6254972', + 'adomain': [ + 'test.com' + ], + 'dealid': '122781928112', + 'w': 320, + 'h': 50, + 'cat': [], + }, + { + 'id': '2', + 'impid': '002', + 'price': 4.75, + 'adm': '', + 'cid': '8472189', + 'crid': '8593271', + 'adomain': [ + 'test.com' + ], + 'dealid': '849328172299', + 'w': 300, + 'h': 250, + 'cat': [], + } + ], + 'seat': 'StackAdapt' + } + ], + 'cur': 'USD' + } + }; + + const bidderRequest1 = { + 'id': '11dd91ds-197k-23e1-9950-q79s37aq0a42', + 'bidder': 'stackadapt', + 'params': { + 'publisherId': 473298, + 'placementId': 'placement1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 320, + 50 + ] + ] + } + }, + 'sizes': [ + [ + 320, + 50 + ] + ], + 'bidId': '001', + 'bidderRequestId': 'r4r90kj7-2816-392j-1d41-31y998t21d2d', + 'auctionId': '7483329d-22il-2hyu-1d78-1098qw89457l', + 'ortb2': {} + }; + + const bidderRequest2 = { + 'id': '11dd91ds-197k-23e1-9950-q79s37aq0a43', + 'bidder': 'stackadapt', + 'params': { + 'publisherId': 473298, + 'placementId': 'placement2' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 728, + 90 + ] + ] + } + }, + 'sizes': [ + [ + 728, + 90 + ] + ], + 'bidId': '002', + 'bidderRequestId': 'r4r90kj7-2816-392j-1d41-31y998t21d2d', + 'auctionId': '7483329d-22il-2hyu-1d78-1098qw89457l', + 'ortb2': {} + }; + + const expectedBids = [ + { + 'requestId': '001', + 'seatBidId': '1', + 'cpm': 3.5, + 'width': 320, + 'height': 50, + 'creativeId': '6254972', + 'creative_id': '6254972', + 'currency': 'USD', + 'dealId': '122781928112', + 'netRevenue': true, + 'ttl': 300, + 'ad': '', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['test.com'], + 'primaryCatId': undefined, + 'secondaryCatIds': [] + } + }, + { + 'requestId': '002', + 'seatBidId': '2', + 'cpm': 4.75, + 'width': 300, + 'height': 250, + 'creativeId': '8593271', + 'creative_id': '8593271', + 'currency': 'USD', + 'dealId': '849328172299', + 'netRevenue': true, + 'ttl': 300, + 'ad': '', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['test.com'], + 'primaryCatId': undefined, + 'secondaryCatIds': [] + } + } + ]; + + it('should match bid response', function () { + const ortbRequest = spec.buildRequests([bidderRequest1, bidderRequest2], { + bids: [bidderRequest1, bidderRequest2] + }) + let result = spec.interpretResponse(ortbResponse, {data: ortbRequest.data}); + expect(result.length).to.equal(2); + expect(result).to.deep.equal(expectedBids); + }); + }); + + if (FEATURES.VIDEO) { + describe('interpretResponse() single-video - complete', function () { + const ortbResponse = { + 'body': { + 'id': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'bidid': '173283728930905039521879', + 'cur': 'USD', + 'seatbid': [ + { + 'bid': [ + { + 'crid': '6254972', + 'ext': { + 'creative_id': '1762289', + 'bid_type': 'cpm', + 'duration': 30, + }, + 'adm': '', + 'h': 480, + 'impid': '001', + 'id': '1', + 'price': 11.5, + 'w': 600 + } + ], + 'seat': 'StackAdapt' + } + ] + }, + 'headers': {} + }; + + const bidderRequest = { + 'id': '748a3c21-908a-25j9-4301-2ca9d11al199', + 'bidder': 'stackadapt', + 'params': { + 'publisherId': 473298, + }, + 'mediaTypes': { + 'video': {} + }, + 'bidId': '001', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + 'ortb2': {} + }; + + const expectedBid = { + 'requestId': '001', + 'seatBidId': '1', + 'cpm': 11.5, + 'creativeId': '6254972', + 'creative_id': '6254972', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'width': 600, + 'height': 480, + 'mediaType': 'video', + 'vastXml': '', + 'meta': {} + }; + + it('should match bid response with adm', function () { + const ortbRequest = spec.buildRequests([bidderRequest], { + bids: [bidderRequest] + }) + + let result = spec.interpretResponse(ortbResponse, {data: ortbRequest.data}); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + }); + } + + describe('isBidRequestValid()', function() { + const bannerBidderRequest = { + 'bidder': 'stackadapt', + 'params': { + 'publisherId': '11111', + 'placementId': '1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [200, 50] + ] + } + }, + 'bidId': '001', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + }; + + describe('basic tests', function () { + it('should be valid with required bid.params', function () { + expect(spec.isBidRequestValid(bannerBidderRequest)).to.equal(true); + }); + + it('should be invalid when missing publisherId param', function () { + const bidderRequest = deepClone(bannerBidderRequest); + delete bidderRequest.params.publisherId; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + + it('should be invalid if bid request is not mediaTypes.banner or mediaTypes.video', function () { + const bidderRequest = deepClone(bannerBidderRequest); + delete bidderRequest.mediaTypes + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + + it('should be invalid if bidfloor is incorrect type', function () { + const bidderRequest = deepClone(bannerBidderRequest); + bidderRequest.params.bidfloor = 'invalid bidfloor'; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + + it('should be valid if bidfloor param is a float', function () { + const bidderRequest = deepClone(bannerBidderRequest); + bidderRequest.params.bidfloor = 3.01; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(true); + }); + }); + + describe('banner tests', function () { + it('should be invalid if banner sizes is wrong format', function () { + const bidderRequest = deepClone(bannerBidderRequest); + bidderRequest.mediaTypes.banner.sizes = 'invalid'; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + + it('should be invalid if missing banner sizes', function () { + const bidderRequest = deepClone(bannerBidderRequest); + delete bidderRequest.mediaTypes.banner.sizes; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + + it('should be invalid when passed valid banner.pos', function () { + const bidderRequest = deepClone(bannerBidderRequest); + bidderRequest.mediaTypes.banner.pos = 1; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(true); + }); + }); + + if (FEATURES.VIDEO) { + describe('video tests', function () { + const videoBidderRequest = { + 'bidder': 'stackadapt', + 'params': { + 'publisherId': '11111', + 'placementId': '1' + }, + 'mediaTypes': { + 'video': { + 'maxduration': 120, + 'api': [2, 7], + 'mimes': [ + 'video/mp4', + 'application/javascript', + 'video/webm' + ], + 'protocols': [2, 3, 5, 6, 7, 8], + 'plcmt': 1, + } + }, + 'sizes': [ + [200, 50] + ], + 'bidId': '001', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + }; + + it('should be valid with required bid.params', function () { + const bidderRequest = deepClone(videoBidderRequest); + expect(spec.isBidRequestValid(bidderRequest)).to.equal(true); + }); + + it('should be invalid if missing bid.mediaTypes.video.maxduration', function () { + const bidderRequest = deepClone(videoBidderRequest); + delete bidderRequest.mediaTypes.video.maxduration; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + + it('should be invalid if missing bid.mediaTypes.video.api', function () { + const bidderRequest = deepClone(videoBidderRequest); + delete bidderRequest.mediaTypes.video.api; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + + it('should be invalid if missing bid.mediaTypes.video.mimes', function () { + const bidderRequest = deepClone(videoBidderRequest); + delete bidderRequest.mediaTypes.video.mimes; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + + it('should be invalid if missing bid.mediaTypes.video.protocols', function () { + const bidderRequest = deepClone(videoBidderRequest); + delete bidderRequest.mediaTypes.video.protocols; + expect(spec.isBidRequestValid(bidderRequest)).to.equal(false); + }); + }); + } + }); + + describe('buildRequests() banner', function () { + const bidRequests = [{ + 'bidder': 'stackadapt', + 'params': { + 'publisherId': '11111', + 'placementId': '1', + 'bidfloor': 1.01 + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[336, 280], [320, 100]] + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '2121283921', + } + }, + 'sizes': [[336, 280], [320, 100]], + 'bidId': '001', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'aa837ec1-ba90-3821-jduq-1cc083921a9a', + 'src': 'client', + 'bidRequestsCount': 10 + }]; + + const bidderRequest = { + 'bidderCode': 'stackadapt', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + ortb2: { + source: { + tid: '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + }, + site: { + domain: 'tech.stacktest.com', + publisher: { + domain: 'stacktest.com' + }, + page: 'https://tech.stacktest.com/', + ref: 'https://www.google.com/' + } + }, + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionStart': 1731042158610, + 'timeout': 1750, + 'refererInfo': { + 'reachedTop': true, + 'numIframes': 0, + 'isAmp': false, + 'page': 'https://www.mobile.com/test', + 'domain': 'www.mobile.com', + 'ref': 'https://testsite.com/', + }, + 'start': 1731042158587 + }; + + bidderRequest.bids = bidRequests; + + it('should have correct request components', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest); + expect(ortbRequest.method).to.equal('POST'); + expect(ortbRequest.url).to.be.not.empty; + expect(ortbRequest.data).to.be.not.null; + }); + + it('should set ortb request.id to bidderRequestId', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.id).to.equal('5ce18294-9682-4ad0-1c92-0ab12bg8dc5e'); + }); + + it('should set impression id from bidId', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].id).to.equal('001'); + }); + + it('should set correct endpoint', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest); + expect(ortbRequest.url).to.equal('https://pjs.srv.stackadapt.com/br'); + }); + + it('should set correct publisherId', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.site?.publisher?.id).to.equal(bidRequests[0].params.publisherId); + }); + + it('should set placementId in tagid', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].tagid).to.equal(bidRequests[0].params.placementId); + }); + + it('should set bidfloor if param set', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].bidfloor).to.equal(bidRequests[0].params.bidfloor); + }); + + it('should set gpid in ortb ext.gpid if present', function () { + const clonedBidderRequest = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + const gpid = 'site-desktop-homepage-banner-top'; + clonedBidRequests[0].ortb2Imp = { + ext: { + gpid: gpid + } + }; + clonedBidderRequest.bids = clonedBidRequests; + const ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderRequest).data; + + expect(ortbRequest.imp[0].ext).to.be.not.null; + expect(ortbRequest.imp[0].ext.gpid).to.equal(gpid); + }); + + it('should set rwdd in imp.rwdd if present', function () { + const clonedBidderRequest = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + const rwdd = 1; + clonedBidRequests[0].ortb2Imp = { + rwdd: rwdd, + }; + clonedBidderRequest.bids = clonedBidRequests; + const ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderRequest).data; + + expect(ortbRequest.imp[0].rwdd).to.be.not.null; + expect(ortbRequest.imp[0].rwdd).to.equal(1); + }); + + it('should set source.tid', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.source?.tid).to.equal(bidderRequest.ortb2.source.tid); + }); + + it('should set ad sizes in the ortb request', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(336); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(280); + expect(ortbRequest.imp[0].banner.format[1].w).to.equal(320); + expect(ortbRequest.imp[0].banner.format[1].h).to.equal(100); + }); + + it('should set referer in the bid request. ortb object takes precedence', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.site.page).to.equal('https://tech.stacktest.com/'); + }); + + it('should set the banner pos if sent', function () { + const clonedBidderRequest = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + clonedBidRequests[0].mediaTypes.banner.pos = 1; + clonedBidderRequest.bids = clonedBidRequests; + const ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderRequest).data; + + expect(ortbRequest.imp[0].banner.pos).to.equal(1); + }); + + it('should set the banner expansion direction if param set', function () { + const clonedBidderRequest = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + const expdir = [1, 3] + clonedBidRequests[0].params.banner = { + expdir: expdir + }; + + clonedBidderRequest.bids = clonedBidRequests; + const ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderRequest).data; + + expect(ortbRequest.imp[0].banner.expdir).to.equal(expdir); + }); + + it('should set first party site data after merge', function () { + const ortb2 = { + site: { + publisher: { + domain: 'https://publisher.com', + } + } + }; + const bidderRequestWithoutRefererDomain = { + ...bidderRequest, + refererInfo: { + ...bidRequests.referer, + domain: null + } + } + + const ortbRequest = spec.buildRequests(bidRequests, {...bidderRequestWithoutRefererDomain, ortb2}).data; + expect(ortbRequest.site.publisher).to.deep.equal({domain: 'https://publisher.com', id: '11111'}); + }); + + it('should set first party side data publisher domain taking precedence over referer domain', function () { + const ortb2 = { + site: { + domain: 'https://publisher.com', + } + }; + const ortbRequest = spec.buildRequests(bidRequests, {...bidderRequest, ortb2}).data; + expect(ortbRequest.site.domain).to.equal('https://publisher.com'); + }); + + it('should set bcat if present', function () { + const ortb2 = { + bcat: ['IAB1', 'IAB2'] + }; + const ortbRequest = spec.buildRequests(bidRequests, {...bidderRequest, ortb2}).data; + expect(ortbRequest.bcat).to.deep.equal(['IAB1', 'IAB2']); + }); + + it('should set badv if present', function () { + const ortb2 = { + badv: ['chargers.com', 'house.com'] + }; + const ortbRequest = spec.buildRequests(bidRequests, {...bidderRequest, ortb2}).data; + expect(ortbRequest.badv).to.deep.equal(['chargers.com', 'house.com']); + }); + + it('should set battr if present', function () { + const clonedBidderRequest = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + const battr = [1, 2, 3]; + clonedBidRequests[0].ortb2Imp = { + banner: { + battr: battr + } + }; + clonedBidderRequest.bids = clonedBidRequests; + const ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderRequest).data; + + expect(ortbRequest.imp[0].banner.battr).to.deep.equal(battr); + }); + + it('should set ortb2 gdpr consent info', function () { + const consentString = 'CQGRvoAQGRvoAAHABAENBKFsAP_gAEPgAAAAKhNV'; + const ortb2 = { + user: { + ext: { + consent: consentString + } + }, + regs: { + ext: { + gdpr: 1 + } + } + }; + let clonedBidderRequest = {...deepClone(bidderRequest), ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, clonedBidderRequest).data; + expect(ortbRequest.user.ext.consent).to.equal(consentString); + expect(ortbRequest.regs.ext.gdpr).to.equal(1); + }); + + it('should set ortb2 usp consent info', function () { + const consentString = 'CQGRvoAQGRvoAAHABAENBKFsAP_gAEPgAAAAKhNV'; + const ortb2 = { + regs: { + ext: { + us_privacy: consentString + } + } + }; + let clonedBidderRequest = {...deepClone(bidderRequest), ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, clonedBidderRequest).data; + expect(ortbRequest.regs.ext.us_privacy).to.equal(consentString); + }); + + it('should set ortb2 coppa consent info', function () { + const ortb2 = { + regs: { + coppa: 1 + } + }; + let clonedBidderRequest = {...deepClone(bidderRequest), ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, clonedBidderRequest).data; + expect(ortbRequest.regs.coppa).to.equal(1); + }); + + it('should set ortb2 gpp consent info', function () { + const ortb2 = { + regs: { + gpp: 'DCACTA~1YAA', + gpp_sid: [9] + } + }; + let clonedBidderRequest = {...deepClone(bidderRequest), ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, clonedBidderRequest).data; + expect(ortbRequest.regs.gpp).to.equal('DCACTA~1YAA'); + expect(ortbRequest.regs.gpp_sid).to.eql([9]); + }); + + it('should set schain info', function () { + const clonedBidderRequest = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + const schain = { + 'nodes': [{ + 'asi': 'adtech.com', + 'sid': '1078492', + 'hp': 1 + }, { + 'asi': 'google.com', + 'sid': 'pub-315292981', + 'hp': 1 + }], + 'complete': 1, + 'ver': '1.0' + }; + + clonedBidRequests[0].schain = schain; + clonedBidderRequest.bids = clonedBidRequests; + + const ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderRequest).data; + expect(ortbRequest.source.ext.schain).to.deep.equal(schain); + }); + + it('should set first party site data', function () { + const ortb2 = { + site: { + id: '144da00b-8309-4b2e-9482-4b3829c0b54a', + name: 'game', + domain: 'game.wiki.com', + cat: ['IAB1'], + sectioncat: ['IAB1-1'], + pagecat: ['IAB1-1'], + page: 'https://game.wiki.com/craft', + ref: 'https://www.google.com/', + keywords: 'device={}' + } + }; + const mergedBidderRequest = {...bidderRequest, ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, mergedBidderRequest).data; + expect(ortbRequest.site.id).to.equal('144da00b-8309-4b2e-9482-4b3829c0b54a'); + expect(ortbRequest.site.name).to.equal('game'); + expect(ortbRequest.site.domain).to.equal('game.wiki.com'); + expect(ortbRequest.site.cat[0]).to.equal('IAB1'); + expect(ortbRequest.site.sectioncat[0]).to.equal('IAB1-1'); + expect(ortbRequest.site.pagecat[0]).to.equal('IAB1-1'); + expect(ortbRequest.site.page).to.equal('https://game.wiki.com/craft'); + expect(ortbRequest.site.ref).to.equal('https://www.google.com/'); + expect(ortbRequest.site.keywords).to.equal('device={}'); + }); + + it('should set from floor module if no bidfloor is sent', function () { + const clonedBidderRequests = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + delete clonedBidRequests[0].params.bidfloor; + const bidfloor = 1.00 + clonedBidRequests[0].getFloor = () => { + return { currency: 'USD', floor: 1.00 }; + }; + clonedBidderRequests.bids = clonedBidRequests; + const ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderRequests).data; + expect(ortbRequest.imp[0].bidfloor).to.equal(bidfloor); + }); + + it('should set default secure value if not present', function () { + const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].secure).to.equal(1); + }); + + it('should set secure to request when present', function () { + const clonedBidderReqest = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + clonedBidRequests[0].ortb2Imp.secure = 0; + clonedBidderReqest.bids = clonedBidRequests; + + let ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderReqest).data; + expect(0).to.equal(ortbRequest.imp[0].secure); + + clonedBidRequests[0].ortb2Imp.secure = 1; + ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderReqest).data; + expect(1).to.equal(ortbRequest.imp[0].secure); + }); + + const extFirstPartyData = { + data: { + firstPartyKey: 'firstPartyValue', + firstPartyKey2: ['value', 'value2'] + }, + custom: 'custom_data', + custom_kvp: { + customKey: 'customValue' + } + } + + function validateExtFirstPartyData(ext) { + expect(ext.data.firstPartyKey).to.equal('firstPartyValue'); + expect(ext.data.firstPartyKey2).to.eql(['value', 'value2']); + expect(ext.custom).to.equal('custom_data'); + expect(ext.custom_kvp.customKey).to.equal('customValue'); + } + + it('should set site first party data', function() { + const ortb2 = { + site: { + ext: extFirstPartyData, + search: 'test search' + } + }; + + const bidderRequestMerged = {...bidderRequest, ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, bidderRequestMerged).data; + + validateExtFirstPartyData(ortbRequest.site.ext) + expect(ortbRequest.site.search).to.equal('test search') + }); + + it('should set user first party data', function() { + const ortb2 = { + user: { + ext: extFirstPartyData, + yob: 1998 + } + }; + + const bidderRequestMerged = {...bidderRequest, ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, bidderRequestMerged).data; + + validateExtFirstPartyData(ortbRequest.user.ext) + expect(ortbRequest.user.yob).to.equal(1998) + }); + + it('should set imp first party data', function() { + const clonedBidderRequest = deepClone(bidderRequest); + const clonedBidRequests = deepClone(bidRequests); + const metric = { type: 'viewability', value: 0.8 }; + clonedBidRequests[0].ortb2Imp = { + ext: extFirstPartyData, + metric: [metric], + clickbrowser: 1 + }; + clonedBidderRequest.bids = clonedBidRequests; + + const ortbRequest = spec.buildRequests(clonedBidRequests, clonedBidderRequest).data; + + validateExtFirstPartyData(ortbRequest.imp[0].ext) + expect(ortbRequest.imp[0].tagid).to.equal('1'); + expect(ortbRequest.imp[0].metric[0]).to.deep.equal(metric); + expect(ortbRequest.imp[0].clickbrowser).to.equal(1) + }); + + it('should set app first party data', function() { + const ortb2 = { + app: { + ext: extFirstPartyData, + ver: 'v1.0' + } + }; + + const bidderRequestMerged = {...bidderRequest, ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, bidderRequestMerged).data; + + validateExtFirstPartyData(ortbRequest.app.ext) + expect(ortbRequest.app.ver).to.equal('v1.0') + }); + + it('should set device first party data', function() { + const ortb2 = { + device: { + ext: extFirstPartyData, + os: 'ios' + } + }; + + const bidderRequestMerged = {...bidderRequest, ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, bidderRequestMerged).data; + + validateExtFirstPartyData(ortbRequest.device.ext) + expect(ortbRequest.device.os).to.equal('ios') + }); + + it('should set pmp first party data', function() { + const ortb2 = { + pmp: { + ext: extFirstPartyData, + private_auction: 1 + } + }; + + let bidderRequestMerged = {...bidderRequest, ortb2}; + const ortbRequest = spec.buildRequests(bidRequests, bidderRequestMerged).data; + + validateExtFirstPartyData(ortbRequest.pmp.ext) + expect(ortbRequest.pmp.private_auction).to.equal(1) + }); + }); + + describe('buildRequests() banner-multiple', function () { + const multiBidRequests = [{ + 'bidder': 'stackadapt', + 'params': { + 'publisherId': '11111', + 'placementId': '1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '2121283921', + } + }, + 'sizes': [[300, 250]], + 'bidId': '001', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'aa837ec1-ba90-3821-jduq-1cc083921a9a', + 'src': 'client', + 'bidRequestsCount': 5 + }, { + 'bidder': 'stackadapt', + 'params': { + 'publisherId': '11111', + 'placementId': '2' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[336, 280], [320, 100]] + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '3728192832', + } + }, + 'sizes': [[336, 280], [320, 100]], + 'bidId': '002', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + 'adUnitCode': 'div-gpt-ad-1460505748561-123', + 'transactionId': 'au289bg3-bc89-3894-dfak-3dp281927l1b', + 'src': 'client', + 'bidRequestsCount': 10 + }]; + + const bidderRequest = { + 'bidderCode': 'stackadapt', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + ortb2: { + source: { + tid: '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + } + }, + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionStart': 1731042158610, + 'timeout': 1750, + 'refererInfo': { + 'reachedTop': true, + 'numIframes': 0, + 'isAmp': false, + 'page': 'https://www.mobile.com/test', + 'domain': 'www.mobile.com', + 'ref': 'https://testsite.com/', + }, + 'start': 1731042158587 + }; + + it('should correctly set multiple impressions', function () { + const ortbRequest = spec.buildRequests(multiBidRequests, bidderRequest).data; + expect(ortbRequest.imp.length).to.equal(2); + expect(ortbRequest.source?.tid).to.equal(bidderRequest.ortb2.source.tid); + expect(ortbRequest.imp[0].ext?.tid).to.equal('2121283921'); + expect(ortbRequest.imp[1].ext?.tid).to.equal('3728192832'); + }); + + it('should correctly set the tagids for each impression', function () { + const ortbRequest = spec.buildRequests(multiBidRequests, bidderRequest).data; + + expect(ortbRequest.imp[0].id).to.equal('001'); + expect(ortbRequest.imp[0].tagid).to.equal('1'); + + expect(ortbRequest.imp[1].id).to.equal('002'); + expect(ortbRequest.imp[1].tagid).to.equal('2'); + }); + + it('should set the sizes for each impression', function () { + const ortbRequest = spec.buildRequests(multiBidRequests, bidderRequest).data; + + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(300); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(250); + + expect(ortbRequest.imp[1].banner.format[0].w).to.equal(336); + expect(ortbRequest.imp[1].banner.format[0].h).to.equal(280); + expect(ortbRequest.imp[1].banner.format[1].w).to.equal(320); + expect(ortbRequest.imp[1].banner.format[1].h).to.equal(100); + }); + }); + + if (FEATURES.VIDEO) { + describe('buildRequests() video', function () { + const videoBidRequests = [{ + 'bidder': 'stackadapt', + 'params': { + 'publisherId': '11111', + 'placementId': '1' + }, + 'mediaTypes': { + 'video': { + 'playerSize': [187, 105], + 'api': [1, 2], + 'mimes': [ + 'video/mp4', + 'video/x-ms-wmv', + 'application/javascript' + ], + 'protocols': [2, 3, 4, 5, 6], + 'minduration': 1, + 'maxduration': 60 + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '2121283921', + } + }, + 'transactionId': 'aa837ec1-ba90-3821-jduq-1cc083921a9a', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '001', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + 'src': 'client', + 'bidRequestsCount': 10 + }]; + + const bidderRequest = { + 'bidderCode': 'stackadapt', + 'auctionId': '8d6e5b89-9c9f-4f25-9d4f-e4c08b0b9d7f', + 'bidderRequestId': '5ce18294-9682-4ad0-1c92-0ab12bg8dc5e', + 'auctionStart': 1731042158610, + 'timeout': 1750, + 'refererInfo': { + 'reachedTop': true, + 'numIframes': 0, + 'isAmp': false, + 'page': 'https://www.mobile.com/test', + 'domain': 'www.mobile.com', + 'ref': 'https://testsite.com/', + }, + 'start': 1731042158587, + }; + + it('should set the ad size', function () { + const ortbRequest = spec.buildRequests(videoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.w).to.equal(187); + expect(ortbRequest.imp[0].video.h).to.equal(105); + }); + + it('should set mimes', function () { + const ortbRequest = spec.buildRequests(videoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.mimes[0]).to.equal('video/mp4'); + expect(ortbRequest.imp[0].video.mimes[1]).to.equal('video/x-ms-wmv'); + expect(ortbRequest.imp[0].video.mimes[2]).to.equal('application/javascript'); + }); + + it('should set min and max duration', function () { + const ortbRequest = spec.buildRequests(videoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.minduration).to.equal(1); + expect(ortbRequest.imp[0].video.maxduration).to.equal(60); + }); + + it('should set api frameworks array', function () { + const ortbRequest = spec.buildRequests(videoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.api[0]).to.equal(1); + expect(ortbRequest.imp[0].video.api[1]).to.equal(2); + }); + + it('should set the protocols array', function () { + const ortbRequest = spec.buildRequests(videoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.protocols[0]).to.equal(2); + expect(ortbRequest.imp[0].video.protocols[1]).to.equal(3); + expect(ortbRequest.imp[0].video.protocols[2]).to.equal(4); + expect(ortbRequest.imp[0].video.protocols[3]).to.equal(5); + expect(ortbRequest.imp[0].video.protocols[4]).to.equal(6); + }); + + it('should set skip if present', function () { + const clonnedVideoBidRequests = deepClone(videoBidRequests); + clonnedVideoBidRequests[0].mediaTypes.video.skip = 1; + clonnedVideoBidRequests[0].mediaTypes.video.skipmin = 5; + clonnedVideoBidRequests[0].mediaTypes.video.skipafter = 10; + + const ortbRequest = spec.buildRequests(clonnedVideoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.skipmin).to.equal(5); + expect(ortbRequest.imp[0].video.skipafter).to.equal(10); + }); + + it('should set bitrate if present', function () { + const clonnedVideoBidRequests = deepClone(videoBidRequests); + clonnedVideoBidRequests[0].mediaTypes.video.minbitrate = 100; + clonnedVideoBidRequests[0].mediaTypes.video.maxbitrate = 500; + + const ortbRequest = spec.buildRequests(clonnedVideoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.minbitrate).to.equal(100); + expect(ortbRequest.imp[0].video.maxbitrate).to.equal(500); + }); + + it('should set pos if present', function () { + const clonnedVideoBidRequests = deepClone(videoBidRequests); + clonnedVideoBidRequests[0].mediaTypes.video.pos = 1; + + const ortbRequest = spec.buildRequests(clonnedVideoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.pos).to.equal(1); + }); + + it('should set playbackmethod if present', function () { + const clonnedVideoBidRequests = deepClone(videoBidRequests); + clonnedVideoBidRequests[0].mediaTypes.video.playbackmethod = [1]; + + const ortbRequest = spec.buildRequests(clonnedVideoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.playbackmethod[0]).to.equal(1); + }); + + it('should set startdelay if present', function () { + const clonnedVideoBidRequests = deepClone(videoBidRequests); + clonnedVideoBidRequests[0].mediaTypes.video.startdelay = -1; + + const ortbRequest = spec.buildRequests(clonnedVideoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.startdelay).to.equal(-1); + }); + + it('should set placement if present', function () { + const clonnedVideoBidRequests = deepClone(videoBidRequests); + clonnedVideoBidRequests[0].mediaTypes.video.plcmt = 3; + + const ortbRequest = spec.buildRequests(clonnedVideoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.plcmt).to.equal(3); + }); + + it('should set plcmt if present', function () { + const clonnedVideoBidRequests = deepClone(videoBidRequests); + clonnedVideoBidRequests[0].mediaTypes.video.plcmt = 3; + + const ortbRequest = spec.buildRequests(clonnedVideoBidRequests, bidderRequest).data; + expect(ortbRequest.imp[0].video.plcmt).to.equal(3); + }); + }); + } + + describe('getUserSyncs', function () { + it('should get usersync', function () { + const syncOptions = { + pixelEnabled: true + }; + const gdprConsentString = 'CQGRvoAQGRvoAAHABAENBKFsAP_gAEPgAAAAKhNV'; + const gdprConsent = { + consentString: gdprConsentString, + gdprApplies: true + }; + const uspConsent = '1YNY'; + const gppConsent = { + gppString: 'DCACTA~1YAB', + applicableSections: [7, 8] + }; + + let syncs = spec.getUserSyncs(syncOptions, [], gdprConsent, uspConsent, gppConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://sync.srv.stackadapt.com/sync?nid=pjs&gdpr=1&gdpr_consent=CQGRvoAQGRvoAAHABAENBKFsAP_gAEPgAAAAKhNV&us_privacy=1YNY&gpp=DCACTA~1YAB&gpp_sid=7,8'); + + let params = new URLSearchParams(new URL(syncs[0].url).search); + expect(params.get('us_privacy')).to.equal(uspConsent); + expect(params.get('gdpr')).to.equal('1'); + expect(params.get('gdpr_consent')).to.equal(gdprConsentString); + expect(params.get('gpp')).to.equal(gppConsent.gppString); + expect(params.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()); + }); + }); +}); diff --git a/test/spec/modules/staqAnalyticsAdapter_spec.js b/test/spec/modules/staqAnalyticsAdapter_spec.js deleted file mode 100644 index 3f28098e1d1..00000000000 --- a/test/spec/modules/staqAnalyticsAdapter_spec.js +++ /dev/null @@ -1,302 +0,0 @@ -import analyticsAdapter, { ExpiringQueue, getUmtSource, storage } from 'modules/staqAnalyticsAdapter.js'; -import { expect } from 'chai'; -import adapterManager from 'src/adapterManager.js'; -import { EVENTS } from 'src/constants.js'; - -const events = require('../../../src/events'); - -const DIRECT = { - source: '(direct)', - medium: '(direct)', - campaign: '(direct)' -}; -const REFERRER = { - source: 'lander.com', - medium: '(referral)', - campaign: '(referral)', - content: '/lander.html' -}; -const GOOGLE_ORGANIC = { - source: 'google', - medium: '(organic)', - campaign: '(organic)' -}; -const CAMPAIGN = { - source: 'adkernel', - medium: 'email', - campaign: 'new_campaign', - c1: '1', - c2: '2', - c3: '3', - c4: '4', - c5: '5' - -}; -describe('', function() { - let sandbox; - - before(function() { - sandbox = sinon.sandbox.create(); - }); - - after(function() { - sandbox.restore(); - analyticsAdapter.disableAnalytics(); - }); - - describe('UTM source parser', function() { - let stubSetItem; - let stubGetItem; - - before(function() { - stubSetItem = sandbox.stub(storage, 'setItem'); - stubGetItem = sandbox.stub(storage, 'getItem'); - }); - - afterEach(function() { - sandbox.reset(); - }); - - it('should parse first direct visit as (direct)', function() { - stubGetItem.withArgs('adk_dpt_analytics').returns(undefined); - stubSetItem.returns(undefined); - let source = getUmtSource('https://example.com'); - expect(source).to.be.eql(DIRECT); - }); - - it('should parse visit from google as organic', function() { - stubGetItem.withArgs('adk_dpt_analytics').returns(undefined); - stubSetItem.returns(undefined); - let source = getUmtSource('https://example.com', 'https://www.google.com/search?q=pikachu'); - expect(source).to.be.eql(GOOGLE_ORGANIC); - }); - - it('should parse referral visit', function() { - stubGetItem.withArgs('adk_dpt_analytics').returns(undefined); - stubSetItem.returns(undefined); - let source = getUmtSource('https://example.com', 'https://lander.com/lander.html'); - expect(source).to.be.eql(REFERRER); - }); - - it('should parse referral visit from same domain as direct', function() { - stubGetItem.withArgs('adk_dpt_analytics').returns(undefined); - stubSetItem.returns(undefined); - let source = getUmtSource('https://lander.com/news.html', 'https://lander.com/lander.html'); - expect(source).to.be.eql(DIRECT); - }); - - it('should parse campaign visit', function() { - stubGetItem.withArgs('adk_dpt_analytics').returns(undefined); - stubSetItem.returns(undefined); - let source = getUmtSource('https://lander.com/index.html?utm_campaign=new_campaign&utm_source=adkernel&utm_medium=email&utm_c1=1&utm_c2=2&utm_c3=3&utm_c4=4&utm_c5=5'); - expect(source).to.be.eql(CAMPAIGN); - }); - }); - - describe('ExpiringQueue', function() { - let timer; - before(function() { - timer = sandbox.useFakeTimers(0); - }); - after(function() { - timer.restore(); - }); - - it('should notify after timeout period', (done) => { - let queue = new ExpiringQueue(() => { - let elements = queue.popAll(); - expect(elements).to.be.eql([1, 2, 3, 4]); - elements = queue.popAll(); - expect(elements).to.have.lengthOf(0); - expect(Date.now()).to.be.equal(200); - done(); - }, 100); - - queue.push(1); - setTimeout(() => { - queue.push([2, 3]); - timer.tick(50); - }, 50); - setTimeout(() => { - queue.push([4]); - timer.tick(100); - }, 100); - timer.tick(50); - }); - }); - - const REQUEST = { - bidderCode: 'AppNexus', - bidderName: 'AppNexus', - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', - bidderRequestId: '1a6fc81528d0f6', - bids: [{ - bidder: 'AppNexus', - params: {}, - adUnitCode: 'container-1', - transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', - sizes: [ - [300, 250] - ], - bidId: '208750227436c1', - bidderRequestId: '1a6fc81528d0f6', - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' - }], - auctionStart: 1509369418387, - timeout: 3000, - start: 1509369418389 - }; - - const RESPONSE = { - bidderCode: 'AppNexus', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '208750227436c1', - mediaType: 'banner', - cpm: 0.015, - ad: '', - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', - responseTimestamp: 1509369418832, - requestTimestamp: 1509369418389, - bidder: 'AppNexus', - adUnitCode: 'container-1', - timeToRespond: 443, - size: '300x250' - }; - - const bidTimeoutArgsV1 = [{ - bidId: '2baa51527bd015', - bidderCode: 'AppNexus', - adUnitCode: 'container-1', - auctionId: '66529d4c-8998-47c2-ab3e-5b953490b98f' - }, - { - bidId: '6fe3b4c2c23092', - bidderCode: 'AppNexus', - adUnitCode: 'container-2', - auctionId: '66529d4c-8998-47c2-ab3e-5b953490b98f' - }]; - - describe('Analytics adapter', function() { - let ajaxStub; - let timer; - - before(function() { - ajaxStub = sandbox.stub(analyticsAdapter, 'ajaxCall'); - timer = sandbox.useFakeTimers(0); - }); - - beforeEach(function() { - sandbox.stub(events, 'getEvents').callsFake(() => { - return [] - }); - }); - - afterEach(function() { - events.getEvents.restore(); - }); - - it('should be configurable', function() { - adapterManager.registerAnalyticsAdapter({ - code: 'staq', - adapter: analyticsAdapter - }); - - adapterManager.enableAnalytics({ - provider: 'staq', - options: { - connId: 777, - queueTimeout: 1000, - url: 'https://localhost/prebid' - } - }); - - expect(analyticsAdapter.context).to.have.property('connectionId', 777); - }); - - it('should handle auction init event', function() { - events.emit(EVENTS.AUCTION_INIT, { config: {}, timeout: 3000 }); - const ev = analyticsAdapter.context.queue.peekAll(); - expect(ev).to.have.length(1); - expect(ev[0]).to.be.eql({ event: 'auctionInit', auctionId: undefined }); - }); - - it('should handle bid request event', function() { - events.emit(EVENTS.BID_REQUESTED, REQUEST); - const ev = analyticsAdapter.context.queue.peekAll(); - expect(ev).to.have.length(2); - expect(ev[1]).to.be.eql({ - adUnitCode: 'container-1', - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', - event: 'bidRequested', - adapter: 'AppNexus', - bidderName: 'AppNexus' - }); - }); - - it('should handle bid response event', function() { - events.emit(EVENTS.BID_RESPONSE, RESPONSE); - const ev = analyticsAdapter.context.queue.peekAll(); - expect(ev).to.have.length(3); - expect(ev[2]).to.be.eql({ - adId: '208750227436c1', - event: 'bidResponse', - adapter: 'AppNexus', - bidderName: 'AppNexus', - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', - adUnitCode: 'container-1', - cpm: 0.015, - timeToRespond: 0.443, - height: 250, - width: 300, - bidWon: false, - }); - }); - - it('should handle timeouts properly', function() { - events.emit(EVENTS.BID_TIMEOUT, bidTimeoutArgsV1); - - const ev = analyticsAdapter.context.queue.peekAll(); - expect(ev).to.have.length(5); // remember, we added 2 timeout events - expect(ev[3]).to.be.eql({ - adapter: 'AppNexus', - auctionId: '66529d4c-8998-47c2-ab3e-5b953490b98f', - bidderName: 'AppNexus', - event: 'adapterTimedOut' - }) - }); - - it('should handle winning bid', function() { - events.emit(EVENTS.BID_WON, RESPONSE); - const ev = analyticsAdapter.context.queue.peekAll(); - expect(ev).to.have.length(6); - expect(ev[5]).to.be.eql({ - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', - adId: '208750227436c1', - event: 'bidWon', - adapter: 'AppNexus', - bidderName: 'AppNexus', - adUnitCode: 'container-1', - cpm: 0.015, - height: 250, - width: 300, - bidWon: true, - }); - }); - - it('should handle auction end event', function() { - timer.tick(447); - events.emit(EVENTS.AUCTION_END, RESPONSE); - let ev = analyticsAdapter.context.queue.peekAll(); - expect(ev).to.have.length(0); - expect(ajaxStub.calledOnce).to.be.equal(true); - let firstCallArgs0 = ajaxStub.firstCall.args[0]; - ev = JSON.parse(firstCallArgs0); - const ev6 = ev['events'][6]; - expect(ev['connId']).to.be.eql(777); - expect(ev6.auctionId).to.be.eql('5018eb39-f900-4370-b71e-3bb5b48d324f'); - expect(ev6.event).to.be.eql('auctionEnd'); - }); - }); -}); diff --git a/test/spec/modules/stnBidAdapter_spec.js b/test/spec/modules/stnBidAdapter_spec.js index 95cab32e41d..98859385828 100644 --- a/test/spec/modules/stnBidAdapter_spec.js +++ b/test/spec/modules/stnBidAdapter_spec.js @@ -2,8 +2,9 @@ import { expect } from 'chai'; import { spec } from 'modules/stnBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; import * as utils from 'src/utils.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native'; const ENDPOINT = 'https://hb.stngo.com/hb-multi'; const TEST_ENDPOINT = 'https://hb.stngo.com/hb-multi-test'; @@ -63,7 +64,6 @@ describe('stnAdapter', function () { 'plcmt': 1 } }, - 'vastXml': '"..."' }, { 'bidder': spec.code, @@ -80,6 +80,59 @@ describe('stnAdapter', function () { 'banner': { } }, + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ 300, 250 ] + ] + }, + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream', + 'plcmt': 1 + }, + 'native': { + 'ortb': { + 'assets': [ + { + 'id': 1, + 'required': 1, + 'img': { + 'type': 3, + 'w': 300, + 'h': 200, + } + }, + { + 'id': 2, + 'required': 1, + 'title': { + 'len': 80 + } + }, + { + 'id': 3, + 'required': 1, + 'data': { + 'type': 1 + } + } + ] + } + }, + }, 'ad': '""' } ]; @@ -151,10 +204,10 @@ describe('stnAdapter', function () { }); it('should send the correct mimes array', function () { - bidRequests[1].mediaTypes.banner.mimes = mimes; + bidRequests[0].mediaTypes.video.mimes = mimes; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bids[1].mimes).to.be.an('array'); - expect(request.data.bids[1].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); + expect(request.data.bids[0].mimes).to.be.an('array'); + expect(request.data.bids[0].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); }); it('should send the correct protocols array', function () { @@ -170,12 +223,21 @@ describe('stnAdapter', function () { expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) expect(request.data.bids[1].sizes).to.be.an('array'); expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + expect(request.data.bids[2].sizes).to.be.an('array'); + expect(request.data.bids[2].sizes).to.eql(bidRequests[2].sizes) + }); + + it('should send nativeOrtbRequest in native bid request', function () { + decorateAdUnitsWithNativeParams(bidRequests) + const request = spec.buildRequests(bidRequests, bidderRequest); + assert.deepEqual(request.data.bids[2].nativeOrtbRequest, bidRequests[2].mediaTypes.native.ortb) }); it('should send the correct media type', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.bids[0].mediaType).to.equal(VIDEO) expect(request.data.bids[1].mediaType).to.equal(BANNER) + expect(request.data.bids[2].mediaType.split(',')).to.include.members([VIDEO, NATIVE, BANNER]) }); it('should respect syncEnabled option', function() { @@ -440,8 +502,10 @@ describe('stnAdapter', function () { width: 640, height: 480, requestId: '21e12606d47ba7', + creativeId: 'creative-id-1', adomain: ['abc.com'], - mediaType: VIDEO + mediaType: VIDEO, + nurl: 'http://example.com/win/1234', }, { cpm: 12.5, @@ -449,8 +513,32 @@ describe('stnAdapter', function () { width: 300, height: 250, requestId: '21e12606d47ba7', + creativeId: 'creative-id-2', adomain: ['abc.com'], - mediaType: BANNER + mediaType: BANNER, + nurl: 'http://example.com/win/1234', + }, + { + cpm: 12.5, + width: 300, + height: 200, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + creativeId: 'creative-id-3', + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + native: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg' + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } }] }; @@ -461,7 +549,7 @@ describe('stnAdapter', function () { width: 640, height: 480, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id-1', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: VIDEO, @@ -476,10 +564,10 @@ describe('stnAdapter', function () { requestId: '21e12606d47ba7', cpm: 12.5, currency: 'USD', - width: 640, - height: 480, + width: 300, + height: 250, ttl: TTL, - creativeId: '21e12606d47ba7', + creativeId: 'creative-id-2', netRevenue: true, nurl: 'http://example.com/win/1234', mediaType: BANNER, @@ -490,10 +578,42 @@ describe('stnAdapter', function () { ad: '""' }; + const expectedNativeResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 300, + height: 200, + ttl: TTL, + creativeId: 'creative-id-3', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: NATIVE, + meta: { + mediaType: NATIVE, + advertiserDomains: ['abc.com'] + }, + native: { + ortb: { + body: 'Advertise with Rise', + clickUrl: 'https://risecodes.com', + cta: 'Start now', + image: { + width: 300, + height: 200, + url: 'https://sdk.streamrail.com/media/rise-image.jpg', + }, + sponsoredBy: 'Rise', + title: 'Rise Ad Tech Solutions' + } + }, + }; + it('should get correct bid response', function () { const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); - expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + expect(result[0]).to.deep.equal(expectedVideoResponse); + expect(result[1]).to.deep.equal(expectedBannerResponse); + expect(result[2]).to.deep.equal(expectedNativeResponse); }); it('video type should have vastXml key', function () { @@ -505,6 +625,11 @@ describe('stnAdapter', function () { const result = spec.interpretResponse({ body: response }); expect(result[1].ad).to.equal(expectedBannerResponse.ad) }); + + it('native type should have native key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[2].native).to.eql(expectedNativeResponse.native) + }); }) describe('getUserSyncs', function() { diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js index 6f4874cef75..d186b0d5cd0 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -169,16 +169,17 @@ describe('stroeerCore bid adapter', function () { } function setupSingleWindow(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) { - const win = createWindow('http://www.xyz.com/', { - parent: win, top: win, frameElement: createElement(undefined, 304), placementElements: placementElements + let singleWin = null + singleWin = createWindow('http://www.xyz.com/', { + parent: singleWin, top: singleWin, frameElement: createElement(undefined, 304), placementElements: placementElements }); - win.innerHeight = 200; + singleWin.innerHeight = 200; - sandBox.stub(utils, 'getWindowSelf').returns(win); - sandBox.stub(utils, 'getWindowTop').returns(win); + sandBox.stub(utils, 'getWindowSelf').returns(singleWin); + sandBox.stub(utils, 'getWindowTop').returns(singleWin); - return win; + return singleWin; } function setupNestedWindows(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) { @@ -407,7 +408,6 @@ describe('stroeerCore bid adapter', function () { 'timeout': expectedTimeout, 'ref': 'https://www.example.com/?search=monkey', 'mpa': true, - 'ssl': false, 'url': 'https://www.example.com/monkey/index.html', 'bids': [{ 'sid': 'NDA=', @@ -533,6 +533,7 @@ describe('stroeerCore bid adapter', function () { 'siz': [[300, 600], [160, 60]], 'fp': undefined }, + 'sfp': undefined, }, { 'sid': 'ABC=', @@ -541,7 +542,8 @@ describe('stroeerCore bid adapter', function () { 'siz': [[100, 200], [300, 500]], 'fp': undefined }, - 'viz': undefined + 'viz': undefined, + 'sfp': undefined, } ]; @@ -555,7 +557,8 @@ describe('stroeerCore bid adapter', function () { 'siz': [640, 480], 'mim': ['video/mp4', 'video/quicktime'], 'fp': undefined - } + }, + 'sfp': undefined, } ]; @@ -597,7 +600,8 @@ describe('stroeerCore bid adapter', function () { 'ban': { 'siz': [[100, 200], [300, 500]], 'fp': undefined - } + }, + 'sfp': undefined, } ]; @@ -611,14 +615,14 @@ describe('stroeerCore bid adapter', function () { 'siz': [640, 480], 'mim': ['video/mp4', 'video/quicktime'], 'fp': undefined - } + }, + 'sfp': undefined, } ]; assert.deepEqual(serverRequestInfo.data.bids, [...expectedBannerBids, ...expectedVideoBids]); }); }); - describe('optional fields', () => { it('should skip viz field when unable to determine visibility of placement', () => { placementElements.length = 0; @@ -898,6 +902,61 @@ describe('stroeerCore bid adapter', function () { assert.deepEqual(sentOrtb2, ortb2); }); + + it('should add the special format parameters', () => { + const bidReq = buildBidderRequest(); + + const sfp0 = { + 'field1': { + 'abc': '123', + } + }; + + const sfp1 = { + 'field3': 'xyz' + }; + + bidReq.bids[0].params.sfp = utils.deepClone(sfp0); + bidReq.bids[1].params.sfp = utils.deepClone(sfp1); + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + assert.deepEqual(serverRequestInfo.data.bids[0].sfp, sfp0); + assert.deepEqual(serverRequestInfo.data.bids[1].sfp, sfp1); + }); + + it('should add the special format parameters even when it is an empty object', () => { + const bidReq = buildBidderRequest(); + + bidReq.bids[0].params.sfp = {}; + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + assert.deepEqual(serverRequestInfo.data.bids[0].sfp, {}); + assert.isUndefined(serverRequestInfo.data.bids[1].sfp); + }); + + it('should add the ortb2 site extension', () => { + const bidReq = buildBidderRequest(); + + const ortb2 = { + site: { + domain: 'example.com', + ext: { + data: { + abc: '123' + } + } + } + }; + + bidReq.ortb2 = utils.deepClone(ortb2); + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + const sentOrtb2 = serverRequestInfo.data.ortb2; + assert.deepEqual(sentOrtb2, {site: {ext: ortb2.site.ext}}) + }); }); }); }); @@ -963,6 +1022,16 @@ describe('stroeerCore bid adapter', function () { assert.deepPropertyVal(result[0].meta, 'dsa', dsaResponse); assert.propertyVal(result[1].meta, 'dsa', undefined); }); + + it('should add campaignType to meta object', () => { + const response = buildBidderResponse(); + response.bids[1] = Object.assign(response.bids[1], {campaignType: 'RTB'}); + + const result = spec.interpretResponse({body: response}); + + assert.propertyVal(result[0].meta, 'campaignType', undefined); + assert.propertyVal(result[1].meta, 'campaignType', 'RTB'); + }); }); describe('get user syncs entry point', () => { diff --git a/test/spec/modules/stvBidAdapter_spec.js b/test/spec/modules/stvBidAdapter_spec.js index 3ef865ed2f1..9dc311562ba 100644 --- a/test/spec/modules/stvBidAdapter_spec.js +++ b/test/spec/modules/stvBidAdapter_spec.js @@ -30,12 +30,12 @@ describe('stvAdapter', function() { }); it('should return false when required params are not passed', function() { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'someIncorrectParam': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); @@ -277,7 +277,7 @@ describe('stvAdapter', function() { 'width': '300', 'height': '250', 'type': 'sspHTML', - 'tag': '', + 'adTag': '', 'requestId': '220ed41385952a', 'currency': 'EUR', 'ttl': 60, @@ -338,7 +338,7 @@ describe('stvAdapter', function() { } }]; let result = spec.interpretResponse(serverResponse, bidRequest[0]); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(Object.keys(result[0])).to.include.members(Object.keys(expectedResponse[0])); expect(result[0].meta.advertiserDomains.length).to.equal(1); expect(result[0].meta.advertiserDomains[0]).to.equal(expectedResponse[0].meta.advertiserDomains[0]); }); @@ -358,7 +358,7 @@ describe('stvAdapter', function() { } }]; let result = spec.interpretResponse(serverVideoResponse, bidRequest[0]); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[1])); + expect(Object.keys(result[0])).to.include.members(Object.keys(expectedResponse[1])); expect(result[0].meta.advertiserDomains.length).to.equal(0); }); diff --git a/test/spec/modules/symitriAnalyticsAdapter_spec.js b/test/spec/modules/symitriAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..c02d5b55696 --- /dev/null +++ b/test/spec/modules/symitriAnalyticsAdapter_spec.js @@ -0,0 +1,90 @@ +import symitriAnalyticsAdapter from 'modules/symitriAnalyticsAdapter.js'; +import { expect } from 'chai'; +import adapterManager from 'src/adapterManager.js'; +import { server } from 'test/mocks/xhr.js'; +import { EVENTS } from 'src/constants.js'; + +let events = require('src/events'); + +describe('symitri analytics adapter', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function () { + events.getEvents.restore(); + }); + + describe('track', function () { + let initOptionsValid = { + apiAuthToken: 'TOKEN1234' + }; + let initOptionsInValid = { + }; + + let bidWon = { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '393976d8770041', + 'requestId': '263efc09896d0c', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'responseTimestamp': 1576823894050, + 'requestTimestamp': 1576823893838, + 'bidder': 'appnexus', + 'timeToRespond': 212, + 'status': 'rendered' + }; + + adapterManager.registerAnalyticsAdapter({ + code: 'symitri', + adapter: symitriAnalyticsAdapter + }); + + afterEach(function () { + symitriAnalyticsAdapter.disableAnalytics(); + }); + + it('Test with valid apiAuthToken', function () { + adapterManager.enableAnalytics({ + provider: 'symitri', + options: initOptionsValid + }); + events.emit(EVENTS.BID_WON, bidWon); + expect(server.requests.length).to.equal(1); + }); + + it('Test with missing apiAuthToken', function () { + adapterManager.enableAnalytics({ + provider: 'symitri', + options: initOptionsInValid + }); + events.emit(EVENTS.BID_WON, bidWon); + expect(server.requests.length).to.equal(0); + }); + + it('Test correct winning bid is sent to server', function () { + adapterManager.enableAnalytics({ + provider: 'symitri', + options: initOptionsValid + }); + events.emit(EVENTS.BID_WON, bidWon); + expect(server.requests.length).to.equal(1); + let winEventData = JSON.parse(server.requests[0].requestBody); + expect(winEventData).to.deep.equal(bidWon); + let authToken = server.requests[0].requestHeaders['Authorization']; + expect(authToken).to.equal(initOptionsValid.apiAuthToken); + }); + }); +}); diff --git a/test/spec/modules/symitriDapRtdProvider_spec.js b/test/spec/modules/symitriDapRtdProvider_spec.js new file mode 100644 index 00000000000..7a773fd23c9 --- /dev/null +++ b/test/spec/modules/symitriDapRtdProvider_spec.js @@ -0,0 +1,708 @@ +import {config} from 'src/config.js'; +import { + dapUtils, + generateRealTimeData, + symitriDapRtdSubmodule, + onBidWonListener, + storage, DAP_MAX_RETRY_TOKENIZE, DAP_SS_ID, DAP_TOKEN, DAP_MEMBERSHIP, DAP_ENCRYPTED_MEMBERSHIP +} from 'modules/symitriDapRtdProvider.js'; +import {server} from 'test/mocks/xhr.js'; +import {hook} from '../../../src/hook.js'; +import { EVENTS } from 'src/constants.js'; +const responseHeader = {'Content-Type': 'application/json'}; + +let events = require('src/events'); + +describe('symitriDapRtdProvider', function() { + const testReqBidsConfigObj = { + adUnits: [ + { + bids: ['bid1', 'bid2'] + } + ] + }; + + const onDone = function() { return true }; + + const sampleGdprConsentConfig = { + 'gdpr': { + 'consentString': null, + 'vendorData': {}, + 'gdprApplies': true + } + }; + + const sampleUspConsentConfig = { + 'usp': '1YYY' + }; + + const sampleIdentity = { + type: 'dap-signature:1.0.0' + }; + + const cmoduleConfig = { + 'name': 'symitriDap', + 'waitForIt': true, + 'params': { + 'apiHostname': 'prebid.dap.akadns.net', + 'apiVersion': 'x1', + 'apiAuthToken': 'Token 1234', + 'domain': 'prebid.org', + 'identityType': 'dap-signature:1.0.0', + 'segtax': 708 + } + } + + const emoduleConfig = { + 'name': 'symitriDap', + 'waitForIt': true, + 'params': { + 'apiHostname': 'prebid.dap.akadns.net', + 'apiVersion': 'x1', + 'domain': 'prebid.org', + 'identityType': 'dap-signature:1.0.0', + 'segtax': 710, + 'pixelUrl': 'https://www.test.com/pixel' + } + } + + const sampleConfig = { + 'api_hostname': 'prebid.dap.akadns.net', + 'api_version': 'x1', + 'domain': 'prebid.org', + 'segtax': 708, + 'identity': sampleIdentity + } + + const sampleX2Config = { + 'api_hostname': 'prebid.dap.akadns.net', + 'api_version': 'x2', + 'domain': 'prebid.org', + 'segtax': 708, + 'identity': sampleIdentity + } + + const esampleConfig = { + 'api_hostname': 'prebid.dap.akadns.net', + 'api_version': 'x1', + 'domain': 'prebid.org', + 'segtax': 710, + 'identity': sampleIdentity + } + let cacheExpiry = Math.round(Date.now() / 1000.0) + 300; // in seconds + const sampleCachedToken = {'expires_at': cacheExpiry, 'token': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..6buzBd2BjtgoyaNbHN8YnQ.l38avCfm3sYNy798-ETYOugz0cOx1cCkjACkAhYszxzrZ0sUJ0AiF-NdDXVTiTyp2Ih3vCWKzS0rKJ8lbS1zhyEVWVu91QwtwseM2fBbwA5ggAgBEo5wV-IXqDLPxVnxsPF0D3hP6cNCiH9Q2c-vULfsLhMhG5zvvZDPBbn4hUY5fKB8LoCBTF9rbuuWGYK1nramnb4AlS5UK82wBsHQea1Ou_Kp5wWCMNZ6TZk5qKIuRBfPIAhQblWvHECaHXkg1wyoM9VASs_yNhne7RR-qkwzbFiPFiMJibNOt9hF3_vPDJO5-06ZBjRTP1BllYGWxI-uQX6InzN18Wtun2WHqg.63sH0SNlIRcsK57v0pMujfB_nhU8Y5CuQbsHqH5MGoM'}; + const cachedEncryptedMembership = {'expires_at': cacheExpiry, 'encryptedSegments': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoic29tZXNlY3JldGludmF1bHQifQ..IvnIUQDqWBVYIS0gbcE9bw.Z4NZGvtogWaWlGH4e-GdYKe_PUc15M2x3Bj85rMWsN1A17mIxQIMOfg2hsQ2tgieLu5LggWPmsFu1Wbph6P0k3kOu1dVReoIhOHzxw50rP0DLHKaEZ5mLMJ7Lcosvwh4miIfFuCHlsX7J0sFgOTAp0zGo1S_UsHLtev1JflhjoSB0AoX95ALbAnyctirPuLJM8gZ1vXTiZ01jpvucGyR1lM4cWjPOeD8jPtgwaPGgSRZXE-3X2Cqy7z4Giam5Uqu74LPWTBuKtUQTGyAXA5QJoP7xwTbsU4O1f69lu3fWNqC92GijeTH1A4Zd_C-WXxWuQlDEURjlkWQoaqTHka2OqlnwukEQIf_v0r5KQQX64CTLhEUH91jeD0-E9ClcIP7pwOLxxqiKoaBmx8Mrnm_6Agj5DtTA1rusy3AL63sI_rsUxrmLrVt0Wft4aCfRkW8QpQxu8clFdOmce0NNCGeBCyCPVw9d9izrILlXJ6rItU2cpFrcbz8uw2otamF5eOFCOY3IzHedWVNNuKHFIUVC_xYSlsYvQ8f2QIP1eiMbmukcuPzmTzjw1h1_7IKaj-jJkXrnrY-TdDgX_4-_Z3rmbpXK2yTR7dBrsg-ubqFbgbKic1b4zlQEO_LbBlgPl3DYdWEuJ8CY2NUt1GfpATQGsufS2FTY1YGw_gkPe3q04l_cgLafDoxHvHh_t_0ZgPjciW82gThB_kN4RP7Mc3krVcXl_P6N1VbV07xyx0hCyVsrrxbLslI8q9wYDiLGci7mNmByM5j7SXV9jPwwPkHtn0HfMJlw2PFbIDPjgG3h7sOyLcBIJTTvuUIgpHPIkRWLIl_4FlIucXbJ7orW2nt5BWleBVHgumzGcnl9ZNcZb3W-dsdYPSOmuj0CY28MRTP2oJ1rzLInbDDpIRffJBtR7SS4nYyy7Vi09PtBigod5YNz1Q0WDSJxr8zeH_aKFaXInw7Bfo_U0IAcLiRgcT0ogsMLeQRjRFy27mr4XNJv3NtHhbdjDAwF2aClCktXyXbQaVdsPH2W71v6m2Q9rB5GQWOktw2s5f-4N1-_EBPGq6TgjF-aJZP22MJVwp1pimT50DfOzoeEqDwi862NNwNNoHmcObH0ZfwAXlhRxsgupNBe20-MNNABj2Phlfv4DUrtQbMdfCnNiypzNCmoTb7G7c_o5_JUwoV_GVkwUtvmi_IUm05P4GeMASSUw8zDKVRAj9h31C2cabM8RjMHGhkbCWpUP2pcz9zlJ7Y76Dh3RLnctfTw7DG9U4w4UlaxNZOgLUiSrGwfyapuSiuGUpuOJkBBLiHmEqAGI5C8oJpcVRccNlHxJAYowgXyFopD5Fr-FkXmv8KMkS0h5C9F6KihmDt5sqDD0qnjM0hHJgq01l7wjVnhEmPpyD-6auFQ-xDnbh1uBOJ_0gCVbRad--FSa5p-dXenggegRxOvZXJ0iAtM6Fal5Og-RCjexIHa9WhVbXhQBJpkSTWwAajZJ64eQ.yih49XB51wE-Xob7COT9OYqBrzBmIMVCQbLFx2UdzkI'}; + const cachedMembership = {'expires_at': cacheExpiry, 'said': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..QwvU5h0NVJYaJbs5EqWCKA.XNaJHSlnsH8P-yBIr3gIEqavLONWDIFyj7QCHFwJVkwXH_EYkxrk0_26b0uMPzfJp5URnqxKZusMH9DzEJsmj8EMrKQv1y3IYYMsW5_0BdP5bcAWfG6fzOqtMOwLiYRkYiQOqn1ZVGzhovheHWEmNr2_oCY0LvAr3iN1eG_K-l-bBKvBWnwvuuGKquUfCqO8NMMq6wtkecEXM9blqFRZ7oNYmW2aIG7qcHUsrUW7HMr9Ev2Ik0sIeEUsOYrgf_X_VA64RgKSTRugS9FupMv1p54JkHokwduF9pOFmW8QLQi8itFogKGbbgvOTNnmahxQUX5FcrjjYLqHwKqC8htLdlHnO5LWU9l4A7vLXrRurvoSnh0cAJy0GsdoyEwTqR9bwVFHoPquxlJjQ4buEd7PIxpBj9Qg9oOPH3b2upbMTu5CQ9oj526eXPhP5G54nwGklm2AZ3Vggd7jCQJn45Jjiq0iIfsXAtpqS2BssCLBN8WhmUTnStK8m5sux6WUBdrpDESQjPj-EEHVS-DB5rA7icRUh6EzRxzen2rndvHvnwVhSG_l6cwPYuJ0HE0KBmYHOoqNpKwzoGiKFHrf4ReA06iWB3V2TEGJucGujhtQ9_18WwHCeJ1XtQiiO1eqa3tp5MwAbFXawVFl3FFOBgadrPyvGmkmUJ6FCLU2MSwHiYZmANMnJsokFX_6DwoAgO3U_QnvEHIVSvefc7ReeJ8fBDdmrH3LtuLrUpXsvLvEIMQdWQ_SXhjKIi7tOODR8CfrhUcdIjsp3PZs1DpuOcDB6YJKbGnKZTluLUJi3TyHgyi-DHXdTm-jSE5i_DYJGW-t2Gf23FoQhexv4q7gdrfsKfcRJNrZLp6Gd6jl4zHhUtY.nprKBsy9taQBk6dCPbA7BFF0CiGhQOEF_MazZ2bedqk', 'cohorts': ['9', '11', '13']}; + const cachedMembershipWithDeals = {'expires_at': cacheExpiry, 'said': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..QwvU5h0NVJYaJbs5EqWCKA.XNaJHSlnsH8P-yBIr3gIEqavLONWDIFyj7QCHFwJVkwXH_EYkxrk0_26b0uMPzfJp5URnqxKZusMH9DzEJsmj8EMrKQv1y3IYYMsW5_0BdP5bcAWfG6fzOqtMOwLiYRkYiQOqn1ZVGzhovheHWEmNr2_oCY0LvAr3iN1eG_K-l-bBKvBWnwvuuGKquUfCqO8NMMq6wtkecEXM9blqFRZ7oNYmW2aIG7qcHUsrUW7HMr9Ev2Ik0sIeEUsOYrgf_X_VA64RgKSTRugS9FupMv1p54JkHokwduF9pOFmW8QLQi8itFogKGbbgvOTNnmahxQUX5FcrjjYLqHwKqC8htLdlHnO5LWU9l4A7vLXrRurvoSnh0cAJy0GsdoyEwTqR9bwVFHoPquxlJjQ4buEd7PIxpBj9Qg9oOPH3b2upbMTu5CQ9oj526eXPhP5G54nwGklm2AZ3Vggd7jCQJn45Jjiq0iIfsXAtpqS2BssCLBN8WhmUTnStK8m5sux6WUBdrpDESQjPj-EEHVS-DB5rA7icRUh6EzRxzen2rndvHvnwVhSG_l6cwPYuJ0HE0KBmYHOoqNpKwzoGiKFHrf4ReA06iWB3V2TEGJucGujhtQ9_18WwHCeJ1XtQiiO1eqa3tp5MwAbFXawVFl3FFOBgadrPyvGmkmUJ6FCLU2MSwHiYZmANMnJsokFX_6DwoAgO3U_QnvEHIVSvefc7ReeJ8fBDdmrH3LtuLrUpXsvLvEIMQdWQ_SXhjKIi7tOODR8CfrhUcdIjsp3PZs1DpuOcDB6YJKbGnKZTluLUJi3TyHgyi-DHXdTm-jSE5i_DYJGW-t2Gf23FoQhexv4q7gdrfsKfcRJNrZLp6Gd6jl4zHhUtY.nprKBsy9taQBk6dCPbA7BFF0CiGhQOEF_MazZ2bedqk', 'cohorts': ['9', '11', '13'], 'deals': ['{"id":"DEMODEAL555","bidfloor":5.0,"at":1,"guar":0}', '{"id":"DEMODEAL111","bidfloor":5.0,"at":1,"guar":0}', '{"id":"DEMODEAL123","bidfloor":5.0,"at":1,"guar":0}']}; + const rtdUserObj = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const encRtdUserObj = { + name: 'www.dataprovider3.com', + ext: { + segtax: 710, + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [] + }; + + const cachedRtd = { + rtd: { + ortb2: { + user: { + data: [rtdUserObj] + } + } + } + }; + + let membership = { + said: cachedMembership.said, + cohorts: cachedMembership.cohorts, + attributes: null + }; + let encMembership = { + encryptedSegments: cachedEncryptedMembership.encryptedSegments + }; + encRtdUserObj.segment.push({ id: encMembership.encryptedSegments }); + const cachedEncRtd = { + rtd: { + ortb2: { + user: { + data: [encRtdUserObj] + } + } + } + }; + + before(() => { + hook.ready(); + }); + + let ortb2, bidConfig; + + beforeEach(function() { + bidConfig = {ortb2Fragments: {}}; + ortb2 = bidConfig.ortb2Fragments.global = {}; + config.resetConfig(); + storage.removeDataFromLocalStorage(DAP_TOKEN); + storage.removeDataFromLocalStorage(DAP_MEMBERSHIP); + storage.removeDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP); + storage.removeDataFromLocalStorage(DAP_SS_ID); + }); + + afterEach(function () { + }); + + describe('symitriDapRtdSubmodule', function() { + it('successfully instantiates', function () { + expect(symitriDapRtdSubmodule.init()).to.equal(true); + }); + }); + + describe('Get Real-Time Data', function() { + it('gets rtd from local storage cache', function() { + let dapGetMembershipFromLocalStorageStub = sinon.stub(dapUtils, 'dapGetMembershipFromLocalStorage').returns(membership) + let dapGetRtdObjStub = sinon.stub(dapUtils, 'dapGetRtdObj').returns(cachedRtd) + let dapGetEncryptedMembershipFromLocalStorageStub = sinon.stub(dapUtils, 'dapGetEncryptedMembershipFromLocalStorage').returns(encMembership) + let dapGetEncryptedRtdObjStub = sinon.stub(dapUtils, 'dapGetEncryptedRtdObj').returns(cachedEncRtd) + let callDapApisStub = sinon.stub(dapUtils, 'callDapAPIs') + try { + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + expect(ortb2).to.eql({}); + generateRealTimeData(bidConfig, () => {}, emoduleConfig, {}); + + expect(ortb2.user.data).to.deep.include.members([encRtdUserObj]); + generateRealTimeData(bidConfig, () => {}, cmoduleConfig, {}); + expect(ortb2.user.data).to.deep.include.members([rtdUserObj]); + } finally { + dapGetRtdObjStub.restore() + dapGetMembershipFromLocalStorageStub.restore() + dapGetEncryptedRtdObjStub.restore() + dapGetEncryptedMembershipFromLocalStorageStub.restore() + callDapApisStub.restore() + } + }); + }); + + describe('calling DAP APIs', function() { + it('Calls callDapAPIs for unencrypted segments flow', function() { + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + let dapExtractExpiryFromTokenStub = sinon.stub(dapUtils, 'dapExtractExpiryFromToken').returns(cacheExpiry) + try { + expect(ortb2).to.eql({}); + dapUtils.callDapAPIs(bidConfig, () => {}, cmoduleConfig, {}); + let membership = {'cohorts': ['9', '11', '13'], 'said': 'sample-said'} + let membershipRequest = server.requests[0]; + membershipRequest.respond(200, responseHeader, JSON.stringify(membership)); + let tokenWithExpiry = 'Sample-token-with-exp' + let tokenizeRequest = server.requests[1]; + tokenizeRequest.requestHeaders['Content-Type'].should.equal('application/json'); + responseHeader['Symitri-DAP-Token'] = tokenWithExpiry; + tokenizeRequest.respond(200, responseHeader, JSON.stringify(tokenWithExpiry)); + let data = dapUtils.dapGetRtdObj(membership, cmoduleConfig.params.segtax); + expect(ortb2.user.data).to.deep.include.members(data.rtd.ortb2.user.data); + } finally { + dapExtractExpiryFromTokenStub.restore(); + } + }); + + it('Calls callDapAPIs for encrypted segments flow', function() { + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + let dapExtractExpiryFromTokenStub = sinon.stub(dapUtils, 'dapExtractExpiryFromToken').returns(cacheExpiry) + try { + expect(ortb2).to.eql({}); + dapUtils.callDapAPIs(bidConfig, () => {}, emoduleConfig, {}); + let encMembership = 'Sample-enc-token'; + let membershipRequest = server.requests[0]; + responseHeader['Symitri-DAP-Token'] = encMembership; + membershipRequest.respond(200, responseHeader, JSON.stringify(encMembership)); + let tokenWithExpiry = 'Sample-token-with-exp' + let tokenizeRequest = server.requests[1]; + tokenizeRequest.requestHeaders['Content-Type'].should.equal('application/json'); + responseHeader['Symitri-DAP-Token'] = tokenWithExpiry; + tokenizeRequest.respond(200, responseHeader, JSON.stringify(tokenWithExpiry)); + let data = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, emoduleConfig.params.segtax); + expect(ortb2.user.data).to.deep.include.members(data.rtd.ortb2.user.data); + } finally { + dapExtractExpiryFromTokenStub.restore(); + } + }); + }); + + describe('dapTokenize', function () { + it('dapTokenize error callback', function () { + let configAsync = JSON.parse(JSON.stringify(sampleConfig)); + let submoduleCallback = dapUtils.dapTokenize(configAsync, sampleIdentity, onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.respond(400, responseHeader, JSON.stringify('error')); + expect(submoduleCallback).to.equal(undefined); + }); + + it('dapTokenize success callback', function () { + let configAsync = JSON.parse(JSON.stringify(sampleConfig)); + let submoduleCallback = dapUtils.dapTokenize(configAsync, sampleIdentity, onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.requestHeaders['Content-Type'].should.equal('application/json'); + request.respond(200, responseHeader, JSON.stringify('success')); + expect(submoduleCallback).to.equal(undefined); + }); + }); + + describe('dapX2Tokenize', function () { + it('dapX2Tokenize error callback', function () { + let configAsync = JSON.parse(JSON.stringify(sampleX2Config)); + let submoduleCallback = dapUtils.dapTokenize(configAsync, sampleIdentity, onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.requestHeaders['Content-Type'].should.equal('application/json'); + request.respond(400, responseHeader, JSON.stringify('error')); + expect(submoduleCallback).to.equal(undefined); + }); + + it('dapX2Tokenize success callback', function () { + let configAsync = JSON.parse(JSON.stringify(sampleX2Config)); + let submoduleCallback = dapUtils.dapTokenize(configAsync, sampleIdentity, onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.requestHeaders['Content-Type'].should.equal('application/json'); + request.respond(200, responseHeader, JSON.stringify('success')); + expect(submoduleCallback).to.equal(undefined); + }); + }); + + describe('dapTokenize and dapMembership incorrect params', function () { + it('Onerror and config are null', function () { + expect(dapUtils.dapTokenize(null, 'identity', onDone, null, null)).to.be.equal(undefined); + expect(dapUtils.dapMembership(null, 'identity', onDone, null, null)).to.be.equal(undefined); + expect(dapUtils.dapEncryptedMembership(null, 'identity', onDone, null, null)).to.be.equal(undefined); + const config = { + 'api_hostname': 'prebid.dap.akadns.net', + 'api_version': 1, + 'domain': '', + 'segtax': 708 + }; + const encConfig = { + 'api_hostname': 'prebid.dap.akadns.net', + 'api_version': 1, + 'domain': '', + 'segtax': 710 + }; + let identity = { + type: 'dap-signature:1.0.0' + }; + expect(dapUtils.dapTokenize(config, identity, onDone, null, null)).to.be.equal(undefined); + expect(dapUtils.dapMembership(config, 'token', onDone, null, null)).to.be.equal(undefined); + expect(dapUtils.dapEncryptedMembership(encConfig, 'token', onDone, null, null)).to.be.equal(undefined); + }); + }); + + describe('Getting dapTokenize, dapMembership and dapEncryptedMembership from localstorage', function () { + it('dapGetTokenFromLocalStorage success', function () { + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + expect(dapUtils.dapGetTokenFromLocalStorage(60)).to.be.equal(sampleCachedToken.token); + }); + + it('dapGetMembershipFromLocalStorage success', function () { + storage.setDataInLocalStorage(DAP_MEMBERSHIP, JSON.stringify(cachedMembership)); + expect(JSON.stringify(dapUtils.dapGetMembershipFromLocalStorage())).to.be.equal(JSON.stringify(membership)); + }); + + it('dapGetEncryptedMembershipFromLocalStorage success', function () { + storage.setDataInLocalStorage(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(cachedEncryptedMembership)); + expect(JSON.stringify(dapUtils.dapGetEncryptedMembershipFromLocalStorage())).to.be.equal(JSON.stringify(encMembership)); + }); + }); + + describe('dapMembership', function () { + it('dapMembership success callback', function () { + let configAsync = JSON.parse(JSON.stringify(sampleConfig)); + let submoduleCallback = dapUtils.dapMembership(configAsync, 'token', onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify('success')); + expect(submoduleCallback).to.equal(undefined); + }); + + it('dapMembership error callback', function () { + let configAsync = JSON.parse(JSON.stringify(sampleConfig)); + let submoduleCallback = dapUtils.dapMembership(configAsync, 'token', onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.respond(400, responseHeader, JSON.stringify('error')); + expect(submoduleCallback).to.equal(undefined); + }); + }); + + describe('dapEncMembership', function () { + it('dapEncMembership success callback', function () { + let configAsync = JSON.parse(JSON.stringify(esampleConfig)); + let submoduleCallback = dapUtils.dapEncryptedMembership(configAsync, 'token', onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify('success')); + expect(submoduleCallback).to.equal(undefined); + }); + + it('dapEncMembership error callback', function () { + let configAsync = JSON.parse(JSON.stringify(esampleConfig)); + let submoduleCallback = dapUtils.dapEncryptedMembership(configAsync, 'token', onDone, + function(token, status, xhr, onDone) { + }, + function(xhr, status, error, onDone) { + } + ); + let request = server.requests[0]; + request.respond(400, responseHeader, JSON.stringify('error')); + expect(submoduleCallback).to.equal(undefined); + }); + }); + + describe('dapMembership', function () { + it('should invoke the getDapToken and getDapMembership', function () { + let membership = { + said: 'item.said1', + cohorts: 'item.cohorts', + attributes: null + }; + + let getDapMembershipStub = sinon.stub(dapUtils, 'dapGetMembershipFromLocalStorage').returns(membership); + let callDapApisStub = sinon.stub(dapUtils, 'callDapAPIs'); + try { + generateRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig); + expect(getDapMembershipStub.calledOnce).to.be.equal(true); + } finally { + getDapMembershipStub.restore(); + callDapApisStub.restore(); + } + }); + }); + + describe('dapEncMembership test', function () { + it('should invoke the getDapToken and getEncDapMembership', function () { + let encMembership = { + encryptedSegments: 'enc.seg', + }; + + let getDapEncMembershipStub = sinon.stub(dapUtils, 'dapGetEncryptedMembershipFromLocalStorage').returns(encMembership); + let callDapApisStub = sinon.stub(dapUtils, 'callDapAPIs'); + try { + generateRealTimeData(testReqBidsConfigObj, onDone, emoduleConfig); + expect(getDapEncMembershipStub.calledOnce).to.be.equal(true); + } finally { + getDapEncMembershipStub.restore(); + callDapApisStub.restore(); + } + }); + }); + + describe('dapGetRtdObj test', function () { + it('dapGetRtdObj', function () { + const config = { + apiHostname: 'prebid.dap.akadns.net', + apiVersion: 'x1', + domain: 'prebid.org', + segtax: 708 + }; + expect(dapUtils.dapRefreshMembership(ortb2, config, 'token', onDone)).to.equal(undefined) + const membership = {cohorts: ['1', '5', '7']} + expect(dapUtils.dapGetRtdObj(membership, config.segtax)).to.not.equal(undefined); + }); + }); + + describe('checkAndAddRealtimeData test', function () { + it('add realtime data for segtax 708 and 710', function () { + dapUtils.checkAndAddRealtimeData(ortb2, cachedEncRtd, 710); + dapUtils.checkAndAddRealtimeData(ortb2, cachedEncRtd, 710); + expect(ortb2.user.data).to.deep.include.members([encRtdUserObj]); + dapUtils.checkAndAddRealtimeData(ortb2, cachedRtd, 708); + expect(ortb2.user.data).to.deep.include.members([rtdUserObj]); + }); + }); + + describe('dapExtractExpiryFromToken test', function () { + it('test dapExtractExpiryFromToken function', function () { + let tokenWithoutExpiry = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..6buzBd2BjtgoyaNbHN8YnQ.l38avCfm3sYNy798-ETYOugz0cOx1cCkjACkAhYszxzrZ0sUJ0AiF-NdDXVTiTyp2Ih3vCWKzS0rKJ8lbS1zhyEVWVu91QwtwseM2fBbwA5ggAgBEo5wV-IXqDLPxVnxsPF0D3hP6cNCiH9Q2c-vULfsLhMhG5zvvZDPBbn4hUY5fKB8LoCBTF9rbuuWGYK1nramnb4AlS5UK82wBsHQea1Ou_Kp5wWCMNZ6TZk5qKIuRBfPIAhQblWvHECaHXkg1wyoM9VASs_yNhne7RR-qkwzbFiPFiMJibNOt9hF3_vPDJO5-06ZBjRTP1BllYGWxI-uQX6InzN18Wtun2WHqg.63sH0SNlIRcsK57v0pMujfB_nhU8Y5CuQbsHqH5MGoM' + expect(dapUtils.dapExtractExpiryFromToken(tokenWithoutExpiry)).to.equal(undefined); + let tokenWithExpiry = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIiwiZXhwIjoxNjQzODMwMzY5fQ..hTbcSQgmmO0HUJJrQ5fRHw.7zjrQXNNVkb-GD0ZhIVhEPcWbyaDBilHTWv-bp1lFZ9mdkSC0QbcAvUbYteiTD7ya23GUwcL2WOW8WgRSHaWHOJe0B5NDqfdUGTzElWfu7fFodRxRgGmwG8Rq5xxteFKLLGHLf1mFYRJKDtjtgajGNUKIDfn9AEt-c5Qz4KU8VolG_KzrLROx-f6Z7MnoPTcwRCj0WjXD6j2D6RAZ80-mKTNIsMIELdj6xiabHcjDJ1WzwtwCZSE2y2nMs451pSYp8W-bFPfZmDDwrkjN4s9ASLlIXcXgxK-H0GsiEbckQOZ49zsIKyFtasBvZW8339rrXi1js-aBh99M7aS5w9DmXPpUDmppSPpwkeTfKiqF0cQiAUq8tpeEQrGDJuw3Qt2.XI8h9Xw-VZj_NOmKtV19wLM63S4snos7rzkoHf9FXCw' + expect(dapUtils.dapExtractExpiryFromToken(tokenWithExpiry)).to.equal(1643830369); + }); + }); + + describe('dapRefreshToken test', function () { + it('test dapRefreshToken success response', function () { + dapUtils.dapRefreshToken(ortb2, sampleConfig, true, onDone) + let request = server.requests[0]; + request.requestHeaders['Content-Type'].should.equal('application/json'); + responseHeader['Symitri-DAP-Token'] = sampleCachedToken.token; + request.respond(200, responseHeader, JSON.stringify(sampleCachedToken.token)); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_TOKEN)).token).to.be.equal(sampleCachedToken.token); + }); + + it('test dapRefreshToken success response with deviceid 100', function () { + dapUtils.dapRefreshToken(ortb2, esampleConfig, true, onDone) + let request = server.requests[0]; + request.requestHeaders['Content-Type'].should.equal('application/json'); + responseHeader['Symitri-DAP-100'] = sampleCachedToken.token; + request.respond(200, responseHeader, ''); + expect(storage.getDataFromLocalStorage('dap_deviceId100')).to.be.equal(sampleCachedToken.token); + }); + + it('test dapRefreshToken success response with exp claim', function () { + dapUtils.dapRefreshToken(ortb2, sampleConfig, true, onDone) + let request = server.requests[0]; + let tokenWithExpiry = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIiwiZXhwIjoxNjQzODMwMzY5fQ..hTbcSQgmmO0HUJJrQ5fRHw.7zjrQXNNVkb-GD0ZhIVhEPcWbyaDBilHTWv-bp1lFZ9mdkSC0QbcAvUbYteiTD7ya23GUwcL2WOW8WgRSHaWHOJe0B5NDqfdUGTzElWfu7fFodRxRgGmwG8Rq5xxteFKLLGHLf1mFYRJKDtjtgajGNUKIDfn9AEt-c5Qz4KU8VolG_KzrLROx-f6Z7MnoPTcwRCj0WjXD6j2D6RAZ80-mKTNIsMIELdj6xiabHcjDJ1WzwtwCZSE2y2nMs451pSYp8W-bFPfZmDDwrkjN4s9ASLlIXcXgxK-H0GsiEbckQOZ49zsIKyFtasBvZW8339rrXi1js-aBh99M7aS5w9DmXPpUDmppSPpwkeTfKiqF0cQiAUq8tpeEQrGDJuw3Qt2.XI8h9Xw-VZj_NOmKtV19wLM63S4snos7rzkoHf9FXCw' + responseHeader['Symitri-DAP-Token'] = tokenWithExpiry; + request.requestHeaders['Content-Type'].should.equal('application/json'); + request.respond(200, responseHeader, JSON.stringify(tokenWithExpiry)); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_TOKEN)).expires_at).to.be.equal(1643830359); + }); + + it('test dapRefreshToken error response', function () { + storage.setDataInLocalStorage(DAP_TOKEN, JSON.stringify(sampleCachedToken)); + dapUtils.dapRefreshToken(ortb2, sampleConfig, false, onDone) + let request = server.requests[0]; + request.requestHeaders['Content-Type'].should.equal('application/json'); + request.respond(400, responseHeader, 'error'); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_TOKEN)).expires_at).to.be.equal(cacheExpiry);// Since the expiry is same, the token is not updated in the cache + }); + }); + + describe('dapRefreshEncryptedMembership test', function () { + it('test dapRefreshEncryptedMembership success response', function () { + let expiry = Math.round(Date.now() / 1000.0) + 3600; // in seconds + let encMembership = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoic29tZXNlY3JldGludmF1bHQifQ..f8_At4OqeQXyQcSwThOJ_w.69ImVQ3bEZ6QP7ROCRpAJjNcKY49SEPYR6qTp_8l7L8kQdPbpi4wmuOzt78j7iBrX64k2wltzmQFjDmVKSxDhrEguxpgx6t-L1tT8ZA0UosMWpVsgmKEZxOn2e9ES3jw8RNCS4WSWocSPQX33xSb51evXjm9E1s0tGoLnwXl0GsUvzRsSU86wQG6RZnAQTi7s-r-M2TKibdDjUqgIt62vJ-aBZ7RWw91MINgOdmDNs1bFfbBX5Cy1kd4-kjvRDz_aJ6zHX4sK_7EmQhGEY3tW-A3_l2I88mw-RSJaPkb_IWg0QpVwXDaE2F2g8NpY1PzCRvG_NIE8r28eK5q44OMVitykHmKmBXGDj7z2JVgoXkfo5u0I-dypZARn4GP_7niK932avB-9JD7Mz3TrlU4GZ7IpYfJ91PMsRhrs5xNPQwLZbpuhF76A7Dp7iss71UjkGCiPTU6udfRb4foyf_7xEF66m1eQVcVaMdxEbMuu9GBfdr-d04TbtJhPfUV8JfxTenvRYoi13n0j5kH0M5OgaSQD9kQ3Mrd9u-Cms-BGtT0vf-N8AaFZY_wn0Y4rkpv5HEaH7z3iT4RCHINWrXb_D0WtjLTKQi2YmF8zMlzUOewNJGwZRwbRwxc7JoDIKEc5RZkJYevfJXOEEOPGXZ7AGZxOEsJawPqFqd_nOUosCZS4akHhcDPcVowoecVAV0hhhoS6JEY66PhPp1snbt6yqA-fQhch7z8Y-DZT3Scibvffww3Scg_KFANWp0KeEvHG0vyv9R2F4o66viSS8y21MDnM7Yjk8C-j7aNMldUQbjN_7Yq1nkfe0jiBX_hsINBRPgJHUY4zCaXuyXs-JZZfU92nwG0RT3A_3RP2rpY8-fXp9d3C2QJjEpnmHvTMsuAZCQSBe5DVrJwN_UKedxcJEoOt0wLz6MaCMyYZPd8tnQeqYK1cd3RgQDXtzKC0HDw1En489DqJXEst4eSSkaaW1lImLeaF8XCOaIqPqoyGk4_6KVLw5Q7OnpczuXqYKMd9UTMovGeuTuo1k0ddfEqTq9QwxkwZL51AiDRnwTCAeYBU1krV8FCJQx-mH_WPB5ftZj-o_3pbvANeRk27QBVmjcS-tgDllJkWBxX-4axRXzLw8pUUUZUT_NOL0OiqUCWVm0qMBEpgRQ57Se42-hkLMTzLhhGJOnVcaXU1j4ep-N7faNvbgREBjf_LgzvaWS90a2NJ9bB_J9FyXelhCN_AMLfdOS3fHkeWlZ0u0PMbn5DxXRMe0l9jB-2VJZhcPQRlWoYyoCO3l4F5ZmuQP5Xh9CU4tvSWih6jlwMDgdVWuTpdfPD5bx8ccog3JDq87enx-QtPzLU3gMgouNARJGgNwKS_GJSE1uPrt2oiqgZ3Z0u_I5MKvPdQPV3o-4rsaE730eB4OwAOF-mkGWpzy8Pbl-Qe5PR9mHBhuyJgZ-WDSCHl5yvet2kfO9mPXZlqBQ26fzTcUYH94MULAZn36og6w.3iKGv-Le-AvRmi26W1v6ibRLGbwKbCR92vs-a9t55hw'; + dapUtils.dapRefreshEncryptedMembership(ortb2, esampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + responseHeader['Symitri-DAP-Token'] = encMembership; + request.respond(200, responseHeader, encMembership); + let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 710) + expect(ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(expiry); + }); + + it('test dapRefreshEncryptedMembership success response with exp claim', function () { + let encMembership = 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoic29tZXNlY3JldGludmF1bHQiLCJleHAiOjE2NDM4MzA2NDB9..inYoxwht_aqTIWqGhEm_Gw.wDcCUOCwtqgnNUouaD723gKfm7X7bgkHgtiX4mr07P3tWk25PUQunmwTLhWBB5CYzzGIfIvveG_u4glNRLi_eRSQV4ihKKk1AN-BSSJ3d0CLAdY9I1WG5vX1VmopXyKnV90bl9SLNqnhg4Vxe6YU4ogTYxsKHuIN1EeIH4hpl-HbCQWQ1DQt4mB-MQF8V9AWTfU0D7sFMSK8f9qj6NGmf1__oHdHUlws0t5V2UAn_dhJexsuREK_gh65pczCuly5eEcziZ82LeP-nOhKWSRHB_tS_mKXrRU6_At_EVDgtfA3PSBJ6eQylCii6bTL42vZzz4jZhJv_3eLfRdKqpVT5CWNBzcDoQ2VcQgKgIBtPJ45KFfAYTQ6kdl21QMSjqtu8GTsv1lEZtrqHY6zRiG8_Mu28-PmjEw4LDdZmBDOeroue_MJD6wuE_jlE7J2iVdo8CkVnoRgzFwNbKBo7CK4z0WahV9rhuOm0LKAN5H0jF_gj696U-3fVTDTIb8ndNKNI2_xAhvWs00BFGtUtWgr8QGDGRTDCNGsDgnb_Vva9xCqVOyAE9O3Fq1QYl-tMA-KkBt3zzvmFFpOxpOyH-lUubKLKlsrxKc3GSyVEQ9DDLhrXXJgR5H5BSE4tjlK7p3ODF5qz0FHtIj7oDcgLazFO7z2MuFy2LjJmd3hKl6ujcfYEDiQ4D3pMIo7oiU33aFBD1YpzI4-WzNfJlUt1FoK0-DAXpbbV95s8p08GOD4q81rPw5hRADKJEr0QzrbDwplTWCzT2fKXMg_dIIc5AGqGKnVRUS6UyF1DnHpudNIJWxyWZjWIEw_QNjU0cDFmyPSyKxNrnfq9w8WE2bfbS5KTicxei5QHnC-cnL7Nh7IXp7WOW6R1YHbNPT7Ad4OhnlV-jjrXwkSv4wMAbfwAWoSCchGh7uvENNAeJymuponlJbOgw_GcYM73hMs8Z8W9qxRfbyF4WX5fDKXg61mMlaieHkc0EnoC5q7uKyXuZUehHZ76JLDFmewslLkQq5SkVCttzJePBnY1ouPEHw5ZTzUnG5f01QQOVcjIN-AqXNDbG5IOwq0heyS6vVfq7lZKJdLDVQ21qRjazGPaqYwLzugkWkzCOzPTgyFdbXzgjfmJwylHSOM5Jpnul84GzxEQF-1mHP2A8wtIT-M7_iX24It2wwWvc8qLA6GEqruWCtNyoug8CXo44mKdSSCGeEZHtfMbzXdLIBHCy2jSHz5i8S7DU_R7rE_5Ssrb81CqIYbgsAQBHtOYoyvzduTOruWcci4De0QcULloqImIEHUuIe2lnYO889_LIx5p7nE3UlSvLBo0sPexavFUtHqI6jdG6ye9tdseUEoNBDXW0aWD4D-KXX1JLtAgToPVUtEaXCJI7QavwO9ZG6UZM6jbfuJ5co0fvUXp6qYrFxPQo2dYHkar0nT6s1Zg5l2g8yWlLUJrHdHAzAw_NScUp71OpM4TmNsLnYaPVPcOxMvtJXTanbNWr0VKc8gy9q3k_1XxAnQwiduNs7f5bA-6qCVpayHv5dE7mUhFEwyh1_w95jEaURsQF_hnnd2OqRkADfiok4ZiPU2b38kFW1LXjpI39XXES3JU0e08Rq2uuelyLbCLWuJWq_axuKSZbZvpYeqWtIAde8FjCiO7RPlEc0nyzWBst8RBxQ-Bekg9UXPhxBRcm0HwA.Q2cBSFOQAC-QKDwmjrQXnVQd3jNOppMl9oZfd2yuKeY'; + dapUtils.dapRefreshEncryptedMembership(ortb2, esampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + responseHeader['Symitri-DAP-Token'] = encMembership; + request.respond(200, responseHeader, encMembership); + let rtdObj = dapUtils.dapGetEncryptedRtdObj({'encryptedSegments': encMembership}, 710) + expect(ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_ENCRYPTED_MEMBERSHIP)).expires_at).to.equal(1643830630); + }); + + it('test dapRefreshEncryptedMembership error response', function () { + dapUtils.dapRefreshEncryptedMembership(ortb2, esampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(400, responseHeader, 'error'); + expect(ortb2).to.eql({}); + }); + + it('test dapRefreshEncryptedMembership 403 error response', function () { + dapUtils.dapRefreshEncryptedMembership(ortb2, esampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(403, responseHeader, 'error'); + let requestTokenize = server.requests[1]; + responseHeader['Symitri-DAP-Token'] = sampleCachedToken.token; + requestTokenize.respond(200, responseHeader, ''); + let requestMembership = server.requests[2]; + requestMembership.respond(403, responseHeader, 'error'); + expect(server.requests.length).to.be.equal(DAP_MAX_RETRY_TOKENIZE + 2); + }); + }); + + describe('dapRefreshMembership test', function () { + it('test dapRefreshMembership success response', function () { + let membership = {'cohorts': ['9', '11', '13'], 'said': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIn0..17wnrhz6FbWx0Cf6LXpm1A.m9PKVCradk3CZokNKzVHzE06TOqiXYeijgxTQUiQy5Syx-yicnO8DyYX6zQ6rgPcNgUNRt4R4XE5MXuK0laUVQJr9yc9g3vUfQfw69OMYGW_vRlLMPzoNOhF2c4gSyfkRrLr7C0qgALmZO1D11sPflaCTNmO7pmZtRaCOB5buHoWcQhp1bUSJ09DNDb31dX3llimPwjNGSrUhyq_EZl4HopnnjxbM4qVNMY2G_43C_idlVOvbFoTxcDRATd-6MplJoIOIHQLDZEetpIOVcbEYN9gQ_ndBISITwuu5YEgs5C_WPHA25nm6e4BT5R-tawSA8yPyQAupqE8gk4ZWq_2-T0cqyTstIHrMQnZ_vysYN7h6bkzE-KeZRk7GMtySN87_fiu904hLD9QentGegamX6UAbVqQh7Htj7SnMHXkEenjxXAM5mRqQvNCTlw8k-9-VPXs-vTcKLYP8VFf8gMOmuYykgWac1gX-svyAg-24mo8cUbqcsj9relx4Qj5HiXUVyDMBZxK-mHZi-Xz6uv9GlggcsjE13DSszar-j2OetigpdibnJIxRZ-4ew3-vlvZ0Dul3j0LjeWURVBWYWfMjuZ193G7lwR3ohh_NzlNfwOPBK_SYurdAnLh7jJgTW-lVLjH2Dipmi9JwX9s03IQq9opexAn7hlM9oBI6x5asByH8JF8WwZ5GhzDjpDwpSmHPQNGFRSyrx_Sh2CPWNK6C1NJmLkyqAtJ5iw0_al7vPDQyZrKXaLTjBCUnbpJhUZ8dUKtWLzGPjzFXp10muoDIutd1NfyKxk1aWGhx5aerYuLdywv6cT_M8RZTi8924NGj5VA30V5OvEwLLyX93eDhntXZSCbkPHpAfiRZNGXrPY.GhCbWGQz11mIRD4uPKmoAuFXDH7hGnils54zg7N7-TU'} + dapUtils.dapRefreshMembership(ortb2, sampleConfig, sampleCachedToken.token, onDone); + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(membership)); + let rtdObj = dapUtils.dapGetRtdObj(membership, 708); + expect(ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); + }); + + it('test dapRefreshMembership success response with exp claim', function () { + let membership = {'cohorts': ['9', '11', '13'], 'said': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoicGFzc3dvcmQxIiwiZXhwIjoxNjQ3OTcxNTU4fQ..ptdM5WO-62ypXlKxFXD4FQ.waEo9MHS2NYQCi-zh_p6HgT9BdqGyQbBq4GfGLfsay4nRBgICsTS-VkV6e7xx5U1T8BgpKkRJIZBwTOY5Pkxk9FpK5nnffDSEljRrp1LXLCkNP4qwrlqHInFbZsonNWW4_mW-7aUPlTwIsTbfjTuyHdXHeQa1ALrwFFFWE7QUmPNd2RsHjDwUsxlJPEb5TnHn5W0Mgo_PQZaxvhJInMbxPgtJLoqnJvOqCBEoQY7au7ALZL_nWK8XIwPMF19J7Z3cBg9vQInhr_E3rMdQcAFHEzYfgoNcIYCCR0t1UOqUE3HNtX-E64kZAYKWdlsBb9eW5Gj9hHYyPNL_4Hntjg5eLXGpsocMg0An-qQKGC6hkrxKzeM-GrjpvSaQLNs4iqDpHUtzA02LW_vkLkMNRUiyXVJ3FUZwfyq6uHSRKWZ6UFdAfL0rfJ8q8x8Ll-qJO2Jfyvidlsi9FIs7x1WJrvDCKepfAQM1UXRTonrQljFBAk83PcL2bmWuJDgJZ0lWS4VnZbIf6A7fDourmkDxdVRptvQq5nSjtzCA6whRw0-wGz8ehNJsaJw9H_nG9k4lRKs7A5Lqsyy7TVFrAPjnA_Q1a2H6xF2ULxrtIqoNqdX7k9RjowEZSQlZgZUOAmI4wzjckdcSyC_pUlYBMcBwmlld34mmOJe9EBHAxjdci7Q_9lvj1HTcwGDcQITXnkW9Ux5Jkt9Naw-IGGrnEIADaT2guUAto8W_Gb05TmwHSd6DCmh4zepQCbqeVe6AvPILtVkTgsTTo27Q-NvS7h-XtthJy8425j5kqwxxpZFJ0l0ytc6DUyNCLJXuxi0JFU6-LoSXcROEMVrHa_Achufr9vHIELwacSAIHuwseEvg_OOu1c1WYEwZH8ynBLSjqzy8AnDj24hYgA0YanPAvDqacrYrTUFqURbHmvcQqLBTcYa_gs7uDx4a1EjtP_NvHRlvCgGAaASrjGMhTX8oJxlTqahhQ.pXm-7KqnNK8sbyyczwkVYhcjgiwkpO8LjBBVw4lcyZE'}; + dapUtils.dapRefreshMembership(ortb2, sampleConfig, sampleCachedToken.token, onDone); + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(membership)); + let rtdObj = dapUtils.dapGetRtdObj(membership, 708) + expect(ortb2.user.data).to.deep.include.members(rtdObj.rtd.ortb2.user.data); + expect(JSON.parse(storage.getDataFromLocalStorage(DAP_MEMBERSHIP)).expires_at).to.be.equal(1647971548); + }); + + it('test dapRefreshMembership 400 error response', function () { + dapUtils.dapRefreshMembership(ortb2, sampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(400, responseHeader, 'error'); + expect(ortb2).to.eql({}); + }); + + it('test dapRefreshMembership 403 error response', function () { + dapUtils.dapRefreshMembership(ortb2, sampleConfig, sampleCachedToken.token, onDone) + let request = server.requests[0]; + request.respond(403, responseHeader, 'error'); + expect(server.requests.length).to.be.equal(DAP_MAX_RETRY_TOKENIZE); + }); + }); + + describe('dapGetEncryptedMembershipFromLocalStorage test', function () { + it('test dapGetEncryptedMembershipFromLocalStorage function with valid cache', function () { + storage.setDataInLocalStorage(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(cachedEncryptedMembership)) + expect(JSON.stringify(dapUtils.dapGetEncryptedMembershipFromLocalStorage())).to.equal(JSON.stringify(encMembership)); + }); + + it('test dapGetEncryptedMembershipFromLocalStorage function with invalid cache', function () { + let expiry = Math.round(Date.now() / 1000.0) - 100; // in seconds + let encMembership = {'expiry': expiry, 'encryptedSegments': cachedEncryptedMembership.encryptedSegments} + storage.setDataInLocalStorage(DAP_ENCRYPTED_MEMBERSHIP, JSON.stringify(encMembership)) + expect(dapUtils.dapGetEncryptedMembershipFromLocalStorage()).to.equal(null); + }); + }); + + describe('Symitri-DAP-SS-ID test', function () { + it('Symitri-DAP-SS-ID present in response header', function () { + let expiry = Math.round(Date.now() / 1000.0) + 300; // in seconds + dapUtils.dapRefreshToken(ortb2, sampleConfig, false, onDone) + let request = server.requests[0]; + request.requestHeaders['Content-Type'].should.equal('application/json'); + let sampleSSID = 'Test_SSID_Spec'; + responseHeader['Symitri-DAP-Token'] = sampleCachedToken.token; + responseHeader['Symitri-DAP-SS-ID'] = sampleSSID; + request.respond(200, responseHeader, ''); + expect(storage.getDataFromLocalStorage(DAP_SS_ID)).to.be.equal(JSON.stringify(sampleSSID)); + }); + + it('Test if Symitri-DAP-SS-ID is present in request header', function () { + let expiry = Math.round(Date.now() / 1000.0) + 100; // in seconds + storage.setDataInLocalStorage(DAP_SS_ID, JSON.stringify('Test_SSID_Spec')) + dapUtils.dapRefreshToken(ortb2, sampleConfig, false, onDone) + let request = server.requests[0]; + request.requestHeaders['Content-Type'].should.equal('application/json'); + let ssidHeader = request.requestHeaders['Symitri-DAP-SS-ID']; + responseHeader['Symitri-DAP-Token'] = sampleCachedToken.token; + request.respond(200, responseHeader, ''); + expect(ssidHeader).to.be.equal('Test_SSID_Spec'); + }); + }); + + describe('Test gdpr and usp consent handling', function () { + it('Gdpr applies and gdpr consent string not present', function () { + expect(symitriDapRtdSubmodule.init(null, sampleGdprConsentConfig)).to.equal(false); + }); + + it('Gdpr applies and gdpr consent string is present', function () { + sampleGdprConsentConfig.gdpr.consentString = 'BOJ/P2HOJ/P2HABABMAAAAAZ+A=='; + expect(symitriDapRtdSubmodule.init(null, sampleGdprConsentConfig)).to.equal(true); + }); + + it('USP consent present and user have opted out', function () { + expect(symitriDapRtdSubmodule.init(null, sampleUspConsentConfig)).to.equal(false); + }); + + it('USP consent present and user have not been provided with option to opt out', function () { + expect(symitriDapRtdSubmodule.init(null, {'usp': '1NYY'})).to.equal(false); + }); + + it('USP consent present and user have not opted out', function () { + expect(symitriDapRtdSubmodule.init(null, {'usp': '1YNY'})).to.equal(true); + }); + }); + + describe('Test identifier is added properly to apiParams', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('passed identifier is handled', async function () { + const test_identity = 'test_identity_1234'; + let identity = { + value: test_identity + }; + let apiParams = { + 'type': identity.type, + }; + + if (window.crypto && window.crypto.subtle) { + let hid = await dapUtils.addIdentifier(identity, apiParams).then(); + expect(hid['identity']).is.equal('843BE0FB20AAE699F27E5BC88C554B716F3DD366F58C1BDE0ACFB7EA0DD90CE7'); + } else { + expect(window.crypto.subtle).is.undefined + } + }); + + it('passed undefined identifier is handled', async function () { + const test_identity = undefined; + let identity = { + identity: test_identity + } + let apiParams = { + 'type': identity.type, + }; + + let hid = await dapUtils.addIdentifier(identity, apiParams); + expect(hid.identity).is.undefined; + }); + }); + + describe('onBidResponseEvent', function () { + const bidResponse = {adId: 'ad_123', bidder: 'test_bidder', bidderCode: 'test_bidder_code', cpm: '1.5', creativeId: 'creative_123', dealId: 'DEMODEAL555', mediaType: 'banner', responseTimestamp: '1725892736147', ad: ''}; + let url = emoduleConfig.params.pixelUrl + '?token=' + sampleCachedToken.token + '&ad_id=' + bidResponse.adId + '&bidder=' + bidResponse.bidder + '&bidder_code=' + bidResponse.bidderCode + '&cpm=' + bidResponse.cpm + '&creative_id=' + bidResponse.creativeId + '&deal_id=' + bidResponse.dealId + '&media_type=' + bidResponse.mediaType + '&response_timestamp=' + bidResponse.responseTimestamp; + let adPixel = `${bidResponse.ad}", + 'adomain': ['google.com'], + 'iurl': 'https://cdn.tapnative.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'tapnative', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + nativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': '{\"native\":{\"ver\":\"1.1\",\"assets\": [{\"id\":1,\"required\":1,\"title\":{\"text\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":2,\"required\":1,\"img\":{\"url\":\"https://cdn.tapnative.com/1_368852_0.png\",\"w\":500,\"h\":300,\"type\":\"3\"}},{\"id\":3,\"required\":0,\"data\":{\"value\":\"Diabetes In Control. A free weekly diabetes newsletter for Medical Professionals.\"}},{\"id\":4,\"required\":1,\"data\":{\"value\":\"Integrative Approaches: Merging Traditional and Alternative \"}},{\"id\":6,\"required\":1,\"data\":{\"value\":\"URL\"}}],\"link\":{\"url\":\"https://r.tapnative.com/adx-rtb-d/servlet/WebF_AdManager.AdLinkManager?qs=H4sIAAAAAAAAAx2U2QHDIAxDVwKDr3F84P1HqNLPtMSRpSeG9RiPXH+5a474KzO/47YX7UoP50m61fLujlNjb76/8ZiblkimHq5nL/ZRedp3031x1tnk55LjSNN6h9/Zq+qmaLLuWTl74m1ZJKnb+m2OtQm/3L4sb933pM92qMOgjJ41MYmPXKnndRVKs+9bfSEumoZIFpTXuXbCP+WXuzl725E3O+9odi5OJrnBzhwjx9+UnFN3nTNt1/HY5aeljKtvZYpoJHNXr8BWa8ysKQY7ZmNA3DHK2qRwY7+zLu+xm9z5eheJ4Pv2usSptvO3p7JHrnXn0T5yVWdccp9Yz7hhoz2iu2zqsXsGFZ9hh14J6yU4TkJ0BgnOY8tY3tS+n2qsw7xZfKuanSNbAo+9nkJ83i20+FwhfbJeDVOllXsdxmDWauYcSRgS9+yG5qHwUDjAxxA0iZnOjlsnI+y09+ATeTEwbAVGgp0Qu/ceP0kjUvpu1Ty7O9MoegfrmLPxdjUh3mJL+XhARby+Ax8iBckf6BQdn9W+DMlvmlzYLuLlIy7YociFOIvXvEiYYCMboVk8BLHbnw3Zmr5us3xbjtXL67L96F15acJXkM5BOmTaUbBkYGdCI+Et8XmlpbuE3xVQwmxryc2y4wP3ByuuP8GogPZz8OpPaBv8diWWUTrC2nnLhdNUrJRTKc9FepDvwHTDwfbbMCTSb4LhUIFkyFrw/i7GtkPi6NCCai6N47TgNsTnzZWRoVtOSLq7FsLiF29y0Gj0GHVPVYG3QOPS7Swc3UuiFAQZJx3YvpHA2geUgVBASMEL4vcDi2Dw3NPtBSC4EQEvH/uMILu6WyUwraywTeVpoqoHTqOoD84FzReKoWemJy6jyuiBieGlQIe6wY2elTaMOwEUFF5NagzPj6nauc0+aXzQN3Q72hxFAgtfORK60RRAHYZLYymIzSJcXLgRFsqrb1UoD+5Atq7TWojaLTfOyUvH9EeJvZEOilQAXrf/ALoI8ZhABQAA\"},\"imptrackers\":[\"https://i.tapnative.com/adx-rtb-d/servlet/WebF_AdManager.ImpCounter?price=${AUCTION_PRICE}&ids=111519,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=https%3A%2F%2Fqa-jboss.audiencelogy.com%2Ftn_native_prod.html\",\"https://i.tapnative.com/adx-rtb-d/servlet/WebF_AdManager.ImpTracker?price=${AUCTION_PRICE}&ids=111519,16703,468132,368852,211356,233,13,16704,1&cb=1728409547&ap=5.00000&vd=223.233.85.189,14,8&nm=0.00&GUIDs=[adx_guid],652c9a4c-66ea-4579-998b-cefe7b4cfecd,652c9a4c-66ea-4579-998b-cefe7b4cfecd,999999,-1_&info=2,-1,IN&adx_custom=&adx_custom_ex=~~~-1~~~0&cat=-1&ref=\",\"https://rtb-east.tapnative.com:9001/beacon?uid=44636f6605b06ec6d4389d6efb7e5054&cc=468132&fccap=5&nid=1\"]}}', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.tapnative.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'tapnative', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidBannerResponse = { + 'body': { + 'id': '006ac3b3-67f0-43bf-a33a-388b2f869fef', + 'seatbid': [{ + 'bid': [{ + 'id': '049d07ed-c07e-4890-9f19-5cf41406a42d', + 'impid': '286e606ac84a09', + 'price': 0.11, + 'adid': '368853', + 'adm': 'invalid response', + 'adomain': ['google.com'], + 'iurl': 'https://cdn.tapnative.com/1_368853_1.png', + 'cid': '468133/368853', + 'crid': '368853', + 'w': 300, + 'h': 250, + 'cat': ['IAB7-19'] + }], + 'seat': 'tapnative', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + invalidNativeResponse = { + 'body': { + 'id': '453ade66-9113-4944-a674-5bbdcb9808ac', + 'seatbid': [{ + 'bid': [{ + 'id': '652c9a4c-66ea-4579-998b-cefe7b4cfecd', + 'impid': '2c3875bdbb1893', + 'price': 1.1, + 'adid': '368852', + 'adm': 'invalid response', + 'adomain': ['www.diabetesincontrol.com'], + 'iurl': 'https://cdn.tapnative.com/1_368852_0.png', + 'cid': '468132/368852', + 'crid': '368852', + 'cat': ['IAB7'] + }], + 'seat': 'tapnative', + 'group': 0 + }], + 'cur': 'USD', + 'bidid': 'BIDDER_-1' + } + }; + }); + + describe('validations', function () { + it('isBidValid : placement_id is passed', function () { + let bid = { + bidder: 'tapnative', + params: { + placement_id: 111520 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(true); + }); + it('isBidValid : placement_id is not passed', function () { + let bid = { + bidder: 'tapnative', + params: { + width: 300, + height: 250, + domain: '', + bid_floor: 0.5 + } + }, + isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equals(false); + }); + }); + describe('Validate Banner Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(bannerRequest), + bidRequest = spec.buildRequests(bannerRequest); + expect(bannerRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(bannerRequest); + expect(_Request.url).to.equal('https://rtb-east.tapnative.com/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(bannerRequest[0].bidId); + expect(data[0].placementId).to.equal(111520); + }); + it('Validate bid request : ad size', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].imp[0].banner).to.be.a('object'); + expect(data[0].imp[0].banner.w).to.equal(300); + expect(data[0].imp[0].banner.h).to.equal(250); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(bannerRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(bannerRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate banner response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(bannerRequest); + let bResponse = spec.interpretResponse(bannerResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(bannerResponse.body.seatbid[0].bid[0].impid); + expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('banner'); + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['google.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId); + }); + it('Invalid bid response check ', function () { + let bRequest = spec.buildRequests(bannerRequest); + let response = spec.interpretResponse(invalidBannerResponse, bRequest); + expect(response[0].ad).to.equal('invalid response'); + }); + }); + describe('Validate Native Request', function () { + it('Immutable bid request validate', function () { + let _Request = utils.deepClone(nativeRequest), + bidRequest = spec.buildRequests(nativeRequest); + expect(nativeRequest).to.deep.equal(_Request); + }); + it('Validate bidder connection', function () { + let _Request = spec.buildRequests(nativeRequest); + expect(_Request.url).to.equal('https://rtb-east.tapnative.com/hb'); + expect(_Request.method).to.equal('POST'); + expect(_Request.options.contentType).to.equal('application/json'); + }); + it('Validate bid request : Impression', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + // expect(data.at).to.equal(1); // auction type + expect(data[0].imp[0].id).to.equal(nativeRequest[0].bidId); + expect(data[0].placementId).to.equal(111519); + }); + it('Validate bid request : user object', function () { + let _Request = spec.buildRequests(nativeRequest); + let data = JSON.parse(_Request.data); + expect(data[0].user).to.be.a('object'); + expect(data[0].user.id).to.be.a('string'); + }); + it('Validate bid request : CCPA Check', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let _Request = spec.buildRequests(nativeRequest, bidRequest); + let data = JSON.parse(_Request.data); + expect(data[0].regs.ext.us_privacy).to.equal('1NYN'); + // let _bidRequest = {}; + // let _Request1 = spec.buildRequests(request, _bidRequest); + // let data1 = JSON.parse(_Request1.data); + // expect(data1.regs).to.equal(undefined); + }); + }); + describe('Validate native response ', function () { + it('Validate bid response : valid bid response', function () { + let _Request = spec.buildRequests(nativeRequest); + let bResponse = spec.interpretResponse(nativeResponse, _Request); + expect(bResponse).to.be.an('array').with.length.above(0); + expect(bResponse[0].requestId).to.equal(nativeResponse.body.seatbid[0].bid[0].impid); + // expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w); + // expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); + expect(bResponse[0].currency).to.equal('USD'); + expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].mediaType).to.equal('native'); + expect(bResponse[0].native.clickUrl).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.impressionTrackers).to.be.an('array').with.length.above(0); + expect(bResponse[0].native.title).to.be.a('string').and.not.be.empty; + expect(bResponse[0].native.image.url).to.be.a('string').and.not.be.empty; + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['www.diabetesincontrol.com']); + expect(bResponse[0].ttl).to.equal(300); + expect(bResponse[0].creativeId).to.equal(nativeResponse.body.seatbid[0].bid[0].crid); + expect(bResponse[0].dealId).to.equal(nativeResponse.body.seatbid[0].bid[0].dealId); + }); + }); + describe('GPP and coppa', function () { + it('Request params check with GPP Consent', function () { + let bidderReq = { gppConsent: { gppString: 'gpp-string-test', applicableSections: [5] } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-string-test'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it('Request params check with GPP Consent read from ortb2', function () { + let bidderReq = { + ortb2: { + regs: { + gpp: 'gpp-test-string', + gpp_sid: [5] + } + } + }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.gpp).to.equal('gpp-test-string'); + expect(data[0].regs.gpp_sid[0]).to.equal(5); + }); + it(' Bid request should have coppa flag if its true', () => { + let bidderReq = { ortb2: { regs: { coppa: 1 } } }; + let _Request = spec.buildRequests(bannerRequest, bidderReq); + let data = JSON.parse(_Request.data); + expect(data[0].regs.coppa).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index 46fac8de1e2..1dd3f1b3c50 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -124,8 +124,8 @@ const c_CONSENTSTRING = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; const c_VALIDBIDREQUESTS = [{'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com\/rtb\/v2\/', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005, 'test': 1}, 'userId': {'haloId': '0000x179MZAzMqUWsFonu7Drm3eDDBMYtj5SPoWQnl89Upk3WTlCvEnKI9SshX0p6eFJ7otPYix179MZAzMqUWsFonu7Drm3eDDBMYtj5SPoWQnl89Upk3WTlCvEnKI9SshX0p6e', 'id5id': {'uid': 'ID5@iu-PJX_OQ0d6FJjKS8kYfUpHriD_rEXbz6UYtYEJelYrDaZOLkh8WcF9J0ZHmEHFKZEBlLXsgP6xqXU3BCj4Ay0Z6fw_jSOaHxMHwd-voRHqFA4Q9NwAxFcVLyPWnNGZ9VbcSAPos1wupq7Xu3MIm-Bw_0vxjhZdWNy4chM9x3i', 'ext': {'linkType': 0}}, 'intentIqId': 'GIF89a\u0000\u0000\u0000\u0000�\u0000\u0000���\u0000\u0000\u0000?�\u0000\u0000\u0000\u0000\u0000\u0000,\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000A\u0000\u0000;', 'lotamePanoramaId': 'xTtLUY7GwqX2MMqSHo9RQ2YUOIBFhlASOR43I9KjvgtcrxIys3RxME96M02LTjWR', 'parrableId': {'eid': '02.YoqC9lWZh8.C8QTSiJTNgI6Pp0KCM5zZgEgwVMSsVP5W51X8cmiUHQESq9WRKB4nreqZJwsWIcNKlORhG4u25Wm6lmDOBmQ0B8hv0KP6uVQ97aouuH52zaz2ctVQTORUKkErPRPcaCJ7dKFcrNoF2i6WOR0S5Nk'}, 'pubcid': 'b1254-152f-12F5-5698-dI1eljK6C7WA', 'pubProvidedId': [{'source': 'domain.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 1, 'ext': {'stype': 'ppuid'}}]}, {'source': '3rdpartyprovided.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 3, 'ext': {'stype': 'sha256email'}}]}]}, 'userIdAsEids': [{'source': 'audigent.com', 'uids': [{'id': '0000fgclxw05ycn0608xiyi90bwpa0c0evvlif0hv1x0i0ku88il0ntek0o0qskvir0trr70u0wqxiix0zq3u1012pa5j315ogh1618nmsj91bmt41c1elzfjf1hl5r1i1kkc2jl', 'atype': 1}]}, {'source': 'id5-sync.com', 'uids': [{'id': 'ID5@iu-PJX_OQ0d6FJjKS8kYfUpHriD_qpoXJUngedfpNva812If1fHEqHHkamLC89txVxk1i9WGqeQrTX97HFCgv9QDa1M_bkHUBsAWFm-D5r1rYrsfMFFiyqwCAEzqNbvsUZXOYCAQSjPcLxR4of22w-U9_JDRThCGRDV3Fmvc38E', 'atype': 1, 'ext': {'linkType': 0}}]}], 'ortb2Imp': {'ext': {'data': {'adserver': {'name': 'gam', 'adslot': '/19968336/header-bid-tag-0'}, 'pbadslot': '/19968336/header-bid-tag-0'}}}, 'mediaTypes': {'banner': {'sizes': [[320, 480], [320, 50]]}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '71c0d86b-4b47-4aff-a6da-1af0b1712439', 'sizes': [[320, 480], [320, 50]], 'bidId': '264d7969b125a5', 'bidderRequestId': '1c674c14a3889c', 'auctionId': '13a8a3a9-ed3a-4101-9435-4699ee77bb62', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}]; const c_VALIDBIDAPPREQUESTS = [{'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com\/rtb\/v2\/', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005, 'test': 1, 'app': {'name': 'Tappx Test', 'bundle': 'com.test.tappx', 'domain': 'tappx.com', 'publisher': { 'name': 'Tappx', 'domain': 'tappx.com' }}}, 'userId': {'haloId': '0000fgclxw05ycn0608xiyi90bwpa0c0evvlif0hv1x0i0ku88il0ntek0o0qskvir0trr70u0wqxiix0zq3u1012pa5j315ogh1618nmsj91bmt41c1elzfjf1hl5r1i1kkc2jl', 'id5id': {'uid': 'ID5@iu-PJX_OQ0d6FJjKS8kYfUpHriD_qpoXJUngedfpNva812If1fHEqHHkamLC89txVxk1i9WGqeQrTX97HFCgv9QDa1M_bkHUBsAWFm-D5r1rYrsfMFFiyqwCAEzqNbvsUZXOYCAQSjPcLxR4of22w-U9_JDRThCGRDV3Fmvc38E', 'ext': {'linkType': 0}}, 'intentIqId': 'GIF89a\u0001\u0000\u0001\u0000�\u0000\u0000���\u0000\u0000\u0000!�\u0004\u0001\u0000\u0000\u0000\u0000,\u0000\u0000\u0000\u0000\u0001\u0000\u0001\u0000\u0000\u0002\u0002D\u0001\u0000;', 'lotamePanoramaId': '8003916b61a95b185690ec103bdf4945a70213e01818a5e5d8690b542730755a', 'parrableId': {'eid': '01.1617088921.7faa68d9570a50ea8e4f359e9b99ca4b7509e948a6175b3e5b0b8cbaf5b62424104ccfb0191ca79366de8368ed267b89a68e236df5f41f96f238e4301659e9023fec05e46399fb1ad0a0'}, 'pubcid': 'b7143795-852f-42f0-8864-5ecbea1ade4e', 'pubProvidedId': [{'source': 'domain.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 1, 'ext': {'stype': 'ppuid'}}]}, {'source': '3rdpartyprovided.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 3, 'ext': {'stype': 'sha256email'}}]}]}, 'userIdAsEids': [{'source': 'audigent.com', 'uids': [{'id': '0000fgclxw05ycn0608xiyi90bwpa0c0evvlif0hv1x0i0ku88il0ntek0o0qskvir0trr70u0wqxiix0zq3u1012pa5j315ogh1618nmsj91bmt41c1elzfjf1hl5r1i1kkc2jl', 'atype': 1}]}, {'source': 'id5-sync.com', 'uids': [{'id': 'ID5@iu-PJX_OQ0d6FJjKS8kYfUpHriD_qpoXJUngedfpNva812If1fHEqHHkamLC89txVxk1i9WGqeQrTX97HFCgv9QDa1M_bkHUBsAWFm-D5r1rYrsfMFFiyqwCAEzqNbvsUZXOYCAQSjPcLxR4of22w-U9_JDRThCGRDV3Fmvc38E', 'atype': 1, 'ext': {'linkType': 0}}]}, {'source': 'intentiq.com', 'uids': [{'id': 'GIF89a\u0001\u0000\u0001\u0000�\u0000\u0000���\u0000\u0000\u0000!�\u0004\u0001\u0000\u0000\u0000\u0000,\u0000\u0000\u0000\u0000\u0001\u0000\u0001\u0000\u0000\u0002\u0002D\u0001\u0000;', 'atype': 1}]}, {'source': 'crwdcntrl.net', 'uids': [{'id': '8003916b61a95b185690ec103bdf4945a70213e01818a5e5d8690b542730755a', 'atype': 1}]}, {'source': 'parrable.com', 'uids': [{'id': '01.1617088921.7faa68d9570a50ea8e4f359e9b99ca4b7509e948a6175b3e5b0b8cbaf5b62424104ccfb0191ca79366de8368ed267b89a68e236df5f41f96f238e4301659e9023fec05e46399fb1ad0a0', 'atype': 1}]}, {'source': 'pubcid.org', 'uids': [{'id': 'b7143795-852f-42f0-8864-5ecbea1ade4e', 'atype': 1}]}, {'source': 'domain.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 1, 'ext': {'stype': 'ppuid'}}]}, {'source': '3rdpartyprovided.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 3, 'ext': {'stype': 'sha256email'}}]}], 'ortb2Imp': {'ext': {'data': {'adserver': {'name': 'gam', 'adslot': '/19968336/header-bid-tag-0'}, 'pbadslot': '/19968336/header-bid-tag-0'}}}, 'mediaTypes': {'banner': {'sizes': [[320, 480], [320, 50]]}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '71c0d86b-4b47-4aff-a6da-1af0b1712439', 'sizes': [[320, 480], [320, 50]], 'bidId': '264d7969b125a5', 'bidderRequestId': '1c674c14a3889c', 'auctionId': '13a8a3a9-ed3a-4101-9435-4699ee77bb62', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}]; const c_BIDDERREQUEST_B = {'bidderCode': 'tappx', 'auctionId': '13a8a3a9-ed3a-4101-9435-4699ee77bb62', 'bidderRequestId': '1c674c14a3889c', 'bids': [{'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com\/rtb\/v2\/', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005, 'test': 1}, 'userId': {'haloId': '0000fgclxw05ycn0608xiyi90bwpa0c0evvlif0hv1x0i0ku88il0ntek0o0qskvir0trr70u0wqxiix0zq3u1012pa5j315ogh1618nmsj91bmt41c1elzfjf1hl5r1i1kkc2jl', 'id5id': {'uid': 'ID5@iu-PJX_OQ0d6FJjKS8kYfUpHriD_qpoXJUngedfpNva812If1fHEqHHkamLC89txVxk1i9WGqeQrTX97HFCgv9QDa1M_bkHUBsAWFm-D5r1rYrsfMFFiyqwCAEzqNbvsUZXOYCAQSjPcLxR4of22w-U9_JDRThCGRDV3Fmvc38E', 'ext': {'linkType': 0}}, 'intentIqId': 'GIF89a\u0000\u0000\u0000\u0000�\u0000\u0000���\u0000\u0000\u0000?�\u0000\u0000\u0000\u0000\u0000\u0000,\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000A\u0000\u0000;', 'lotamePanoramaId': '8003916b61a95b185690ec103bdf4945a70213e01818a5e5d8690b542730755a', 'parrableId': {'eid': '01.1617088921.7faa68d9570a50ea8e4f359e9b99ca4b7509e948a6175b3e5b0b8cbaf5b62424104ccfb0191ca79366de8368ed267b89a68e236df5f41f96f238e4301659e9023fec05e46399fb1ad0a0'}, 'pubcid': 'b7143795-852f-42f0-8864-5ecbea1ade4e', 'pubProvidedId': [{'source': 'domain.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 1, 'ext': {'stype': 'ppuid'}}]}, {'source': '3rdpartyprovided.com', 'uids': [{'id': 'value read from cookie or local storage', 'atype': 3, 'ext': {'stype': 'sha256email'}}]}]}, 'userIdAsEids': [{'source': 'audigent.com', 'uids': [{'id': '0000fgclxw05ycn0608xiyi90bwpa0c0evvlif0hv1x0i0ku88il0ntek0o0qskvir0trr70u0wqxiix0zq3u1012pa5j315ogh1618nmsj91bmt41c1elzfjf1hl5r1i1kkc2jl', 'atype': 1}]}, {'source': 'id5-sync.com', 'uids': [{'id': 'ID5@iu-PJX_OQ0d6FJjKS8kYfUpHriD_qpoXJUngedfpNva812If1fHEqHHkamLC89txVxk1i9WGqeQrTX97HFCgv9QDa1M_bkHUBsAWFm-D5r1rYrsfMFFiyqwCAEzqNbvsUZXOYCAQSjPcLxR4of22w-U9_JDRThCGRDV3Fmvc38E', 'atype': 1, 'ext': {'linkType': 0}}]}], 'ortb2Imp': {'ext': {'data': {'adserver': {'name': 'gam', 'adslot': '/19968336/header-bid-tag-0'}, 'pbadslot': '/19968336/header-bid-tag-0'}}}, 'mediaTypes': {'banner': {'sizes': [[320, 480], [320, 50]]}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '71c0d86b-4b47-4aff-a6da-1af0b1712439', 'sizes': [[320, 480], [320, 50]], 'bidId': '264d7969b125a5', 'bidderRequestId': '1c674c14a3889c', 'auctionId': '13a8a3a9-ed3a-4101-9435-4699ee77bb62', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}], 'auctionStart': 1617088922120, 'timeout': 700, 'refererInfo': {'page': 'http://localhost:9999/integrationExamples/gpt/gdpr_hello_world.html?pbjs_debug=true', 'reachedTop': true, 'isAmp': false, 'numIframes': 0, 'stack': ['http://localhost:9999/integrationExamples/gpt/gdpr_hello_world.html?pbjs_debug=true'], 'canonicalUrl': null}, 'gdprConsent': {'consentString': c_CONSENTSTRING, 'vendorData': {'metadata': 'BO-JeiTPABAOkAAABAENABA', 'gdprApplies': true, 'hasGlobalScope': false, 'cookieVersion': 1, 'created': '2020-12-09T09:22:09.900Z', 'lastUpdated': '2021-01-14T15:44:03.600Z', 'cmpId': 0, 'cmpVersion': 1, 'consentScreen': 0, 'consentLanguage': 'EN', 'vendorListVersion': 1, 'maxVendorId': 0, 'purposeConsents': {}, 'vendorConsents': {}}, 'gdprApplies': true, 'apiVersion': 1}, 'uspConsent': '1YCC', 'start': 1611308859099}; -const c_BIDDERREQUEST_V = {'method': 'POST', 'url': 'https://testing.ssp.tappx.com/rtb/v2//VZ12TESTCTV?type_cnn=prebidjs&v=0.1.10329', 'data': '{"site":{"name":"localhost","bundle":"localhost","domain":"localhost"},"user":{"ext":{}},"id":"0fecfa84-c541-49f8-8c45-76b90fddc30e","test":1,"at":1,"tmax":1000,"bidder":"tappx","imp":[{"video":{"mimes":["video/mp4","application/javascript"],"minduration":3,"maxduration":30,"startdelay":5,"playbackmethod":[1,3],"api":[1,2],"protocols":[2,3],"battr":[13,14],"linearity":1,"placement":2,"minbitrate":10,"maxbitrate":10,"w":320,"h":250},"id":"2398241a5a860b","tagid":"localhost_typeAdBanVid_windows","secure":1,"bidfloor":0.005,"ext":{"bidder":{"tappxkey":"pub-1234-desktop-1234","endpoint":"vz34906po","host":"https://vz34906po.pub.tappx.com/rtb/","bidfloor":0.005}}}],"device":{"os":"windows","ip":"peer","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36","h":864,"w":1536,"dnt":0,"language":"en","make":"Google Inc."},"params":{"host":"tappx.com","bidfloor":0.005},"regs":{"gdpr":0,"ext":{}}}', 'bids': {'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com/rtb/v2/', 'tappxkey': 'pub-1234-desktop-1234', 'endpoint': 'VZ12TESTCTV', 'bidfloor': 0.005, 'test': true}, 'crumbs': {'pubcid': 'dccfe922-3823-4676-b7b2-e5ed8743154e'}, 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video-ad-div'}}}, 'renderer': {'options': {'text': 'Tappx Outstream Video'}}, 'mediaTypes': {'video': {'mimes': ['video/mp4', 'application/javascript'], 'minduration': 3, 'maxduration': 30, 'startdelay': 5, 'playbackmethod': [1, 3], 'api': [1, 2], 'protocols': [2, 3], 'battr': [13, 14], 'linearity': 1, 'placement': 2, 'minbitrate': 10, 'maxbitrate': 10, 'w': 320, 'h': 250}}, 'adUnitCode': 'video-ad-div', 'transactionId': 'ed41c805-d14c-49c3-954d-26b98b2aa2c2', 'sizes': [[320, 250]], 'bidId': '28f49c71b13f2f', 'bidderRequestId': '1401710496dc7', 'auctionId': 'e807363f-3095-43a8-a4a6-f44196cb7318', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}} -const c_BIDDERREQUEST_VOutstream = {'method': 'POST', 'url': 'https://testing.ssp.tappx.com/rtb/v2//VZ12TESTCTV?type_cnn=prebidjs&v=0.1.10329', 'data': '{"site":{"name":"localhost","bundle":"localhost","domain":"localhost"},"user":{"ext":{}},"id":"0fecfa84-c541-49f8-8c45-76b90fddc30e","test":1,"at":1,"tmax":1000,"bidder":"tappx","imp":[{"video":{"context": "outstream","playerSize":[640, 480],"mimes":["video/mp4","application/javascript"],"minduration":3,"maxduration":30,"startdelay":5,"playbackmethod":[1,3],"api":[1,2],"protocols":[2,3],"battr":[13,14],"linearity":1,"placement":2,"minbitrate":10,"maxbitrate":10,"w":320,"h":250},"id":"2398241a5a860b","tagid":"localhost_typeAdBanVid_windows","secure":1,"bidfloor":0.005,"ext":{"bidder":{"tappxkey":"pub-1234-desktop-1234","endpoint":"vz34906po","host":"https://vz34906po.pub.tappx.com/rtb/","bidfloor":0.005}}}],"device":{"os":"windows","ip":"peer","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36","h":864,"w":1536,"dnt":0,"language":"en","make":"Google Inc."},"params":{"host":"tappx.com","bidfloor":0.005},"regs":{"gdpr":0,"ext":{}}}', 'bids': {'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com/rtb/v2/', 'tappxkey': 'pub-1234-desktop-1234', 'endpoint': 'VZ12TESTCTV', 'bidfloor': 0.005, 'test': true}, 'crumbs': {'pubcid': 'dccfe922-3823-4676-b7b2-e5ed8743154e'}, 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video-ad-div'}}}, 'renderer': {'options': {'text': 'Tappx Outstream Video'}}, 'mediaTypes': {'video': {'mimes': ['video/mp4', 'application/javascript'], 'minduration': 3, 'maxduration': 30, 'startdelay': 5, 'playbackmethod': [1, 3], 'api': [1, 2], 'protocols': [2, 3], 'battr': [13, 14], 'linearity': 1, 'placement': 2, 'minbitrate': 10, 'maxbitrate': 10, 'w': 320, 'h': 250}}, 'adUnitCode': 'video-ad-div', 'transactionId': 'ed41c805-d14c-49c3-954d-26b98b2aa2c2', 'sizes': [[320, 250]], 'bidId': '28f49c71b13f2f', 'bidderRequestId': '1401710496dc7', 'auctionId': 'e807363f-3095-43a8-a4a6-f44196cb7318', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}} +const c_BIDDERREQUEST_V = {'method': 'POST', 'url': 'https://testing.ssp.tappx.com/rtb/v2//VZ12TESTCTV?type_cnn=prebidjs&v=0.1.10329', 'data': '{"site":{"name":"localhost","bundle":"localhost","domain":"localhost"},"user":{"ext":{}},"id":"0fecfa84-c541-49f8-8c45-76b90fddc30e","test":1,"at":1,"tmax":1000,"bidder":"tappx","imp":[{"video":{"mimes":["video/mp4","application/javascript"],"minduration":3,"maxduration":30,"startdelay":5,"playbackmethod":[1,3],"api":[1,2],"protocols":[2,3],"battr":[13,14],"linearity":1,"plcmt":2,"minbitrate":10,"maxbitrate":10,"w":320,"h":250},"id":"2398241a5a860b","tagid":"localhost_typeAdBanVid_windows","secure":1,"bidfloor":0.005,"ext":{"bidder":{"tappxkey":"pub-1234-desktop-1234","endpoint":"vz34906po","host":"https://vz34906po.pub.tappx.com/rtb/","bidfloor":0.005}}}],"device":{"os":"windows","ip":"peer","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36","h":864,"w":1536,"dnt":0,"language":"en","make":"Google Inc."},"params":{"host":"tappx.com","bidfloor":0.005},"regs":{"gdpr":0,"ext":{}}}', 'bids': {'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com/rtb/v2/', 'tappxkey': 'pub-1234-desktop-1234', 'endpoint': 'VZ12TESTCTV', 'bidfloor': 0.005, 'test': true}, 'crumbs': {'pubcid': 'dccfe922-3823-4676-b7b2-e5ed8743154e'}, 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video-ad-div'}}}, 'renderer': {'options': {'text': 'Tappx Outstream Video'}}, 'mediaTypes': {'video': {'mimes': ['video/mp4', 'application/javascript'], 'minduration': 3, 'maxduration': 30, 'startdelay': 5, 'playbackmethod': [1, 3], 'api': [1, 2], 'protocols': [2, 3], 'battr': [13, 14], 'linearity': 1, 'plcmt': 2, 'minbitrate': 10, 'maxbitrate': 10, 'w': 320, 'h': 250}}, 'adUnitCode': 'video-ad-div', 'transactionId': 'ed41c805-d14c-49c3-954d-26b98b2aa2c2', 'sizes': [[320, 250]], 'bidId': '28f49c71b13f2f', 'bidderRequestId': '1401710496dc7', 'auctionId': 'e807363f-3095-43a8-a4a6-f44196cb7318', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}} +const c_BIDDERREQUEST_VOutstream = {'method': 'POST', 'url': 'https://testing.ssp.tappx.com/rtb/v2//VZ12TESTCTV?type_cnn=prebidjs&v=0.1.10329', 'data': '{"site":{"name":"localhost","bundle":"localhost","domain":"localhost"},"user":{"ext":{}},"id":"0fecfa84-c541-49f8-8c45-76b90fddc30e","test":1,"at":1,"tmax":1000,"bidder":"tappx","imp":[{"video":{"context": "outstream","playerSize":[640, 480],"mimes":["video/mp4","application/javascript"],"minduration":3,"maxduration":30,"startdelay":5,"playbackmethod":[1,3],"api":[1,2],"protocols":[2,3],"battr":[13,14],"linearity":1,"plcmt":2,"minbitrate":10,"maxbitrate":10,"w":320,"h":250},"id":"2398241a5a860b","tagid":"localhost_typeAdBanVid_windows","secure":1,"bidfloor":0.005,"ext":{"bidder":{"tappxkey":"pub-1234-desktop-1234","endpoint":"vz34906po","host":"https://vz34906po.pub.tappx.com/rtb/","bidfloor":0.005}}}],"device":{"os":"windows","ip":"peer","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36","h":864,"w":1536,"dnt":0,"language":"en","make":"Google Inc."},"params":{"host":"tappx.com","bidfloor":0.005},"regs":{"gdpr":0,"ext":{}}}', 'bids': {'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com/rtb/v2/', 'tappxkey': 'pub-1234-desktop-1234', 'endpoint': 'VZ12TESTCTV', 'bidfloor': 0.005, 'test': true}, 'crumbs': {'pubcid': 'dccfe922-3823-4676-b7b2-e5ed8743154e'}, 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video-ad-div'}}}, 'renderer': {'options': {'text': 'Tappx Outstream Video'}}, 'mediaTypes': {'video': {'mimes': ['video/mp4', 'application/javascript'], 'minduration': 3, 'maxduration': 30, 'startdelay': 5, 'playbackmethod': [1, 3], 'api': [1, 2], 'protocols': [2, 3], 'battr': [13, 14], 'linearity': 1, 'plcmt': 2, 'minbitrate': 10, 'maxbitrate': 10, 'w': 320, 'h': 250}}, 'adUnitCode': 'video-ad-div', 'transactionId': 'ed41c805-d14c-49c3-954d-26b98b2aa2c2', 'sizes': [[320, 250]], 'bidId': '28f49c71b13f2f', 'bidderRequestId': '1401710496dc7', 'auctionId': 'e807363f-3095-43a8-a4a6-f44196cb7318', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}} describe('Tappx bid adapter', function () { /** @@ -323,14 +323,14 @@ describe('Tappx bid adapter', function () { * INTERPRET RESPONSE TESTS */ describe('interpretResponse', function () { - it('receive banner reponse with single placement', function () { + it('receive banner reponse with single plcmt', function () { const bids = spec.interpretResponse(c_SERVERRESPONSE_B, c_BIDDERREQUEST_B); const bid = bids[0]; expect(bid.cpm).to.exist; expect(bid.ad).to.match(/^', - 'price': 4.2, - 'adid': '12345asdfg', - 'currency': 'EUR', - 'statusMessage': 'Bid available', - 'requestId': 'bidId123', - 'width': 300, - 'height': 250, - 'netRevenue': true, - 'mediaType': 'video' + it('should skip responses which do not contain advertiser domains', function() { + let resObjectWithoutAdvertiserDomains = Object.assign({}, resObject); + resObjectWithoutAdvertiserDomains.meta = Object.assign({}, resObject.meta); + delete resObjectWithoutAdvertiserDomains.meta.advertiserDomains; + let bidResponses = { + body: [ resObjectWithoutAdvertiserDomains, resObject ] + } + expect(spec.interpretResponse(bidResponses)).to.deep.equal([ resObject ]); + }); + it('should return responses which contain empty advertiser domains', function() { + let resObjectWithEmptyAdvertiserDomains = Object.assign({}, resObject); + resObjectWithEmptyAdvertiserDomains.meta = Object.assign({}, resObject.meta); + resObjectWithEmptyAdvertiserDomains.meta.advertiserDomains = []; + let bidResponses = { + body: [ resObjectWithEmptyAdvertiserDomains, resObject ] + } + expect(spec.interpretResponse(bidResponses)).to.deep.equal([resObjectWithEmptyAdvertiserDomains, resObject]); + }); + it('should skip responses which do not contain meta media type', function() { + let resObjectWithoutMetaMediaType = Object.assign({}, resObject); + resObjectWithoutMetaMediaType.meta = Object.assign({}, resObject.meta); + delete resObjectWithoutMetaMediaType.meta.mediaType; + let bidResponses = { + body: [ resObjectWithoutMetaMediaType, resObject ] + } + expect(spec.interpretResponse(bidResponses)).to.deep.equal([ resObject ]); + }); + }); + describe('getUserSyncs', function () { + it('should return trackers for lm(only iframe) if server responses contain lm user sync header and iframe and image enabled', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] } + ]; + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true }; - let bidRequest = [ - { - 'method': 'POST', - 'url': ENDPOINT_URL, - 'data': { - 'placementId': 'testPlacementId', - 'width': '300', - 'height': '200', - 'bidId': 'bidId123', - 'referer': 'www.example.com', - 'mediaType': 'video' - } + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ + { + type: 'iframe', + url: 'https://tracker-lm.ortb.net/sync.html' + } + ]); + }); + it('should return empty array if all sync types are disabled', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-1.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-1.ortb.net/sync.html'; + } + } + }, + body: [] } ]; - - let result = spec.interpretResponse(serverResponse, bidRequest[0]); - expect(result[0]).to.have.property('vastXml'); - expect(result[0]).to.have.property('mediaType', 'video'); + const syncOptions = { + iframeEnabled: false, + pixelEnabled: false + }; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.be.an('array').that.is.empty; + }); + it('should return no pixels if iframe sync is enabled and headers are blank', function () { + const serverResponses = [ + { + headers: null, + body: [] + } + ]; + const syncOptions = { + iframeEnabled: true, + pixelEnabled: false + }; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.be.an('array').that.is.empty; + }); + it('should return image sync urls for lm if pixel sync is enabled and headers have lm pixel', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] + } + ]; + const syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ + { + type: 'image', + url: 'https://tracker-lm.ortb.net/sync' + } + ]); + }); + it('should return image sync urls for client1 and clien2 if pixel sync is enabled and two responses and headers have two pixels', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-1.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-1.ortb.net/sync.html'; + } + } + }, + body: [] + }, + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-2.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-2.ortb.net/sync.html'; + } + } + }, + body: [] + } + ]; + const syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ + { + type: 'image', + url: 'https://tracker-1.ortb.net/sync' + }, + { + type: 'image', + url: 'https://tracker-2.ortb.net/sync' + } + ]); + }); + it('should return image sync url for pll if pixel sync is enabled and two responses and headers have two same pixels', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] + }, + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] + } + ]; + const syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ + { + type: 'image', + url: 'https://tracker-lm.ortb.net/sync' + } + ]); + }); + it('should return iframe sync url for pll if pixel sync is enabled and iframe is enables and headers have both iframe and img pixels', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] + } + ]; + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ + { + type: 'iframe', + url: 'https://tracker-lm.ortb.net/sync.html' + } + ]); }); }); }); + +function validateAdUnit(adUnit, bid) { + expect(adUnit.id).to.equal(bid.params.adUnitId); + expect(adUnit.bidId).to.equal(bid.bidId); + expect(adUnit.type).to.equal(bid.params.adUnitType.toUpperCase()); + expect(adUnit.transactionId).to.equal(bid.ortb2Imp.ext.tid); + let bidSizes = []; + if (bid.mediaTypes) { + if (bid.mediaTypes.video && bid.mediaTypes.video.playerSize) { + bidSizes = bidSizes.concat([bid.mediaTypes.video.playerSize]); + } + if (bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) { + bidSizes = bidSizes.concat(bid.mediaTypes.banner.sizes); + } + } + if (bid.sizes) { + bidSizes = bidSizes.concat(bid.sizes || []); + } + expect(adUnit.sizes).to.deep.equal(bidSizes.map(size => { + return { + width: size[0], + height: size[1] + } + })); + expect(adUnit.publisherId).to.equal(bid.params.publisherId); + expect(adUnit.userIdAsEids).to.deep.equal(bid.userIdAsEids); + expect(adUnit.supplyChain).to.deep.equal(bid.schain); + expect(adUnit.ortb2Imp).to.deep.equal(bid.ortb2Imp); +} diff --git a/test/spec/modules/verizonMediaIdSystem_spec.js b/test/spec/modules/verizonMediaIdSystem_spec.js index 10054b5674c..623097b48ce 100644 --- a/test/spec/modules/verizonMediaIdSystem_spec.js +++ b/test/spec/modules/verizonMediaIdSystem_spec.js @@ -30,7 +30,7 @@ describe('Verizon Media ID Submodule', () => { gdprApplies: 1, consentString: 'GDPR_CONSENT_STRING' }, - uspConsent: 'USP_CONSENT_STRING' + usp: 'USP_CONSENT_STRING' }; }); @@ -88,7 +88,7 @@ describe('Verizon Media ID Submodule', () => { '1p': '0', gdpr: '1', gdpr_consent: consentData.gdpr.consentString, - us_privacy: consentData.uspConsent + us_privacy: consentData.usp }; const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); @@ -108,7 +108,7 @@ describe('Verizon Media ID Submodule', () => { '1p': '0', gdpr: '1', gdpr_consent: consentData.gdpr.consentString, - us_privacy: consentData.uspConsent + us_privacy: consentData.usp }; const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); diff --git a/test/spec/modules/viantOrtbBidAdapter_spec.js b/test/spec/modules/viantOrtbBidAdapter_spec.js index 73fdb7f3dc8..67ae8b07821 100644 --- a/test/spec/modules/viantOrtbBidAdapter_spec.js +++ b/test/spec/modules/viantOrtbBidAdapter_spec.js @@ -1,8 +1,9 @@ -import { spec, converter } from 'modules/viantOrtbBidAdapter.js'; +import {spec, converter} from 'modules/viantOrtbBidAdapter.js'; import {assert, expect} from 'chai'; -import { deepClone } from '../../../src/utils'; +import {deepClone} from '../../../src/utils'; import {buildWindowTree} from '../../helpers/refererDetectionHelper'; import {detectReferer} from '../../../src/refererDetection'; + describe('viantOrtbBidAdapter', function () { function testBuildRequests(bidRequests, bidderRequestBase) { let clonedBidderRequest = deepClone(bidderRequestBase); @@ -10,7 +11,8 @@ describe('viantOrtbBidAdapter', function () { let requests = spec.buildRequests(bidRequests, clonedBidderRequest); return requests } - describe('isBidRequestValid', function() { + + describe('isBidRequestValid', function () { function makeBid() { return { 'bidder': 'viant', @@ -46,9 +48,7 @@ describe('viantOrtbBidAdapter', function () { it('should return true if placementId is not passed ', function () { let bid = makeBid(); delete bid.params.placementId; - bid.ortb2Imp = { - - } + bid.ortb2Imp = {} expect(spec.isBidRequestValid(bid)).to.equal(true); }); @@ -98,6 +98,7 @@ describe('viantOrtbBidAdapter', function () { 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' } } + it('should return true when required params found', function () { expect(spec.isBidRequestValid(makeBid())).to.equal(true); }); @@ -141,6 +142,7 @@ describe('viantOrtbBidAdapter', function () { 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' } } + it('should return true when required params found', function () { expect(spec.isBidRequestValid(makeBid())).to.equal(true); }); @@ -180,6 +182,57 @@ describe('viantOrtbBidAdapter', function () { 'src': 'client', 'bidRequestsCount': 1 }]; + const basePMPDealsBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'ortb2Imp': { + 'pmp': { + 'private_auction': 0, + 'deals': [ + { + 'id': '1234567', + 'at': 3, + 'bidfloor': 25, + 'bidfloorcur': 'USD', + 'ext': { + 'must_bid': 1, + 'private_auction': 1 + } + }, + { + 'id': '1234568', + 'at': 3, + 'bidfloor': 25, + 'bidfloorcur': 'USD', + 'ext': { + 'must_bid': 0 + } + } + ] + }, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'gdprConsent': { + 'consentString': 'consentString', + 'gdprApplies': true, + }, + 'uspConsent': '1YYY', + 'sizes': [[728, 90]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); const baseBidderRequestReferer = detectReferer(testWindow)(); @@ -215,7 +268,7 @@ describe('viantOrtbBidAdapter', function () { }); it('sends bid requests to the correct endpoint', function () { const url = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].url; - expect(url).to.equal('https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder'); + expect(url).to.equal('https://bidders-us.adelphic.net/d/rtb/v25/prebid/bidder'); }); it('sends site', function () { @@ -236,6 +289,34 @@ describe('viantOrtbBidAdapter', function () { const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest)[0].data; expect(requestBody.imp[0].banner.pos).to.equal(1); }); + it('includes the deals in the bid request', function () { + const requestBody = testBuildRequests(basePMPDealsBidRequests, baseBidderRequest)[0].data; + expect(requestBody.imp[0].pmp).to.be.not.null; + expect(requestBody.imp[0].pmp).to.deep.equal({ + 'private_auction': 0, + 'deals': [ + { + 'id': '1234567', + 'at': 3, + 'bidfloor': 25, + 'bidfloorcur': 'USD', + 'ext': { + 'must_bid': 1, + 'private_auction': 1 + } + }, + { + 'id': '1234568', + 'at': 3, + 'bidfloor': 25, + 'bidfloorcur': 'USD', + 'ext': { + 'must_bid': 0 + } + } + ] + }); + }); }); if (FEATURES.VIDEO) { @@ -259,6 +340,7 @@ describe('viantOrtbBidAdapter', function () { 'skip': 1, 'skipafter': 5, 'minduration': 10, + 'placement': 1, 'maxduration': 31 } }, diff --git a/test/spec/modules/vibrantmediaBidAdapter_spec.js b/test/spec/modules/vibrantmediaBidAdapter_spec.js index c6ce7d52fb3..3fdc27a7a3b 100644 --- a/test/spec/modules/vibrantmediaBidAdapter_spec.js +++ b/test/spec/modules/vibrantmediaBidAdapter_spec.js @@ -3,6 +3,7 @@ import {spec} from 'modules/vibrantmediaBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from 'src/mediaTypes.js'; import {INSTREAM, OUTSTREAM} from 'src/video.js'; +import { getWinDimensions } from '../../../src/utils'; const EXPECTED_PREBID_SERVER_URL = 'https://prebid.intellitxt.com/prebid'; @@ -62,12 +63,6 @@ describe('VibrantMediaBidAdapter', function () { }); }); - describe('transformBidParams', function () { - it('transforms bid params correctly', function () { - expect(spec.transformBidParams(VALID_VIDEO_BID_PARAMS)).to.deep.equal(VALID_VIDEO_BID_PARAMS); - }); - }) - let bidRequest; beforeEach(function () { @@ -551,9 +546,9 @@ describe('VibrantMediaBidAdapter', function () { const request = spec.buildRequests(bidRequests, {}); const payload = JSON.parse(request.data); - expect(payload.window).to.exist; - expect(payload.window.width).to.equal(window.innerWidth); - expect(payload.window.height).to.equal(window.innerHeight); + expect(payload.window).to.exist; + expect(payload.window.width).to.equal(getWinDimensions().innerWidth); + expect(payload.window.height).to.equal(getWinDimensions().innerHeight); }); it('should add the top-level sizes to the bid request, if present', function () { @@ -1077,13 +1072,9 @@ describe('VibrantMediaBidAdapter', function () { describe('Flow tests', function () { describe('For successive API calls to the public functions', function () { it('should succeed with one media type per bid', function () { - const transformedBannerBidParams = spec.transformBidParams(VALID_BANNER_BID_PARAMS); - const transformedVideoBidParams = spec.transformBidParams(VALID_VIDEO_BID_PARAMS); - const transformedNativeBidParams = spec.transformBidParams(VALID_NATIVE_BID_PARAMS); - const bannerBid = { bidder: 'vibrantmedia', - params: transformedBannerBidParams, + params: VALID_BANNER_BID_PARAMS, mediaTypes: { banner: { sizes: DEFAULT_BID_SIZES, @@ -1097,7 +1088,7 @@ describe('VibrantMediaBidAdapter', function () { }; const videoBid = { bidder: 'vibrantmedia', - params: transformedVideoBidParams, + params: VALID_VIDEO_BID_PARAMS, mediaTypes: { video: { context: OUTSTREAM, @@ -1112,7 +1103,7 @@ describe('VibrantMediaBidAdapter', function () { }; const nativeBid = { bidder: 'vibrantmedia', - params: transformedNativeBidParams, + params: VALID_NATIVE_BID_PARAMS, mediaTypes: { native: { image: { @@ -1178,10 +1169,9 @@ describe('VibrantMediaBidAdapter', function () { }); it('should succeed with multiple media types for a single bid', function () { - const bidParams = spec.transformBidParams(VALID_VIDEO_BID_PARAMS); const bid = { bidder: 'vibrantmedia', - params: bidParams, + params: VALID_VIDEO_BID_PARAMS, mediaTypes: { banner: { sizes: DEFAULT_BID_SIZES diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 5515002a054..1f60a1cffbb 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -1,7 +1,11 @@ import {expect} from 'chai'; import { spec as adapter, + storage, createDomain, + webSessionId +} from 'modules/vidazooBidAdapter.js'; +import { hashCode, extractPID, extractCID, @@ -11,9 +15,9 @@ import { tryParseJSON, getUniqueDealId, getNextDealId, - getVidazooSessionId, - webSessionId -} from 'modules/vidazooBidAdapter.js'; + getTopWindowQueryParams, + getVidazooSessionId +} from 'libraries/vidazooUtils/bidderUtils.js' import * as utils from 'src/utils.js'; import {version} from 'package.json'; import {useFakeTimers} from 'sinon'; @@ -21,7 +25,7 @@ import {BANNER, VIDEO} from '../../../src/mediaTypes'; import {config} from '../../../src/config'; import {deepSetValue} from 'src/utils.js'; -export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; +export const TEST_ID_SYSTEMS = ['criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'pubcid', 'tdid', 'pubProvidedId']; const SUB_DOMAIN = 'openrtb'; @@ -88,12 +92,42 @@ const VIDEO_BID = { 'minduration': 0, 'startdelay': 0, 'linearity': 1, - 'api': [2], + 'api': [2, 7], 'placement': 1 } } } +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': '' + }, + 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'}, +}; + const BIDDER_REQUEST = { 'gdprConsent': { 'consentString': 'consent_string', @@ -111,6 +145,7 @@ const BIDDER_REQUEST = { 'cat': ['IAB2'], 'pagecat': ['IAB2-2'], 'content': { + 'language': 'en', 'data': [{ 'name': 'example.com', 'ext': { @@ -125,26 +160,10 @@ const BIDDER_REQUEST = { }, 'regs': { 'gpp': 'gpp_string', - 'gpp_sid': [7] - }, - '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': '' - } + 'gpp_sid': [7], + 'coppa': 0 }, + device: ORTB2_DEVICE, user: { data: [ { @@ -153,8 +172,14 @@ const BIDDER_REQUEST = { segment: [{id: '243'}], }, ], + }, + source: { + ext: { + omidpn: 'MyIntegrationPartner', + omidpv: '7.1' + } } - }, + } }; const SERVER_RESPONSE = { @@ -205,15 +230,6 @@ const REQUEST = { } }; -function getTopWindowQueryParams() { - try { - const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); - return parsedUrl.search; - } catch (e) { - return ''; - } -} - describe('VidazooBidAdapter', function () { describe('validtae spec', function () { it('exists and is a function', function () { @@ -317,6 +333,7 @@ describe('VidazooBidAdapter', function () { gpid: '', prebidVersion: version, ptrace: '1000', + vdzhum: '1000', publisherId: '59ac17c192832d0011283fe3', url: 'https%3A%2F%2Fwww.greatsite.com', referrer: 'https://www.somereferrer.com', @@ -340,6 +357,9 @@ describe('VidazooBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + contentLang: 'en', + coppa: 0, + device: ORTB2_DEVICE, contentData: [{ 'name': 'example.com', 'ext': { @@ -363,7 +383,7 @@ describe('VidazooBidAdapter', function () { webSessionId: webSessionId, mediaTypes: { video: { - api: [2], + api: [2, 7], context: 'instream', linearity: 1, maxduration: 60, @@ -377,7 +397,9 @@ describe('VidazooBidAdapter', function () { protocols: [2, 3, 5, 6], startdelay: 0 } - } + }, + omidpn: 'MyIntegrationPartner', + omidpv: '7.1' } }) ; @@ -422,6 +444,7 @@ describe('VidazooBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + device: ORTB2_DEVICE, url: 'https%3A%2F%2Fwww.greatsite.com', referrer: 'https://www.somereferrer.com', cb: 1000, @@ -436,6 +459,7 @@ describe('VidazooBidAdapter', function () { prebidVersion: version, schain: BID.schain, ptrace: '1000', + vdzhum: '1000', res: `${window.top.screen.width}x${window.top.screen.height}`, mediaTypes: [BANNER], uqs: getTopWindowQueryParams(), @@ -445,6 +469,8 @@ describe('VidazooBidAdapter', function () { gpid: '1234567890', cat: ['IAB2'], pagecat: ['IAB2-2'], + contentLang: 'en', + coppa: 0, contentData: [{ 'name': 'example.com', 'ext': { @@ -512,6 +538,7 @@ describe('VidazooBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + device: ORTB2_DEVICE, url: 'https%3A%2F%2Fwww.greatsite.com', referrer: 'https://www.somereferrer.com', cb: 1000, @@ -526,6 +553,7 @@ describe('VidazooBidAdapter', function () { prebidVersion: version, schain: BID.schain, ptrace: '1000', + vdzhum: '1000', res: `${window.top.screen.width}x${window.top.screen.height}`, mediaTypes: [BANNER], uqs: getTopWindowQueryParams(), @@ -535,6 +563,8 @@ describe('VidazooBidAdapter', function () { gpid: '1234567890', cat: ['IAB2'], pagecat: ['IAB2-2'], + contentLang: 'en', + coppa: 0, contentData: [{ 'name': 'example.com', 'ext': { @@ -600,7 +630,7 @@ describe('VidazooBidAdapter', function () { it('should set fledge correctly if enabled', function () { config.resetConfig(); const bidderRequest = utils.deepClone(BIDDER_REQUEST); - bidderRequest.fledgeEnabled = true; + bidderRequest.paapi = {enabled: true}; deepSetValue(bidderRequest, 'ortb2Imp.ext.ae', 1); const requests = adapter.buildRequests([BID], bidderRequest); expect(requests[0].data.fledge).to.equal(1); @@ -619,7 +649,7 @@ describe('VidazooBidAdapter', function () { expect(result).to.deep.equal([{ type: 'iframe', - url: 'https://sync.cootlogix.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + url: 'https://sync.cootlogix.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' }]); }); @@ -627,7 +657,7 @@ describe('VidazooBidAdapter', function () { const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ type: 'iframe', - url: 'https://sync.cootlogix.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + url: 'https://sync.cootlogix.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' }]); }); @@ -635,10 +665,21 @@ describe('VidazooBidAdapter', function () { const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ - 'url': 'https://sync.cootlogix.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'url': 'https://sync.cootlogix.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', 'type': 'image' }]); - }) + }); + + it('should have valid user sync with coppa 1 on response', function () { + config.setConfig({ + coppa: 1 + }); + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.cootlogix.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1' + }]); + }); }); describe('interpret response', function () { @@ -750,8 +791,6 @@ describe('VidazooBidAdapter', function () { switch (idSystemProvider) { case 'lipb': return {lipbid: id}; - case 'parrableId': - return {eid: id}; case 'id5id': return {uid: id}; default: @@ -802,14 +841,14 @@ describe('VidazooBidAdapter', function () { $$PREBID_GLOBAL$$.bidderSettings = {}; }); it('should get undefined vidazoo session id', function () { - const sessionId = getVidazooSessionId(); + const sessionId = getVidazooSessionId(storage); expect(sessionId).to.be.empty; }); it('should get vidazoo session id from storage', function () { const vidSid = '1234-5678'; window.localStorage.setItem('vidSid', vidSid); - const sessionId = getVidazooSessionId(); + const sessionId = getVidazooSessionId(storage); expect(sessionId).to.be.equal(vidSid); }); }); @@ -828,15 +867,15 @@ describe('VidazooBidAdapter', function () { const key = 'myDealKey'; it('should get the next deal id', function () { - const dealId = getNextDealId(key); - const nextDealId = getNextDealId(key); + const dealId = getNextDealId(storage, key); + const nextDealId = getNextDealId(storage, key); expect(dealId).to.be.equal(1); expect(nextDealId).to.be.equal(2); }); it('should get the first deal id on expiration', function (done) { setTimeout(function () { - const dealId = getNextDealId(key, 100); + const dealId = getNextDealId(storage, key, 100); expect(dealId).to.be.equal(1); done(); }, 200); @@ -857,13 +896,13 @@ describe('VidazooBidAdapter', function () { const key = 'myKey'; let uniqueDealId; beforeEach(() => { - uniqueDealId = getUniqueDealId(key, 0); + uniqueDealId = getUniqueDealId(storage, key, 0); }) it('should get current unique deal id', function (done) { // waiting some time so `now` will become past setTimeout(() => { - const current = getUniqueDealId(key); + const current = getUniqueDealId(storage, key); expect(current).to.be.equal(uniqueDealId); done(); }, 200); @@ -871,7 +910,7 @@ describe('VidazooBidAdapter', function () { it('should get new unique deal id on expiration', function (done) { setTimeout(() => { - const current = getUniqueDealId(key, 100); + const current = getUniqueDealId(storage, key, 100); expect(current).to.not.be.equal(uniqueDealId); done(); }, 200) @@ -895,8 +934,8 @@ describe('VidazooBidAdapter', function () { shouldAdvanceTime: true, now }); - setStorageItem('myKey', 2020); - const {value, created} = getStorageItem('myKey'); + 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'); @@ -907,7 +946,7 @@ describe('VidazooBidAdapter', function () { it('should get external stored value', function () { const value = 'superman' window.localStorage.setItem('myExternalKey', value); - const item = getStorageItem('myExternalKey'); + const item = getStorageItem(storage, 'myExternalKey'); expect(item).to.be.equal(value); }); diff --git a/test/spec/modules/videoModule/adQueue_spec.js b/test/spec/modules/videoModule/adQueue_spec.js index 4002e0b6dcc..8c4ad7fd8c7 100644 --- a/test/spec/modules/videoModule/adQueue_spec.js +++ b/test/spec/modules/videoModule/adQueue_spec.js @@ -9,6 +9,7 @@ describe('Ad Queue Coordinator', function () { onEvents: sinon.spy(), offEvents: sinon.spy(), setAdTagUrl: sinon.spy(), + setAdXml: sinon.spy(), } }; @@ -58,6 +59,24 @@ describe('Ad Queue Coordinator', function () { expect(mockVideoCore.setAdTagUrl.calledOnce).to.be.true; }); + it('should run setAdXml instead of setAdTagUrl if vast has been prefetched', function () { + const mockVideoCore = mockVideoCoreFactory(); + const mockEvents = mockEventsFactory(); + let setupComplete; + mockVideoCore.onEvents = function(events, callback, id) { + if (events[0] === SETUP_COMPLETE && id === testId) { + setupComplete = callback; + } + }; + const coordinator = AdQueueCoordinator(mockVideoCore, mockEvents); + coordinator.registerProvider(testId); + coordinator.queueAd('testAdTag', testId, {prefetchedVastXml: ''}); + + setupComplete('', { divId: testId }); + expect(mockVideoCore.setAdXml.calledOnce).to.be.true; + expect(mockVideoCore.setAdTagUrl.calledOnce).to.be.false; + }); + it('should load ads without queueing', function () { const mockVideoCore = mockVideoCoreFactory(); const mockEvents = mockEventsFactory(); diff --git a/test/spec/modules/videoModule/shared/vastXmlEditor_spec.js b/test/spec/modules/videoModule/shared/vastXmlEditor_spec.js index 2304b2f2833..5ea75e8a6b1 100644 --- a/test/spec/modules/videoModule/shared/vastXmlEditor_spec.js +++ b/test/spec/modules/videoModule/shared/vastXmlEditor_spec.js @@ -1,5 +1,6 @@ import { vastXmlEditorFactory } from 'libraries/video/shared/vastXmlEditor.js'; import { expect } from 'chai'; +import { server } from '../../../../mocks/xhr'; describe('Vast XML Editor', function () { const adWrapperXml = ` diff --git a/test/spec/modules/videoModule/submodules/adplayerproVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/adplayerproVideoProvider_spec.js new file mode 100644 index 00000000000..227e61494b6 --- /dev/null +++ b/test/spec/modules/videoModule/submodules/adplayerproVideoProvider_spec.js @@ -0,0 +1,542 @@ +// Using require style imports for fine grained control of import time +import { + AD_CLICK, + AD_COMPLETE, + AD_ERROR, + AD_IMPRESSION, + AD_LOADED, + AD_PAUSE, + AD_PLAY, + AD_REQUEST, + AD_SKIPPED, + AD_STARTED, + DESTROYED, + PLAYER_RESIZE, + SETUP_COMPLETE, + SETUP_FAILED, + VOLUME +} from 'libraries/video/constants/events.js'; +import adPlayerProSubmoduleFactory, {callbackStorageFactory} from '../../../../../modules/adplayerproVideoProvider.js'; +import {PLACEMENT} from '../../../../../libraries/video/constants/ortb'; +import sinon from 'sinon'; + +const {AdPlayerProProvider, utils} = require('modules/adplayerproVideoProvider.js'); + +const { + PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, VPAID_MIME_TYPE, PLCMT +} = require('libraries/video/constants/ortb.js'); + +function getPlayerMock() { + return { + setup: function () { + return this; + }, + load: function () { + }, + resize: function () { + }, + remove: function () { + }, + on: function () { + return this; + }, + off: function () { + return this; + }, + getAdWidth: function () { + return 600 + }, + getAdHeight: function () { + return 400; + } + }; +} + +function makePlayerFactoryMock(playerMock_) { + return () => playerMock_; +} + +function getUtilsMock() { + return { + getConfig: function () { + }, + getPlayerEvent: event => event, + getSupportedMediaTypes: function () { + }, + getPlacement: function () { + }, + getPlaybackMethod: function () { + }, + getPlcmt: function () { + } + }; +} + +function addDiv() { + const div = document.createElement('div'); + div.setAttribute('id', 'test'); + document.body.appendChild(div); +} + +function removeDiv() { + const div = document.getElementById('test'); + if (div) { + div.remove(); + } +} + +describe('AdPlayerProProvider', function () { + let config; + let callbackStorage; + let utilsMock; + let player; + + beforeEach(() => { + addDiv(); + config = {divId: 'test', playerConfig: {placementId: 'testId'}}; + callbackStorage = callbackStorageFactory(); + utilsMock = getUtilsMock(); + player = getPlayerMock(); + }); + + afterEach(() => { + removeDiv(); + }); + + describe('init', function () { + it('should trigger failure when Adplayer.Pro is missing', function () { + const provider = AdPlayerProProvider(config, null, callbackStorage, utilsMock); + const setupFailed = sinon.spy(); + provider.onEvent(SETUP_FAILED, setupFailed, {}); + provider.init(); + expect(setupFailed.calledOnce).to.be.true; + const payload = setupFailed.args[0][1]; + expect(payload.errorCode).to.be.equal(-1); + }); + + it('should trigger failure when the div is not found', function () { + config.divId = 'fake-div' + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utilsMock); + const setupFailed = sinon.spy(); + provider.onEvent(SETUP_FAILED, setupFailed, {}); + provider.init(); + expect(setupFailed.calledOnce).to.be.true; + const payload = setupFailed.args[0][1]; + expect(payload.errorCode).to.be.equal(-3); + }); + + it('should trigger failure when the placementId is not found', function () { + config.playerConfig.placementId = ''; + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utilsMock); + const setupFailed = sinon.spy(); + provider.onEvent(SETUP_FAILED, setupFailed, {}); + provider.init(); + expect(setupFailed.calledOnce).to.be.true; + const payload = setupFailed.args[0][1]; + expect(payload.errorCode).to.be.equal(-4); + }); + + it('should instantiate the player after setAdTagUrl', function () { + const setupSpy = player.setup = sinon.spy(player.setup); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + expect(setupSpy.calledOnce).to.be.false; + provider.setAdTagUrl('https://test.com', {}); + expect(setupSpy.calledOnce).to.be.true; + const payload = setupSpy.args[0][0]; + expect(payload.advertising.tag.url).to.be.equal('https://test.com'); + }); + + it('should instantiate the player after setAdTagUrl for adPlayerProSubmoduleFactory', function () { + const setupSpy = player.setup = sinon.spy(player.setup); + window.playerPro = makePlayerFactoryMock(player); + const provider = adPlayerProSubmoduleFactory(config, {}); + provider.init(); + expect(setupSpy.calledOnce).to.be.false; + provider.setAdTagUrl('https://test.com', {}); + expect(setupSpy.calledOnce).to.be.true; + const payload = setupSpy.args[0][0]; + expect(payload.advertising.tag.url).to.be.equal('https://test.com'); + }); + + it('should trigger setup complete when player is already instantiated', function () { + const setupSpy = player.setup = sinon.spy(player.setup); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + const setupComplete = sinon.spy(); + provider.onEvent(SETUP_COMPLETE, setupComplete, {}); + provider.init(); + expect(setupComplete.calledOnce).to.be.true; + expect(setupSpy.called).to.be.false; + }); + + it('should support multiple setup complete event handlers', function () { + const setupSpy = player.setup = sinon.spy(player.setup); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + const setupComplete = sinon.spy(); + const setupComplete2 = sinon.spy(); + provider.onEvent(SETUP_COMPLETE, setupComplete, {}); + provider.onEvent(SETUP_COMPLETE, setupComplete2, {}); + provider.init(); + expect(setupComplete.calledOnce).to.be.true; + expect(setupComplete2.calledOnce).to.be.true; + expect(setupSpy.called).to.be.false; + }); + + it('should not reinstantiate player', function () { + const setupSpy = player.setup = sinon.spy(player.setup); + const onSpy = player.on = sinon.spy(player.on); + + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + expect(setupSpy.calledOnce).to.be.false; + provider.setAdTagUrl('https://test.com', {}); + expect(setupSpy.calledOnce).to.be.true; + const payload = setupSpy.args[0][0]; + expect(payload.advertising.tag.url).to.be.equal('https://test.com'); + + // test that the player is not reinitialized + provider.setAdTagUrl('https://test.com', {}); + expect(setupSpy.calledOnce).to.be.true; + + // get and call AdStopped event + const args = onSpy.args[0]; + expect(args[0]).to.be.equal('AdStopped'); + args[1](); + + provider.setAdTagUrl('https://test.com', {}); + expect(setupSpy.calledTwice).to.be.true; + }); + }); + + describe('getId', function () { + it('should return configured div id', function () { + const provider = AdPlayerProProvider(config, undefined, undefined, utils); + expect(provider.getId()).to.be.equal('test'); + }); + }); + + describe('getOrtbVideo', function () { + it('should populate oRTB Video params', function () { + const test_media_type = VIDEO_MIME_TYPE.MP4; + const test_placement = PLACEMENT.ARTICLE; + const test_playback_method = PLAYBACK_METHODS.CLICK_TO_PLAY; + const test_plcmt = PLCMT.OUTSTREAM; + + utilsMock.getSupportedMediaTypes = () => [test_media_type]; + utilsMock.getPlacement = () => test_placement; + utilsMock.getPlaybackMethod = () => test_playback_method; + utilsMock.getPlcmt = () => test_plcmt; + + const provider = AdPlayerProProvider(config, null, null, utilsMock); + provider.init(); + let video = provider.getOrtbVideo(); + + expect(video.mimes).to.include(VIDEO_MIME_TYPE.MP4); + expect(video.protocols).to.include.members([ + PROTOCOLS.VAST_2_0, + PROTOCOLS.VAST_3_0, + PROTOCOLS.VAST_4_0, + PROTOCOLS.VAST_2_0_WRAPPER, + PROTOCOLS.VAST_3_0_WRAPPER, + PROTOCOLS.VAST_4_0_WRAPPER + ]); + expect(video.placement).to.equal(test_placement); + expect(video.maxextended).to.equal(-1); + expect(video.boxingallowed).to.equal(1); + expect(video.playbackmethod).to.include(test_playback_method); + expect(video.playbackend).to.equal(1); + expect(video.api).to.have.length(2); + expect(video.api).to.include.members([API_FRAMEWORKS.VPAID_2_0, API_FRAMEWORKS.OMID_1_0]); + expect(video.plcmt).to.equal(test_plcmt); + }); + }); + + describe('getOrtbContent', function () { + it('should populate oRTB Content params', function () { + const provider = AdPlayerProProvider(config, null, null, utils); + provider.init(); + expect(provider.getOrtbContent()).to.be.undefined; + }); + }); + + describe('setAdTagUrl', function () { + it('should call setup', function () { + const setupSpy = player.setup = sinon.spy(player.setup); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + provider.setAdTagUrl('', {adXml: 'https://test.com'}); + expect(setupSpy.calledOnce).to.be.true; + }); + + it('should not call setup', function () { + const setupSpy = player.setup = sinon.spy(player.setup); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + provider.setAdTagUrl('', {}); + expect(setupSpy.calledOnce).to.be.false; + }); + }); + + describe('events', function () { + it('should register event listener on player', function () { + const onSpy = player.on = sinon.spy(); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + provider.setAdTagUrl('https://test.com', {}); + const callback = () => { + }; + provider.onEvent(AD_REQUEST, callback, {}); + provider.onEvent('test', callback, {}); + expect(onSpy.calledTwice).to.be.true; + expect(onSpy.args[0][0]).to.be.equal('AdStopped'); + expect(onSpy.args[1][0]).to.be.equal('AdRequest'); + }); + + it('should remove event listener on player', function () { + const offSpy = player.off = sinon.spy(); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + provider.setAdTagUrl('https://test.com', {}); + const callback = () => { + }; + provider.onEvent(AD_IMPRESSION, callback, {}); + provider.offEvent(AD_IMPRESSION, callback); + expect(offSpy.calledOnce).to.be.true; + const eventName = offSpy.args[0][0]; + const eventCallback = offSpy.args[0][1]; + expect(eventName).to.be.equal('AdImpression'); + expect(eventCallback).to.be.exist; + }); + + it('should remove event listener on player by eventName', function () { + const offSpy = player.off = sinon.spy(); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + provider.setAdTagUrl('https://test.com', {}); + const callback = () => { + }; + provider.onEvent(AD_IMPRESSION, callback, {}); + provider.offEvent(AD_IMPRESSION); + expect(offSpy.calledOnce).to.be.true; + const eventName = offSpy.args[0][0]; + expect(eventName).to.be.equal('AdImpression'); + }); + + it('should call event player resize', function () { + const onSpy = player.on = sinon.spy(); + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + provider.setAdTagUrl('https://test.com', {}); + const callbackSpy = sinon.spy(); + provider.onEvent(PLAYER_RESIZE, callbackSpy, {}); + expect(onSpy.calledTwice).to.be.true; + expect(onSpy.args[1][0]).to.be.equal('AdSizeChange'); + expect(callbackSpy.notCalled).to.be.true; + onSpy.args[1][1](); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.args[0][1].width).to.be.equal(600); + expect(callbackSpy.args[0][1].height).to.be.equal(400); + }); + }); + + describe('destroy', function () { + it('should remove and null the player', function () { + const removeSpy = player.remove = sinon.spy(); + player.remove = removeSpy; + const provider = AdPlayerProProvider(config, makePlayerFactoryMock(player), callbackStorage, utils); + provider.init(); + provider.setAdTagUrl('https://test.com', {}); + provider.destroy(); + provider.destroy(); + expect(removeSpy.calledOnce).to.be.true; + }); + }); +}); + +describe('AdPlayerProProvider utils', function () { + it('getConfig', function () { + expect(utils.getConfig()).to.be.undefined; + expect(utils.getConfig({})).to.be.undefined; + const config = utils.getConfig({}, 'https://test.com'); + expect(config.advertising.tag.url).to.be.equal('https://test.com'); + expect(config._pType).to.be.equal('pbjs'); + }); + + it('getPlayerEvent', function () { + function test(event, expected) { + expect(utils.getPlayerEvent(event)).to.be.equal(expected); + } + + test(DESTROYED, 'AdStopped'); + test(AD_REQUEST, 'AdRequest'); + test(AD_LOADED, 'AdLoaded'); + test(AD_STARTED, 'AdStarted'); + test(AD_IMPRESSION, 'AdImpression'); + test(AD_PLAY, 'AdPlaying'); + test(AD_PAUSE, 'AdPaused'); + test(AD_CLICK, 'AdClickThru'); + test(AD_SKIPPED, 'AdSkipped'); + test(AD_ERROR, 'AdError'); + test(AD_COMPLETE, 'AdCompleted'); + test(VOLUME, 'AdVolumeChange'); + test(PLAYER_RESIZE, 'AdSizeChange'); + test('test', 'test'); + }); + + it('getSupportedMediaTypes', function () { + let supportedMediaTypes = utils.getSupportedMediaTypes([]); + expect(supportedMediaTypes).to.include(VPAID_MIME_TYPE); + + supportedMediaTypes = utils.getSupportedMediaTypes([VIDEO_MIME_TYPE.MP4]); + expect(supportedMediaTypes).to.include(VPAID_MIME_TYPE); + }); + + it('getPlacement', function () { + function test(config, expected) { + expect(utils.getPlacement(config)).to.be.equal(expected); + } + + test(false, PLACEMENT.BANNER); + test({}, PLACEMENT.BANNER); + test({type: 'test'}, PLACEMENT.BANNER); + test({type: 'inPage'}, PLACEMENT.ARTICLE); + test({type: 'rewarded'}, PLACEMENT.INTERSTITIAL_SLIDER_FLOATING); + test({type: 'inView'}, PLACEMENT.INTERSTITIAL_SLIDER_FLOATING); + }); + + it('getPlaybackMethod', function () { + function test(autoplay, mute, expected) { + expect(utils.getPlaybackMethod({autoplay, mute})).to.be.equal(expected); + } + + test(false, false, PLAYBACK_METHODS.CLICK_TO_PLAY); + test(false, true, PLAYBACK_METHODS.CLICK_TO_PLAY); + test(true, false, PLAYBACK_METHODS.AUTOPLAY); + test(true, true, PLAYBACK_METHODS.AUTOPLAY_MUTED); + }); + + it('getPlcmt', function () { + function test(type, autoplay, muted, file, expected) { + expect(utils.getPlcmt({type, autoplay, muted, file})).to.be.equal(expected); + } + + test('inStream', false, false, 'f', PLCMT.INSTREAM); + test(undefined, false, false, 'f', PLCMT.INSTREAM); + test('inStream', false, true, 'f', PLCMT.INSTREAM); + test('inStream', true, false, 'f', PLCMT.INSTREAM); + test('inStream', true, true, 'f', PLCMT.ACCOMPANYING_CONTENT); + + test('rewarded', true, false, undefined, PLCMT.INTERSTITIAL); + test('inView', true, false, undefined, PLCMT.INTERSTITIAL); + test('InPage', true, false, undefined, PLCMT.OUTSTREAM); + }); +}); + +describe('AdPlayerProProvider callbackStorageFactory', function () { + let player; + let callbackStorage; + + beforeEach(() => { + player = getPlayerMock(); + callbackStorage = callbackStorageFactory(); + }); + + it('storeCallback and getCallback', function () { + const eventType = 'test'; + const callback = () => 11; + const eventHandler = () => 12; + callbackStorage.storeCallback(eventType, eventHandler, callback); + expect(callbackStorage.getCallback(eventType, callback)).to.be.equal(eventHandler); + + const callback2 = () => 21; + const eventHandler2 = () => 22; + callbackStorage.storeCallback(eventType, eventHandler2, callback2); + expect(callbackStorage.getCallback(eventType, callback2)).to.be.equal(eventHandler2); + + expect(callbackStorage.getCallback(eventType, callback)).to.be.equal(eventHandler); + }); + + it('clearCallback', function () { + const eventType = 'test'; + const callback = () => 11; + const callback2 = () => 21; + callbackStorage.storeCallback(eventType, () => 12, callback); + callbackStorage.storeCallback(eventType, () => 22, callback2); + + expect(callbackStorage.getCallback(eventType, callback)()).to.be.equal(12); + expect(callbackStorage.getCallback(eventType, callback2)()).to.be.equal(22); + + callbackStorage.clearCallback(eventType); + + expect(callbackStorage.getCallback(eventType, callback)).to.be.undefined; + expect(callbackStorage.getCallback(eventType, callback2)).to.be.undefined; + + callbackStorage.storeCallback(eventType, () => 12, callback); + callbackStorage.storeCallback(eventType, () => 22, callback2); + + callbackStorage.clearCallback(eventType, callback); + + expect(callbackStorage.getCallback(eventType, callback)).to.be.undefined; + expect(callbackStorage.getCallback(eventType, callback2)()).to.be.equal(22); + }); + + it('addAllCallbacks', function () { + const eventType = 'test'; + const eventType2 = 'test2'; + const callback = () => 11; + const callback2 = () => 21; + const eventHandler = () => 12; + const eventHandler2 = () => 22; + callbackStorage.storeCallback(eventType, eventHandler, callback); + callbackStorage.storeCallback(eventType, eventHandler2, callback2); + callbackStorage.storeCallback(eventType2, eventHandler, callback); + callbackStorage.storeCallback(eventType2, eventHandler2, callback2); + + let spy = sinon.spy(); + callbackStorage.addAllCallbacks(spy); + expect(spy.callCount).to.be.equal(4); + expect(spy.calledWith(eventType, eventHandler)).to.be.true; + expect(spy.calledWith(eventType, eventHandler2)).to.be.true; + expect(spy.calledWith(eventType2, eventHandler)).to.be.true; + expect(spy.calledWith(eventType2, eventHandler2)).to.be.true; + + callbackStorage.clearCallback(eventType, callback); + spy = sinon.spy(); + callbackStorage.addAllCallbacks(spy); + expect(spy.callCount).to.be.equal(3); + expect(spy.calledWith(eventType, eventHandler2)).to.be.true; + expect(spy.calledWith(eventType2, eventHandler)).to.be.true; + expect(spy.calledWith(eventType2, eventHandler2)).to.be.true; + + callbackStorage.clearCallback(eventType2); + spy = sinon.spy(); + callbackStorage.addAllCallbacks(spy); + expect(spy.callCount).to.be.equal(1); + expect(spy.calledWith(eventType, eventHandler2)).to.be.true; + + callbackStorage.storeCallback(eventType, eventHandler, callback); + callbackStorage.storeCallback(eventType2, eventHandler, callback); + spy = sinon.spy(); + callbackStorage.addAllCallbacks(spy); + expect(spy.callCount).to.be.equal(3); + expect(spy.calledWith(eventType, eventHandler)).to.be.true; + expect(spy.calledWith(eventType, eventHandler2)).to.be.true; + expect(spy.calledWith(eventType2, eventHandler)).to.be.true; + + callbackStorage.clearStorage(); + spy = sinon.spy(); + callbackStorage.addAllCallbacks(spy); + expect(spy.callCount).to.be.equal(0); + }); + + it('clearStorage', function () { + const eventType = 'test'; + const callback = () => 11; + const eventHandler = () => 12; + callbackStorage.storeCallback(eventType, eventHandler, callback); + expect(callbackStorage.getCallback(eventType, callback)).to.be.equal(eventHandler); + + callbackStorage.clearStorage(); + expect(callbackStorage.getCallback(eventType, callback)).to.be.undefined; + }); +}); diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index 3cede6c8eda..1a9643c7d3d 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -26,6 +26,7 @@ function getPlayerMock() { getVolume: function () {}, getConfig: function () {}, getHeight: function () {}, + getContainer: function () {}, getWidth: function () {}, getFullscreen: function () {}, getPlaylistItem: function () {}, @@ -51,6 +52,9 @@ function makePlayerFactoryMock(playerMock_) { function getUtilsMock() { return { getJwConfig: function () {}, + getPlayerHeight: function () {}, + getPlayerWidth: function () {}, + getPlayerSizeFromAspectRatio: function () {}, getSupportedMediaTypes: function () {}, getStartDelay: function () {}, getPlacement: function () {}, @@ -212,6 +216,8 @@ describe('JWPlayerProvider', function () { player.getWidth = () => test_width; player.getFullscreen = () => true; // + utils.getPlayerHeight = () => 100; + utils.getPlayerWidth = () => 200; utils.getSupportedMediaTypes = () => [test_media_type]; utils.getStartDelay = () => test_start_delay; utils.getPlacement = () => test_placement; @@ -690,6 +696,81 @@ describe('utils', function () { expect(jwConfig.advertising).to.have.property('client', 'vast'); }); }); + + describe('getPlayerHeight', function () { + const getPlayerHeight = utils.getPlayerHeight; + + it('should return height from API when defined', function () { + const expectedHeight = 500; + const playerMock = { getHeight: () => expectedHeight }; + expect(getPlayerHeight(playerMock, {})).to.equal(expectedHeight); + }); + + it('should return height from config when API returns undefined', function () { + const expectedHeight = 500; + const playerMock = { getHeight: () => undefined }; + expect(getPlayerHeight(playerMock, { height: 500 })).to.equal(expectedHeight); + }); + }); + + describe('getPlayerWidth', function () { + const getPlayerWidth = utils.getPlayerWidth; + + it('should return width from API when defined', function () { + const expectedWidth = 1000; + const playerMock = { getWidth: () => expectedWidth }; + expect(getPlayerWidth(playerMock, {})).to.equal(expectedWidth); + }); + + it('should return width from config when API returns undefined', function () { + const expectedWidth = 1000; + const playerMock = { getWidth: () => undefined }; + expect(getPlayerWidth(playerMock, { width: expectedWidth })).to.equal(expectedWidth); + }); + + it('should return undefined when width is string', function () { + const playerMock = { getWidth: () => undefined }; + expect(getPlayerWidth(playerMock, { width: '50%' })).to.be.undefined; + }); + }); + + describe('getPlayerSizeFromAspectRatio', function () { + const getPlayerSizeFromAspectRatio = utils.getPlayerSizeFromAspectRatio; + const testContainer = { + clientWidth: 640, + clientHeight: 480 + }; + + it('should return an empty object when width and aspectratio are not strings', function () { + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {})).to.deep.equal({}); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {width: 100})).to.deep.equal({}); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: '1:2', width: 100})).to.deep.equal({}); + }); + + it('should return an empty object when aspectratio is malformed', function () { + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: '0.5', width: '100%'})).to.deep.equal({}); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: '1-2', width: '100%'})).to.deep.equal({}); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: '1:', width: '100%'})).to.deep.equal({}); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: ':2', width: '100%'})).to.deep.equal({}); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: ':', width: '100%'})).to.deep.equal({}); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: '1:2:3', width: '100%'})).to.deep.equal({}); + }); + + it('should return an empty object when player container cannot be obtained', function () { + expect(getPlayerSizeFromAspectRatio({}, {aspectratio: '1:2', width: '100%'})).to.deep.equal({}); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => undefined }, {aspectratio: '1:2', width: '100%'})).to.deep.equal({}); + }); + + it('should calculate the size given the width percentage and aspect ratio', function () { + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: '2:1', width: '100%'})).to.deep.equal({ height: 320, width: 640 }); + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: '4:1', width: '70%'})).to.deep.equal({ height: 112, width: 448 }); + }); + + it('should return the container height when smaller than the calculated height', function () { + expect(getPlayerSizeFromAspectRatio({ getContainer: () => testContainer }, {aspectratio: '1:1', width: '100%'})).to.deep.equal({ height: 480, width: 640 }); + }); + }); + describe('getSkipParams', function () { const getSkipParams = utils.getSkipParams; diff --git a/test/spec/modules/videoModule/submodules/videojsVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/videojsVideoProvider_spec.js index a7379ccbab2..80ddf50fe18 100644 --- a/test/spec/modules/videoModule/submodules/videojsVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/videojsVideoProvider_spec.js @@ -2,11 +2,12 @@ import { SETUP_COMPLETE, SETUP_FAILED } from 'libraries/video/constants/events.js'; +import { getWinDimensions } from '../../../../../src/utils'; const {VideojsProvider, utils} = require('modules/videojsVideoProvider'); const { - PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE, AD_POSITION + PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLCMT, VPAID_MIME_TYPE, AD_POSITION } = require('libraries/video/constants/ortb.js'); const videojs = require('video.js').default; @@ -139,7 +140,7 @@ describe('videojsProvider', function () { expect(video.playbackmethod).to.include(PLAYBACK_METHODS.CLICK_TO_PLAY); expect(video.playbackend).to.equal(1); expect(video.api).to.deep.equal([2]); - expect(video.placement).to.be.equal(PLACEMENT.INSTREAM); + expect(video.plcmt).to.be.equal(PLCMT.ACCOMPANYING_CONTENT); }); it('should populate oRTB Content', function () { @@ -293,31 +294,34 @@ describe('utils', function() { describe('getPositionCode', function() { it('should return the correct position when video is above the fold', function () { + const {innerWidth, innerHeight} = getWinDimensions(); const code = utils.getPositionCode({ - left: window.innerWidth / 10, + left: innerWidth / 10, top: 0, - width: window.innerWidth - window.innerWidth / 10, - height: window.innerHeight, + width: innerWidth - innerWidth / 10, + height: innerHeight, }) expect(code).to.equal(AD_POSITION.ABOVE_THE_FOLD) }); it('should return the correct position when video is below the fold', function () { + const {innerWidth, innerHeight} = getWinDimensions(); const code = utils.getPositionCode({ - left: window.innerWidth / 10, - top: window.innerHeight, - width: window.innerWidth - window.innerWidth / 10, - height: window.innerHeight / 2, + left: innerWidth / 10, + top: innerHeight, + width: innerWidth - innerWidth / 10, + height: innerHeight / 2, }) expect(code).to.equal(AD_POSITION.BELOW_THE_FOLD) }); it('should return the unkown position when the video is out of bounds', function () { + const {innerWidth, innerHeight} = getWinDimensions(); const code = utils.getPositionCode({ - left: window.innerWidth / 10, - top: window.innerHeight, - width: window.innerWidth, - height: window.innerHeight, + left: innerWidth / 10, + top: innerHeight, + width: innerWidth, + height: innerHeight, }) expect(code).to.equal(AD_POSITION.UNKNOWN) }); diff --git a/test/spec/modules/videoheroesBidAdapter_spec.js b/test/spec/modules/videoheroesBidAdapter_spec.js index 8f99ca4d17d..1bdbebf36ab 100644 --- a/test/spec/modules/videoheroesBidAdapter_spec.js +++ b/test/spec/modules/videoheroesBidAdapter_spec.js @@ -304,18 +304,18 @@ describe('VideoheroesBidAdapter', function() { creativeId: response_video.seatbid[0].bid[0].crid, dealId: response_video.seatbid[0].bid[0].dealid, mediaType: 'video', - vastUrl: response_video.seatbid[0].bid[0].adm + vastXml: response_video.seatbid[0].bid[0].adm } let videoResponses = spec.interpretResponse(videoResponse); expect(videoResponses).to.be.an('array').that.is.not.empty; let dataItem = videoResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastUrl', 'ttl', 'creativeId', + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'ttl', 'creativeId', 'netRevenue', 'currency', 'dealId', 'mediaType'); expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.vastUrl).to.equal(expectedBidResponse.vastUrl) + expect(dataItem.vastXml).to.equal(expectedBidResponse.vastXml) expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); expect(dataItem.netRevenue).to.be.true; diff --git a/test/spec/modules/videoreachBidAdapter_spec.js b/test/spec/modules/videoreachBidAdapter_spec.js index 67ad89eac3d..dc81ec74ff8 100644 --- a/test/spec/modules/videoreachBidAdapter_spec.js +++ b/test/spec/modules/videoreachBidAdapter_spec.js @@ -21,12 +21,12 @@ describe('videoreachBidAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'TagId': '' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/vidoomyBidAdapter_spec.js b/test/spec/modules/vidoomyBidAdapter_spec.js index 38fa872e6b8..6f7c0beb29f 100644 --- a/test/spec/modules/vidoomyBidAdapter_spec.js +++ b/test/spec/modules/vidoomyBidAdapter_spec.js @@ -44,9 +44,9 @@ describe('vidoomyBidAdapter', function() { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when mediaType is video with INSTREAM context and lacks playerSize property', function () { @@ -140,13 +140,11 @@ describe('vidoomyBidAdapter', function() { expect(request[1].url).to.equal(ENDPOINT); }); - it('only accepts first width and height sizes', function () { - expect('' + request[0].data.w).to.equal('300'); - expect('' + request[0].data.h).to.equal('250'); - expect('' + request[0].data.w).to.not.equal('200'); - expect('' + request[0].data.h).to.not.equal('100'); - expect('' + request[1].data.w).to.equal('400'); - expect('' + request[1].data.h).to.equal('225'); + it('accepts all width and height sizes', function () { + expect(request[0].data.w).to.equal('300,200'); + expect(request[0].data.h).to.equal('250,100'); + expect(request[1].data.w).to.equal('400'); + expect(request[1].data.h).to.equal('225'); }); it('should send id and pid parameters', function () { diff --git a/test/spec/modules/viewdeosDXBidAdapter_spec.js b/test/spec/modules/viewdeosDXBidAdapter_spec.js index 31df9244ada..b60037aab4a 100644 --- a/test/spec/modules/viewdeosDXBidAdapter_spec.js +++ b/test/spec/modules/viewdeosDXBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from 'modules/viewdeosDXBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import {cloneDeep} from 'lodash'; const ENDPOINT = 'https://ghb.sync.viewdeos.com/auction/'; @@ -176,13 +177,15 @@ describe('viewdeosDXBidAdapter', function () { describe('user syncs with both types', function () { it('should be returned if pixel and iframe enabled', function () { + const mockedServerResponse = cloneDeep(SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS); + mockedServerResponse.cookieURLs = ['link7', 'link8']; const syncs = spec.getUserSyncs({ iframeEnabled: true, - pixelEnabled: true - }, [{body: SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS}]); + pixelEnabled: true, + }, [{body: mockedServerResponse}]); - expect(syncs.map(s => s.url)).to.deep.equal(SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS.cookieURLs); - expect(syncs.map(s => s.type)).to.deep.equal(SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS.cookieURLSTypes); + expect(syncs.map(s => s.url)).to.deep.equal(mockedServerResponse.cookieURLs); + expect(syncs.map(s => s.type)).to.deep.equal(mockedServerResponse.cookieURLSTypes); }) }) diff --git a/test/spec/modules/viqeoBidAdapter_spec.js b/test/spec/modules/viqeoBidAdapter_spec.js index 8f597318af9..af397393a51 100644 --- a/test/spec/modules/viqeoBidAdapter_spec.js +++ b/test/spec/modules/viqeoBidAdapter_spec.js @@ -6,9 +6,7 @@ describe('viqeoBidAdapter', function () { expect(spec.isBidRequestValid({ bidder: 'viqeo', params: { - user: { - buyeruid: '1', - }, + tagId: '2', playerOptions: { videoId: 'ed584da454c7205ca7e4', profileId: 1382, @@ -27,9 +25,7 @@ describe('viqeoBidAdapter', function () { bidId: 'id1', bidder: 'viqeo', params: { - user: { - buyeruid: '1', - }, + tagId: '2', currency: 'EUR', floor: 0.5, playerOptions: { @@ -48,7 +44,7 @@ describe('viqeoBidAdapter', function () { expect(requestData.imp[0].bidfloor).to.equal(0.5); expect(requestData.imp[0].video.w).to.equal(240); expect(requestData.imp[0].video.h).to.equal(400); - expect(requestData.user.buyeruid).to.equal('1'); + expect(requestData.imp[0].tagid).to.equal('2'); }); it('build request check url', function () { const bidRequestData = [{ @@ -58,14 +54,13 @@ describe('viqeoBidAdapter', function () { videoId: 'ed584da454c7205ca7e4', profileId: 1382, }, - sspId: 42, }, mediaTypes: { video: { playerSize: [[240, 400]] } }, }]; const request = spec.buildRequests(bidRequestData); - expect(request[0].url).to.equal('https://ads.betweendigital.com/openrtb_bid/?sspId=42') + expect(request[0].url).to.equal('https://ad.vqserve.com/ads/prebid') }); it('response_params common case', function () { const bidRequestData = { diff --git a/test/spec/modules/visiblemeasuresBidAdapter_spec.js b/test/spec/modules/visiblemeasuresBidAdapter_spec.js index ad75e17699f..a6efeca73e2 100644 --- a/test/spec/modules/visiblemeasuresBidAdapter_spec.js +++ b/test/spec/modules/visiblemeasuresBidAdapter_spec.js @@ -3,11 +3,21 @@ import { spec } from '../../../modules/visiblemeasuresBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'visiblemeasures' +const bidder = 'visiblemeasures'; const adUrl = 'https://us-e.visiblemeasures.com/pbjs'; const syncUrl = 'https://cs.visiblemeasures.com'; describe('VisibleMeasuresBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; const bids = [ { bidId: getUniqueIdentifierStr(), @@ -18,8 +28,9 @@ describe('VisibleMeasuresBidAdapter', function () { } }, params: { - placementId: 'testBanner', - } + placementId: 'testBanner' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -32,8 +43,9 @@ describe('VisibleMeasuresBidAdapter', function () { } }, params: { - placementId: 'testVideo', - } + placementId: 'testVideo' + }, + userIdAsEids }, { bidId: getUniqueIdentifierStr(), @@ -55,8 +67,9 @@ describe('VisibleMeasuresBidAdapter', function () { } }, params: { - placementId: 'testNative', - } + placementId: 'testNative' + }, + userIdAsEids } ]; @@ -75,9 +88,20 @@ describe('VisibleMeasuresBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { - referer: 'https://test.com' + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } }, timeout: 500 }; @@ -114,6 +138,7 @@ describe('VisibleMeasuresBidAdapter', function () { expect(data).to.be.an('object'); expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', + 'device', 'language', 'secure', 'host', @@ -122,7 +147,11 @@ describe('VisibleMeasuresBidAdapter', function () { 'coppa', 'ccpa', 'gdpr', - 'tmax' + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' ); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); @@ -131,7 +160,7 @@ describe('VisibleMeasuresBidAdapter', function () { 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('string'); + 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); @@ -147,6 +176,56 @@ describe('VisibleMeasuresBidAdapter', function () { 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 + } + ]; + + let 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'); @@ -172,8 +251,10 @@ describe('VisibleMeasuresBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + 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; }); @@ -188,12 +269,38 @@ describe('VisibleMeasuresBidAdapter', function () { 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] + }; - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([], bidderRequest); + let serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); + 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]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + bidderRequest.ortb2; + }) }); describe('interpretResponse', function () { @@ -397,5 +504,17 @@ describe('VisibleMeasuresBidAdapter', function () { expect(syncData[0].url).to.be.a('string') expect(syncData[0].url).to.equal(`${syncUrl}/image?pbjs=1&ccpa_consent=1---&coppa=0`) }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal(`${syncUrl}/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0`); + }); }); }); diff --git a/test/spec/modules/vistarsBidAdapter_spec.js b/test/spec/modules/vistarsBidAdapter_spec.js new file mode 100644 index 00000000000..26be79e5e1a --- /dev/null +++ b/test/spec/modules/vistarsBidAdapter_spec.js @@ -0,0 +1,248 @@ +import { expect } from 'chai'; +import { spec } from 'modules/vistarsBidAdapter.js'; +import { deepClone } from 'src/utils.js'; + +describe('vistarsBidAdapterTests', function () { + let bidRequestData = { + bids: [ + { + adUnitCode: 'div-banner-id', + bidId: 'bid-123', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bidder: 'vistars', + params: { + source: 'ssp1', + }, + requestId: 'request-123', + } + ] + }; + + it('validate_pub_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'vistars', + params: { + source: 'ssp1', + } + }) + ).to.equal(true); + }); + + it('validate_generated_url', function () { + const request = spec.buildRequests(deepClone(bidRequestData.bids), { timeout: 1234 }); + let req_url = request[0].url; + + expect(req_url).to.equal('https://ex-asr.vistarsagency.com/bid?source=ssp1'); + }); + + it('validate_response_params', function () { + let serverResponse = { + body: { + id: 'bid123', + seatbid: [ + { + bid: [ + { + id: '1bh7jku7-ko2g-8654-ab72-h268abcde271', + impid: 'bid-123', + price: 0.6565, + adm: '

AD

', + adomain: ['abc.com'], + cid: '1242512', + crid: '535231', + w: 300, + h: 600, + mtype: 1, + ext: { + prebid: { + type: 'banner', + } + } + }, + ], + seat: '4212', + }, + ], + cur: 'EUR', + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + } + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('

AD

'); + expect(bid.cpm).to.equal(0.6565); + expect(bid.currency).to.equal('EUR'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(600); + expect(bid.creativeId).to.equal('535231'); + expect(bid.meta.advertiserDomains).to.deep.equal(['abc.com']); + }); + + it('validate_invalid_response', function () { + let serverResponse = { + body: {} + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + } + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(0); + }) + + if (FEATURES.VIDEO) { + it('video_bid', function () { + const bidRequest = deepClone(bidRequestData.bids); + bidRequest[0].mediaTypes = { + video: { + playerSize: [234, 765] + } + }; + + const request = spec.buildRequests(bidRequest, { timeout: 1234 }); + const vastXml = ''; + let serverResponse = { + body: { + id: 'bid123', + seatbid: [ + { + bid: [ + { + id: '1bh7jku7-ko2g-8654-ab72-h268abcde271', + impid: 'bid-123', + price: 0.6565, + adm: vastXml, + adomain: ['abc.com'], + cid: '1242512', + crid: '535231', + w: 300, + h: 600, + mtype: 1, + ext: { + prebid: { + type: 'banner', + } + } + }, + ], + seat: '4212', + }, + ], + cur: 'EUR', + } + }; + + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(vastXml); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(600); + }); + } +}); + +describe('getUserSyncs', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + + expect(spec.getUserSyncs(syncOptions)).to.deep.equal([]); + }); + + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({ + pixelEnabled: true, + }, {}, {}, '1---'); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.vistarsagency.com/match/sp?us_privacy=1---&gdpr_consent=') + }); + + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: true + }, + }, + } + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.vistarsagency.com/match/sp.ifr?us_privacy=&gdpr_consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=1') + }); + + it('Should return array of objects with proper sync config , include GDPR, no purpose', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: false + }, + }, + } + }, ''); + expect(syncData).is.empty; + }); + + it('Should return array of objects with proper sync config , GDPR not applies', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: false, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.vistarsagency.com/match/sp.ifr?us_privacy=&gdpr_consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=0') + }); +}) diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 5528705efd7..923ff7e86b2 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -4,6 +4,9 @@ import { config } from 'src/config.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; import { makeSlot } from '../integration/faker/googletag.js'; +import { mergeDeep } from '../../../src/utils.js'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency.js'; +import { addFPDToBidderRequest } from '../../helpers/fpd.js'; describe('VisxAdapter', function () { const adapter = newBidder(spec); @@ -32,21 +35,21 @@ describe('VisxAdapter', function () { }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'uid': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when uid can not be parsed as number', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'uid': 'sdvsdv' }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('it should fail on invalid video bid', function () { @@ -89,6 +92,46 @@ describe('VisxAdapter', function () { timeout: 3000, refererInfo: { page: 'https://example.com' + }, + ortb2: { + device: { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + site: { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } } }; const referrer = bidderRequest.refererInfo.page; @@ -109,7 +152,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + 'auctionId': '1d1a030790a475' }, { 'bidder': 'visx', @@ -120,7 +163,7 @@ describe('VisxAdapter', function () { 'sizes': [[728, 90], [300, 250]], 'bidId': '3150ccb55da321', 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + 'auctionId': '1d1a030790a475' }, { 'bidder': 'visx', @@ -131,7 +174,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '42dbe3a7168a6a', 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + 'auctionId': '1d1a030790a475' }, { 'bidder': 'visx', @@ -151,7 +194,7 @@ describe('VisxAdapter', function () { }, 'bidId': '39a4e3a7168a6a', 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + 'auctionId': '1d1a030790a475' } ]; @@ -217,7 +260,44 @@ describe('VisxAdapter', function () { 'tmax': 3000, 'cur': ['EUR'], 'source': {'ext': {'wrapperType': 'Prebid_js', 'wrapperVersion': '$prebid.version$'}}, - 'site': {'page': referrer} + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } }); }); @@ -235,13 +315,49 @@ describe('VisxAdapter', function () { 'tmax': 3000, 'cur': ['EUR'], 'source': {'ext': {'wrapperType': 'Prebid_js', 'wrapperVersion': '$prebid.version$'}}, - 'site': {'page': referrer} + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } }); }); it('should add currency from currency.bidderCurrencyDefault', function () { - const getConfigStub = sinon.stub(config, 'getConfig').callsFake( - arg => arg === 'currency.bidderCurrencyDefault.visx' ? 'GBP' : 'USD'); + config.setConfig({currency: {bidderCurrencyDefault: {visx: 'GBP'}}}) const request = spec.buildRequests(bidRequests, bidderRequest); const payload = parseRequest(request.url); expect(payload).to.be.an('object'); @@ -255,32 +371,62 @@ describe('VisxAdapter', function () { 'tmax': 3000, 'cur': ['GBP'], 'source': {'ext': {'wrapperType': 'Prebid_js', 'wrapperVersion': '$prebid.version$'}}, - 'site': {'page': referrer} + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } }); - getConfigStub.restore(); + config.resetConfig(); }); it('should add currency from currency.adServerCurrency', function () { - const getConfigStub = sinon.stub(config, 'getConfig').callsFake( - arg => arg === 'currency.bidderCurrencyDefault.visx' ? '' : 'USD'); - const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = parseRequest(request.url); - expect(payload).to.be.an('object'); - expect(payload).to.have.property('auids', '903535,903535,903536,903537'); - - const postData = request.data; - expect(postData).to.be.an('object'); - expect(postData).to.deep.equal({ - 'id': '22edbae2733bf6', - 'imp': expectedFullImps, - 'tmax': 3000, - 'cur': ['USD'], - 'source': {'ext': {'wrapperType': 'Prebid_js', 'wrapperVersion': '$prebid.version$'}}, - 'site': {'page': referrer} + setCurrencyConfig({ adServerCurrency: 'USD' }) + return addFPDToBidderRequest(bidderRequest).then(res => { + const request = spec.buildRequests(bidRequests, res); + const payload = parseRequest(request.url); + expect(payload).to.be.an('object'); + expect(payload).to.have.property('auids', '903535,903535,903536,903537'); + + const postData = request.data; + expect(postData).to.be.an('object'); + expect(postData.cur).to.deep.equal(['USD']); + setCurrencyConfig({}) }); - - getConfigStub.restore(); }); it('if gdprConsent is present payload must have gdpr params', function () { @@ -294,9 +440,50 @@ describe('VisxAdapter', function () { 'tmax': 3000, 'cur': ['EUR'], 'source': {'ext': {'wrapperType': 'Prebid_js', 'wrapperVersion': '$prebid.version$'}}, - 'site': {'page': referrer}, + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'regs': { + 'ext': { + 'gdpr': 1 + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + }, 'user': {'ext': {'consent': 'AAA'}}, - 'regs': {'ext': {'gdpr': 1}} }); }); @@ -311,7 +498,44 @@ describe('VisxAdapter', function () { 'tmax': 3000, 'cur': ['EUR'], 'source': {'ext': {'wrapperType': 'Prebid_js', 'wrapperVersion': '$prebid.version$'}}, - 'site': {'page': referrer}, + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + }, 'user': {'ext': {'consent': 'AAA'}}, 'regs': {'ext': {'gdpr': 0}} }); @@ -328,7 +552,44 @@ describe('VisxAdapter', function () { 'tmax': 3000, 'cur': ['EUR'], 'source': {'ext': {'wrapperType': 'Prebid_js', 'wrapperVersion': '$prebid.version$'}}, - 'site': {'page': referrer}, + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + }, 'user': {'ext': {'consent': 'AAA'}}, 'regs': {'ext': {'gdpr': 1}} }); @@ -359,7 +620,44 @@ describe('VisxAdapter', function () { 'schain': schainObject } }, - 'site': {'page': referrer}, + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } }); }); @@ -408,7 +706,44 @@ describe('VisxAdapter', function () { 'wrapperVersion': '$prebid.version$' } }, - 'site': {'page': referrer}, + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + }, 'user': {'ext': {'eids': eids}} }); }); @@ -429,7 +764,44 @@ describe('VisxAdapter', function () { 'wrapperVersion': '$prebid.version$' } }, - 'site': {'page': referrer} + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } }); }); }); @@ -448,6 +820,46 @@ describe('VisxAdapter', function () { timeout: 3000, refererInfo: { page: 'https://example.com' + }, + 'ortb2': { + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } } }; const referrer = bidderRequest.refererInfo.page; @@ -467,7 +879,7 @@ describe('VisxAdapter', function () { }, 'bidId': '39aff3a7169a6a', 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a476', + 'auctionId': '1d1a030790a476' } ]; @@ -494,7 +906,6 @@ describe('VisxAdapter', function () { const payload = parseRequest(request.url); expect(payload).to.be.an('object'); expect(payload).to.have.property('auids', '903538'); - const postData = request.data; expect(postData).to.be.an('object'); expect(postData).to.deep.equal({ @@ -512,7 +923,50 @@ describe('VisxAdapter', function () { 'wrapperVersion': '$prebid.version$' } }, - 'site': {'page': referrer} + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + }, + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ + '124' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '124' + ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ + '99' + ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + } }); }); }); @@ -531,6 +985,46 @@ describe('VisxAdapter', function () { timeout: 3000, refererInfo: { page: 'https://example.com' + }, + ortb2: { + device: { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + site: { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } } }; const referrer = bidderRequest.refererInfo.page; @@ -544,7 +1038,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + 'auctionId': '1d1a030790a475' }, { 'bidder': 'visx', @@ -555,7 +1049,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + 'auctionId': '1d1a030790a475' } ]; let sandbox; @@ -612,7 +1106,44 @@ describe('VisxAdapter', function () { 'wrapperVersion': '$prebid.version$' } }, - 'site': {'page': referrer} + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } }); }); @@ -641,7 +1172,44 @@ describe('VisxAdapter', function () { 'wrapperVersion': '$prebid.version$' } }, - 'site': {'page': referrer} + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } }); }); }); @@ -669,7 +1237,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '300bfeb0d71a5b', 'bidderRequestId': '5f2009617a7c0a', - 'auctionId': '1cbd2feafe5e8b', + 'auctionId': '1cbd2feafe5e8b' } ]; const request = spec.buildRequests(bidRequests); @@ -719,7 +1287,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '300bfeb0d71a5b', 'bidderRequestId': '2c2bb1972df9a', - 'auctionId': '1fa09aee5c8c99', + 'auctionId': '1fa09aee5c8c99' }, { 'bidder': 'visx', @@ -730,7 +1298,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '4dff80cc4ee346', 'bidderRequestId': '2c2bb1972df9a', - 'auctionId': '1fa09aee5c8c99', + 'auctionId': '1fa09aee5c8c99' }, { 'bidder': 'visx', @@ -741,7 +1309,7 @@ describe('VisxAdapter', function () { 'sizes': [[728, 90]], 'bidId': '5703af74d0472a', 'bidderRequestId': '2c2bb1972df9a', - 'auctionId': '1fa09aee5c8c99', + 'auctionId': '1fa09aee5c8c99' } ]; const request = spec.buildRequests(bidRequests); @@ -823,7 +1391,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '300bfeb0d71a5b', 'bidderRequestId': '5f2009617a7c0a', - 'auctionId': '1cbd2feafe5e8b', + 'auctionId': '1cbd2feafe5e8b' } ]; const getConfigStub = sinon.stub(config, 'getConfig').returns('PLN'); @@ -888,7 +1456,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '300bfeb0d71321', 'bidderRequestId': '2c2bb1972d23af', - 'auctionId': '1fa09aee5c84d34', + 'auctionId': '1fa09aee5c84d34' }, { 'bidder': 'visx', @@ -899,7 +1467,7 @@ describe('VisxAdapter', function () { 'sizes': [[728, 90]], 'bidId': '300bfeb0d7183bb', 'bidderRequestId': '2c2bb1972d23af', - 'auctionId': '1fa09aee5c84d34', + 'auctionId': '1fa09aee5c84d34' } ]; const request = spec.buildRequests(bidRequests); @@ -925,7 +1493,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '2164be6358b9', 'bidderRequestId': '106efe3247', - 'auctionId': '32a1f276cb87cb8', + 'auctionId': '32a1f276cb87cb8' }, { 'bidder': 'visx', @@ -936,7 +1504,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '326bde7fbf69', 'bidderRequestId': '106efe3247', - 'auctionId': '32a1f276cb87cb8', + 'auctionId': '32a1f276cb87cb8' }, { 'bidder': 'visx', @@ -947,7 +1515,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '4e111f1b66e4', 'bidderRequestId': '106efe3247', - 'auctionId': '32a1f276cb87cb8', + 'auctionId': '32a1f276cb87cb8' }, { 'bidder': 'visx', @@ -958,7 +1526,7 @@ describe('VisxAdapter', function () { 'sizes': [[728, 90]], 'bidId': '26d6f897b516', 'bidderRequestId': '106efe3247', - 'auctionId': '32a1f276cb87cb8', + 'auctionId': '32a1f276cb87cb8' }, { 'bidder': 'visx', @@ -969,7 +1537,7 @@ describe('VisxAdapter', function () { 'sizes': [[728, 90]], 'bidId': '1751cd90161', 'bidderRequestId': '106efe3247', - 'auctionId': '32a1f276cb87cb8', + 'auctionId': '32a1f276cb87cb8' } ]; const request = spec.buildRequests(bidRequests); @@ -1075,7 +1643,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '5126e301f4be', 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', + 'auctionId': '35bcbc0f7e79c' }, { 'bidder': 'visx', @@ -1086,7 +1654,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '57b2ebe70e16', 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', + 'auctionId': '35bcbc0f7e79c' }, { 'bidder': 'visx', @@ -1097,7 +1665,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '225fcd44b18c', 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', + 'auctionId': '35bcbc0f7e79c' } ]; const request = spec.buildRequests(bidRequests); @@ -1162,7 +1730,7 @@ describe('VisxAdapter', function () { 'sizes': [[400, 300]], 'bidId': '2164be6358b9', 'bidderRequestId': '106efe3247', - 'auctionId': '32a1f276cb87cb8', + 'auctionId': '32a1f276cb87cb8' } ]; const request = spec.buildRequests(bidRequests); @@ -1214,7 +1782,7 @@ describe('VisxAdapter', function () { 'sizes': [[400, 300]], 'bidId': '2164be6358b9', 'bidderRequestId': '106efe3247', - 'auctionId': '32a1f276cb87cb8', + 'auctionId': '32a1f276cb87cb8' } ]; const request = spec.buildRequests(bidRequests); @@ -1251,7 +1819,7 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '300bfeb0d71a5b', 'bidderRequestId': '5f2009617a7c0a', - 'auctionId': '1cbd2feafe5e8b', + 'auctionId': '1cbd2feafe5e8b' } ]; const request = spec.buildRequests(bidRequests); @@ -1434,13 +2002,53 @@ describe('VisxAdapter', function () { 'sizes': [[300, 250], [300, 600]], 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + 'auctionId': '1d1a030790a475' } ]; const bidderRequest = { timeout: 3000, refererInfo: { page: 'https://example.com' + }, + 'ortb2': { + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + } } }; @@ -1511,4 +2119,183 @@ describe('VisxAdapter', function () { expect(request.data.user.ext.vads).to.be.a('string'); }); }); + + describe('ortb2 data', function () { + const bidRequests = [ + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + } + ]; + const bidderRequest = { + timeout: 3000, + refererInfo: { + page: 'https://example.com' + }, + 'ortb2': { + 'device': { + 'w': 1259, + 'h': 934, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '124' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '124' ] + }, + { + 'brand': 'Not-A.Brand', + 'version': [ '99' ] + } + ], + 'mobile': 0 + }, + 'ext': { + 'cdep': 'treatment_1.1' + } + }, + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html' + }, + 'user': { + 'keywords': 'x,y', + 'data': [ + { + 'name': 'exampleprovider.de', + 'ext': { + 'segtax': 5 + }, + 'segment': [ + { + 'id': '1' + } + ] + }, + { + 'ext': { + 'segtax': 601, + 'segclass': '5' + }, + 'segment': [ + { + 'id': '140' + } + ], + 'name': 'pa.openx.net' + }, + { + 'ext': { + 'segtax': 601, + 'segclass': '5' + }, + 'segment': [ + { + 'id': '140' + } + ], + 'name': 'ads.pubmatic.com' + } + ], + 'ext': { + 'data': { + 'registered': true, + 'interests': [ + 'ads' + ] + } + } + } + } + }; + + it('should pass interests if ortb2 has interests in user data', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.user.ext.data.interests).not.to.be.undefined; + }); + + it('should pass device if ortb2 has device', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.device).not.to.be.undefined; + }); + + it('should pass site if ortb2 has site', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.site).not.to.be.undefined; + }); + + it('should merge if user object exists', function () { + const user = { + 'ext': { + 'vads': 'cXaIRA425BmynEN1ratEnc_5e', + 'data': { + 'registered': true, + 'interests': [ + 'ads' + ] + } + }, + 'keywords': 'x,y', + 'data': [ + { + 'name': 'exampleprovider.de', + 'ext': { + 'segtax': 5 + }, + 'segment': [ + { + 'id': '1' + } + ] + } + ] + }; + const userOrtb2 = { + 'keywords': 'x,y', + 'data': [ + { + 'name': 'exampleprovider.de', + 'ext': { + 'segtax': 5 + }, + 'segment': [ + { + 'id': '1' + } + ] + } + ], + 'ext': { + 'data': { + 'registered': true, + 'interests': [ + 'ads' + ] + } + } + } + const userReq = mergeDeep(user, userOrtb2); + expect(userReq.ext.vads).not.to.be.undefined; + }); + }); }); diff --git a/test/spec/modules/voxBidAdapter_spec.js b/test/spec/modules/voxBidAdapter_spec.js index 5f4ada06c65..65752837b23 100644 --- a/test/spec/modules/voxBidAdapter_spec.js +++ b/test/spec/modules/voxBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai' import { spec } from 'modules/voxBidAdapter.js' -import {config} from 'src/config.js' +import { setConfig as setCurrencyConfig } from '../../../modules/currency' +import { addFPDToBidderRequest } from '../../helpers/fpd' function getSlotConfigs(mediaTypes, params) { return { @@ -247,14 +248,17 @@ describe('VOX Adapter', function() { }) it('should request floor price in adserver currency', function () { - const configCurrency = 'DKK' - config.setConfig({ currency: { adServerCurrency: configCurrency } }) - const request = spec.buildRequests([ getBidWithFloor() ], bidderRequest) - const data = JSON.parse(request.data) - data.bidRequests.forEach(bid => { - expect(bid.floorInfo.currency).to.equal(configCurrency) - }) - }) + const configCurrency = 'DKK'; + setCurrencyConfig({ adServerCurrency: configCurrency }); + return addFPDToBidderRequest(bidderRequest).then(res => { + const request = spec.buildRequests([ getBidWithFloor() ], res) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.floorInfo.currency).to.equal(configCurrency) + }) + setCurrencyConfig({}); + }); + }); function getBidWithFloor(floor) { return { diff --git a/test/spec/modules/weboramaRtdProvider_spec.js b/test/spec/modules/weboramaRtdProvider_spec.js index d562d9ffd13..964c42eff07 100644 --- a/test/spec/modules/weboramaRtdProvider_spec.js +++ b/test/spec/modules/weboramaRtdProvider_spec.js @@ -1509,7 +1509,7 @@ describe('weboramaRtdProvider', function() { } }; const data = { - webo_cs: ['foo', 'bar'], + webo_cs: [12, 345], webo_audiences: ['baz'], }; @@ -1620,7 +1620,7 @@ describe('weboramaRtdProvider', function() { }; const data = { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], }; const entry = { @@ -1746,7 +1746,7 @@ describe('weboramaRtdProvider', function() { }; const data = { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], }; const entry = { @@ -1868,7 +1868,7 @@ describe('weboramaRtdProvider', function() { }; const data = { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], }; const entry = { @@ -2013,7 +2013,7 @@ describe('weboramaRtdProvider', function() { }; const data = { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], }; const entry = { @@ -2156,7 +2156,7 @@ describe('weboramaRtdProvider', function() { }; const data = { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], }; const entry = { @@ -2217,7 +2217,7 @@ describe('weboramaRtdProvider', function() { }; const data = { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], }; const entry = { @@ -2289,7 +2289,7 @@ describe('weboramaRtdProvider', function() { expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ foo: ['bar'], webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], }); expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ inventory: { @@ -2312,7 +2312,7 @@ describe('weboramaRtdProvider', function() { it('should use default profile in case of nothing on local storage', function() { const defaultProfile = { - webo_audiences: ['baz'] + webo_audiences: [12345] }; const moduleConfig = { params: { @@ -2377,9 +2377,85 @@ describe('weboramaRtdProvider', function() { }) }); + it('should use default profile in case of something malformed on local storage', function() { + const defaultProfile = { + webo_audiences: [12345] + }; + const moduleConfig = { + params: { + weboUserDataConf: { + accoundId: 12345, + setPrebidTargeting: true, + defaultProfile: defaultProfile, + } + } + }; + + const malformed = { + targeting: { + webo_audiences: 'must be an array, not a string', + }, + }; + + sandbox.stub(storage, 'hasLocalStorage').returns(true); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(malformed)); + + const adUnitCode = 'adunit1'; + const reqBidsConfigObj = { + ortb2Fragments: { + global: {}, + bidder: {} + }, + adUnits: [{ + code: adUnitCode, + bids: [{ + bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData([adUnitCode], moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': defaultProfile, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params).to.be.undefined; + expect(reqBidsConfigObj.adUnits[0].bids[1].params).to.be.undefined; + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(defaultProfile); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.be.undefined; + ['smartadserver', 'pubmatic', 'appnexus', 'rubicon', 'other'].forEach((v) => { + expect(reqBidsConfigObj.ortb2Fragments.bidder[v]).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); + }) + }); + it('should use default profile if cant read from local storage', function() { const defaultProfile = { - webo_audiences: ['baz'] + webo_audiences: [12345] }; let onDataResponse = {}; const moduleConfig = { @@ -2489,7 +2565,7 @@ describe('weboramaRtdProvider', function() { }; const data = { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], }; const entry = { @@ -2550,7 +2626,7 @@ describe('weboramaRtdProvider', function() { expect(targeting).to.deep.equal({ 'adunit1': { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], webo_foo: ['bar'], }, 'adunit2': data, @@ -2569,7 +2645,7 @@ describe('weboramaRtdProvider', function() { ext: { data: { webo_cs: ['foo', 'bar'], - webo_audiences: ['baz'], + webo_audiences: [12345], webo_bar: ['baz'], } }, diff --git a/test/spec/modules/winrBidAdapter_spec.js b/test/spec/modules/winrBidAdapter_spec.js index 95d1473d1cb..b0d8d72f0a1 100644 --- a/test/spec/modules/winrBidAdapter_spec.js +++ b/test/spec/modules/winrBidAdapter_spec.js @@ -93,22 +93,22 @@ describe('WinrAdapter', function () { }); it('should return false when mediaType is not banner', function () { - let bid = Object.assign({}, bid); - delete bid.mediaTypes; - bid.mediaTypes = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.mediaTypes; + invalidBid.mediaTypes = { 'video': {} }; - expect(getMediaTypeFromBid(bid)).to.not.equal('banner'); - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(getMediaTypeFromBid(invalidBid)).to.not.equal('banner'); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = { 'placementId': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/wipesBidAdapter_spec.js b/test/spec/modules/wipesBidAdapter_spec.js index a458dcf69c8..a45e324f4fd 100644 --- a/test/spec/modules/wipesBidAdapter_spec.js +++ b/test/spec/modules/wipesBidAdapter_spec.js @@ -29,9 +29,9 @@ describe('wipesBidAdapter', function () { }); it('should return false when require params are not passed', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + let invalidBid = Object.assign({}, bid); + invalidBid.params = {}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); }); }); diff --git a/test/spec/modules/wurflRtdProvider_spec.js b/test/spec/modules/wurflRtdProvider_spec.js new file mode 100644 index 00000000000..fa6ea2e642f --- /dev/null +++ b/test/spec/modules/wurflRtdProvider_spec.js @@ -0,0 +1,489 @@ +import { + bidderData, + enrichBidderRequest, + lowEntropyData, + wurflSubmodule, + makeOrtb2DeviceType, + toNumber, +} from 'modules/wurflRtdProvider'; +import * as ajaxModule from 'src/ajax'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; + +describe('wurflRtdProvider', function () { + describe('wurflSubmodule', function () { + const altHost = 'http://example.local/wurfl.js'; + + const wurfl_pbjs = { + low_entropy_caps: ['is_mobile', 'complete_device_name', 'form_factor'], + caps: ['advertised_browser', 'advertised_browser_version', 'advertised_device_os', 'advertised_device_os_version', 'ajax_support_javascript', 'brand_name', 'complete_device_name', 'density_class', 'form_factor', 'is_android', 'is_app_webview', 'is_connected_tv', 'is_full_desktop', 'is_ios', 'is_mobile', 'is_ott', 'is_phone', 'is_robot', 'is_smartphone', 'is_smarttv', 'is_tablet', 'manufacturer_name', 'marketing_name', 'max_image_height', 'max_image_width', 'model_name', 'physical_screen_height', 'physical_screen_width', 'pixel_density', 'pointing_method', 'resolution_height', 'resolution_width'], + authorized_bidders: { + bidder1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], + bidder2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 25, 28, 30, 31] + } + } + const WURFL = { + advertised_browser: 'Chrome Mobile', + advertised_browser_version: '130.0.0.0', + advertised_device_os: 'Android', + advertised_device_os_version: '6.0', + ajax_support_javascript: !0, + brand_name: 'Google', + complete_device_name: 'Google Nexus 5', + density_class: '3.0', + form_factor: 'Feature Phone', + is_android: !0, + is_app_webview: !1, + is_connected_tv: !1, + is_full_desktop: !1, + is_ios: !1, + is_mobile: !0, + is_ott: !1, + is_phone: !0, + is_robot: !1, + is_smartphone: !1, + is_smarttv: !1, + is_tablet: !1, + manufacturer_name: 'LG', + marketing_name: '', + max_image_height: 640, + max_image_width: 360, + model_name: 'Nexus 5', + physical_screen_height: 110, + physical_screen_width: 62, + pixel_density: 443, + pointing_method: 'touchscreen', + resolution_height: 1920, + resolution_width: 1080, + wurfl_id: 'lg_nexus5_ver1', + }; + + // expected analytics values + const expectedStatsURL = 'https://prebid.wurflcloud.com/v1/prebid/stats'; + const expectedData = JSON.stringify({ bidders: ['bidder1', 'bidder2'] }); + + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + window.WURFLPromises = { + init: new Promise(function (resolve, reject) { resolve({ WURFL, wurfl_pbjs }) }), + complete: new Promise(function (resolve, reject) { resolve({ WURFL, wurfl_pbjs }) }), + }; + }); + + afterEach(() => { + // Restore the original functions + sandbox.restore(); + window.WURFLPromises = undefined; + }); + + // Bid request config + const reqBidsConfigObj = { + adUnits: [{ + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' }, + { bidder: 'bidder3' }, + ] + }], + ortb2Fragments: { + global: { + device: {}, + }, + bidder: {}, + } + }; + + it('initialises the WURFL RTD provider', function () { + expect(wurflSubmodule.init()).to.be.true; + }); + + it('should enrich the bid request data', (done) => { + const expectedURL = new URL(altHost); + expectedURL.searchParams.set('debug', true); + expectedURL.searchParams.set('mode', 'prebid'); + expectedURL.searchParams.set('wurfl_id', true); + + const callback = () => { + const v = { + bidder1: { + device: { + make: 'Google', + model: 'Nexus 5', + devicetype: 1, + os: 'Android', + osv: '6.0', + hwv: 'Nexus 5', + h: 1920, + w: 1080, + ppi: 443, + pxratio: 3.0, + js: 1, + ext: { + wurfl: { + advertised_browser: 'Chrome Mobile', + advertised_browser_version: '130.0.0.0', + advertised_device_os: 'Android', + advertised_device_os_version: '6.0', + ajax_support_javascript: !0, + brand_name: 'Google', + complete_device_name: 'Google Nexus 5', + density_class: '3.0', + form_factor: 'Feature Phone', + is_app_webview: !1, + is_connected_tv: !1, + is_full_desktop: !1, + is_mobile: !0, + is_ott: !1, + is_phone: !0, + is_robot: !1, + is_smartphone: !1, + is_smarttv: !1, + is_tablet: !1, + manufacturer_name: 'LG', + marketing_name: '', + max_image_height: 640, + max_image_width: 360, + model_name: 'Nexus 5', + physical_screen_height: 110, + physical_screen_width: 62, + pixel_density: 443, + pointing_method: 'touchscreen', + resolution_height: 1920, + resolution_width: 1080, + wurfl_id: 'lg_nexus5_ver1', + }, + }, + }, + }, + bidder2: { + device: { + make: 'Google', + model: 'Nexus 5', + devicetype: 1, + os: 'Android', + osv: '6.0', + hwv: 'Nexus 5', + h: 1920, + w: 1080, + ppi: 443, + pxratio: 3.0, + js: 1, + ext: { + wurfl: { + advertised_device_os: 'Android', + advertised_device_os_version: '6.0', + ajax_support_javascript: !0, + brand_name: 'Google', + complete_device_name: 'Google Nexus 5', + density_class: '3.0', + form_factor: 'Feature Phone', + is_android: !0, + is_app_webview: !1, + is_connected_tv: !1, + is_full_desktop: !1, + is_ios: !1, + is_mobile: !0, + is_ott: !1, + is_phone: !0, + is_tablet: !1, + manufacturer_name: 'LG', + model_name: 'Nexus 5', + pixel_density: 443, + resolution_height: 1920, + resolution_width: 1080, + wurfl_id: 'lg_nexus5_ver1', + }, + }, + }, + }, + bidder3: { + device: { + make: 'Google', + model: 'Nexus 5', + ext: { + wurfl: { + complete_device_name: 'Google Nexus 5', + form_factor: 'Feature Phone', + is_mobile: !0, + model_name: 'Nexus 5', + brand_name: 'Google', + wurfl_id: 'lg_nexus5_ver1', + }, + }, + }, + }, + }; + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal(v); + done(); + }; + + const config = { + params: { + altHost: altHost, + debug: true, + } + }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + expect(loadExternalScriptStub.calledOnce).to.be.true; + const loadExternalScriptCall = loadExternalScriptStub.getCall(0); + expect(loadExternalScriptCall.args[0]).to.equal(expectedURL.toString()); + expect(loadExternalScriptCall.args[2]).to.equal('wurfl'); + }); + + it('onAuctionEndEvent: should send analytics data using navigator.sendBeacon, if available', () => { + const auctionDetails = {}; + const config = {}; + const userConsent = {}; + + const sendBeaconStub = sandbox.stub(navigator, 'sendBeacon'); + + // Call the function + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Assertions + expect(sendBeaconStub.calledOnce).to.be.true; + expect(sendBeaconStub.calledWithExactly(expectedStatsURL, expectedData)).to.be.true; + }); + + it('onAuctionEndEvent: should send analytics data using fetch as fallback, if navigator.sendBeacon is not available', () => { + const auctionDetails = {}; + const config = {}; + const userConsent = {}; + + const sendBeaconStub = sandbox.stub(navigator, 'sendBeacon').value(undefined); + const windowFetchStub = sandbox.stub(window, 'fetch'); + const fetchAjaxStub = sandbox.stub(ajaxModule, 'fetch'); + + // Call the function + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Assertions + expect(sendBeaconStub.called).to.be.false; + + expect(fetchAjaxStub.calledOnce).to.be.true; + const fetchAjaxCall = fetchAjaxStub.getCall(0); + expect(fetchAjaxCall.args[0]).to.equal(expectedStatsURL); + expect(fetchAjaxCall.args[1].method).to.equal('POST'); + expect(fetchAjaxCall.args[1].body).to.equal(expectedData); + expect(fetchAjaxCall.args[1].mode).to.equal('no-cors'); + }); + }); + + describe('bidderData', () => { + it('should return the WURFL data for a bidder', () => { + const wjsData = { + capability1: 'value1', + capability2: 'value2', + capability3: 'value3', + }; + const caps = ['capability1', 'capability2', 'capability3']; + const filter = [0, 2]; + + const result = bidderData(wjsData, caps, filter); + + expect(result).to.deep.equal({ + capability1: 'value1', + capability3: 'value3', + }); + }); + + it('should return an empty object if the filter is empty', () => { + const wjsData = { + capability1: 'value1', + capability2: 'value2', + capability3: 'value3', + }; + const caps = ['capability1', 'capability3']; + const filter = []; + + const result = bidderData(wjsData, caps, filter); + + expect(result).to.deep.equal({}); + }); + }); + + describe('lowEntropyData', () => { + it('should return the correct low entropy data for Apple devices', () => { + const wjsData = { + complete_device_name: 'Apple iPhone X', + form_factor: 'Smartphone', + is_mobile: !0, + brand_name: 'Apple', + model_name: 'iPhone X', + }; + const lowEntropyCaps = ['complete_device_name', 'form_factor', 'is_mobile']; + const expectedData = { + complete_device_name: 'Apple iPhone', + form_factor: 'Smartphone', + is_mobile: !0, + brand_name: 'Apple', + model_name: 'iPhone', + }; + const result = lowEntropyData(wjsData, lowEntropyCaps); + expect(result).to.deep.equal(expectedData); + }); + + it('should return the correct low entropy data for Android devices', () => { + const wjsData = { + complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', + form_factor: 'Smartphone', + is_mobile: !0, + }; + const lowEntropyCaps = ['complete_device_name', 'form_factor', 'is_mobile']; + const expectedData = { + complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', + form_factor: 'Smartphone', + is_mobile: !0, + }; + const result = lowEntropyData(wjsData, lowEntropyCaps); + expect(result).to.deep.equal(expectedData); + }); + + it('should return an empty object if the lowEntropyCaps array is empty', () => { + const wjsData = { + complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', + form_factor: 'Smartphone', + is_mobile: !0, + }; + const lowEntropyCaps = []; + const expectedData = {}; + const result = lowEntropyData(wjsData, lowEntropyCaps); + expect(result).to.deep.equal(expectedData); + }); + }); + + describe('enrichBidderRequest', () => { + it('should enrich the bidder request with WURFL data', () => { + const reqBidsConfigObj = { + ortb2Fragments: { + global: { + device: {}, + }, + bidder: { + exampleBidder: { + device: { + ua: 'user-agent', + } + } + } + } + }; + const bidderCode = 'exampleBidder'; + const wjsData = { + capability1: 'value1', + capability2: 'value2' + }; + + enrichBidderRequest(reqBidsConfigObj, bidderCode, wjsData); + + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({ + exampleBidder: { + device: { + ua: 'user-agent', + ext: { + wurfl: { + capability1: 'value1', + capability2: 'value2' + } + } + } + } + }); + }); + }); + + describe('makeOrtb2DeviceType', function () { + it('should return 1 when wurflData is_mobile and is_phone is true', function () { + const wurflData = { is_mobile: true, is_phone: true, is_tablet: false }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.equal(1); + }); + + it('should return 1 when wurflData is_mobile and is_tablet is true', function () { + const wurflData = { is_mobile: true, is_phone: false, is_tablet: true }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.equal(1); + }); + + it('should return 6 when wurflData is_mobile but is_phone and is_tablet are false', function () { + const wurflData = { is_mobile: true, is_phone: false, is_tablet: false }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.equal(6); + }); + + it('should return 2 when wurflData is_full_desktop is true', function () { + const wurflData = { is_full_desktop: true }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.equal(2); + }); + + it('should return 3 when wurflData is_connected_tv is true', function () { + const wurflData = { is_connected_tv: true }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.equal(3); + }); + + it('should return 4 when wurflData is_phone is true and is_mobile is false or undefined', function () { + const wurflData = { is_phone: true }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.equal(4); + }); + + it('should return 5 when wurflData is_tablet is true and is_mobile is false or undefined', function () { + const wurflData = { is_tablet: true }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.equal(5); + }); + + it('should return 7 when wurflData is_ott is true', function () { + const wurflData = { is_ott: true }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.equal(7); + }); + + it('should return undefined when wurflData is_mobile is true but is_phone and is_tablet are missing', function () { + const wurflData = { is_mobile: true }; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.be.undefined; + }); + + it('should return undefined when no conditions are met', function () { + const wurflData = {}; + const result = makeOrtb2DeviceType(wurflData); + expect(result).to.be.undefined; + }); + }); + + describe('toNumber', function () { + it('converts valid numbers', function () { + expect(toNumber(42)).to.equal(42); + expect(toNumber(3.14)).to.equal(3.14); + expect(toNumber('100')).to.equal(100); + expect(toNumber('3.14')).to.equal(3.14); + expect(toNumber(' 50 ')).to.equal(50); + }); + + it('converts booleans correctly', function () { + expect(toNumber(true)).to.equal(1); + expect(toNumber(false)).to.equal(0); + }); + + it('handles special cases', function () { + expect(toNumber(null)).to.be.undefined; + expect(toNumber('')).to.be.undefined; + }); + + it('returns undefined for non-numeric values', function () { + expect(toNumber('abc')).to.be.undefined; + expect(toNumber(undefined)).to.be.undefined; + expect(toNumber(NaN)).to.be.undefined; + expect(toNumber({})).to.be.undefined; + expect(toNumber([1, 2, 3])).to.be.undefined; + // WURFL.js cannot return [] so it is safe to not handle and return undefined + expect(toNumber([])).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/xeBidAdapter_spec.js b/test/spec/modules/xeBidAdapter_spec.js index 914b0cacd71..c4e91d9943c 100644 --- a/test/spec/modules/xeBidAdapter_spec.js +++ b/test/spec/modules/xeBidAdapter_spec.js @@ -1,16 +1,21 @@ -import { expect } from 'chai'; -import { config } from 'src/config.js'; -import { spec, getBidFloor } from 'modules/xeBidAdapter.js'; -import { deepClone } from 'src/utils'; -import { createEidsArray } from 'modules/userId/eids.js'; +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec} from 'modules/xeBidAdapter.js'; +import {deepClone} from 'src/utils'; +import {getBidFloor} from '../../../libraries/xeUtils/bidderUtils.js'; const ENDPOINT = 'https://pbjs.xe.works/bid'; const defaultRequest = { + tmax: 0, adUnitCode: 'test', bidId: '1', requestId: 'qwerty', - auctionId: 'auctionId', + ortb2: { + source: { + tid: 'auctionId' + } + }, ortb2Imp: { ext: { tid: 'tr1', @@ -27,7 +32,7 @@ const defaultRequest = { bidder: 'xe', params: { env: 'xe', - placement: 'test-banner', + pid: '40', ext: {} }, bidRequestsCount: 1 @@ -41,6 +46,17 @@ defaultRequestVideo.mediaTypes = { skipppable: true } }; + +const videoBidderRequest = { + bidderCode: 'xe', + bids: [{mediaTypes: {video: {}}, bidId: 'qwerty'}] +}; + +const displayBidderRequest = { + bidderCode: 'xe', + bids: [{bidId: 'qwerty'}] +}; + describe('xeBidAdapter', () => { describe('isBidRequestValid', function () { it('should return false when request params is missing', function () { @@ -55,9 +71,9 @@ describe('xeBidAdapter', () => { expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); }); - it('should return false when required placement param is missing', function () { + it('should return false when required pid param is missing', function () { const invalidRequest = deepClone(defaultRequest); - delete invalidRequest.params.placement; + delete invalidRequest.params.pid; expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); }); @@ -80,30 +96,29 @@ describe('xeBidAdapter', () => { it('should send request with correct structure', function () { const request = spec.buildRequests([defaultRequest], {}); expect(request.method).to.equal('POST'); - expect(request.url).to.equal(ENDPOINT); + expect(request.url).to.equal(ENDPOINT + '/bid'); expect(request.options).to.have.property('contentType').and.to.equal('application/json'); expect(request).to.have.property('data'); }); it('should build basic request structure', function () { const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('tmax').and.to.equal(defaultRequest.tmax); expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); - expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.auctionId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); expect(request).to.have.property('bc').and.to.equal(1); expect(request).to.have.property('floor').and.to.equal(null); - expect(request).to.have.property('banner').and.to.deep.equal({ sizes: [[300, 250], [300, 200]] }); - expect(request).to.have.property('gdprApplies').and.to.equal(0); - expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprConsent').and.to.deep.equal({}); expect(request).to.have.property('userEids').and.to.deep.equal([]); expect(request).to.have.property('usPrivacy').and.to.equal(''); - expect(request).to.have.property('coppa').and.to.equal(0); expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); expect(request).to.have.property('ext').and.to.deep.equal({}); expect(request).to.have.property('env').and.to.deep.equal({ env: 'xe', - placement: 'test-banner' + pid: '40' }); expect(request).to.have.property('device').and.to.deep.equal({ ua: navigator.userAgent, @@ -186,23 +201,11 @@ describe('xeBidAdapter', () => { it('should build request with valid bidfloor', function () { const bfRequest = deepClone(defaultRequest); - bfRequest.getFloor = () => ({ floor: 5, currency: 'USD' }); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; expect(request).to.have.property('floor').and.to.equal(5); }); - it('should build request with gdpr consent data if applies', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'qwerty' - } - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('gdprApplies').and.equals(1); - expect(request).to.have.property('consentString').and.equals('qwerty'); - }); - it('should build request with usp consent data if applies', function () { const bidderRequest = { uspConsent: '1YA-' @@ -211,19 +214,11 @@ describe('xeBidAdapter', () => { expect(request).to.have.property('usPrivacy').and.equals('1YA-'); }); - it('should build request with coppa 1', function () { - config.setConfig({ - coppa: true - }); - const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; - expect(request).to.have.property('coppa').and.equals(1); - }); - it('should build request with extended ids', function () { const idRequest = deepClone(defaultRequest); idRequest.userIdAsEids = [ - { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, - { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} ]; const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); @@ -263,19 +258,19 @@ describe('xeBidAdapter', () => { height: 250, ttl: 600, meta: { - advertiserDomains: ['xe.works'] + advertiserDomains: ['xe'] }, ext: { pixels: [ - [ 'iframe', 'surl1' ], - [ 'image', 'surl2' ], + ['iframe', 'surl1'], + ['image', 'surl2'], ] } }] } }; - const validResponse = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: displayBidderRequest}); const bid = validResponse[0]; expect(validResponse).to.be.an('array').that.is.not.empty; expect(bid.requestId).to.equal('qwerty'); @@ -284,7 +279,7 @@ describe('xeBidAdapter', () => { expect(bid.width).to.equal(300); expect(bid.height).to.equal(250); expect(bid.ttl).to.equal(600); - expect(bid.meta).to.deep.equal({ advertiserDomains: ['xe.works'] }); + expect(bid.meta).to.deep.equal({advertiserDomains: ['xe']}); }); it('should interpret valid banner response', function () { @@ -305,7 +300,7 @@ describe('xeBidAdapter', () => { } }; - const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: displayBidderRequest}); const bid = validResponseBanner[0]; expect(validResponseBanner).to.be.an('array').that.is.not.empty; expect(bid.mediaType).to.equal('banner'); @@ -331,7 +326,7 @@ describe('xeBidAdapter', () => { } }; - const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequestVideo }); + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: videoBidderRequest}); const bid = validResponseBanner[0]; expect(validResponseBanner).to.be.an('array').that.is.not.empty; expect(bid.mediaType).to.equal('video'); @@ -358,8 +353,8 @@ describe('xeBidAdapter', () => { requestId: 'qwerty', ext: { pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], ] } }] @@ -377,8 +372,8 @@ describe('xeBidAdapter', () => { requestId: 'qwerty', ext: { pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], ] } }] @@ -396,8 +391,8 @@ describe('xeBidAdapter', () => { requestId: 'qwerty', ext: { pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], ] } }] @@ -414,20 +409,20 @@ describe('xeBidAdapter', () => { describe('getBidFloor', function () { it('should return null when getFloor is not a function', () => { - const bid = { getFloor: 2 }; + const bid = {getFloor: 2}; const result = getBidFloor(bid); expect(result).to.be.null; }); it('should return null when getFloor doesnt return an object', () => { - const bid = { getFloor: () => 2 }; + const bid = {getFloor: () => 2}; const result = getBidFloor(bid); expect(result).to.be.null; }); it('should return null when floor is not a number', () => { const bid = { - getFloor: () => ({ floor: 'string', currency: 'USD' }) + getFloor: () => ({floor: 'string', currency: 'USD'}) }; const result = getBidFloor(bid); expect(result).to.be.null; @@ -435,7 +430,7 @@ describe('xeBidAdapter', () => { it('should return null when currency is not USD', () => { const bid = { - getFloor: () => ({ floor: 5, currency: 'EUR' }) + getFloor: () => ({floor: 5, currency: 'EUR'}) }; const result = getBidFloor(bid); expect(result).to.be.null; @@ -443,10 +438,10 @@ describe('xeBidAdapter', () => { it('should return floor value when everything is correct', () => { const bid = { - getFloor: () => ({ floor: 5, currency: 'USD' }) + getFloor: () => ({floor: 5, currency: 'USD'}) }; const result = getBidFloor(bid); expect(result).to.equal(5); }); }); -}) +}); diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahooAdsBidAdapter_spec.js similarity index 94% rename from test/spec/modules/yahoosspBidAdapter_spec.js rename to test/spec/modules/yahooAdsBidAdapter_spec.js index 40dc2b3c63b..f8ed7693f3f 100644 --- a/test/spec/modules/yahoosspBidAdapter_spec.js +++ b/test/spec/modules/yahooAdsBidAdapter_spec.js @@ -1,8 +1,9 @@ import { expect } from 'chai'; import { config } from 'src/config.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; -import { spec } from 'modules/yahoosspBidAdapter.js'; +import { spec } from 'modules/yahooAdsBidAdapter.js'; import {createEidsArray} from '../../../modules/userId/eids'; +import {deepAccess} from '../../../src/utils'; const DEFAULT_BID_ID = '84ab500420319d'; const DEFAULT_BID_DCN = '2093845709823475'; @@ -193,7 +194,7 @@ describe('Yahoo Advertising Bid Adapter:', () => { }); it('should define the correct bidder aliases', () => { - expect(spec.aliases).to.deep.equal(['yahoossp', 'yahooAdvertising']); + expect(spec.aliases).to.deep.equal([{ 'code': 'yahoossp', 'gvlid': 25 }, { 'code': 'yahooAdvertising', 'gvlid': 25 }]); }); it('should define the correct vendor ID', () => { @@ -1169,7 +1170,8 @@ describe('Yahoo Advertising Bid Adapter:', () => { pos: undefined, playbackmethod: undefined, rewarded: undefined, - placement: undefined + placement: undefined, + plcmt: undefined }); }); @@ -1201,7 +1203,8 @@ describe('Yahoo Advertising Bid Adapter:', () => { pos: undefined, playbackmethod: undefined, rewarded: undefined, - placement: undefined + placement: undefined, + plcmt: undefined }); }); }); @@ -1298,7 +1301,8 @@ describe('Yahoo Advertising Bid Adapter:', () => { pos: undefined, playbackmethod: undefined, rewarded: undefined, - placement: undefined + placement: undefined, + plcmt: undefined }); }); }); @@ -1351,7 +1355,8 @@ describe('Yahoo Advertising Bid Adapter:', () => { pos: undefined, playbackmethod: undefined, rewarded: undefined, - placement: undefined + placement: undefined, + plcmt: undefined }); }); }); @@ -1381,7 +1386,8 @@ describe('Yahoo Advertising Bid Adapter:', () => { pos: 123456, playbackmethod: 1, rewarded: 1, - placement: 1 + placement: 1, + plcmt: 1 } } } @@ -1410,7 +1416,8 @@ describe('Yahoo Advertising Bid Adapter:', () => { delivery: 1, pos: 123456, playbackmethod: 1, - placement: 1 + placement: 1, + plcmt: 1 } const validBidRequests = [bidRequest]; bidderRequest.bids = validBidRequests; @@ -1430,6 +1437,7 @@ describe('Yahoo Advertising Bid Adapter:', () => { pos: 123456, playbackmethod: 1, placement: 1, + plcmt: 1, rewarded: undefined }); }); @@ -1488,15 +1496,6 @@ describe('Yahoo Advertising Bid Adapter:', () => { expect(response[0].mediaType).to.equal('video'); }) - it('should insert video VAST win notification into vastUrl', () => { - const { serverResponse, bidderRequest } = generateResponseMock('video', 'vast'); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ad).to.be.undefined; - expect(response[0].vastUrl).to.equal('https://yahoo.com?event=adAttempt'); - expect(response[0].vastXml).to.equal(''); - expect(response[0].mediaType).to.equal('video'); - }) - describe('wrapped in video players for display inventory', () => { beforeEach(() => { config.setConfig({ @@ -1621,5 +1620,74 @@ describe('Yahoo Advertising Bid Adapter:', () => { expect(response[0].bidderCode).to.be.undefined; }); }); + + describe('Renderer:', () => { + it('should create and set renderer when bidder request context is outstream, bidder request renderer is falsy, and bid response mediaType is VIDEO', () => { + config.setConfig({ + yahooAds: { + mode: VIDEO + } + }); + const { serverResponse, bidderRequest } = generateResponseMock('video', 'vast'); + bidderRequest.mediaTypes = { + video: { + context: 'outstream' + } + }; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(deepAccess(bidderRequest, 'mediaTypes.video.context')).to.equal('outstream'); + expect(bidderRequest.renderer).to.be.undefined; + expect(response[0].mediaType).to.equal('video'); + expect(response[0].renderer).to.not.be.undefined; + }); + + it('should not create and set renderer when bidder request renderer is falsy and bid response mediaType is VIDEO, but bidder request context is not outstream,', () => { + config.setConfig({ + yahooAds: { + mode: VIDEO + } + }); + const { serverResponse, bidderRequest } = generateResponseMock('video', 'vast'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(deepAccess(bidderRequest, 'mediaTypes.video.context')).to.not.equal('outstream'); + expect(bidderRequest.renderer).to.be.undefined; + expect(response[0].mediaType).to.equal('video'); + expect(response[0].renderer).to.be.undefined; + }); + + it('should not create and set renderer when bidder request context is outstream and bid response mediaType is VIDEO, but bidder request renderer is not falsy', () => { + config.setConfig({ + yahooAds: { + mode: VIDEO + } + }); + const { serverResponse, bidderRequest } = generateResponseMock('video', 'vast'); + bidderRequest.mediaTypes = { + video: { + context: 'outstream' + } + }; + bidderRequest.renderer = 'not falsy'; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(deepAccess(bidderRequest, 'mediaTypes.video.context')).to.equal('outstream'); + expect(bidderRequest.renderer).to.not.be.undefined; + expect(response[0].mediaType).to.equal('video'); + expect(response[0].renderer).to.be.undefined; + }); + + it('should not create and set renderer when bidder request context is outstream and bidder request renderer is falsy, but bid response mediaType is not VIDEO', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + bidderRequest.mediaTypes = { + video: { + context: 'outstream' + } + }; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(deepAccess(bidderRequest, 'mediaTypes.video.context')).to.equal('outstream'); + expect(bidderRequest.renderer).to.be.undefined; + expect(response[0].mediaType).to.not.equal('video'); + expect(response[0].renderer).to.be.undefined; + }); + }); }); }); diff --git a/test/spec/modules/yandexAnalyticsAdapter_spec.js b/test/spec/modules/yandexAnalyticsAdapter_spec.js index ca9b29d13a5..a7a850b7cf2 100644 --- a/test/spec/modules/yandexAnalyticsAdapter_spec.js +++ b/test/spec/modules/yandexAnalyticsAdapter_spec.js @@ -1,7 +1,8 @@ import * as sinon from 'sinon'; -import yandexAnalytics, { EVENTS_TO_TRACK } from 'modules/yandexAnalyticsAdapter.js'; +import yandexAnalytics, { EVENTS_TO_TRACK, PBJS_INIT_EVENT_NAME } from 'modules/yandexAnalyticsAdapter.js'; import * as log from '../../../src/utils.js' import * as events from '../../../src/events.js'; +import * as globalUtils from '../../../src/prebidGlobal.js'; describe('Yandex analytics adapter testing', () => { const sandbox = sinon.createSandbox(); @@ -11,6 +12,13 @@ describe('Yandex analytics adapter testing', () => { let onEvent; const counterId = 123; const counterWindowKey = 'yaCounter123'; + const prebidVersion = '123.0'; + const prebidInitEvent = { + event: PBJS_INIT_EVENT_NAME, + data: { + version: prebidVersion, + }, + } beforeEach(() => { yandexAnalytics.counters = {}; @@ -19,6 +27,9 @@ describe('Yandex analytics adapter testing', () => { yandexAnalytics.oneCounterInited = false; clock = sinon.useFakeTimers(); logError = sandbox.stub(log, 'logError'); + sandbox.stub(globalUtils, 'getGlobal').returns({ + version: prebidVersion, + }); sandbox.stub(log, 'logInfo'); getEvents = sandbox.stub(events, 'getEvents').returns([]); onEvent = sandbox.stub(events, 'on'); @@ -86,12 +97,15 @@ describe('Yandex analytics adapter testing', () => { eventType: 'Some_untracked_event', } ]); - const eventsToSend = [{ - event: EVENTS_TO_TRACK[0], - data: { - eventType: EVENTS_TO_TRACK[0], + const eventsToSend = [ + prebidInitEvent, + { + event: EVENTS_TO_TRACK[0], + data: { + eventType: EVENTS_TO_TRACK[0], + } } - }]; + ]; yandexAnalytics.enableAnalytics({ options: { @@ -139,9 +153,12 @@ describe('Yandex analytics adapter testing', () => { clock.tick(2001); const [ sentEvents ] = counterPbjsMethod.getCall(0).args; - chai.expect(sentEvents).to.deep.equal([{ - event, - data: {}, - }]); + chai.expect(sentEvents).to.deep.equal([ + prebidInitEvent, + { + event, + data: {}, + } + ]); }); }); diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js index 140be4121ec..15b000f562f 100644 --- a/test/spec/modules/yandexBidAdapter_spec.js +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -1,8 +1,9 @@ import { assert, expect } from 'chai'; import { NATIVE_ASSETS, spec } from 'modules/yandexBidAdapter.js'; import * as utils from 'src/utils.js'; -import { config } from '../../../src/config'; +import { setConfig as setCurrencyConfig } from '../../../modules/currency'; import { BANNER, NATIVE } from '../../../src/mediaTypes'; +import { addFPDToBidderRequest } from '../../helpers/fpd'; describe('Yandex adapter', function () { describe('isBidRequestValid', function () { @@ -41,6 +42,49 @@ describe('Yandex adapter', function () { }); describe('buildRequests', function () { + let mockBidRequests; + let mockBidderRequest; + + beforeEach(function () { + mockBidRequests = [{ + bidId: 'bid123', + params: { + placementId: 'R-I-123456-2', + } + }]; + mockBidderRequest = { + ortb2: { + device: { + language: 'fr' + }, + site: { + ext: { + data: { + documentLang: 'en' + } + } + } + } + }; + }); + + it('should set site.content.language from document language if it is not set', function () { + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + expect(requests[0].data.site.content.language).to.equal('en'); + }); + + it('should preserve existing site.content.language if it is set', function () { + mockBidderRequest.ortb2.site.content = {language: 'es'}; + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + expect(requests[0].data.site.content.language).to.equal('es'); + }); + + it('should do nothing when document language does not exist', function () { + delete mockBidderRequest.ortb2.site.ext.data.documentLang; + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + expect(requests[0].data.site?.content?.language).to.be.undefined; + }); + /** @type {import('../../../src/auction').BidderRequest} */ const bidderRequest = { ortb2: { @@ -125,19 +169,21 @@ describe('Yandex adapter', function () { }); it('should send currency if defined', function () { - config.setConfig({ - currency: { - adServerCurrency: 'USD' - } + setCurrencyConfig({ + adServerCurrency: 'USD' }); const bannerRequest = getBidRequest(); - const requests = spec.buildRequests([bannerRequest], bidderRequest); - const { url } = requests[0]; - const parsedRequestUrl = utils.parseUrl(url); - const { search: query } = parsedRequestUrl - expect(query['ssp-cur']).to.equal('USD'); + return addFPDToBidderRequest(bidderRequest).then(res => { + const requests = spec.buildRequests([bannerRequest], res); + const { url } = requests[0]; + const parsedRequestUrl = utils.parseUrl(url); + const { search: query } = parsedRequestUrl + + expect(query['ssp-cur']).to.equal('USD'); + setCurrencyConfig({}); + }); }); it('should send eids and ortb2 user data if defined', function() { diff --git a/test/spec/modules/yandexIdSystem_spec.js b/test/spec/modules/yandexIdSystem_spec.js new file mode 100644 index 00000000000..593ce0841d4 --- /dev/null +++ b/test/spec/modules/yandexIdSystem_spec.js @@ -0,0 +1,201 @@ +// @ts-check + +import { + BIDDER_EID_KEY, + YANDEX_ID_KEY, + YANDEX_EXT_COOKIE_NAMES, + BIDDER_CODE, + YANDEX_USER_ID_KEY, + YANDEX_STORAGE_TYPE, + YANDEX_MIN_EXPIRE_DAYS, + PREBID_STORAGE, + yandexIdSubmodule, +} from '../../../modules/yandexIdSystem.js'; +import {attachIdSystem} from '../../../modules/userId/index.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; +import {createSandbox} from 'sinon' +import * as utils from '../../../src/utils.js'; + +/** + * @typedef {import('sinon').SinonStub} SinonStub + * @typedef {import('sinon').SinonSpy} SinonSpy + * @typedef {import('sinon').SinonSandbox} SinonSandbox + */ + +const MIN_METRICA_ID_LEN = 17; + +/** @satisfies {import('../../../modules/userId/index.js').SubmoduleConfig} */ +const CORRECT_SUBMODULE_CONFIG = { + name: BIDDER_CODE, + storage: { + expires: YANDEX_MIN_EXPIRE_DAYS, + name: YANDEX_USER_ID_KEY, + type: YANDEX_STORAGE_TYPE, + refreshInSeconds: undefined, + }, + params: undefined, + value: undefined, +}; + +/** @type {import('../../../modules/userId/index.js').SubmoduleConfig[]} */ +const INCORRECT_SUBMODULE_CONFIGS = [ + { + ...CORRECT_SUBMODULE_CONFIG, + storage: { + ...CORRECT_SUBMODULE_CONFIG.storage, + expires: 0, + } + }, + { + ...CORRECT_SUBMODULE_CONFIG, + storage: { + ...CORRECT_SUBMODULE_CONFIG.storage, + type: 'html5' + } + }, + { + ...CORRECT_SUBMODULE_CONFIG, + storage: { + ...CORRECT_SUBMODULE_CONFIG.storage, + name: 'custom_key' + } + }, +]; + +const YANDEX_EXT_COOKIE_VALUE = 'ext_cookie_value'; + +describe('YandexId module', () => { + /** @type {SinonSandbox} */ + let sandbox; + /** @type {SinonStub} */ + let getCryptoRandomValuesStub; + /** @type {SinonStub} */ + let randomStub; + /** @type {SinonStub} */ + let getCookieStub; + /** @type {SinonStub} */ + let cookiesAreEnabledStub; + /** @type {SinonSpy} */ + let logErrorSpy; + + beforeEach(() => { + sandbox = createSandbox(); + getCookieStub = sandbox.stub(PREBID_STORAGE, 'getCookie').returns(YANDEX_EXT_COOKIE_VALUE); + cookiesAreEnabledStub = sandbox.stub(PREBID_STORAGE, 'cookiesAreEnabled'); + logErrorSpy = sandbox.spy(utils, 'logError'); + + getCryptoRandomValuesStub = sandbox + .stub(window.crypto, 'getRandomValues') + .callsFake((bufferView) => { + if (bufferView != null) { + bufferView[0] = 10000; + } + + return null; + }); + randomStub = sandbox.stub(window.Math, 'random').returns(0.555); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('getId()', () => { + it('user id matches Yandex Metrica format', () => { + const generatedId = yandexIdSubmodule.getId(CORRECT_SUBMODULE_CONFIG)?.id; + + expect(isNaN(Number(generatedId))).to.be.false; + expect(generatedId).to.have.length.greaterThanOrEqual( + MIN_METRICA_ID_LEN + ); + }); + + it('uses stored id', () => { + const storedId = '11111111111111111'; + const generatedId = yandexIdSubmodule.getId(CORRECT_SUBMODULE_CONFIG, undefined, storedId)?.id; + + expect(generatedId).to.be.equal(storedId); + }) + + describe('config validation', () => { + INCORRECT_SUBMODULE_CONFIGS.forEach((config, i) => { + it(`invalid config #${i} fails`, () => { + const generatedId = yandexIdSubmodule.getId(config)?.id; + + expect(generatedId).to.be.undefined; + expect(logErrorSpy.called).to.be.true; + }) + }) + }) + + describe('crypto', () => { + it('uses Math.random when crypto is not available', () => { + const cryptoTmp = window.crypto; + + // @ts-expect-error -- Crypto is always defined in modern JS. TS yells when trying to delete non-nullable property. + delete window.crypto; + + yandexIdSubmodule.getId(CORRECT_SUBMODULE_CONFIG); + + expect(randomStub.calledOnce).to.be.true; + expect(getCryptoRandomValuesStub.called).to.be.false; + + window.crypto = cryptoTmp; + }); + + it('uses crypto when it is available', () => { + yandexIdSubmodule.getId(CORRECT_SUBMODULE_CONFIG); + + expect(randomStub.called).to.be.false; + expect(getCryptoRandomValuesStub.calledOnce).to.be.true; + }); + }); + }); + + describe('decode()', () => { + it('should not transform value', () => { + const value = 'test value'; + + expect(yandexIdSubmodule.decode(value).yandexId).to.equal(value); + }); + }); + + describe('eid', () => { + const id = '11111111111111111'; + const userId = { [YANDEX_ID_KEY]: id }; + + before(() => { + attachIdSystem(yandexIdSubmodule); + }); + + it('with enabled cookies', () => { + cookiesAreEnabledStub.returns(true); + const [eid] = createEidsArray(userId); + + expect(eid).to.deep.equal({ + source: BIDDER_EID_KEY, + uids: [{ + id, + atype: 1, + ext: YANDEX_EXT_COOKIE_NAMES.reduce((acc, cookieName) => ({ + ...acc, + [cookieName]: YANDEX_EXT_COOKIE_VALUE, + }), {}), + }] + }); + }); + + it('with disabled cookies', () => { + cookiesAreEnabledStub.returns(false); + const [eid] = createEidsArray(userId); + + expect(eid).to.deep.equal({ + source: BIDDER_EID_KEY, + uids: [{ + id, + atype: 1, + }] + }); + }); + }); +}); diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index 225186dd326..92fd20fb37d 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -263,7 +263,7 @@ const REQPARAMS = { const REQPARAMS_GDPR = Object.assign({}, REQPARAMS, { gdpr: true, - consent: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA', + gdpr_consent: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA', }); const REQPARAMS_IAB_CONTENT = Object.assign({}, REQPARAMS, { @@ -457,8 +457,8 @@ describe('yieldlabBidAdapter', () => { }, }); - expect(gdprRequest.url).to.include('consent=BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA'); - expect(gdprRequest.url).to.include('gdpr=true'); + expect(gdprRequest.url).to.include('&gdpr_consent=BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA'); + expect(gdprRequest.url).to.include('&gdpr=true'); }); describe('sizes handling', () => { @@ -682,7 +682,7 @@ describe('yieldlabBidAdapter', () => { expect(result[0].netRevenue).to.equal(false); expect(result[0].ttl).to.equal(300); expect(result[0].referrer).to.equal(''); - expect(result[0].meta.advertiserDomains).to.equal('yieldlab'); + expect(result[0].meta.advertiserDomains).to.deep.equal(['yieldlab']); expect(result[0].ad).to.include('" }); adResponse.ad = ""; - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - assert.ok(doc.write.calledWith(adResponse.ad), 'ad was written to doc'); - assert.ok(doc.close.called, 'close method called'); + return renderAd(doc, bidId).then(() => { + assert.ok(doc.write.calledWith(adResponse.ad), 'ad was written to doc'); + assert.ok(doc.close.called, 'close method called'); + }) }); it('should place the url inside an iframe on the doc', function () { pushBidResponseToAuction({ adUrl: 'http://server.example.com/ad/ad.js' }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(doc.createElement, 'iframe'); + return renderAd(doc, bidId).then(() => { + sinon.assert.calledWith(doc.createElement, 'iframe'); + }); }); it('should log an error when no ad or url', function () { pushBidResponseToAuction({}); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.called(spyLogError); + return renderAd(doc, bidId).then(() => { + sinon.assert.called(spyLogError); + }); }); it('should log an error when not in an iFrame', function () { @@ -1287,17 +1346,29 @@ describe('Unit: Prebid Module', function () { ad: "" }); inIframe = false; - $$PREBID_GLOBAL$$.renderAd(document, bidId); - const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`; - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + return renderAd(document, bidId).then(() => { + const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`; + assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + }); + }); + + it('should emit AD_RENDER_SUCCEEDED', () => { + sandbox.stub(events, 'emit'); + pushBidResponseToAuction({ + ad: "" + }); + return renderAd(document, bidId).then(() => { + sinon.assert.calledWith(events.emit, EVENTS.AD_RENDER_SUCCEEDED, sinon.match({adId: bidId})); + }); }); it('should not render videos', function () { pushBidResponseToAuction({ mediatype: 'video' }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.notCalled(doc.write); + return renderAd(doc, bidId).then(() => { + sinon.assert.notCalled(doc.write); + }); }); it('should catch errors thrown when trying to write ads to the page', function () { @@ -1307,51 +1378,57 @@ describe('Unit: Prebid Module', function () { var error = { message: 'doc write error' }; doc.write = sinon.stub().throws(error); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var errorMessage = `Error rendering ad (id: ${bidId}): doc write error` - assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); + return renderAd(doc, bidId).then(() => { + var errorMessage = `Error rendering ad (id: ${bidId}): doc write error` + assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); + }); }); it('should log an error when ad not found', function () { var fakeId = 99; - $$PREBID_GLOBAL$$.renderAd(doc, fakeId); - var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'` - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + return renderAd(doc, fakeId).then(() => { + var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'` + assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + }); }); it('should save bid displayed to winning bid', function () { pushBidResponseToAuction({ ad: "" }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); + return renderAd(doc, bidId).then(() => { + assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); + }); }); - it('fires billing url if present on s2s bid', function () { - const burl = 'http://www.example.com/burl'; + it('fires impression trackers if present', function () { + const url = 'http://www.example.com/burl'; pushBidResponseToAuction({ ad: '
ad
', source: 's2s', - burl + eventtrackers: [ + {event: 1, method: 1, url} + ] }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - - sinon.assert.calledOnce(triggerPixelStub); - sinon.assert.calledWith(triggerPixelStub, burl); + return renderAd(doc, bidId).then(() => { + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, url); + }); }); it('should call addWinningBid', function () { pushBidResponseToAuction({ ad: "" }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var message = 'Calling renderAd with adId :' + bidId; - sinon.assert.calledWith(spyLogMessage, message); + return renderAd(doc, bidId).then(() => { + var message = 'Calling renderAd with adId :' + bidId; + sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + }); }); it('should warn stale rendering', function () { @@ -1368,38 +1445,36 @@ describe('Unit: Prebid Module', function () { }); // First render should pass with no warning and added to winning bids - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.neverCalledWith(spyLogWarn, warning); - - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - - sinon.assert.calledWith(onWonEvent, adResponse); - sinon.assert.notCalled(onStaleEvent); - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); - - // Reset call history for spies and stubs - spyLogMessage.resetHistory(); - spyLogWarn.resetHistory(); - spyAddWinningBid.resetHistory(); - onWonEvent.resetHistory(); - onStaleEvent.resetHistory(); - - // Second render should have a warning but still added to winning bids - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.calledWith(spyLogWarn, warning); - - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - - sinon.assert.calledWith(onWonEvent, adResponse); - sinon.assert.calledWith(onStaleEvent, adResponse); - - // Clean up - $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent); - $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent); + return renderAd(doc, bidId).then(() => { + sinon.assert.calledWith(spyLogMessage, message); + sinon.assert.neverCalledWith(spyLogWarn, warning); + + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + + sinon.assert.calledWith(onWonEvent, adResponse); + sinon.assert.notCalled(onStaleEvent); + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + + // Reset call history for spies and stubs + spyLogMessage.resetHistory(); + spyLogWarn.resetHistory(); + spyAddWinningBid.resetHistory(); + onWonEvent.resetHistory(); + onStaleEvent.resetHistory(); + doc.write.resetHistory(); + return renderAd(doc, bidId); + }).then(() => { + // Second render should have a warning but still be rendered + sinon.assert.calledWith(spyLogMessage, message); + sinon.assert.calledWith(spyLogWarn, warning); + sinon.assert.calledWith(onStaleEvent, adResponse); + sinon.assert.called(doc.write); + + // Clean up + $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent); + $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent); + }); }); it('should stop stale rendering', function () { @@ -1419,38 +1494,40 @@ describe('Unit: Prebid Module', function () { }); // First render should pass with no warning and added to winning bids - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.neverCalledWith(spyLogWarn, warning); + return renderAd(doc, bidId).then(() => { + sinon.assert.calledWith(spyLogMessage, message); + sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); - sinon.assert.calledWith(onWonEvent, adResponse); - sinon.assert.notCalled(onStaleEvent); + sinon.assert.calledWith(onWonEvent, adResponse); + sinon.assert.notCalled(onStaleEvent); - // Reset call history for spies and stubs - spyLogMessage.resetHistory(); - spyLogWarn.resetHistory(); - spyAddWinningBid.resetHistory(); - onWonEvent.resetHistory(); - onStaleEvent.resetHistory(); + // Reset call history for spies and stubs + spyLogMessage.resetHistory(); + spyLogWarn.resetHistory(); + spyAddWinningBid.resetHistory(); + onWonEvent.resetHistory(); + onStaleEvent.resetHistory(); - // Second render should have a warning and do not proceed further - $$PREBID_GLOBAL$$.renderAd(doc, bidId); - sinon.assert.calledWith(spyLogMessage, message); - sinon.assert.calledWith(spyLogWarn, warning); + // Second render should have a warning and do not proceed further + return renderAd(doc, bidId); + }).then(() => { + sinon.assert.calledWith(spyLogMessage, message); + sinon.assert.calledWith(spyLogWarn, warning); - sinon.assert.notCalled(spyAddWinningBid); + sinon.assert.notCalled(spyAddWinningBid); - sinon.assert.notCalled(onWonEvent); - sinon.assert.calledWith(onStaleEvent, adResponse); + sinon.assert.notCalled(onWonEvent); + sinon.assert.calledWith(onStaleEvent, adResponse); - // Clean up - $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent); - $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent); - configObj.setConfig({'auctionOptions': {}}); + // Clean up + $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent); + $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent); + configObj.setConfig({'auctionOptions': {}}); + }); }); }); @@ -1501,7 +1578,7 @@ describe('Unit: Prebid Module', function () { 'start': 1000 }]; - let spec, indexStub, auction, completeAuction; + let spec, indexStub, auction, completeAuction, auctionStarted; beforeEach(function () { logMessageSpy = sinon.spy(utils, 'logMessage'); @@ -1526,12 +1603,16 @@ describe('Unit: Prebid Module', function () { completeAuction = (bidsReceived) => { bidsReceived.forEach((bid) => addBidResponse(bid.adUnitCode, Object.assign(createBid(), bid))); bidRequests.forEach((req) => adapterDone.call(req)); + return auction.end; } }) const origNewAuction = auctionModule.newAuction; - sinon.stub(auctionModule, 'newAuction').callsFake(function (opts) { - auction = origNewAuction(opts); - return auction; + auctionStarted = new Promise((resolve) => { + sinon.stub(auctionModule, 'newAuction').callsFake(function (opts) { + auction = origNewAuction(opts); + resolve(auction); + return auction; + }) }) spec = { code: BIDDER_CODE, @@ -1559,19 +1640,24 @@ describe('Unit: Prebid Module', function () { utils.logMessage.restore(); }); - it('should execute callback after timeout', function () { + async function runAuction(request = {}) { + $$PREBID_GLOBAL$$.requestBids(request); + await auctionStarted; + } + + it('should execute callback after timeout', async function () { let requestObj = { bidsBackHandler: sinon.stub(), timeout: 2000, adUnits: adUnits }; + await runAuction(requestObj); - $$PREBID_GLOBAL$$.requestBids(requestObj); let re = new RegExp('^Auction [a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12} timedOut$'); - clock.tick(requestObj.timeout - 1); + await clock.tick(requestObj.timeout - 1); assert.ok(logMessageSpy.neverCalledWith(sinon.match(re)), 'executeCallback not called'); - clock.tick(1); + await clock.tick(1); assert.ok(logMessageSpy.calledWith(sinon.match(re)), 'executeCallback called'); expect(requestObj.bidsBackHandler.getCall(0).args[1]).to.equal(true, @@ -1580,7 +1666,7 @@ describe('Unit: Prebid Module', function () { sinon.assert.called(spec.onTimeout); }); - it('should execute `onSetTargeting` after setTargetingForGPTAsync', function () { + it('should execute `onSetTargeting` after setTargetingForGPTAsync', async function () { const bidId = 1; const auctionId = 1; let adResponse = Object.assign({ @@ -1606,8 +1692,8 @@ describe('Unit: Prebid Module', function () { adUnits: adUnits }; - $$PREBID_GLOBAL$$.requestBids(requestObj); - completeAuction([adResponse]); + await runAuction(requestObj); + await completeAuction([adResponse]); $$PREBID_GLOBAL$$.setTargetingForGPTAsync(); sinon.assert.called(spec.onSetTargeting); @@ -1687,8 +1773,8 @@ describe('Unit: Prebid Module', function () { }) }) - it('should transfer ttlBuffer to adUnit.ttlBuffer', () => { - $$PREBID_GLOBAL$$.requestBids({ + it('should transfer ttlBuffer to adUnit.ttlBuffer', async () => { + await runAuction({ ttlBuffer: 123, adUnits: [adUnits[0], {...adUnits[0], ttlBuffer: 0}] }); @@ -1707,20 +1793,21 @@ describe('Unit: Prebid Module', function () { sandbox.restore(); }); describe('bidRequests is empty', function () { - it('should log warning message and execute callback if bidRequests is empty', function () { - let bidsBackHandler = function bidsBackHandlerCallback() {}; + it('should log warning message and execute callback if bidRequests is empty', async function () { + let bidsBackHandler = function bidsBackHandlerCallback() { + }; let spyExecuteCallback = sinon.spy(bidsBackHandler); let logWarnSpy = sandbox.spy(utils, 'logWarn'); - $$PREBID_GLOBAL$$.requestBids({ + await $$PREBID_GLOBAL$$.requestBids({ adUnits: [ { code: 'test1', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [], }, { code: 'test2', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [], } ], @@ -1733,11 +1820,13 @@ describe('Unit: Prebid Module', function () { }); describe('starts auction', () => { - let startAuctionStub; + let startAuctionStub, auctionStarted, __started; function saHook(fn, ...args) { + __started(); return startAuctionStub(...args); } beforeEach(() => { + auctionStarted = new Promise(resolve => { __started = resolve }); startAuctionStub = sinon.stub(); pbjsModule.startAuction.before(saHook); configObj.resetConfig(); @@ -1749,6 +1838,11 @@ describe('Unit: Prebid Module', function () { configObj.resetConfig(); }); + async function runAuction(request = {}) { + $$PREBID_GLOBAL$$.requestBids(request); + await auctionStarted; + } + describe('with FPD', () => { let globalFPD, auctionFPD, mergedFPD; beforeEach(() => { @@ -1777,25 +1871,49 @@ describe('Unit: Prebid Module', function () { }; }); - it('merged from setConfig and requestBids', () => { + it('merged from setConfig and requestBids', async () => { configObj.setConfig({ortb2: globalFPD}); - $$PREBID_GLOBAL$$.requestBids({ortb2: auctionFPD}); + await runAuction({ortb2: auctionFPD}); sinon.assert.calledWith(startAuctionStub, sinon.match({ ortb2Fragments: {global: mergedFPD} })); }); - it('enriched through enrichFPD', () => { + it('that cannot alter global config', () => { + configObj.setConfig({ortb2: {value: 'old'}}); + startAuctionStub.callsFake(({ortb2Fragments}) => { + ortb2Fragments.global.value = 'new' + }); + $$PREBID_GLOBAL$$.requestBids({ortb2: auctionFPD}); + expect(configObj.getAnyConfig('ortb2').value).to.eql('old'); + }); + + it('that cannot alter bidder config', () => { + configObj.setBidderConfig({ + bidders: ['mockBidder'], + config: { + ortb2: {value: 'old'} + } + }) + startAuctionStub.callsFake(({ortb2Fragments}) => { + ortb2Fragments.bidder.mockBidder.value = 'new'; + }) + $$PREBID_GLOBAL$$.requestBids({ortb2: auctionFPD}); + expect(configObj.getBidderConfig().mockBidder.ortb2.value).to.eql('old'); + }) + + it('enriched through enrichFPD', async () => { function enrich(next, fpd) { next.bail(fpd.then(ortb2 => { ortb2.enrich = true; return ortb2; })) } + enrichFPD.before(enrich); try { configObj.setConfig({ortb2: globalFPD}); - $$PREBID_GLOBAL$$.requestBids({ortb2: auctionFPD}); + await runAuction({ortb2: auctionFPD}); sinon.assert.calledWith(startAuctionStub, sinon.match({ ortb2Fragments: {global: {...mergedFPD, enrich: true}} })); @@ -1805,17 +1923,34 @@ describe('Unit: Prebid Module', function () { }) }); - it('filtering adUnits by adUnitCodes', () => { - $$PREBID_GLOBAL$$.requestBids({ + it('filtering adUnits by adUnitCodes', async () => { + await runAuction({ adUnits: [{code: 'one'}, {code: 'two'}], adUnitCodes: 'two' }); sinon.assert.calledWith(startAuctionStub, sinon.match({ - adUnits: [{code: 'two'}] + adUnits: [{code: 'two'}], + adUnitCodes: ['two'] + })); + }); + + it('does not repeat ad unit codes on twin ad units', async () => { + await runAuction({ + adUnits: [{code: 'au1'}, {code: 'au2'}, {code: 'au1'}, {code: 'au2'}], + }); + sinon.assert.calledWith(startAuctionStub, sinon.match({ + adUnitCodes: ['au1', 'au2'] + })); + }); + + it('filters out repeated ad unit codes from input', async () => { + await runAuction({adUnitCodes: ['au1', 'au1', 'au2']}); + sinon.assert.calledWith(startAuctionStub, sinon.match({ + adUnitCodes: ['au1', 'au2'] })); }); - it('passing bidder-specific FPD as ortb2Fragments.bidder', () => { + it('passing bidder-specific FPD as ortb2Fragments.bidder', async () => { configObj.setBidderConfig({ bidders: ['bidderA', 'bidderC'], config: { @@ -1832,7 +1967,7 @@ describe('Unit: Prebid Module', function () { } } }); - $$PREBID_GLOBAL$$.requestBids({}); + await runAuction({}) sinon.assert.calledWith(startAuctionStub, sinon.match({ ortb2Fragments: { bidder: { @@ -1853,21 +1988,27 @@ describe('Unit: Prebid Module', function () { }); describe('startAuction', () => { - let sandbox, newAuctionStub; + let sandbox, newAuctionStub, auctionStarted; + beforeEach(() => { sandbox = sinon.createSandbox(); - newAuctionStub = sandbox.stub(auctionManager, 'createAuction').callsFake(() => ({ - getAuctionId: () => 'mockAuctionId', - callBids: sinon.stub() - })); + auctionStarted = new Promise(resolve => { + newAuctionStub = sandbox.stub(auctionManager, 'createAuction').callsFake(() => { + resolve(); + return { + getAuctionId: () => 'mockAuctionId', + callBids: sinon.stub() + } + }); + }); }); afterEach(() => { sandbox.restore(); }); - it('passes ortb2 fragments to createAuction', () => { - const ortb2Fragments = {}; + it('passes ortb2 fragments to createAuction', async () => { + const ortb2Fragments = {global: {}, bidder: {}}; pbjsModule.startAuction({ adUnits: [{ code: 'au', @@ -1877,6 +2018,7 @@ describe('Unit: Prebid Module', function () { adUnitCodes: ['au'], ortb2Fragments }); + await auctionStarted; sinon.assert.calledWith(newAuctionStub, sinon.match({ ortb2Fragments: sinon.match.same(ortb2Fragments) })); @@ -1900,15 +2042,17 @@ describe('Unit: Prebid Module', function () { registerBidder(spec); describe('part 1', function () { - let auctionArgs; + let auctionArgs, auctionStarted; beforeEach(function () { - auctionArgs = null; adUnitsBackup = auction.getAdUnits - auctionManagerStub = sinon.stub(auctionManager, 'createAuction').callsFake(function() { - auctionArgs = arguments[0]; - return auction; - }); + auctionStarted = new Promise(resolve => { + auctionManagerStub = sinon.stub(auctionManager, 'createAuction').callsFake(function() { + auctionArgs = arguments[0]; + resolve(); + return auction; + }); + }) logMessageSpy = sinon.spy(utils, 'logMessage'); logInfoSpy = sinon.spy(utils, 'logInfo'); logErrorSpy = sinon.spy(utils, 'logError'); @@ -1923,27 +2067,31 @@ describe('Unit: Prebid Module', function () { resetAuction(); }); - it('should log message when adUnits not configured', function () { + function runAuction(request = {}) { + $$PREBID_GLOBAL$$.requestBids(request); + return auctionStarted; + } + + it('should log message when adUnits not configured', async function () { $$PREBID_GLOBAL$$.adUnits = []; try { - $$PREBID_GLOBAL$$.requestBids({}); + await $$PREBID_GLOBAL$$.requestBids({}); } catch (e) { - console.log(e); // eslint-disable-line } assert.ok(logMessageSpy.calledWith('No adUnits configured. No bids requested.'), 'expected message was logged'); }); - it('should always attach new transactionIds to adUnits passed to requestBids', function () { - $$PREBID_GLOBAL$$.requestBids({ + it('should always attach new transactionIds to adUnits passed to requestBids', async function () { + await runAuction({ adUnits: [ { code: 'test1', transactionId: 'd0676a3c-ff32-45a5-af65-8175a8e7ddca', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] }, { code: 'test2', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] } ] @@ -1956,16 +2104,16 @@ describe('Unit: Prebid Module', function () { .and.to.match(/[a-f0-9\-]{36}/i); }); - it('should use the same transactionID for ad units with the same code', () => { - $$PREBID_GLOBAL$$.requestBids({ + it('should use the same transactionID for ad units with the same code', async () => { + await runAuction({ adUnits: [ { code: 'twin', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] }, { code: 'twin', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] } ] @@ -1975,16 +2123,16 @@ describe('Unit: Prebid Module', function () { expect(auctionArgs.adUnits[1].transactionId).to.eql(tid); }); - it('should re-use pub-provided transaction ID for ad units with the same code', () => { - $$PREBID_GLOBAL$$.requestBids({ + it('should re-use pub-provided transaction ID for ad units with the same code', async () => { + await runAuction({ adUnits: [ { code: 'twin', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [], }, { code: 'twin', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [], ortb2Imp: { ext: { @@ -1997,12 +2145,12 @@ describe('Unit: Prebid Module', function () { expect(auctionArgs.adUnits.map(au => au.transactionId)).to.eql(['pub-tid', 'pub-tid']); }); - it('should use pub-provided TIDs when they conflict for ad units with the same code', () => { - $$PREBID_GLOBAL$$.requestBids({ + it('should use pub-provided TIDs when they conflict for ad units with the same code', async () => { + await runAuction({ adUnits: [ { code: 'twin', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [], ortb2Imp: { ext: { @@ -2011,7 +2159,7 @@ describe('Unit: Prebid Module', function () { } }, { code: 'twin', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [], ortb2Imp: { ext: { @@ -2024,21 +2172,21 @@ describe('Unit: Prebid Module', function () { expect(auctionArgs.adUnits.map(au => au.transactionId)).to.eql(['t1', 't2']); }); - it('should generate unique adUnitId', () => { - $$PREBID_GLOBAL$$.requestBids({ + it('should generate unique adUnitId', async () => { + await runAuction({ adUnits: [ { code: 'single', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] }, { code: 'twin', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] }, { code: 'twin', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] } ] @@ -2069,8 +2217,8 @@ describe('Unit: Prebid Module', function () { ] }; }); - it('should be set to ortb2Imp.ext.tid, if specified', () => { - $$PREBID_GLOBAL$$.requestBids({ + it('should be set to ortb2Imp.ext.tid, if specified', async () => { + await runAuction({ adUnits: [ {...adUnit, ortb2Imp: {ext: {tid: 'custom-tid'}}} ] @@ -2084,8 +2232,8 @@ describe('Unit: Prebid Module', function () { } }) }); - it('should be copied to ortb2Imp.ext.tid, if not specified', () => { - $$PREBID_GLOBAL$$.requestBids({ + it('should be copied to ortb2Imp.ext.tid, if not specified', async () => { + await runAuction({ adUnits: [ adUnit ] @@ -2096,16 +2244,16 @@ describe('Unit: Prebid Module', function () { }); }); - it('should always set ortb2.ext.tid same as transactionId in adUnits', function () { - $$PREBID_GLOBAL$$.requestBids({ + it('should always set ortb2.ext.tid same as transactionId in adUnits', async function () { + await runAuction({ adUnits: [ { code: 'test1', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] }, { code: 'test2', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] } ] @@ -2119,19 +2267,19 @@ describe('Unit: Prebid Module', function () { expect(auctionArgs.adUnits[1].transactionId).to.equal(auctionArgs.adUnits[1].ortb2Imp.ext.tid); }); - it('should notify targeting of the latest auction for each adUnit', function () { + it('should notify targeting of the latest auction for each adUnit', async function () { let latestStub = sinon.stub(targeting, 'setLatestAuctionForAdUnit'); let getAuctionStub = sinon.stub(auction, 'getAuctionId').returns(2); - $$PREBID_GLOBAL$$.requestBids({ + await runAuction({ adUnits: [ { code: 'test1', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] }, { code: 'test2', - mediaTypes: { banner: { sizes: [] } }, + mediaTypes: {banner: {sizes: []}}, bids: [] } ] @@ -2144,12 +2292,13 @@ describe('Unit: Prebid Module', function () { getAuctionStub.restore(); }); - it('should execute callback immediately if adUnits is empty', function () { - var bidsBackHandler = function bidsBackHandlerCallback() {}; + it('should execute callback immediately if adUnits is empty', async function () { + var bidsBackHandler = function bidsBackHandlerCallback() { + }; var spyExecuteCallback = sinon.spy(bidsBackHandler); $$PREBID_GLOBAL$$.adUnits = []; - $$PREBID_GLOBAL$$.requestBids({ + await $$PREBID_GLOBAL$$.requestBids({ bidsBackHandler: spyExecuteCallback }); @@ -2174,111 +2323,116 @@ describe('Unit: Prebid Module', function () { describe('checkAdUnitSetup', function() { describe('positive tests for validating adUnits', function() { - it('should maintain adUnit structure and adUnit.sizes is replaced', function () { - let fullAdUnit = [{ - code: 'test1', - sizes: [[300, 250], [300, 600]], - mediaTypes: { - banner: { - sizes: [[300, 250]] - }, - video: { - playerSize: [[640, 480]] - }, - native: { - image: { - sizes: [150, 150], - aspect_ratios: [140, 140] + describe('should maintain adUnit structure and adUnit.sizes is replaced', () => { + it('full ad unit', async () => { + let fullAdUnit = [{ + code: 'test1', + sizes: [[300, 250], [300, 600]], + mediaTypes: { + banner: { + sizes: [[300, 250]] }, - icon: { - sizes: [75, 75] - } - } - }, - bids: [] - }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: fullAdUnit - }); - expect(auctionArgs.adUnits[0].sizes).to.deep.equal( - FEATURES.VIDEO ? [[640, 480]] : [[300, 250]] - ); - expect(auctionArgs.adUnits[0].mediaTypes.video.playerSize).to.deep.equal([[640, 480]]); - expect(auctionArgs.adUnits[0].mediaTypes.native.image.sizes).to.deep.equal([150, 150]); - expect(auctionArgs.adUnits[0].mediaTypes.native.icon.sizes).to.deep.equal([75, 75]); - expect(auctionArgs.adUnits[0].mediaTypes.native.image.aspect_ratios).to.deep.equal([140, 140]); - - let noOptnlFieldAdUnit = [{ - code: 'test2', - bids: [], - sizes: [[300, 250], [300, 600]], - mediaTypes: { - banner: { - sizes: [[300, 250]] - }, - video: { - context: 'outstream' - }, - native: { - image: { - required: true + video: { + playerSize: [[640, 480]] }, - icon: { - required: true + native: { + image: { + sizes: [150, 150], + aspect_ratios: [140, 140] + }, + icon: { + sizes: [75, 75] + } } - } - } - }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: noOptnlFieldAdUnit - }); - expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[300, 250]]); - expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; - - if (FEATURES.VIDEO) { - let mixedAdUnit = [{ - code: 'test3', + }, + bids: [] + }]; + await runAuction({ + adUnits: fullAdUnit + }); + expect(auctionArgs.adUnits[0].sizes).to.deep.equal( + FEATURES.VIDEO ? [[640, 480]] : [[300, 250]] + ); + expect(auctionArgs.adUnits[0].mediaTypes.video.playerSize).to.deep.equal([[640, 480]]); + expect(auctionArgs.adUnits[0].mediaTypes.native.image.sizes).to.deep.equal([150, 150]); + expect(auctionArgs.adUnits[0].mediaTypes.native.icon.sizes).to.deep.equal([75, 75]); + expect(auctionArgs.adUnits[0].mediaTypes.native.image.aspect_ratios).to.deep.equal([140, 140]); + }) + it('no optional field', async () => { + let noOptnlFieldAdUnit = [{ + code: 'test2', bids: [], sizes: [[300, 250], [300, 600]], mediaTypes: { + banner: { + sizes: [[300, 250]] + }, video: { - context: 'outstream', - playerSize: [[400, 350]] + context: 'outstream' }, native: { image: { - aspect_ratios: [200, 150], + required: true + }, + icon: { required: true } } } }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: mixedAdUnit + await runAuction({ + adUnits: noOptnlFieldAdUnit }); - expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[400, 350]]); + expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[300, 250]]); expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; - - let altVideoPlayerSize = [{ - code: 'test4', - bids: [], - sizes: [[600, 600]], - mediaTypes: { - video: { - playerSize: [640, 480] + }) + if (FEATURES.VIDEO) { + it('mixed ad unit', async () => { + let mixedAdUnit = [{ + code: 'test3', + bids: [], + sizes: [[300, 250], [300, 600]], + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[400, 350]] + }, + native: { + image: { + aspect_ratios: [200, 150], + required: true + } + } } - } - }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: altVideoPlayerSize + }]; + await runAuction({ + adUnits: mixedAdUnit + }); + expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[400, 350]]); + expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; }); - expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[640, 480]]); - expect(auctionArgs.adUnits[0].mediaTypes.video.playerSize).to.deep.equal([[640, 480]]); - expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; + it('alternative video size', async () => { + let altVideoPlayerSize = [{ + code: 'test4', + bids: [], + sizes: [[600, 600]], + mediaTypes: { + video: { + playerSize: [640, 480] + } + } + }]; + await runAuction({ + adUnits: altVideoPlayerSize + }); + expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[640, 480]]); + expect(auctionArgs.adUnits[0].mediaTypes.video.playerSize).to.deep.equal([[640, 480]]); + expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; + }) } - }); + }) - it('should normalize adUnit.sizes and adUnit.mediaTypes.banner.sizes', function () { + it('should normalize adUnit.sizes and adUnit.mediaTypes.banner.sizes', async function () { let normalizeAdUnit = [{ code: 'test5', bids: [], @@ -2289,14 +2443,14 @@ describe('Unit: Prebid Module', function () { } } }]; - $$PREBID_GLOBAL$$.requestBids({ + await runAuction({ adUnits: normalizeAdUnit }); expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[300, 250]]); expect(auctionArgs.adUnits[0].mediaTypes.banner.sizes).to.deep.equal([[300, 250]]); }); - it('should filter mediaType pos value if not integer', function () { + it('should filter mediaType pos value if not integer', async function () { let adUnit = [{ code: 'test5', bids: [], @@ -2308,13 +2462,13 @@ describe('Unit: Prebid Module', function () { } } }]; - $$PREBID_GLOBAL$$.requestBids({ + await runAuction({ adUnits: adUnit }); expect(auctionArgs.adUnits[0].mediaTypes.banner.pos).to.be.undefined; }); - it('should pass mediaType pos value if integer', function () { + it('should pass mediaType pos value if integer', async function () { let adUnit = [{ code: 'test5', bids: [], @@ -2325,8 +2479,7 @@ describe('Unit: Prebid Module', function () { pos: 2 } } - }, - { + }, { code: 'test6', bids: [], sizes: [300, 250], @@ -2337,14 +2490,14 @@ describe('Unit: Prebid Module', function () { } } }]; - $$PREBID_GLOBAL$$.requestBids({ + await runAuction({ adUnits: adUnit }); expect(auctionArgs.adUnits[0].mediaTypes.banner.pos).to.equal(2); expect(auctionArgs.adUnits[1].mediaTypes.banner.pos).to.equal(0); }); - it(`should allow no bids if 'ortb2Imp' is specified`, () => { + it(`should allow no bids if 'ortb2Imp' is specified`, async () => { const adUnit = { code: 'test', mediaTypes: { @@ -2354,128 +2507,249 @@ describe('Unit: Prebid Module', function () { }, ortb2Imp: {} }; - $$PREBID_GLOBAL$$.requestBids({ + await runAuction({ adUnits: [adUnit] }); sinon.assert.match(auctionArgs.adUnits[0], adUnit); }); - }); - describe('negative tests for validating adUnits', function() { - it('should throw error message and delete an object/property', function () { - let badBanner = [{ - code: 'testb1', - bids: [], - sizes: [[300, 250], [300, 600]], - mediaTypes: { - banner: { - name: 'test' - } - } - }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: badBanner - }); - expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[300, 250], [300, 600]]); - expect(auctionArgs.adUnits[0].mediaTypes.banner).to.be.undefined; - assert.ok(logErrorSpy.calledWith('Detected a mediaTypes.banner object without a proper sizes field. Please ensure the sizes are listed like: [[300, 250], ...]. Removing invalid mediaTypes.banner object from request.')); - - if (FEATURES.VIDEO) { - let badVideo1 = [{ - code: 'testb2', + describe('banner.format', () => { + let au; + beforeEach(() => { + au = { + code: 'test', bids: [], - sizes: [[600, 600]], mediaTypes: { - video: { - playerSize: ['600x400'] - } + banner: {} } - }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: badVideo1 + }; + }); + const EXPDIR = ['ortb2Imp.banner.expdir', 'mediaTypes.banner.expdir']; + EXPDIR.forEach(prop => { + it(`should make ${prop} avaliable under both ${EXPDIR.join(' and ')}`, async () => { + au.mediaTypes.banner.sizes = [1, 2]; + deepSetValue(au, prop, [1, 2]); + await runAuction({ + adUnits: [au] + }) + EXPDIR.forEach(dest => { + expect(deepAccess(auctionArgs.adUnits[0], dest)).to.eql([1, 2]); + }); + }) + }); + ['ortb2Imp.banner.format', 'mediaTypes.banner.format'].forEach(prop => { + it(`should accept ${prop} instead of sizes`, async () => { + deepSetValue(au, prop, [{w: 123, h: 321}, {w: 444, h: 555}]); + await runAuction({ + adUnits: [au] + }) + expect(auctionArgs.adUnits[0].mediaTypes.banner.sizes).to.deep.equal([[123, 321], [444, 555]]); }); - expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[600, 600]]); - expect(auctionArgs.adUnits[0].mediaTypes.video.playerSize).to.be.undefined; - expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; - assert.ok(logErrorSpy.calledWith('Detected incorrect configuration of mediaTypes.video.playerSize. Please specify only one set of dimensions in a format like: [[640, 480]]. Removing invalid mediaTypes.video.playerSize property from request.')); - let badVideo2 = [{ - code: 'testb3', + it(`should make ${prop} available under both mediaTypes.banner and ortb2Imp.format`, async () => { + const format = [{w: 123, h: 321}]; + deepSetValue(au, prop, format); + await runAuction({ + adUnits: [au] + }) + expect(auctionArgs.adUnits[0].mediaTypes.banner.format).to.deep.equal(format); + expect(auctionArgs.adUnits[0].ortb2Imp.banner.format).to.deep.equal(format); + }) + + it(`should transform wratio/hratio from ${prop} into placeholder sizes`, async () => { + deepSetValue(au, prop, [{w: 123, h: 321}, {wratio: 2, hratio: 1}]); + await runAuction({ + adUnits: [au] + }) + expect(auctionArgs.adUnits[0].mediaTypes.banner.sizes).to.deep.equal([[123, 321], [2, 1]]); + }); + it(`should ignore ${prop} elements that specify both w/h and wratio/hratio`, async () => { + deepSetValue(au, prop, [{w: 333, hratio: 2}, {w: 123, h: 321}]); + await runAuction({ + adUnits: [au] + }) + expect(auctionArgs.adUnits[0].mediaTypes.banner.sizes).to.deep.equal([[123, 321]]); + }); + + it('should ignore incomplete formats', async () => { + deepSetValue(au, prop, [{w: 123, h: 321}, {w: 123}, {wratio: 2}]); + await runAuction({ + adUnits: [au] + }) + expect(auctionArgs.adUnits[0].mediaTypes.banner.sizes).to.deep.equal([[123, 321]]); + }) + }); + }) + }); + + describe('negative tests for validating adUnits', function() { + describe('should throw error message and delete an object/property', () => { + it('bad banner', async () => { + let badBanner = [{ + code: 'testb1', bids: [], - sizes: [[600, 600]], + sizes: [[300, 250], [300, 600]], mediaTypes: { - video: { - playerSize: [['300', '200']] + banner: { + name: 'test' } } }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: badVideo2 + await runAuction({ + adUnits: badBanner }); - expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[600, 600]]); - expect(auctionArgs.adUnits[0].mediaTypes.video.playerSize).to.be.undefined; - expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; - assert.ok(logErrorSpy.calledWith('Detected incorrect configuration of mediaTypes.video.playerSize. Please specify only one set of dimensions in a format like: [[640, 480]]. Removing invalid mediaTypes.video.playerSize property from request.')); + expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[300, 250], [300, 600]]); + expect(auctionArgs.adUnits[0].mediaTypes.banner).to.be.undefined; + assert.ok(logErrorSpy.calledWith('Detected a mediaTypes.banner object without a proper sizes field. Please ensure the sizes are listed like: [[300, 250], ...]. Removing invalid mediaTypes.banner object from request.')); + }); + if (FEATURES.VIDEO) { + it('bad video 1', async () => { + let badVideo1 = [{ + code: 'testb2', + bids: [], + sizes: [[600, 600]], + mediaTypes: { + video: { + playerSize: ['600x400'] + } + } + }]; + await runAuction({ + adUnits: badVideo1 + }); + expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[600, 600]]); + expect(auctionArgs.adUnits[0].mediaTypes.video.playerSize).to.be.undefined; + expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; + assert.ok(logErrorSpy.calledWith('Detected incorrect configuration of mediaTypes.video.playerSize. Please specify only one set of dimensions in a format like: [[640, 480]]. Removing invalid mediaTypes.video.playerSize property from request.')); + }); + it('bad video 2', async () => { + let badVideo2 = [{ + code: 'testb3', + bids: [], + sizes: [[600, 600]], + mediaTypes: { + video: { + playerSize: [['300', '200']] + } + } + }]; + await runAuction({ + adUnits: badVideo2 + }); + expect(auctionArgs.adUnits[0].sizes).to.deep.equal([[600, 600]]); + expect(auctionArgs.adUnits[0].mediaTypes.video.playerSize).to.be.undefined; + expect(auctionArgs.adUnits[0].mediaTypes.video).to.exist; + assert.ok(logErrorSpy.calledWith('Detected incorrect configuration of mediaTypes.video.playerSize. Please specify only one set of dimensions in a format like: [[640, 480]]. Removing invalid mediaTypes.video.playerSize property from request.')); + }) } - if (FEATURES.NATIVE) { - let badNativeImgSize = [{ - code: 'testb4', - bids: [], - mediaTypes: { - native: { - image: { - sizes: '300x250' + it('bad native img size', async () => { + let badNativeImgSize = [{ + code: 'testb4', + bids: [], + mediaTypes: { + native: { + image: { + sizes: '300x250' + } } } - } - }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: badNativeImgSize + }]; + await runAuction({ + adUnits: badNativeImgSize + }); + expect(auctionArgs.adUnits[0].mediaTypes.native.image.sizes).to.be.undefined; + expect(auctionArgs.adUnits[0].mediaTypes.native.image).to.exist; + assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.image.sizes field. Removing invalid mediaTypes.native.image.sizes property from request.')); }); - expect(auctionArgs.adUnits[0].mediaTypes.native.image.sizes).to.be.undefined; - expect(auctionArgs.adUnits[0].mediaTypes.native.image).to.exist; - assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.image.sizes field. Removing invalid mediaTypes.native.image.sizes property from request.')); - - let badNativeImgAspRat = [{ - code: 'testb5', - bids: [], - mediaTypes: { - native: { - image: { - aspect_ratios: '300x250' + it('bad native aspect ratio', async () => { + let badNativeImgAspRat = [{ + code: 'testb5', + bids: [], + mediaTypes: { + native: { + image: { + aspect_ratios: '300x250' + } } } - } - }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: badNativeImgAspRat + }]; + await runAuction({ + adUnits: badNativeImgAspRat + }); + expect(auctionArgs.adUnits[0].mediaTypes.native.image.aspect_ratios).to.be.undefined; + expect(auctionArgs.adUnits[0].mediaTypes.native.image).to.exist; + assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.image.aspect_ratios field. Removing invalid mediaTypes.native.image.aspect_ratios property from request.')); }); - expect(auctionArgs.adUnits[0].mediaTypes.native.image.aspect_ratios).to.be.undefined; - expect(auctionArgs.adUnits[0].mediaTypes.native.image).to.exist; - assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.image.aspect_ratios field. Removing invalid mediaTypes.native.image.aspect_ratios property from request.')); - - let badNativeIcon = [{ - code: 'testb6', - bids: [], - mediaTypes: { - native: { - icon: { - sizes: '300x250' + it('bad native icon', async () => { + let badNativeIcon = [{ + code: 'testb6', + bids: [], + mediaTypes: { + native: { + icon: { + sizes: '300x250' + } } } - } - }]; - $$PREBID_GLOBAL$$.requestBids({ - adUnits: badNativeIcon - }); - expect(auctionArgs.adUnits[0].mediaTypes.native.icon.sizes).to.be.undefined; - expect(auctionArgs.adUnits[0].mediaTypes.native.icon).to.exist; - assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.')); + }]; + await runAuction({ + adUnits: badNativeIcon + }); + expect(auctionArgs.adUnits[0].mediaTypes.native.icon.sizes).to.be.undefined; + expect(auctionArgs.adUnits[0].mediaTypes.native.icon).to.exist; + assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.')); + }) } - }); + }) + + if (FEATURES.NATIVE) { + Object.entries({ + missing: {}, + negative: {id: -1}, + 'not an integer': {id: 1.23}, + NaN: {id: 'garbage'} + }).forEach(([t, props]) => { + it(`should reject native ortb when asset ID is ${t}`, async () => { + const adUnit = { + code: 'au', + mediaTypes: { + native: { + ortb: { + assets: [props] + } + } + }, + bids: [{bidder: 'appnexus'}] + }; + await runAuction({ + adUnits: [adUnit] + }); + expect(auctionArgs.adUnits[0].bids.length).to.equal(0); + }); + }); + + ['sendTargetingKeys', 'types'].forEach(key => { + it(`should reject native that includes both ortb and ${key}`, async () => { + const adUnit = { + code: 'au', + mediaTypes: { + native: { + ortb: {}, + [key]: {} + } + }, + bids: [{bidder: 'appnexus'}] + }; + await runAuction({ + adUnits: [adUnit] + }); + expect(auctionArgs.adUnits[0].bids.length).to.equal(0); + }) + }); + } - it('should throw error message and remove adUnit if adUnit.bids is not defined correctly', function () { + it('should throw error message and remove adUnit if adUnit.bids is not defined correctly', async function () { const adUnits = [{ code: 'ad-unit-1', mediaTypes: { @@ -2493,7 +2767,7 @@ describe('Unit: Prebid Module', function () { } }]; - $$PREBID_GLOBAL$$.requestBids({ + await runAuction({ adUnits: adUnits }); expect(auctionArgs.adUnits.length).to.equal(1); @@ -2508,7 +2782,7 @@ describe('Unit: Prebid Module', function () { if (!FEATURES.NATIVE) { return; } - let adUnits; + let adUnits, auctionStarted; beforeEach(function () { adUnits = [{ @@ -2527,15 +2801,21 @@ describe('Unit: Prebid Module', function () { }]; adUnitCodes = ['adUnit-code']; configObj.setConfig({maxRequestsPerOrigin: Number.MAX_SAFE_INTEGER || 99999999}); - sinon.spy(adapterManager, 'callBids'); + auctionStarted = new Promise(resolve => { + sinon.stub(adapterManager, 'callBids').callsFake(function() { + resolve(); + return adapterManager.callBids.wrappedMethod.apply(this, arguments); + }); + }) }) afterEach(function () { adapterManager.callBids.restore(); }); - it('bidders that support one of the declared formats are allowed to participate', function () { + it('bidders that support one of the declared formats are allowed to participate', async function () { $$PREBID_GLOBAL$$.requestBids({adUnits}); + await auctionStarted; sinon.assert.calledOnce(adapterManager.callBids); const spyArgs = adapterManager.callBids.getCall(0); @@ -2545,10 +2825,11 @@ describe('Unit: Prebid Module', function () { expect(biddersCalled.length).to.equal(2); }); - it('bidders that do not support one of the declared formats are dropped', function () { + it('bidders that do not support one of the declared formats are dropped', async function () { delete adUnits[0].mediaTypes.banner; $$PREBID_GLOBAL$$.requestBids({adUnits}); + await auctionStarted; sinon.assert.calledOnce(adapterManager.callBids); const spyArgs = adapterManager.callBids.getCall(0); @@ -2563,44 +2844,10 @@ describe('Unit: Prebid Module', function () { return; } let spyCallBids; - let createAuctionStub; - let adUnits; - - before(function () { - adUnits = [{ - code: 'adUnit-code', - mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, - bids: [ - {bidder: 'appnexus', params: {placementId: '10433394'}} - ] - }]; - let adUnitCodes = ['adUnit-code']; - let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); - adUnits[0]['mediaTypes'] = { native: {} }; - adUnitCodes = ['adUnit-code']; - let auction1 = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); - - adUnits = [{ - code: 'adUnit-code', - mediaTypes: { native: { type: 'image' } }, - sizes: [[300, 250], [300, 600]], - bids: [ - {bidder: 'appnexus', params: {placementId: 'id'}} - ] - }]; - let auction3 = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); - - let createAuctionStub = sinon.stub(auctionModule, 'newAuction'); - createAuctionStub.onCall(0).returns(auction1); - createAuctionStub.onCall(2).returns(auction3); - createAuctionStub.returns(auction); - }); - - after(function () { - auctionModule.newAuction.restore(); - }); + let adUnits, adUnitCodes; beforeEach(function () { + adUnitCodes = ['adUnit-code']; spyCallBids = sinon.spy(adapterManager, 'callBids'); }) @@ -2608,32 +2855,50 @@ describe('Unit: Prebid Module', function () { adapterManager.callBids.restore(); }) - it('should callBids if a native adUnit has all native bidders', function () { - $$PREBID_GLOBAL$$.requestBids({adUnits}); + function runAuction(request = {}) { + const auctionStarted = new Promise(resolve => { + sandbox.stub(auctionModule, 'newAuction').callsFake((...args) => { + resolve(); + return auctionModule.newAuction.wrappedMethod(...args); + }); + }) + $$PREBID_GLOBAL$$.requestBids(request); + return auctionStarted; + } + + it('should callBids if a native adUnit has all native bidders', async function () { + adUnits = [{ + code: 'adUnit-code', + mediaTypes: { native: {} }, + bids: [ + {bidder: 'appnexus', params: {placementId: '10433394'}} + ] + }]; + await runAuction({adUnits}); sinon.assert.calledOnce(adapterManager.callBids); }); - it('should call callBids function on adapterManager', function () { - let adUnits = [{ + it('should call callBids function on adapterManager', async function () { + adUnits = [{ code: 'adUnit-code', - mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}, bids: [ {bidder: 'appnexus', params: {placementId: '10433394'}} ] }]; - $$PREBID_GLOBAL$$.requestBids({adUnits}); + await runAuction({adUnits}); assert.ok(spyCallBids.called, 'called adapterManager.callBids'); }); - it('splits native type to individual native assets', function () { - let adUnits = [{ + it('splits native type to individual native assets', async function () { + adUnits = [{ code: 'adUnit-code', mediaTypes: {native: {type: 'image'}}, bids: [ {bidder: 'appnexus', params: {placementId: 'id'}} ] }]; - $$PREBID_GLOBAL$$.requestBids({adUnits}); + await runAuction({adUnits}); const spyArgs = adapterManager.callBids.getCall(0); const nativeRequest = spyArgs.args[1][0].bids[0].nativeParams; expect(nativeRequest.ortb.assets).to.deep.equal([ @@ -2729,11 +2994,19 @@ describe('Unit: Prebid Module', function () { }); }; + let auctionsStarted; + beforeEach(function() { spyCallBids = sinon.spy(adapterManager, 'callBids'); auctionManagerStub = sinon.stub(auctionManager, 'createAuction'); - auctionManagerStub.onCall(0).returns(auction1); - auctionManagerStub.onCall(1).returns(auction2); + auctionsStarted = Promise.all( + [auction1, auction2].map((au, i) => new Promise((resolve) => { + auctionManagerStub.onCall(i).callsFake(() => { + resolve(); + return au; + }); + })) + ); }); afterEach(function() { @@ -2741,15 +3014,17 @@ describe('Unit: Prebid Module', function () { adapterManager.callBids.restore(); }); - it('should not queue bid requests when a previous bid request is in process', function () { + it('should not queue bid requests when a previous bid request is in process', async function () { var requestObj1 = { - bidsBackHandler: function bidsBackHandlerCallback() {}, + bidsBackHandler: function bidsBackHandlerCallback() { + }, timeout: 2000, adUnits: auction1.getAdUnits() }; var requestObj2 = { - bidsBackHandler: function bidsBackHandlerCallback() {}, + bidsBackHandler: function bidsBackHandlerCallback() { + }, timeout: 2000, adUnits: auction2.getAdUnits() }; @@ -2758,6 +3033,7 @@ describe('Unit: Prebid Module', function () { $$PREBID_GLOBAL$$.requestBids(requestObj1); $$PREBID_GLOBAL$$.requestBids(requestObj2); + await auctionsStarted; assert.ok(spyCallBids.calledTwice, 'When two requests for bids are made both should be' + ' callBids immediately'); @@ -2765,7 +3041,7 @@ describe('Unit: Prebid Module', function () { let result = targeting.getAllTargeting(['/19968336/header-bid-tag-0', '/19968336/header-bid-tag1']); // $$PREBID_GLOBAL$$.getAdserverTargeting(); let expected = { '/19968336/header-bid-tag-0': { - 'foobar': '0x0,300x250,300x600', + 'foobar': '300x250,300x600,0x0', [TARGETING_KEYS.SIZE]: '300x250', [TARGETING_KEYS.PRICE_BUCKET]: '10.00', [TARGETING_KEYS.AD_ID]: '233bcbee889d46d', @@ -2821,6 +3097,7 @@ describe('Unit: Prebid Module', function () { let bidRequestedHandler; let beforeRequestBidsHandler; let bidsBackHandler = function bidsBackHandler() {}; + let auctionStarted; let bidsBackSpy; let bidRequestedSpy; @@ -2830,6 +3107,10 @@ describe('Unit: Prebid Module', function () { resetAuction(); bidsBackSpy = sinon.spy(bidsBackHandler); googletag.pubads().setSlots(createSlotArrayScenario2()); + auctionStarted = new Promise((resolve) => sandbox.stub(adapterManager, 'callBids').callsFake(function() { + resolve() + return adapterManager.callBids.wrappedMethod.apply(this, arguments); + })); }); afterEach(function () { @@ -2846,7 +3127,7 @@ describe('Unit: Prebid Module', function () { } }); - it('should allow creation of a fpd.context.pbAdSlot property on adUnits from inside the event handler', function () { + it('should allow creation of a fpd.context.pbAdSlot property on adUnits from inside the event handler', async function () { // verify adUnits passed to handler then alter the adUnits beforeRequestBidsHandler = function beforeRequestBidsHandler(beforeRequestBidsAdUnits) { expect(beforeRequestBidsAdUnits).to.be.a('array'); @@ -2900,11 +3181,13 @@ describe('Unit: Prebid Module', function () { bidsBackHandler: bidsBackSpy }); + await auctionStarted; + sinon.assert.calledOnce(beforeRequestBidsSpy); sinon.assert.calledOnce(bidRequestedSpy); }); - it('should allow creation of a fpd.context.pbAdSlot property on adUnits from inside the event handler', function () { + it('should allow creation of a fpd.context.pbAdSlot property on adUnits from inside the event handler', async function () { // verify adUnits passed to handler then alter the adUnits beforeRequestBidsHandler = function beforeRequestBidsHandler(beforeRequestBidsAdUnits) { expect(beforeRequestBidsAdUnits).to.be.a('array'); @@ -2986,12 +3269,13 @@ describe('Unit: Prebid Module', function () { }], bidsBackHandler: bidsBackSpy }); + await auctionStarted; sinon.assert.calledOnce(beforeRequestBidsSpy); sinon.assert.calledOnce(bidRequestedSpy); }); - it('should not create a context property on adUnits if not added by handler', function () { + it('should not create a context property on adUnits if not added by handler', async function () { // verify adUnits passed to handler then alter the adUnits beforeRequestBidsHandler = function beforeRequestBidsHandler(beforeRequestBidsAdUnits) { expect(beforeRequestBidsAdUnits).to.be.a('array'); @@ -3033,6 +3317,7 @@ describe('Unit: Prebid Module', function () { }], bidsBackHandler: bidsBackSpy }); + await auctionStarted; sinon.assert.calledOnce(beforeRequestBidsSpy); sinon.assert.calledOnce(bidRequestedSpy); @@ -3436,7 +3721,7 @@ describe('Unit: Prebid Module', function () { if (FEATURES.VIDEO) { describe('markWinningBidAsUsed', function () { const adUnitCode = '/19968336/header-bid-tag-0'; - let winningBid; + let winningBid, markedBid; beforeEach(() => { const bidsReceived = $$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode(adUnitCode); @@ -3445,20 +3730,56 @@ describe('Unit: Prebid Module', function () { // mark the bid and verify the state has changed to RENDERED winningBid = targeting.getWinningBids(adUnitCode)[0]; auction.getAuctionId = function() { return winningBid.auctionId }; + sandbox.stub(events, 'emit'); + markedBid = find($$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode(adUnitCode).bids, + bid => bid.adId === winningBid.adId); }) afterEach(() => { resetAuction(); }) - it('marks the bid object as used for the given adUnitCode/adId combination', function () { - // make sure the auction has "state" and does not reload the fixtures - $$PREBID_GLOBAL$$.markWinningBidAsUsed({ adUnitCode, adId: winningBid.adId }); - const markedBid = find($$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode(adUnitCode).bids, - bid => bid.adId === winningBid.adId); - + function checkBidRendered() { expect(markedBid.status).to.equal(BID_STATUS.RENDERED); - }); + } + + Object.entries({ + 'events=true': { + mark(options = {}) { + $$PREBID_GLOBAL$$.markWinningBidAsUsed(Object.assign({events: true}, options)) + }, + checkBidWon() { + sinon.assert.calledWith(events.emit, EVENTS.BID_WON, markedBid); + } + }, + 'events=false': { + mark(options = {}) { + $$PREBID_GLOBAL$$.markWinningBidAsUsed(options) + }, + checkBidWon() { + sinon.assert.notCalled(events.emit) + } + } + }).forEach(([t, {mark, checkBidWon}]) => { + describe(`when ${t}`, () => { + it('marks the bid object as used for the given adUnitCode/adId combination', function () { + mark({ adUnitCode, adId: winningBid.adId }); + checkBidRendered(); + checkBidWon(); + }); + it('marks the winning bid object as used for the given adUnitCode', function () { + mark({ adUnitCode }); + checkBidRendered(); + checkBidWon(); + }); + + it('marks a bid object as used for the given adId', function () { + mark({ adId: winningBid.adId }); + checkBidRendered(); + checkBidWon(); + }); + }) + }) it('try and mark the bid object, but fail because we supplied the wrong adId', function () { $$PREBID_GLOBAL$$.markWinningBidAsUsed({ adUnitCode, adId: 'miss' }); @@ -3467,24 +3788,6 @@ describe('Unit: Prebid Module', function () { expect(markedBid.status).to.not.equal(BID_STATUS.RENDERED); }); - - it('marks the winning bid object as used for the given adUnitCode', function () { - // make sure the auction has "state" and does not reload the fixtures - $$PREBID_GLOBAL$$.markWinningBidAsUsed({ adUnitCode }); - const markedBid = find($$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode(adUnitCode).bids, - bid => bid.adId === winningBid.adId); - - expect(markedBid.status).to.equal(BID_STATUS.RENDERED); - }); - - it('marks a bid object as used for the given adId', function () { - // make sure the auction has "state" and does not reload the fixtures - $$PREBID_GLOBAL$$.markWinningBidAsUsed({ adId: winningBid.adId }); - const markedBid = find($$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode(adUnitCode).bids, - bid => bid.adId === winningBid.adId); - - expect(markedBid.status).to.equal(BID_STATUS.RENDERED); - }); }); } @@ -3616,67 +3919,39 @@ describe('Unit: Prebid Module', function () { auctionManagerStub.returns(bidsReceived) let bids = $$PREBID_GLOBAL$$.getAllPrebidWinningBids(); - expect(bids.length).to.equal(1); + expect(bids.length).to.equal(1); sandbox expect(bids[0].adId).to.equal('adid-1'); }); }); describe('deferred billing', function () { - const sandbox = sinon.createSandbox(); - - let adUnits = [ - { - code: 'adUnit-code-1', - mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, - adUnitId: '1234567890', - bids: [ - { bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1' } - ] - }, - { - code: 'adUnit-code-2', - deferBilling: true, - mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, - adUnitId: '0987654321', - bids: [ - { bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2' } - ] - } - ]; - - let winningBid1 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1', adUnitId: '1234567890', adId: 'abcdefg' } - let winningBid2 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2', adUnitId: '0987654321' } - let adUnitCodes = ['adUnit-code-1', 'adUnit-code-2']; - let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: 2000}); + let bid; beforeEach(function () { - sandbox.spy(adapterManager, 'callBidWonBidder'); - sandbox.spy(adapterManager, 'callBidBillableBidder'); - sandbox.stub(auctionManager, 'getBidsReceived').returns([winningBid1]); + bid = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1', adUnitId: '1234567890', adId: 'abcdefg' }; + sandbox.spy(adapterManager, 'triggerBilling'); + sandbox.stub(auctionManager, 'getAllWinningBids').returns([bid]); }); - afterEach(function () { - sandbox.resetHistory(); - sandbox.restore(); - }); - - it('should by default invoke callBidWonBidder and callBidBillableBidder', function () { - auction.addWinningBid(winningBid1); - sinon.assert.calledOnce(adapterManager.callBidWonBidder); - sinon.assert.calledOnce(adapterManager.callBidBillableBidder); - }); + Object.entries({ + 'bid': () => bid, + 'adUnitCode': () => ({adUnitCode: bid.adUnitCode}) + }).forEach(([t, val]) => { + it(`should trigger billing when invoked with ${t}`, () => { + $$PREBID_GLOBAL$$.triggerBilling(val()); + sinon.assert.calledWith(adapterManager.triggerBilling, bid); + }) + }) + }); - it('should only invoke callBidWonBidder and NOT callBidBillableBidder if deferBilling is present and true within the winning adUnit object', function () { - auction.addWinningBid(winningBid2); - sinon.assert.calledOnce(adapterManager.callBidWonBidder); - sinon.assert.notCalled(adapterManager.callBidBillableBidder); + describe('clearAllAuctions', () => { + after(() => { + resetAuction(); }); - - it('should invoke callBidBillableBidder when pbjs.triggerBilling is invoked', function () { - $$PREBID_GLOBAL$$.triggerBilling(winningBid1); - sinon.assert.calledOnce(auctionManager.getBidsReceived); - sinon.assert.notCalled(adapterManager.callBidWonBidder); - sinon.assert.calledOnce(adapterManager.callBidBillableBidder); + it('clears auction data', function () { + expect(auctionManager.getBidsReceived().length).to.not.equal(0); + $$PREBID_GLOBAL$$.clearAllAuctions(); + expect(auctionManager.getBidsReceived().length).to.equal(0); }); }); }); diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index 189066f7f88..1ec2c752fc6 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -13,11 +13,24 @@ import 'modules/nativeRendering.js'; import {expect} from 'chai'; -import { AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS } from 'src/constants.js'; +import {AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS} from 'src/constants.js'; +import {getBidToRender} from '../../../src/adRendering.js'; +import {PUC_MIN_VERSION} from 'src/creativeRenderers.js'; describe('secureCreatives', () => { let sandbox; + function getBidToRenderHook(next, adId) { + // make sure that bids can be retrieved asynchronously + next(adId, new Promise((resolve) => setTimeout(resolve))) + } + before(() => { + getBidToRender.before(getBidToRenderHook); + }); + after(() => { + getBidToRender.getHooks({hook: getBidToRenderHook}).remove() + }); + beforeEach(() => { sandbox = sinon.sandbox.create(); }); @@ -30,6 +43,10 @@ describe('secureCreatives', () => { return Object.assign({origin: 'mock-origin', ports: []}, ev) } + function receive(ev) { + return Promise.resolve(receiveMessage(ev)); + } + describe('getReplier', () => { it('should use source.postMessage if no MessagePort is available', () => { const ev = { @@ -153,17 +170,17 @@ describe('secureCreatives', () => { data: JSON.stringify(data), }); - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledOnce(adResponse.renderer.render); - sinon.assert.calledWith(adResponse.renderer.render, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + return receive(ev).then(() => { + sinon.assert.neverCalledWith(spyLogWarn, warning); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(adResponse.renderer.render); + sinon.assert.calledWith(adResponse.renderer.render, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + }); }); it('should allow stale rendering without config', function () { @@ -180,29 +197,23 @@ describe('secureCreatives', () => { data: JSON.stringify(data) }); - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledOnce(adResponse.renderer.render); - sinon.assert.calledWith(adResponse.renderer.render, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); - - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); - - resetHistories(adResponse.renderer.render); - - receiveMessage(ev); - - sinon.assert.calledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledOnce(adResponse.renderer.render); - sinon.assert.calledWith(adResponse.renderer.render, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse); + return receive(ev).then(() => { + sinon.assert.neverCalledWith(spyLogWarn, warning); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(adResponse.renderer.render); + sinon.assert.calledWith(adResponse.renderer.render, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + resetHistories(adResponse.renderer.render); + return receive(ev); + }).then(() => { + sinon.assert.calledWith(spyLogWarn, warning); + sinon.assert.calledOnce(adResponse.renderer.render); + sinon.assert.calledWith(adResponse.renderer.render, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse); + }); }); it('should stop stale rendering with config', function () { @@ -221,29 +232,27 @@ describe('secureCreatives', () => { data: JSON.stringify(data) }); - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.calledWith(spyAddWinningBid, adResponse); - sinon.assert.calledOnce(adResponse.renderer.render); - sinon.assert.calledWith(adResponse.renderer.render, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); - - expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); - - resetHistories(adResponse.renderer.render); - - receiveMessage(ev); - - sinon.assert.calledWith(spyLogWarn, warning); - sinon.assert.notCalled(spyAddWinningBid); - sinon.assert.notCalled(adResponse.renderer.render); - sinon.assert.neverCalledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse); - - configObj.setConfig({'auctionOptions': {}}); + return receive(ev).then(() => { + sinon.assert.neverCalledWith(spyLogWarn, warning); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.calledWith(spyAddWinningBid, adResponse); + sinon.assert.calledOnce(adResponse.renderer.render); + sinon.assert.calledWith(adResponse.renderer.render, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + + expect(adResponse).to.have.property('status', BID_STATUS.RENDERED); + + resetHistories(adResponse.renderer.render); + return receive(ev) + }).then(() => { + sinon.assert.calledWith(spyLogWarn, warning); + sinon.assert.notCalled(spyAddWinningBid); + sinon.assert.notCalled(adResponse.renderer.render); + sinon.assert.neverCalledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse); + configObj.setConfig({'auctionOptions': {}}); + }); }); it('should emit AD_RENDER_FAILED if requested missing adId', () => { @@ -253,11 +262,12 @@ describe('secureCreatives', () => { adId: 'missing' }) }); - receiveMessage(ev); - sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({ - reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, - adId: 'missing' - })); + return receive(ev).then(() => { + sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({ + reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + adId: 'missing' + })); + }); }); it('should emit AD_RENDER_FAILED if creative can\'t be sent to rendering frame', () => { @@ -271,11 +281,12 @@ describe('secureCreatives', () => { adId: bidId }) }); - receiveMessage(ev) - sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({ - reason: AD_RENDER_FAILED_REASON.EXCEPTION, - adId: bidId - })); + return receive(ev).then(() => { + sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({ + reason: AD_RENDER_FAILED_REASON.EXCEPTION, + adId: bidId + })); + }) }); it('should include renderers in responses', () => { @@ -287,8 +298,12 @@ describe('secureCreatives', () => { }, data: JSON.stringify({adId: bidId, message: 'Prebid Request'}) }); - receiveMessage(ev); - sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => JSON.parse(ob).renderer === 'mock-renderer')); + return receive(ev).then(() => { + sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => { + const {renderer, rendererVersion} = JSON.parse(ob); + return renderer === 'mock-renderer' && rendererVersion === PUC_MIN_VERSION; + })); + }); }); if (FEATURES.NATIVE) { @@ -318,23 +333,24 @@ describe('secureCreatives', () => { }, data: JSON.stringify({adId: bidId, message: 'Prebid Request'}) }) - receiveMessage(ev); - sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => { - const data = JSON.parse(ob); - ['width', 'height'].forEach(prop => expect(data[prop]).to.not.exist); - const native = data.native; - sinon.assert.match(native, { - ortb: bid.native.ortb, - adTemplate: bid.native.adTemplate, - rendererUrl: bid.native.rendererUrl, - }) - expect(Object.fromEntries(native.assets.map(({key, value}) => [key, value]))).to.eql({ - adTemplate: bid.native.adTemplate, - rendererUrl: bid.native.rendererUrl, - body: 'vbody' - }); - return true; - })) + return receive(ev).then(() => { + sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => { + const data = JSON.parse(ob); + ['width', 'height'].forEach(prop => expect(data[prop]).to.not.exist); + const native = data.native; + sinon.assert.match(native, { + ortb: bid.native.ortb, + adTemplate: bid.native.adTemplate, + rendererUrl: bid.native.rendererUrl, + }) + expect(Object.fromEntries(native.assets.map(({key, value}) => [key, value]))).to.eql({ + adTemplate: bid.native.adTemplate, + rendererUrl: bid.native.rendererUrl, + body: 'vbody' + }); + return true; + })) + }); }) } }); @@ -361,16 +377,16 @@ describe('secureCreatives', () => { origin: 'any origin' }); - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(stubGetAllAssetsMessage); - sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); - sinon.assert.calledOnce(ev.source.postMessage); - sinon.assert.notCalled(stubFireNativeTrackers); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - sinon.assert.calledOnce(spyAddWinningBid); - sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + return receive(ev).then(() => { + sinon.assert.neverCalledWith(spyLogWarn, warning); + sinon.assert.calledOnce(stubGetAllAssetsMessage); + sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse); + sinon.assert.calledOnce(ev.source.postMessage); + sinon.assert.notCalled(stubFireNativeTrackers); + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + sinon.assert.calledOnce(spyAddWinningBid); + sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER); + }); }); it('Prebid native should not fire BID_WON when receiveMessage is called more than once', () => { @@ -391,12 +407,75 @@ describe('secureCreatives', () => { origin: 'any origin' }); - receiveMessage(ev); - sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); - - receiveMessage(ev); - stubEmit.withArgs(EVENTS.BID_WON, adResponse).calledOnce; + return receive(ev).then(() => { + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + return receive(ev); + }).then(() => { + stubEmit.withArgs(EVENTS.BID_WON, adResponse).calledOnce; + }); }); + + it('should fire BID_WON when no asset is requested', () => { + pushBidResponseToAuction({}); + const data = { + adId: bidId, + message: 'Prebid Native', + }; + + const ev = makeEvent({ + data: JSON.stringify(data), + }); + return receive(ev).then(() => { + sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse); + }); + }) + + describe('resizing', () => { + let container, slot; + before(() => { + const [gtag, atag] = [window.googletag, window.apntag]; + delete window.googletag; + delete window.apntag; + after(() => { + window.googletag = gtag; + window.apntag = atag; + }) + }) + beforeEach(() => { + pushBidResponseToAuction({ + adUnitCode: 'mock-au' + }); + container = document.createElement('div'); + container.id = 'mock-au'; + slot = document.createElement('div'); + container.appendChild(slot); + document.body.appendChild(container) + }); + afterEach(() => { + if (container) { + document.body.removeChild(container); + } + }) + it('should handle resize request', () => { + const ev = makeEvent({ + data: JSON.stringify({ + adId: bidId, + message: 'Prebid Native', + action: 'resizeNativeHeight', + width: 123, + height: 321 + }), + source: { + postMessage: sinon.stub() + }, + origin: 'any origin' + }); + return receive(ev).then(() => { + expect(slot.style.width).to.eql('123px'); + expect(slot.style.height).to.eql('321px'); + }); + }) + }) }); describe('Prebid Event', () => { @@ -422,13 +501,14 @@ describe('secureCreatives', () => { }, }) }); - receiveMessage(event); - expect(stubEmit.calledWith(EVENTS.AD_RENDER_FAILED, { - adId: bidId, - bid: adResponse, - reason: 'Fail reason', - message: 'Fail message' - })).to.equal(shouldEmit); + return receive(event).then(() => { + expect(stubEmit.calledWith(EVENTS.AD_RENDER_FAILED, { + adId: bidId, + bid: adResponse, + reason: 'Fail reason', + message: 'Fail message' + })).to.equal(shouldEmit); + }); }); it(`should${shouldEmit ? ' ' : ' not '}emit AD_RENDER_SUCCEEDED`, () => { @@ -439,12 +519,13 @@ describe('secureCreatives', () => { adId: bidId, }) }); - receiveMessage(event); - expect(stubEmit.calledWith(EVENTS.AD_RENDER_SUCCEEDED, { - adId: bidId, - bid: adResponse, - doc: null - })).to.equal(shouldEmit); + return receive(event).then(() => { + expect(stubEmit.calledWith(EVENTS.AD_RENDER_SUCCEEDED, { + adId: bidId, + bid: adResponse, + doc: null + })).to.equal(shouldEmit); + }); }); }); }); @@ -498,5 +579,15 @@ describe('secureCreatives', () => { sinon.assert.called(slots[1].getSlotElementId); sinon.assert.calledWith(document.getElementById, 'div2'); }); + + it('should not resize interstitials', () => { + resizeRemoteCreative({ + instl: true, + adId: 'adId', + width: 300, + height: 250, + }); + sinon.assert.notCalled(document.getElementById); + }) }) }); diff --git a/test/spec/unit/utils/focusTimeout_spec.js b/test/spec/unit/utils/focusTimeout_spec.js new file mode 100644 index 00000000000..3fcc4af18fe --- /dev/null +++ b/test/spec/unit/utils/focusTimeout_spec.js @@ -0,0 +1,87 @@ +import {setFocusTimeout, reset} from '../../../../src/utils/focusTimeout'; + +export const setDocumentHidden = (hidden) => { + Object.defineProperty(document, 'hidden', { + configurable: true, + get: () => hidden, + }); + document.dispatchEvent(new Event('visibilitychange')); +}; + +describe('focusTimeout', () => { + let clock, callback; + + beforeEach(() => { + reset() + clock = sinon.useFakeTimers(); + callback = sinon.spy(); + }); + + afterEach(() => { + clock.restore(); + }) + + it('should invoke callback when page is visible', () => { + setFocusTimeout(callback, 2000); + clock.tick(2000); + expect(callback.called).to.be.true; + }); + + it('should not choke if page starts hidden', () => { + setDocumentHidden(true); + reset(); + setDocumentHidden(false); + setFocusTimeout(callback, 1000); + clock.tick(1000); + sinon.assert.called(callback); + }) + + it('should not invoke callback if page was hidden', () => { + setFocusTimeout(callback, 2000); + setDocumentHidden(true); + clock.tick(3000); + expect(callback.called).to.be.false; + }); + + it('should defer callback execution when page is hidden', () => { + setFocusTimeout(callback, 4000); + clock.tick(2000); + setDocumentHidden(true); + clock.tick(2000); + setDocumentHidden(false); + expect(callback.called).to.be.false; + clock.tick(2000); + expect(callback.called).to.be.true; + }); + + it('should not execute deferred callbacks again', () => { + setDocumentHidden(true); + setFocusTimeout(callback, 1000); + clock.tick(2000); + [false, true, false].forEach(setDocumentHidden); + clock.tick(2000); + sinon.assert.calledOnce(callback); + }); + + it('should run callbacks that expire while page is hidden', () => { + setFocusTimeout(callback, 1000); + clock.tick(500); + setDocumentHidden(true); + clock.tick(1000); + setDocumentHidden(false); + sinon.assert.notCalled(callback); + clock.tick(1000); + sinon.assert.called(callback); + }) + + it('should return updated timerId after page was showed again', () => { + const getCurrentTimerId = setFocusTimeout(callback, 4000); + const oldTimerId = getCurrentTimerId(); + clock.tick(2000); + setDocumentHidden(true); + clock.tick(2000); + setDocumentHidden(false); + const newTimerId = getCurrentTimerId(); + expect(oldTimerId).to.not.equal(newTimerId); + }); +}); diff --git a/test/spec/unit/utils/ipUtils_spec.js b/test/spec/unit/utils/ipUtils_spec.js new file mode 100644 index 00000000000..8cd82a8c4fe --- /dev/null +++ b/test/spec/unit/utils/ipUtils_spec.js @@ -0,0 +1,58 @@ +import { scrubIPv4, scrubIPv6 } from '../../../../src/utils/ipUtils' + +describe('ipUtils', () => { + describe('ipv4', () => { + it('should mask ip v4', () => { + let input = '192.168.1.1'; + let output = scrubIPv4(input); + expect(output).to.deep.equal('192.168.1.0'); + input = '192.168.255.255'; + output = scrubIPv4(input); + expect(output).to.deep.equal('192.168.255.0'); + }); + + it('should return null for null input', () => { + let input = null; + let output = scrubIPv4(input); + expect(output).to.deep.equal(null); + }); + + it('should convert invalid format to null', () => { + let invalidIp = '192.130.2'; + let output = scrubIPv4(invalidIp); + expect(output).to.deep.equal(null); + }); + + it('should convert invalid format to null', () => { + let invalidIp = '2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF'; + let output = scrubIPv4(invalidIp); + expect(output).to.deep.equal(null); + }); + }); + + describe('ipv6', () => { + it('should mask ip v6', () => { + let input = '2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF'; + let output = scrubIPv6(input); + expect(output).to.deep.equal('2001:db8:3333:4444:0:0:0:0'); + }); + + it('should return null for null input', () => { + let input = null; + let output = scrubIPv6(input); + expect(output).to.deep.equal(null); + }); + + it('should convert invalid format to null', () => { + let invalidIp = '2001:db8:3333:4444:CCCC:DDDD:EEEE'; + let output = scrubIPv4(invalidIp); + expect(output).to.deep.equal(null); + }); + + it('should convert invalid format to null', () => { + let invalidIp = 'invalid'; + let output = scrubIPv4(invalidIp); + expect(output).to.deep.equal(null); + }); + }); +}) diff --git a/test/spec/unit/utils/promise_spec.js b/test/spec/unit/utils/promise_spec.js index bd8b0390b2e..c0456a11747 100644 --- a/test/spec/unit/utils/promise_spec.js +++ b/test/spec/unit/utils/promise_spec.js @@ -1,191 +1,6 @@ -import {GreedyPromise, defer} from '../../../../src/utils/promise.js'; +import {defer} from '../../../../src/utils/promise.js'; -describe('GreedyPromise', () => { - it('throws when resolver is not a function', () => { - expect(() => new GreedyPromise()).to.throw(); - }) - - Object.entries({ - 'resolved': (use) => new GreedyPromise((resolve) => use(resolve)), - 'rejected': (use) => new GreedyPromise((_, reject) => use(reject)) - }).forEach(([t, makePromise]) => { - it(`runs callbacks immediately when ${t}`, () => { - let cbRan = false; - const cb = () => { cbRan = true }; - let resolver; - makePromise((fn) => { resolver = fn }).then(cb, cb); - resolver(); - expect(cbRan).to.be.true; - }) - }); - - describe('idioms', () => { - let makePromise, pendingFailure, pendingSuccess; - - Object.entries({ - // eslint-disable-next-line no-throw-literal - 'resolver that throws': (P) => new P(() => { throw 'error' }), - 'resolver that resolves multiple times': (P) => new P((resolve) => { resolve('first'); resolve('second'); }), - 'resolver that rejects multiple times': (P) => new P((resolve, reject) => { reject('first'); reject('second') }), - 'resolver that resolves and rejects': (P) => new P((resolve, reject) => { reject('first'); resolve('second') }), - 'resolver that resolves with multiple arguments': (P) => new P((resolve) => resolve('one', 'two')), - 'resolver that rejects with multiple arguments': (P) => new P((resolve, reject) => reject('one', 'two')), - 'resolver that resolves to a promise': (P) => new P((resolve) => resolve(makePromise(P, 'val'))), - 'resolver that resolves to a promise that resolves to a promise': (P) => new P((resolve) => resolve(makePromise(P, makePromise(P, 'val')))), - 'resolver that resolves to a rejected promise': (P) => new P((resolve) => resolve(makePromise(P, 'err', true))), - 'simple .then': (P) => makePromise(P, 'value').then((v) => `${v} and then`), - 'chained .then': (P) => makePromise(P, 'value').then((v) => makePromise(P, `${v} and then`)), - '.then with error handler': (P) => makePromise(P, 'err', true).then(null, (e) => `${e} and then`), - '.then with chained error handler': (P) => makePromise(P, 'err', true).then(null, (e) => makePromise(P, `${e} and then`)), - '.then that throws': (P) => makePromise(P, 'value').then((v) => { throw v }), - '.then that throws in error handler': (P) => makePromise(P, 'err', true).then(null, (e) => { throw e }), - '.then with no args': (P) => makePromise(P, 'value').then(), - '.then that rejects': (P) => makePromise(P, 'value').then((v) => P.reject(v)), - '.then that rejects in error handler': (P) => makePromise(P, 'err', true).then(null, (err) => P.reject(err)), - '.then with no error handler on a rejection': (P) => makePromise(P, 'err', true).then((v) => `resolved ${v}`), - '.then with no success handler on a resolution': (P) => makePromise(P, 'value').then(null, (e) => `caught ${e}`), - 'simple .catch': (P) => makePromise(P, 'err', true).catch((err) => `caught ${err}`), - 'identity .catch': (P) => makePromise(P, 'err', true).catch((err) => err).then((v) => v), - '.catch that throws': (P) => makePromise(P, 'err', true).catch((err) => { throw err }), - 'chained .catch': (P) => makePromise(P, 'err', true).catch((err) => makePromise(P, err)), - 'chained .catch that rejects': (P) => makePromise(P, 'err', true).catch((err) => P.reject(`reject with ${err}`)), - 'simple .finally': (P) => { - let fval; - return makePromise(P, 'value') - .finally(() => fval = 'finally ran') - .then((val) => `${val} ${fval}`) - }, - 'chained .finally': (P) => { - let fval; - return makePromise(P, 'value') - .finally(() => pendingSuccess.then(() => { fval = 'finally ran' })) - .then((val) => `${val} ${fval}`) - }, - '.finally on a rejection': (P) => { - let fval; - return makePromise(P, 'error', true) - .finally(() => { fval = 'finally' }) - .catch((err) => `${err} ${fval}`) - }, - 'chained .finally on a rejection': (P) => { - let fval; - return makePromise(P, 'error', true) - .finally(() => pendingSuccess.then(() => { fval = 'finally' })) - .catch((err) => `${err} ${fval}`) - }, - // eslint-disable-next-line no-throw-literal - '.finally that throws': (P) => makePromise(P, 'value').finally(() => { throw 'error' }), - 'chained .finally that rejects': (P) => makePromise(P, 'value').finally(() => P.reject('error')), - 'scalar Promise.resolve': (P) => P.resolve('scalar'), - 'null Promise.resolve': (P) => P.resolve(null), - 'chained Promise.resolve': (P) => P.resolve(pendingSuccess), - 'chained Promise.resolve on failure': (P) => P.resolve(pendingFailure), - 'scalar Promise.reject': (P) => P.reject('scalar'), - 'chained Promise.reject': (P) => P.reject(pendingSuccess), - 'chained Promise.reject on failure': (P) => P.reject(pendingFailure), - 'simple Promise.all': (P) => P.all([makePromise(P, 'one'), makePromise(P, 'two')]), - 'empty Promise.all': (P) => P.all([]), - 'Promise.all with scalars': (P) => P.all([makePromise(P, 'one'), 'two']), - 'Promise.all with errors': (P) => P.all([makePromise(P, 'one'), makePromise(P, 'two'), makePromise(P, 'err', true)]), - 'Promise.allSettled': (P) => P.allSettled([makePromise(P, 'one', true), makePromise(P, 'two'), makePromise(P, 'three', true)]), - 'empty Promise.allSettled': (P) => P.allSettled([]), - 'Promise.allSettled with scalars': (P) => P.allSettled([makePromise(P, 'value'), 'scalar']), - 'Promise.race that succeeds': (P) => P.race([makePromise(P, 'error', true, 10), makePromise(P, 'success')]), - 'Promise.race that fails': (P) => P.race([makePromise(P, 'success', false, 10), makePromise(P, 'error', true)]), - 'Promise.race with scalars': (P) => P.race(['scalar', makePromise(P, 'success')]), - }).forEach(([t, op]) => { - describe(t, () => { - describe('when mixed with deferrals', () => { - beforeEach(() => { - makePromise = function(ctor, value, fail = false, delay = 0) { - // eslint-disable-next-line new-cap - return new ctor((resolve, reject) => { - setTimeout(() => fail ? reject(value) : resolve(value), delay) - }) - }; - pendingSuccess = makePromise(Promise, 'pending result', false, 10); - pendingFailure = makePromise(Promise, 'pending failure', true, 10); - }); - - it(`behaves like vanilla promises`, () => { - const vanilla = op(Promise); - const greedy = op(GreedyPromise); - // note that we are not using `allSettled` & co to resolve our promises, - // to avoid transformations those methods do under the hood - const {actual = {}, expected = {}} = {}; - return new Promise((resolve) => { - let pending = 2; - function collect(dest, slot) { - return function (value) { - dest[slot] = value; - pending--; - if (pending === 0) { - resolve() - } - } - } - vanilla.then(collect(expected, 'success'), collect(expected, 'failure')); - greedy.then(collect(actual, 'success'), collect(actual, 'failure')); - }).then(() => { - expect(actual).to.eql(expected); - }); - }); - - it(`once resolved, runs callbacks immediately`, () => { - const promise = op(GreedyPromise).catch(() => null); - return promise.then(() => { - let cbRan = false; - promise.then(() => { cbRan = true }); - expect(cbRan).to.be.true; - }); - }); - }); - - describe('when all promises involved are greedy', () => { - beforeEach(() => { - makePromise = function(ctor, value, fail = false, delay = 0) { - // eslint-disable-next-line new-cap - return new ctor((resolve, reject) => { - const run = () => fail ? reject(value) : resolve(value); - delay === 0 ? run() : setTimeout(run, delay); - }) - }; - pendingSuccess = makePromise(GreedyPromise, 'pending result'); - pendingFailure = makePromise(GreedyPromise, 'pending failure', true); - }); - - it('resolves immediately', () => { - let cbRan = false; - op(GreedyPromise).catch(() => null).then(() => { cbRan = true }); - expect(cbRan).to.be.true; - }); - }); - }); - }); - }); - - describe('.timeout', () => { - const timeout = GreedyPromise.timeout; - - it('should resolve immediately when ms is 0', () => { - let cbRan = false; - timeout(0.0).then(() => { cbRan = true }); - expect(cbRan).to.be.true; - }); - - it('should schedule timeout on ms > 0', (done) => { - let cbRan = false; - timeout(5).then(() => { cbRan = true }); - expect(cbRan).to.be.false; - setTimeout(() => { - expect(cbRan).to.be.true; - done(); - }, 10) - }); - }); -}); - -describe('promiseControls', () => { +describe('defer', () => { Object.entries({ 'resolve': (p) => p, 'reject': (p) => p.then(() => 'wrong', (v) => v) diff --git a/test/spec/utils/prerendering_spec.js b/test/spec/utils/prerendering_spec.js new file mode 100644 index 00000000000..409edb93747 --- /dev/null +++ b/test/spec/utils/prerendering_spec.js @@ -0,0 +1,53 @@ +import {delayIfPrerendering} from '../../../src/utils/prerendering.js'; + +describe('delayIfPrerendering', () => { + let sandbox, enabled, ran; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + enabled = true; + ran = false; + }); + + afterEach(() => { + sandbox.restore(); + }) + + const delay = delayIfPrerendering(() => enabled, () => { + ran = true; + }) + + it('should not delay if page is not prerendering', () => { + delay(); + expect(ran).to.be.true; + }) + + describe('when page is prerendering', () => { + before(() => { + if (!('prerendering' in document)) { + document.prerendering = null; + after(() => { + delete document.prerendering; + }) + } + }) + beforeEach(() => { + sandbox.stub(document, 'prerendering').get(() => true); + }); + function prerenderingDone() { + document.dispatchEvent(new Event('prerenderingchange')); + } + + it('should run fn only after prerenderingchange event', async () => { + delay(); + expect(ran).to.be.false; + prerenderingDone(); + expect(ran).to.be.true; + }); + + it('should not delay if not enabled', () => { + enabled = false; + delay(); + expect(ran).to.be.true; + }) + }) +}) diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 8ea68aadecc..32a9d9e0390 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -1,10 +1,10 @@ import {getAdServerTargeting} from 'test/fixtures/fixtures.js'; import {expect} from 'chai'; -import { TARGETING_KEYS } from 'src/constants.js'; +import {TARGETING_KEYS} from 'src/constants.js'; import * as utils from 'src/utils.js'; -import {getHighestCpm, getLatestHighestCpmBid, getOldestHighestCpmBid} from '../../src/utils/reducers.js'; -import {binarySearch, deepEqual, encodeMacroURI, memoize, waitForElementToLoad} from 'src/utils.js'; +import {binarySearch, deepEqual, encodeMacroURI, memoize, sizesToSizeTuples, waitForElementToLoad} from 'src/utils.js'; import {convertCamelToUnderscore} from '../../libraries/appnexusUtils/anUtils.js'; +import { getWinDimensions, internal } from '../../src/utils.js'; var assert = require('assert'); @@ -21,6 +21,54 @@ describe('Utils', function () { type_array = 'Array', type_function = 'Function'; + describe('canAccessWindowTop', function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + it('should return true if window.top is accessible', function () { + assert.equal(utils.canAccessWindowTop(), true); + }); + + it('should return false if window.top is not accessible', function () { + sandbox.stub(utils.internal, 'getWindowTop').returns(false); + assert.equal(utils.canAccessWindowTop(), false); + }); + }); + + describe('isSafeFrameWindow', function () { + // SafeFrames implementation + // https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf + const $sf = { + ext: { + geom: function() {} + } + }; + + afterEach(function() { + delete window.$sf; + }) + + it('should return true if window.$sf is accessible', function () { + window.$sf = $sf; + assert.equal(utils.isSafeFrameWindow(), true); + }); + + it('should return false if window.$sf is missimplemented', function () { + window.$sf = {}; + assert.equal(utils.isSafeFrameWindow(), false); + }); + + it('should return false if window.$sf is missing', function () { + assert.equal(utils.isSafeFrameWindow(), false); + }); + }); + describe('getBidIdParameter', function () { it('should return value of the key in input object', function () { var obj = { @@ -65,7 +113,7 @@ describe('Utils', function () { var obj = getAdServerTargeting(); var output = utils.transformAdServerTargetingObj(obj[Object.keys(obj)[0]]); - var expected = 'foobar=0x0%2C300x250%2C300x600&' + TARGETING_KEYS.SIZE + '=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '=10.00&' + TARGETING_KEYS.AD_ID + '=233bcbee889d46d&' + TARGETING_KEYS.BIDDER + '=appnexus&' + TARGETING_KEYS.SIZE + '_triplelift=0x0&' + TARGETING_KEYS.PRICE_BUCKET + '_triplelift=10.00&' + TARGETING_KEYS.AD_ID + '_triplelift=222bb26f9e8bd&' + TARGETING_KEYS.BIDDER + '_triplelift=triplelift&' + TARGETING_KEYS.SIZE + '_appnexus=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_appnexus=10.00&' + TARGETING_KEYS.AD_ID + '_appnexus=233bcbee889d46d&' + TARGETING_KEYS.BIDDER + '_appnexus=appnexus&' + TARGETING_KEYS.SIZE + '_pagescience=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_pagescience=10.00&' + TARGETING_KEYS.AD_ID + '_pagescience=25bedd4813632d7&' + TARGETING_KEYS.BIDDER + '_pagescienc=pagescience&' + TARGETING_KEYS.SIZE + '_brightcom=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_brightcom=10.00&' + TARGETING_KEYS.AD_ID + '_brightcom=26e0795ab963896&' + TARGETING_KEYS.BIDDER + '_brightcom=brightcom&' + TARGETING_KEYS.SIZE + '_brealtime=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_brealtime=10.00&' + TARGETING_KEYS.AD_ID + '_brealtime=275bd666f5a5a5d&' + TARGETING_KEYS.BIDDER + '_brealtime=brealtime&' + TARGETING_KEYS.SIZE + '_pubmatic=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_pubmatic=10.00&' + TARGETING_KEYS.AD_ID + '_pubmatic=28f4039c636b6a7&' + TARGETING_KEYS.BIDDER + '_pubmatic=pubmatic&' + TARGETING_KEYS.SIZE + '_rubicon=300x600&' + TARGETING_KEYS.PRICE_BUCKET + '_rubicon=10.00&' + TARGETING_KEYS.AD_ID + '_rubicon=29019e2ab586a5a&' + TARGETING_KEYS.BIDDER + '_rubicon=rubicon'; + var expected = 'foobar=300x250%2C300x600%2C0x0&' + TARGETING_KEYS.SIZE + '=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '=10.00&' + TARGETING_KEYS.AD_ID + '=233bcbee889d46d&' + TARGETING_KEYS.BIDDER + '=appnexus&' + TARGETING_KEYS.SIZE + '_triplelift=0x0&' + TARGETING_KEYS.PRICE_BUCKET + '_triplelift=10.00&' + TARGETING_KEYS.AD_ID + '_triplelift=222bb26f9e8bd&' + TARGETING_KEYS.BIDDER + '_triplelift=triplelift&' + TARGETING_KEYS.SIZE + '_appnexus=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_appnexus=10.00&' + TARGETING_KEYS.AD_ID + '_appnexus=233bcbee889d46d&' + TARGETING_KEYS.BIDDER + '_appnexus=appnexus&' + TARGETING_KEYS.SIZE + '_pagescience=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_pagescience=10.00&' + TARGETING_KEYS.AD_ID + '_pagescience=25bedd4813632d7&' + TARGETING_KEYS.BIDDER + '_pagescienc=pagescience&' + TARGETING_KEYS.SIZE + '_brightcom=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_brightcom=10.00&' + TARGETING_KEYS.AD_ID + '_brightcom=26e0795ab963896&' + TARGETING_KEYS.BIDDER + '_brightcom=brightcom&' + TARGETING_KEYS.SIZE + '_brealtime=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_brealtime=10.00&' + TARGETING_KEYS.AD_ID + '_brealtime=275bd666f5a5a5d&' + TARGETING_KEYS.BIDDER + '_brealtime=brealtime&' + TARGETING_KEYS.SIZE + '_pubmatic=300x250&' + TARGETING_KEYS.PRICE_BUCKET + '_pubmatic=10.00&' + TARGETING_KEYS.AD_ID + '_pubmatic=28f4039c636b6a7&' + TARGETING_KEYS.BIDDER + '_pubmatic=pubmatic&' + TARGETING_KEYS.SIZE + '_rubicon=300x600&' + TARGETING_KEYS.PRICE_BUCKET + '_rubicon=10.00&' + TARGETING_KEYS.AD_ID + '_rubicon=29019e2ab586a5a&' + TARGETING_KEYS.BIDDER + '_rubicon=rubicon'; assert.equal(output, expected); }); @@ -119,6 +167,43 @@ describe('Utils', function () { }); }); + describe('sizesToSizeTuples', () => { + Object.entries({ + 'single size, numerical': { + in: [1, 2], + out: [[1, 2]] + }, + 'single size, numerical, nested': { + in: [[1, 2]], + out: [[1, 2]] + }, + 'multiple sizes, numerical': { + in: [[1, 2], [3, 4]], + out: [[1, 2], [3, 4]] + }, + 'single size, string': { + in: '1x2', + out: [[1, 2]] + }, + 'multiple sizes, string': { + in: '1x2, 4x3', + out: [[1, 2], [4, 3]] + }, + 'incorrect size, numerical': { + in: [1], + out: [] + }, + 'incorrect size, string': { + in: '1x', + out: [] + } + }).forEach(([t, {in: input, out}]) => { + it(`can parse ${t}`, () => { + expect(sizesToSizeTuples(input)).to.eql(out); + }) + }) + }) + describe('parseSizesInput', function () { it('should return query string using multi size array', function () { var sizes = [[728, 90], [970, 90]]; @@ -1020,7 +1105,6 @@ describe('Utils', function () { }); it('should work when adding properties to the prototype of Array', () => { after(function () { - // eslint-disable-next-line no-extend-native delete Array.prototype.unitTestTempProp; }); // eslint-disable-next-line no-extend-native @@ -1086,6 +1170,44 @@ describe('Utils', function () { }); }); + describe('getUnixTimestampFromNow', () => { + it('correctly obtains unix timestamp', () => { + const nowValue = new Date('2024-01-01').valueOf(); + sinon.stub(Date, 'now').returns(nowValue); + let val = utils.getUnixTimestampFromNow(); + expect(val).equal(nowValue); + + val = utils.getUnixTimestampFromNow(1); + expect(val).equal(nowValue + (1000 * 60 * 60 * 24)); + + val = utils.getUnixTimestampFromNow(1, 'd'); + expect(val).equal(nowValue + (1000 * 60 * 60 * 24)); + + val = utils.getUnixTimestampFromNow(1, 'm'); + expect(val).equal(nowValue + (1000 * 60 * 60 * 24 / 1440)); + + val = utils.getUnixTimestampFromNow(2, 'm'); + expect(val).equal(nowValue + (1000 * 60 * 60 * 24 * 2 / 1440)); + + // any value that isn't 'm' or 'd' gets treated as Date.now(); + val = utils.getUnixTimestampFromNow(10, 'o'); + expect(val).equal(nowValue); + }); + }); + + describe('convertObjectToArray', () => { + it('correctly converts object to array', () => { + const obj = {key: 1, anotherKey: 'fred', third: ['fred'], fourth: {sub: {obj: 'test'}}}; + const array = utils.convertObjectToArray(obj); + + expect(JSON.stringify(array[0])).equal(JSON.stringify({'key': 1})) + expect(JSON.stringify(array[1])).equal(JSON.stringify({'anotherKey': 'fred'})) + expect(JSON.stringify(array[2])).equal(JSON.stringify({'third': ['fred']})) + expect(JSON.stringify(array[3])).equal(JSON.stringify({'fourth': {sub: {obj: 'test'}}})); + expect(array.length).to.equal(4); + }); + }); + describe('setScriptAttributes', () => { it('correctly adds attributes from an object', () => { const script = document.createElement('script'), @@ -1101,6 +1223,25 @@ describe('Utils', function () { expect(script.id).to.equal('newId'); }); }); + + describe('safeJSONParse', () => { + it('correctly encodes valid input', () => { + const jsonObj = { + key1: 'val1', + key2: { + key3: 100, + key4: true + } + }; + const result = utils.safeJSONEncode(jsonObj); + expect(result).to.equal(`{"key1":"val1","key2":{"key3":100,"key4":true}}`); + }); + it('return empty string for stringify errors', () => { + const jsonObj = {k: 2n}; + const result = utils.safeJSONEncode(jsonObj); + expect(result).to.equal(''); + }); + }); }); describe('memoize', () => { @@ -1185,3 +1326,30 @@ describe('memoize', () => { }) }); }) + +describe('getWinDimensions', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers({ now: new Date() }); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should invoke resetWinDimensions once per 20ms', () => { + const resetWinDimensionsSpy = sinon.spy(internal, 'resetWinDimensions'); + getWinDimensions(); + clock.tick(1); + getWinDimensions(); + clock.tick(1); + getWinDimensions(); + clock.tick(1); + getWinDimensions(); + sinon.assert.calledOnce(resetWinDimensionsSpy); + clock.tick(18); + getWinDimensions(); + sinon.assert.calledTwice(resetWinDimensionsSpy); + }); +}); diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index fc6e71779cb..ae753d1794c 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -1,39 +1,14 @@ import chai from 'chai'; -import {getCacheUrl, store} from 'src/videoCache.js'; +import {batchingCache, getCacheUrl, store, _internal, storeBatch} from 'src/videoCache.js'; import {config} from 'src/config.js'; import {server} from 'test/mocks/xhr.js'; import {auctionManager} from '../../src/auctionManager.js'; import {AuctionIndex} from '../../src/auctionIndex.js'; -import {batchingCache} from '../../src/auction.js'; +import * as utils from 'src/utils.js'; +import { storeLocally } from '../../src/videoCache.js'; const should = chai.should(); -function getMockBid(bidder, auctionId, bidderRequestId) { - return { - 'bidder': bidder, - 'params': { - 'placementId': '10433394', - 'member': 123, - 'keywords': { - 'foo': ['bar', 'baz'], - 'fizz': ['buzz'] - } - }, - 'bid_id': '12345abc', - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250]] - } - }, - 'transactionId': '4ef956ad-fd83-406d-bd35-e4bb786ab86c', - 'sizes': [300, 250], - 'bidId': '123', - 'bidderRequestId': bidderRequestId, - 'auctionId': auctionId - }; -} - describe('The video cache', function () { function assertError(callbackSpy) { callbackSpy.calledOnce.should.equal(true); @@ -126,9 +101,7 @@ describe('The video cache', function () { prebid.org wrapper - - - + \n \n `; @@ -335,34 +308,36 @@ describe('The video cache', function () { JSON.parse(request.requestBody).should.deep.equal(payload); }); - it('should wait the duration of the batchTimeout and pass the correct batchSize if batched requests are enabled in the config', () => { - const mockAfterBidAdded = function() {}; - let callback = null; - let mockTimeout = sinon.stub().callsFake((cb) => { callback = cb }); + if (FEATURES.VIDEO) { + it('should wait the duration of the batchTimeout and pass the correct batchSize if batched requests are enabled in the config', () => { + const mockAfterBidAdded = function() {}; + let callback = null; + let mockTimeout = sinon.stub().callsFake((cb) => { callback = cb }); - config.setConfig({ - cache: { - url: 'https://prebid.adnxs.com/pbc/v1/cache', - batchSize: 3, - batchTimeout: 20 - } - }); + config.setConfig({ + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache', + batchSize: 3, + batchTimeout: 20 + } + }); - let stubCache = sinon.stub(); - const batchAndStore = batchingCache(mockTimeout, stubCache); - for (let i = 0; i < 3; i++) { - batchAndStore({}, {}, mockAfterBidAdded); - } + let stubCache = sinon.stub(); + const batchAndStore = batchingCache(mockTimeout, stubCache); + for (let i = 0; i < 3; i++) { + batchAndStore({}, {}, mockAfterBidAdded); + } - sinon.assert.calledOnce(mockTimeout); - sinon.assert.calledWith(mockTimeout, sinon.match.any, 20); + sinon.assert.calledOnce(mockTimeout); + sinon.assert.calledWith(mockTimeout, sinon.match.any, 20); - const expectedBatch = [{ afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }, { afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }, { afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }]; + const expectedBatch = [{ afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }, { afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }, { afterBidAdded: mockAfterBidAdded, auctionInstance: { }, bidResponse: { } }]; - callback(); + callback(); - sinon.assert.calledWith(stubCache, expectedBatch); - }); + sinon.assert.calledWith(stubCache, expectedBatch); + }); + } function assertRequestMade(bid, expectedValue) { store([bid], function () { }); @@ -393,6 +368,52 @@ describe('The video cache', function () { return callback; } }); + + describe('storeBatch', () => { + let sandbox; + let err, cacheIds + beforeEach(() => { + err = null; + cacheIds = []; + sandbox = sinon.createSandbox(); + sandbox.stub(utils, 'logError'); + sandbox.stub(_internal, 'store').callsFake((_, cb) => cb(err, cacheIds)); + }); + afterEach(() => { + sandbox.restore(); + }) + it('should log an error when store replies with an error', () => { + err = new Error('err'); + storeBatch([]); + sinon.assert.called(utils.logError); + }); + it('should not process returned uuids if they do not match the batch size', () => { + const el = {auctionInstance: {}, bidResponse: {}, afterBidAdded: sinon.stub()} + const batch = [el, el]; + cacheIds = [{uuid: 'mock-id'}] + storeBatch(batch); + expect(el.bidResponse.videoCacheKey).to.not.exist; + sinon.assert.notCalled(batch[0].afterBidAdded); + sinon.assert.called(utils.logError); + }) + }) + + describe('local video cache', function() { + afterEach(function () { + config.resetConfig(); + }); + + it('should store bid vast locally with blob by default', () => { + const bid = { + vastXml: `` + }; + + storeLocally(bid); + + expect(bid.vastUrl.startsWith('blob:http://')).to.be.true; + expect(bid.videoCacheKey).to.not.be.empty; + }); + }); }); describe('The getCache function', function () { diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 3252c58c687..0d2a32659e9 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,12 +1,27 @@ -import {fillVideoDefaults, isValidVideoBid} from 'src/video.js'; +import {fillVideoDefaults, isValidVideoBid, validateOrtbVideoFields} from 'src/video.js'; import {hook} from '../../src/hook.js'; import {stubAuctionIndex} from '../helpers/indexStub.js'; +import * as utils from '../../src/utils.js'; +import { syncOrtb2 } from '../../src/prebid.js'; describe('video.js', function () { + let sandbox; + let utilsMock; + before(() => { hook.ready(); }); + beforeEach(() => { + sandbox = sinon.createSandbox(); + utilsMock = sandbox.mock(utils); + }) + + afterEach(() => { + utilsMock.restore(); + sandbox.restore(); + }); + describe('fillVideoDefaults', () => { function fillDefaults(videoMediaType = {}) { const adUnit = {mediaTypes: {video: videoMediaType}}; @@ -77,6 +92,94 @@ describe('video.js', function () { }) }) + describe('validateOrtbVideoFields', () => { + it('remove incorrect ortb properties, and keep non ortb ones', () => { + sandbox.spy(utils, 'logWarn'); + + const mt = { + content: 'outstream', + + mimes: ['video/mp4'], + minduration: 5, + maxduration: 15, + startdelay: 0, + maxseq: 0, + poddur: 0, + protocols: [7], + w: 600, + h: 480, + podid: 'an-id', + podseq: 0, + rqddurs: [5], + placement: 1, + plcmt: 1, + linearity: 1, + skip: 0, + skipmin: 3, + skipafter: 3, + sequence: 0, + slotinpod: 0, + mincpmpersec: 2.5, + battr: [6, 7], + maxextended: 0, + minbitrate: 800, + maxbitrate: 1000, + boxingallowed: 1, + playbackmethod: [1], + playbackend: 1, + delivery: [2], + pos: 0, + api: 6, // -- INVALID + companiontype: [1, 2, 3], + poddedupe: [1], + + otherOne: 'test', + }; + + const expected = {...mt}; + delete expected.api; + + const adUnit = { + code: 'adUnitCode', + mediaTypes: { video: mt } + }; + validateOrtbVideoFields(adUnit); + + expect(adUnit.mediaTypes.video).to.eql(expected); + sinon.assert.callCount(utils.logWarn, 1); + }); + + it('Early return when 1st param is not a plain object', () => { + sandbox.spy(utils, 'logWarn'); + + validateOrtbVideoFields(); + validateOrtbVideoFields([]); + validateOrtbVideoFields(null); + validateOrtbVideoFields('hello'); + validateOrtbVideoFields(() => {}); + + sinon.assert.callCount(utils.logWarn, 5); + }); + + it('Calls onInvalidParam when a property is invalid', () => { + const onInvalidParam = sandbox.spy(); + const adUnit = { + code: 'adUnitCode', + mediaTypes: { + video: { + content: 'outstream', + mimes: ['video/mp4'], + api: 6 + } + } + }; + validateOrtbVideoFields(adUnit, onInvalidParam); + + sinon.assert.calledOnce(onInvalidParam); + sinon.assert.calledWith(onInvalidParam, 'api', 6, adUnit); + }); + }) + describe('isValidVideoBid', () => { it('validates valid instream bids', function () { const bid = { @@ -172,4 +275,207 @@ describe('video.js', function () { expect(valid).to.equal(false); }); }) + + describe('syncOrtb2', () => { + if (!FEATURES.VIDEO) { + return; + } + + let logWarnSpy; + + beforeEach(function () { + logWarnSpy = sinon.spy(utils, 'logWarn'); + }); + + afterEach(function () { + utils.logWarn.restore(); + }); + + it('should properly sync fields if both present', () => { + const adUnit = { + mediaTypes: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + linearity: 5, // should be overwritten with value from ortb2Imp + w: 100, + h: 200, + foo: 'omitted_value' // should be omitted during copying - not part of video obj spec + } + }, + ortb2Imp: { + video: { + minbitrate: 10, + maxbitrate: 50, + delivery: [1, 2, 3, 4], + linearity: 10, + bar: 'omitted_value' // should be omitted during copying - not part of video obj spec + } + } + }; + + const expected = { + mediaTypes: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + w: 100, + h: 200, + minbitrate: 10, + maxbitrate: 50, + delivery: [1, 2, 3, 4], + linearity: 10, + foo: 'omitted_value' + } + }, + ortb2Imp: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + w: 100, + h: 200, + minbitrate: 10, + maxbitrate: 50, + delivery: [1, 2, 3, 4], + linearity: 10, + bar: 'omitted_value' + } + } + }; + + syncOrtb2(adUnit, 'video'); + expect(adUnit).to.deep.eql(expected); + + assert.ok(logWarnSpy.calledOnce, 'expected warning was logged due to conflicting linearity'); + }); + + it('should omit sync if video mediaType not present on adUnit', () => { + const adUnit = { + mediaTypes: { + native: { + fieldToOmit: 'omitted_value' + } + }, + ortb2Imp: { + native: { + fieldToOmit2: 'omitted_value' + } + } + }; + + syncOrtb2(adUnit, 'video'); + + expect(adUnit.mediaTypes.video).to.be.undefined; + expect(adUnit.ortb2Imp.video).to.be.undefined; + }); + + it('should properly sync if mediaTypes is not present on any of side', () => { + const adUnit = { + mediaTypes: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + linearity: 5, + w: 100, + h: 200, + foo: 'omitted_value' + } + }, + ortb2Imp: { + // lack of video field + } + }; + + const expected1 = { + mediaTypes: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + linearity: 5, + w: 100, + h: 200, + foo: 'omitted_value' + } + }, + ortb2Imp: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + linearity: 5, + w: 100, + h: 200, + } + } + }; + + syncOrtb2(adUnit, 'video'); + expect(adUnit).to.deep.eql(expected1); + + const adUnit2 = { + mediaTypes: { + // lack of video field + }, + ortb2Imp: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + linearity: 5, + w: 100, + h: 200, + foo: 'omitted_value' + } + } + }; + + const expected2 = { + mediaTypes: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + linearity: 5, + w: 100, + h: 200, + } + }, + ortb2Imp: { + video: { + minduration: 500, + maxduration: 1000, + protocols: [1, 2, 3], + linearity: 5, + w: 100, + h: 200, + foo: 'omitted_value', + } + } + }; + + syncOrtb2(adUnit2, 'video'); + expect(adUnit2).to.deep.eql(expected2); + }); + + it('should not create empty video object on ortb2Imp if there was nothing to copy', () => { + const adUnit2 = { + mediaTypes: { + video: { + noOrtbVideoField1: 'value', + noOrtbVideoField2: 'value' + } + }, + ortb2Imp: { + // lack of video field + } + }; + syncOrtb2(adUnit2, 'video'); + expect(adUnit2.ortb2Imp.video).to.be.undefined + }); + }); }); diff --git a/test/test_deps.js b/test/test_deps.js index c8a3bcc9426..1afcb435061 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -14,8 +14,10 @@ window.addEventListener('unhandledrejection', function (ev) { console.error('Unhandled rejection:', ev.reason); }) +require('test/helpers/global_hooks.js'); require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); require('test/mocks/adloaderStub.js'); require('test/mocks/xhr.js'); require('test/mocks/analyticsStub.js'); +require('test/mocks/ortbConverter.js') diff --git a/test/test_index.js b/test/test_index.js index ce9b671be89..030082735c3 100644 --- a/test/test_index.js +++ b/test/test_index.js @@ -1,25 +1,4 @@ -[it, describe].forEach((ob) => { - ob.only = function () { - [ - 'describe.only and it.only are disabled unless you provide a single spec --file,', - 'because they can silently break the pipeline tests', - // eslint-disable-next-line no-console - ].forEach(l => console.error(l)) - throw new Error('do not use .only()') - }; -}); - -[it, describe].forEach((ob) => { - ob.skip = function () { - [ - 'describe.skip and it.skip are disabled,', - 'because they pollute the pipeline test output', - // eslint-disable-next-line no-console - ].forEach(l => console.error(l)) - throw new Error('do not use .skip()') - }; -}); - +require('./pipeline_setup.js'); require('./test_deps.js'); var testsContext = require.context('.', true, /_spec$/); diff --git a/wdio.shared.conf.js b/wdio.shared.conf.js index 34e1ee9c675..08b46eaab91 100644 --- a/wdio.shared.conf.js +++ b/wdio.shared.conf.js @@ -16,7 +16,7 @@ exports.config = { mochaOpts: { ui: 'bdd', timeout: 60000, - compilers: ['js:babel-register'], + compilers: ['js:@babel/register'], }, // if you see error, update this to spec reporter and logLevel above to get detailed report. reporters: ['spec'] diff --git a/webpack.conf.js b/webpack.conf.js index 1035e985b22..5b0d864045e 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -126,7 +126,6 @@ module.exports = { }) ); const core = path.resolve('./src'); - const paapiMod = path.resolve('./modules/paapi.js'); return Object.assign(libraries, { core: { @@ -135,16 +134,6 @@ module.exports = { return module.resource && module.resource.startsWith(core); } }, - paapi: { - // fledgeForGpt imports paapi to keep backwards compat for NPM consumers - // this makes the paapi module its own chunk, pulled in by both paapi and fledgeForGpt entry points, - // to avoid duplication - // TODO: remove this in prebid 9 - name: 'chunk-paapi', - test: (module) => { - return module.resource === paapiMod; - } - } }, { default: false, defaultVendors: false